From 4bddf1f284daf04cd206278352e507178622d2b2 Mon Sep 17 00:00:00 2001 From: Wayland Yang Date: Fri, 25 Sep 2026 01:02:25 +0800 Subject: [PATCH] Export says which family a rule belongs to, and an axiom rule its kind and declaring predicate A derivation's generating rule reached the export as a prov:Activity with only an rdfs:label, so a reader parsed the label to learn whether an axiom or a business rule produced a conclusion, and for inverse and sub_property could not recover which predicate the axiom was declared on without re-deriving the engine's convention from the exported owl:inverseOf / rdfs:subPropertyOf. Every rule resource is now typed utopia:AxiomRule or utopia:BusinessRule. An axiom rule carries utopia:axiomKind (the closed enum) and utopia:declaredOn, the IRI of the predicate the declaration sits on, read from rules.predicate_id in the same derived_page query. The label stays. Business-rule conditions and expressions are not exported: a business rule is edited in place, so its IRI cannot vouch for the definition an older conclusion was drawn under; that waits for rule versioning. Recorded as a dated revision of 0020; the MCP guide says the same. Co-Authored-By: Claude Fable 5.1 Signed-off-by: Wayland Yang --- crates/utopia-server/src/rdf.rs | 46 +++++++++++++++++++ crates/utopia-store/src/export.rs | 6 ++- .../0020-an-auditor-reads-it-without-us.md | 27 +++++++++-- docs/decisions/README.md | 2 +- web/src/docs/mcp.md | 15 +++--- 5 files changed, 83 insertions(+), 13 deletions(-) diff --git a/crates/utopia-server/src/rdf.rs b/crates/utopia-server/src/rdf.rs index 72de7e1d7..363069a6d 100644 --- a/crates/utopia-server/src/rdf.rs +++ b/crates/utopia-server/src/rdf.rs @@ -605,6 +605,19 @@ pub fn emit_derived( sink.l(&stmt, &utopia("confidence"), &confidence(d.confidence))?; sink.r(&stmt, &prov("wasGeneratedBy"), &rule)?; sink.r(&rule, &nn(rdf::TYPE.as_str()), &prov("Activity"))?; + // 规则的家族与身份(0020 的 2026-09-25 revision,#902):读的人不再从标签里猜它来自 + // 哪张表。公理规则再写出种类(闭合枚举)和声明所在的谓词——inverse 与 sub_property + // 时它不是结论的谓词,从导出的 owl:inverseOf / rdfs:subPropertyOf 反推是有歧义的。 + // 业务规则的条件与表达式不导出:规则原地更新,这个 IRI 担保不了旧结论当时依据的定义 + if d.rule_id.is_some() { + sink.r(&rule, &nn(rdf::TYPE.as_str()), &utopia("AxiomRule"))?; + sink.l(&rule, &utopia("axiomKind"), &text(d.rule.clone()))?; + if let Some(p) = d.rule_predicate.and_then(|p| vocab.relation(p).cloned()) { + sink.r(&rule, &utopia("declaredOn"), &p)?; + } + } else { + sink.r(&rule, &nn(rdf::TYPE.as_str()), &utopia("BusinessRule"))?; + } // 标签用规则自己的名字(业务规则),公理退回它的种类名——审计读到的是 // 「Gas-bearing well」而不是「business」 sink.l( @@ -1288,6 +1301,7 @@ mod tests { invalidated_at: None, confidence: 0.9, rule: "business".into(), + rule_predicate: None, rule_name: Some("Gas-bearing well".into()), premises: vec![id(5)], premises_derived: Vec::new(), @@ -1325,6 +1339,16 @@ mod tests { "分类结论的宾语该是类名本身的字符串字面量,拿到的是 {}", obj[0] ); + // 规则资源说出自己的家族(#902):业务规则,没有公理种类可言 + let rule = Names::new(kb(), None).unwrap().rule(id(9)).to_string(); + assert!(has( + &quads, + &rule, + "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", + "" + )); + assert!(objects(&quads, &rule, "urn:utopia:ns:axiomKind").is_empty()); + assert!(objects(&quads, &rule, "urn:utopia:ns:declaredOn").is_empty()); // 前提照常挂着:审计顺着 prov:used 走得到那两条读数 assert_eq!( objects(&quads, stmt, "http://www.w3.org/ns/prov#used").len(), @@ -1423,6 +1447,7 @@ mod tests { invalidated_at: None, confidence: 0.8, rule: "transitive".into(), + rule_predicate: Some(id(2)), rule_name: None, premises: vec![id(5)], premises_derived: vec![id(6)], @@ -1458,6 +1483,27 @@ mod tests { !has(&quads, SUBJ, WORKS_FOR, OBJ), "推出来的边不写成平铺三元组:那会让人把引擎的结论当成文档里的话" ); + // 规则资源说出自己的家族、种类和声明所在的谓词(#902):读的人不再 + // 从 rdfs:label 里猜。这里声明谓词就是结论谓词(传递),指向同一个 IRI + let rule = Names::new(kb(), None).unwrap().rule(id(8)).to_string(); + assert!(has( + &quads, + &rule, + "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", + "" + )); + assert_eq!( + objects(&quads, &rule, "urn:utopia:ns:axiomKind"), + vec!["\"transitive\""] + ); + assert_eq!( + objects(&quads, &rule, "urn:utopia:ns:declaredOn"), + objects( + &quads, + stmt, + "http://www.w3.org/1999/02/22-rdf-syntax-ns#predicate" + ) + ); } } diff --git a/crates/utopia-store/src/export.rs b/crates/utopia-store/src/export.rs index 778c7b0e7..fd0246286 100644 --- a/crates/utopia-store/src/export.rs +++ b/crates/utopia-store/src/export.rs @@ -261,6 +261,9 @@ pub struct ExportDerived { pub confidence: f32, /// transitive | symmetric | inverse | sub_property,或 business pub rule: String, + /// 公理规则声明在哪个谓词上(`rules.predicate_id`)。inverse 与 sub_property 时它 + /// 不是结论的谓词,导出要写明(0020 的 2026-09-25 revision,#902)。业务规则为 None + pub rule_predicate: Option, /// 业务规则的名字,进 RDF 当这条推理活动的标签 pub rule_name: Option, /// 前提事实。审计要顺着它往下走到句子 @@ -632,7 +635,8 @@ pub async fn derived_page( d.rule_id, d.attribute_rule_id, d.valid_from, d.valid_from_precision, d.valid_to, d.valid_to_precision, d.derived_at, d.invalidated_at, d.confidence, - COALESCE(ru.kind, 'business') AS rule, ar.name AS rule_name, + COALESCE(ru.kind, 'business') AS rule, ru.predicate_id AS rule_predicate, + ar.name AS rule_name, COALESCE(ARRAY(SELECT fd.premise_fact_id FROM fact_derivations fd WHERE fd.derived_fact_id = d.id AND fd.premise_fact_id IS NOT NULL diff --git a/docs/decisions/0020-an-auditor-reads-it-without-us.md b/docs/decisions/0020-an-auditor-reads-it-without-us.md index 0f7a7f9e0..7bce5ebf2 100644 --- a/docs/decisions/0020-an-auditor-reads-it-without-us.md +++ b/docs/decisions/0020-an-auditor-reads-it-without-us.md @@ -53,6 +53,19 @@ Facts **currently held and currently valid** are additionally written as the pla ## Lineage is PROV-O because that is what PROV-O is +**Revision 2026-09-25 ([#902](https://github.com/deeplethe/utopia/issues/902)).** +The rule a derivation is `prov:wasGeneratedBy` used to carry only an `rdfs:label`, +so a reader parsed a label to learn whether it came from an axiom or a business +rule, and for `inverse` and `sub_property` could not recover which predicate the +axiom was declared on without re-deriving the engine's convention from the exported +`owl:inverseOf` / `rdfs:subPropertyOf`. Three minted terms close that: every rule +resource is typed `utopia:AxiomRule` or `utopia:BusinessRule`; an axiom rule +carries `utopia:axiomKind` (the closed enum `transitive`, `symmetric`, `inverse`, +`sub_property`) and `utopia:declaredOn`, the IRI of the predicate the declaration +sits on, which for `inverse` and `sub_property` differs from the conclusion's +`rdf:predicate`. The label stays. Nothing about business-rule bodies is added, for +the reason under **What is not here**. Storage names stay out of the vocabulary. + Each statement is `prov:wasDerivedFrom` the documents its evidence chunks belong to, and carries the quoted sentence. Documents are `prov:Entity` with their title and the source key they arrived under. Derived facts ([0002](0002-reasoning-engine.md)) are `prov:wasGeneratedBy` the rule that produced them, with `prov:used` on each premise statement, so a reader can walk from a conclusion to the sentences underneath it without our API — which is the sentence in the README that this record exists to make true. One of the five built-in packs is PROV-O, so a base that has it loaded already knows these terms. @@ -62,11 +75,15 @@ One of the five built-in packs is PROV-O, so a base that has it loaded already k - **Conflict/review state and chunk identity behind a quote.** Quotes and source documents are exported, but these are separate gaps; conflict state is tracked in [#564](https://github.com/deeplethe/utopia/issues/564). -- **Machine-readable rule definitions.** A generating rule is currently exported - as `prov:Activity` with an `rdfs:label`, under `…:rule:{id}`. Its criteria, - operands and expressions are not exported; [#902](https://github.com/deeplethe/utopia/issues/902) - discusses that extension. Business rules are edited in place, so their stable - identifiers do not identify the definitions that were used for older conclusions. +- **Business-rule criteria.** A generating rule is exported as `prov:Activity` + under `…:rule:{id}` with an `rdfs:label`; since the revision below it also says + which family it belongs to, and an axiom rule says its kind and the predicate it + is declared on. A business rule's conditions, operands and expressions are still + not exported, and the reason is not effort: a business rule is edited in place, + so `…:rule:{id}` cannot vouch for the definition an older conclusion was drawn + under, and exporting today's threshold on it would tell an auditor something + false about yesterday's conclusion. That waits for rule versioning, a storage + and provenance change of its own ([#902](https://github.com/deeplethe/utopia/issues/902)). - **Historical proof snapshots.** Premise links can be rewritten when a conclusion is reproved. The record-time lifetime survives; earlier versions of the proof do not (0019). RDF's `prov:used` edges identify premises, not their sequence. diff --git a/docs/decisions/README.md b/docs/decisions/README.md index 54fbada6e..ff7c8a2d9 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -45,7 +45,7 @@ The test for writing one: if someone (including us) looks at a piece of code in | 0017 | [A contradiction points at an error upstream](0017-a-contradiction-points-upstream.md) | Implemented · B2a: engine and queue, per-item cap, aggregation by rule pair, cards with clues and repairs (#238) · B2b: contested edges in the alert colour, ghost edges for blocked derivations, the disputed chip and the "did not land" section in the panel (#243) | | 0018 | [The lakehouse is one protocol away](0018-the-lakehouse-is-one-protocol-away.md) | Implemented: Trino (Iceberg / Delta / Hive), Databricks and Snowflake behind the same trait, scheme picks the engine (#239) · Trino verified against a real cluster (#327); Databricks and Snowflake still want one (#241, #242) · MaxCompute waits | | 0019 | [The second clock can be rewound](0019-the-second-clock-can-be-rewound.md) | Implemented in three cuts · `held_at` and `as_of` on every graph read (#317), entities' own clock by unwinding `entity_merges` (#337), retrieval as of a moment · the entity panel and `entity_facts` rewind derivations too (#549) · the control on the graph page is still open (#307), full-text recall is still "now" only | -| 0020 | [An auditor reads it without us](0020-an-auditor-reads-it-without-us.md) | Implemented · `GET /kbs/{id}/export?format=turtle\|jsonld` streams the base as RDF, `rdf.rs` holds the mapping · SPARQL waits, and the record says why (#308) | +| 0020 | [An auditor reads it without us](0020-an-auditor-reads-it-without-us.md) | Implemented · revised 2026-09-25 (#902: a rule resource says its family, an axiom rule its kind and declaring predicate) · `GET /kbs/{id}/export?format=turtle\|jsonld` streams the base as RDF, `rdf.rs` holds the mapping · SPARQL waits, and the record says why (#308) | | 0021 | [A rule reads attributes and concludes a type](0021-a-rule-reads-attributes-and-concludes-a-type.md) | Implemented (#359) · `derived_facts` widened to match `facts`, rules authored in `attribute_rules` from the ontology page, evaluated in the materialisation job, explained in the entity panel with their premises · read-only over MCP, writing a rule stays out · no canvas marker, and a conclusion is rewritten rather than edited | | 0022 | [An unknown date is not an open one](0022-an-unknown-date-is-not-an-open-one.md) | Implemented in two cuts (#394 and the derived cut) · `world_axis` predicate beside `record_axis`, `facts.attested_at` anchors a missing start or an undated end at the document that attests it, every read and both client filters on the read interval, an undated ending closes the dated row it ends, derived rows intersect premise intervals as read and carry no precision on an anchored bound · two anchors (`attested_from` / `attested_to`), so a bare open row closes too (#393) · the temporal engine orders a start-less row by its earliest dated evidence and closes its predecessor as ended-unknown there; ends the engine drew are marked and recomputed from the rows a timeline has (0057); a relative deadline is stored as written (#679) | | 0023 | [RSS observations are not documents](0023-rss-observations-are-not-documents.md) | Implemented in #326 | diff --git a/web/src/docs/mcp.md b/web/src/docs/mcp.md index 3f2150b71..f61562911 100644 --- a/web/src/docs/mcp.md +++ b/web/src/docs/mcp.md @@ -72,12 +72,15 @@ key-based IRI. It copies declarations within the base; it does not add reciproca links or compute a transitive closure. A derivation links to `…:rule:{id}` through `prov:wasGeneratedBy`. The rule -resource is typed `prov:Activity` and carries an `rdfs:label`: the business -rule's name, or the axiom rule's kind. The label is human-readable text, not a -machine-readable rule definition; it does not reliably identify the rule family. -Rule criteria, operands and expressions are not currently exported. Their RDF -representation is under discussion in [#902](https://github.com/deeplethe/utopia/issues/902). -A rule IRI identifies the stored rule, not a historical version of its definition. +resource is typed `prov:Activity` and, by family, `urn:utopia:ns:AxiomRule` or +`urn:utopia:ns:BusinessRule`; it carries an `rdfs:label` (the business rule's +name, or the axiom rule's kind). An axiom rule also states `urn:utopia:ns:axiomKind` +(`transitive`, `symmetric`, `inverse` or `sub_property`) and +`urn:utopia:ns:declaredOn`, the predicate the axiom is declared on, which for +`inverse` and `sub_property` is not the conclusion's predicate. A business rule's +criteria, operands and expressions are not exported: a rule IRI identifies the +stored rule, not a historical version of its definition, and a business rule is +edited in place ([#902](https://github.com/deeplethe/utopia/issues/902)). MCP remains the agent-facing surface. The structured results below use the same UUIDs, so an integration can join a selected result to the exported ledger.