diff --git a/crates/utopia-core/src/models.rs b/crates/utopia-core/src/models.rs
index 06e1a0b41..ab05c1848 100644
--- a/crates/utopia-core/src/models.rs
+++ b/crates/utopia-core/src/models.rs
@@ -867,10 +867,10 @@ pub struct ConceptMapping {
///
/// **两条事实都展开成 主-谓-宾 文本**:Review 页要让人一眼看出矛盾在哪,
/// 而两个 UUID 看不出任何东西。自反那一类两条相同——它就是一条事实。
-#[derive(Debug, Clone, Serialize, sqlx::FromRow)]
+#[derive(Debug, Clone, Serialize)]
pub struct AxiomViolation {
pub id: Uuid,
- /// self_loop | asymmetry | cycle | functional
+ /// self_loop | asymmetry | cycle | functional | signature | derived_contradiction
pub kind: String,
/// 判据来自哪条关系。人若判「公理写错了」,从这里进本体去改
pub predicate: Option,
@@ -881,6 +881,12 @@ pub struct AxiomViolation {
/// 环的长度(含首尾)。其余三类为 0——前端据此决定要不要显示「查看路径」
pub path_len: i32,
pub detected_at: chrono::DateTime,
+ /// `derived_contradiction` 独有(0017):推出来的那条三元组——它没有落库,
+ /// 只能在这里写出来。字段见 `reasoning::run`。其余种类是 `{}`
+ pub detail: serde_json::Value,
+ /// 审核线索(0017 §2):`stale`(旧断言没写结束日期)、`duplicate`(有同名
+ /// 实体)、`unsure`(抽取置信度低)。只给一条,没有就空
+ pub hint: Option,
}
/// 本体自己的一处自相矛盾(见 `ontology_defects`)。
@@ -891,8 +897,11 @@ pub struct AxiomViolation {
pub struct OntologyDefect {
pub id: Uuid,
/// symmetric_and_asymmetric | transitive_and_functional | subclass_cycle
- /// | disjoint_with_ancestor | inherits_disjoint
+ /// | disjoint_with_ancestor | inherits_disjoint | inverse_of_itself
+ /// | inverse_not_mutual | sub_property_cycle | rules_disagree
pub kind: String,
+ /// `rules_disagree` 独有(0017):哪两条规则、撞在哪条公理上、几对、几个例子
+ pub detail: serde_json::Value,
/// 出问题那个对象的标签(类或谓词)。查不到就是它已经被删了
pub subject_label: Option,
/// 另一方:互斥的那个类
diff --git a/crates/utopia-reason/src/derive.rs b/crates/utopia-reason/src/derive.rs
index 73c8ab2e1..aa6c473dc 100644
--- a/crates/utopia-reason/src/derive.rs
+++ b/crates/utopia-reason/src/derive.rs
@@ -20,7 +20,7 @@
//! 前提 A `[2020,2023)`、前提 B `[2022,∞)` → 派生 `[2022,2023)`。交集为空
//! 就不推——两段没有重叠的时候,链本身在任何时刻都不成立。
-use crate::{Axioms, Edge, MAX_DEPTH};
+use crate::{Axioms, Edge, Kind, MAX_DEPTH};
use std::collections::{HashMap, HashSet};
use uuid::Uuid;
@@ -31,7 +31,7 @@ use uuid::Uuid;
/// ——悄悄截断会让「推完了」和「推了一部分」长得一模一样。
pub const MAX_DERIVED_PER_PREDICATE: usize = 20_000;
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Rule {
/// `A p B` ∧ `B p C` ⟹ `A p C`
Transitive,
@@ -379,6 +379,248 @@ pub fn validity(
Some(acc)
}
+// ===================== 矛盾:派生撞上了什么(0017) =====================
+
+/// 一条派生撞上了一条断言。
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct Clash {
+ /// `Derivation::facts` 里的下标
+ pub derived: usize,
+ /// 撞在哪条公理上:`Functional`(含 inverse_functional)、`Asymmetry`、`SelfLoop`
+ pub axiom: Kind,
+ /// 被撞的断言。自环没有对方,取派生的最后一条前提
+ pub against: Uuid,
+}
+
+/// 两条规则加在一起产出了互相矛盾的派生。
+///
+/// **按规则对聚合,不逐对报**:`ceo_of ⊑ works_at` 加 `works_at` functional,每个有
+/// 两个 ceo 的组织就撞一对——根子是那两条声明,逐对进队列只会淹掉 Review。
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RuleClash {
+ /// (声明所在的谓词, 规则种类),两条按 (谓词, 种类) 排过序,a ≤ b
+ pub a: (Uuid, Rule),
+ pub b: (Uuid, Rule),
+ pub axiom: Kind,
+ /// 互撞的派生对,按 `Derivation::facts` 的下标
+ pub pairs: Vec<(usize, usize)>,
+}
+
+/// 半开区间 `[from, to)`,两端可空
+type Span = (Option, Option);
+/// (谓词, 一端) → 另一端的边:(另一端, 事实, 区间)。functional 两个方向各一份
+type ByEnd = HashMap<(Uuid, Uuid), Vec<(Uuid, Uuid, Span)>>;
+/// 一条规则的身份:声明所在的谓词 + 规则种类
+type RuleSide = (Uuid, Rule);
+/// 互撞的派生对,按 (规则 a, 规则 b, 撞在哪条公理上) 分组
+type Grouped = HashMap<(RuleSide, RuleSide, Kind), Vec<(usize, usize)>>;
+
+#[derive(Debug, Clone, Default, PartialEq, Eq)]
+pub struct Contradictions {
+ pub with_assertions: Vec,
+ pub between_derivations: Vec,
+}
+
+impl Contradictions {
+ /// 不该落地的派生下标:撞过断言的,和撞过别的派生的。**写图宁少勿错**(0002)
+ pub fn blocked(&self) -> HashSet {
+ let mut out: HashSet = self.with_assertions.iter().map(|c| c.derived).collect();
+ for rc in &self.between_derivations {
+ for (i, j) in &rc.pairs {
+ out.insert(*i);
+ out.insert(*j);
+ }
+ }
+ out
+ }
+}
+
+/// 拿公理量一遍派生:与断言撞的逐条列出,派生之间撞的按规则对聚合。
+///
+/// 只查四类——`functional`(含 inverse)、`asymmetric`、`irreflexive`——因为只有它们
+/// 能由**两条边**判出矛盾;传递环那类要走闭包,派生本身就是闭包的一部分,R0 对断言
+/// 查过就够了。functional 与 asymmetric 都要求**有效区间重叠**:Mira 走了 Devin
+/// 接任,两条 `ceo_of` 区间不交,那是接任,不是矛盾。
+///
+/// 撞上断言的派生一律**不落地**(asserted > derived,硬性);这一步把「让路」这件事
+/// 从静默变成可见——0002 那张表里写了没做的那一行。
+pub fn contradictions(
+ derivation: &Derivation,
+ edges: &[TimedEdge],
+ axioms: &HashMap,
+ spans: &HashMap, Option)>,
+) -> Contradictions {
+ // 断言的三份索引:(谓词, 主) → 宾;(谓词, 宾) → 主;(谓词, 主, 宾) → 边
+ let mut by_ps: ByEnd = HashMap::new();
+ let mut by_po: ByEnd = HashMap::new();
+ let mut by_spo: HashMap<(Uuid, Uuid, Uuid), Vec<(Uuid, Span)>> = HashMap::new();
+ for e in edges {
+ let span = (e.from, e.to);
+ let x = e.edge;
+ by_ps
+ .entry((x.predicate, x.subject))
+ .or_default()
+ .push((x.object, x.fact, span));
+ by_po
+ .entry((x.predicate, x.object))
+ .or_default()
+ .push((x.subject, x.fact, span));
+ by_spo
+ .entry((x.predicate, x.subject, x.object))
+ .or_default()
+ .push((x.fact, span));
+ }
+
+ let mut out = Contradictions::default();
+ // 派生的区间:与落库那一侧同一个函数算,算不出的(前提区间不交)本来就不会落
+ let derived_spans: Vec
)}
+ {rules.length > 0 && (
+
+
{S.review.rulesDisagreeCount(d.detail.count ?? 0)}
+ {rules.map((r, i) => (
+
+
+ {S.review.rulesDisagreeRule(r.rule_a, r.via_a, r.rule_b, r.via_b, r.axiom)}
+
+ {r.examples.map(([x, y], j) => (
+
+ {x} · {y}
+
+ ))}
+
+ ))}
+
+ )}
diff --git a/web/src/styles.css b/web/src/styles.css
index 40cdb6aa6..85397ce83 100644
--- a/web/src/styles.css
+++ b/web/src/styles.css
@@ -31,6 +31,8 @@
--u-warn: #f2b66d;
/* 浅玫瑰:深底上的危险"文字/描边/点"专用(够亮才可读) */
--u-danger: #ff9daf;
+ /* 0017:争议色。派生撞上断言、被挡下的派生——警告与危险之外的第三种 */
+ --u-contest: #ff6a3d;
/* 深红:危险"实底按钮"专用(浅玫瑰做底会发粉;亮红在单色 chrome 上嗓门过大),配白字 */
--u-danger-solid: #c9353a;
--u-violet: #c4a5ff;