Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions crates/utopia-server/src/rdf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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",
"<urn:utopia:ns:BusinessRule>"
));
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(),
Expand Down Expand Up @@ -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)],
Expand Down Expand Up @@ -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",
"<urn:utopia:ns:AxiomRule>"
));
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"
)
);
}
}

Expand Down
6 changes: 5 additions & 1 deletion crates/utopia-store/src/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Uuid>,
/// 业务规则的名字,进 RDF 当这条推理活动的标签
pub rule_name: Option<String>,
/// 前提事实。审计要顺着它往下走到句子
Expand Down Expand Up @@ -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
Expand Down
27 changes: 22 additions & 5 deletions docs/decisions/0020-an-auditor-reads-it-without-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion docs/decisions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
15 changes: 9 additions & 6 deletions web/src/docs/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading