You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The supported RDF export (ADR 0020, clarified in #550) links every derived
fact to the rule that produced it. Verified on 9f50e3c
(emit_derived in crates/utopia-server/src/rdf.rs, fed by derived_page
in crates/utopia-store/src/export.rs), a derived fact serializes as:
The rule identity is not dangling: it is minted, typed, and labelled, and it
is stable across exports. What it does not carry is any machine-readable
account of what the rule does. A prov:Activity plus a human label lets an
auditor cite that something produced a conclusion, but not what operation
produced it — which is the question a derived conclusion actually raises.
(The rule:{uuid} mint itself is also absent from the documented IRI list in web/src/docs/mcp.md, which names only entity/fact/derived/document.)
What is missing differs by rule family (the same rule:{uuid} mint serves
both, so the body is also the only reliable way to tell which family a rule
came from — a business rule can perfectly well be named "transitive"):
Axiom rules (rules) — one row per (kb_id, predicate_id, kind), kinds transitive | symmetric | inverse | sub_property:
The kind survives only as rdfs:label text; a consumer must parse a label
to recover a closed enum.
The bound predicate (rules.predicate_id) is not emitted. For transitive
and symmetric it coincides with the conclusion's rdf:predicate and can
be inferred. For inverse and sub_property the rule is bound to the declaring predicate (Derived.via: ceo_of ⊑ works_at concludes on works_at), which differs from the conclusion's predicate — recovering it
means re-deriving the engine's compile convention from the exported owl:inverseOf / rdfs:subPropertyOf declarations, and is ambiguous when
several predicates declare toward the same target.
Nothing of subject_type_id, conclusion, conclude_type_id, conclude_predicate_id, conclude_value, conclude_expr, or the
conditions is emitted.
Some of it is visible on the derived fact — an attribute conclusion's
value is the fact's rdf:object, a typing conclusion's class sits inside
the object's {"class": …} literal — but the rule's declared criterion is
nowhere: prov:used says which readings fired, never the thresholds, ops,
or sets the rule was declared with. For a computed conclusion the
expression itself is unrecoverable; only its result survives.
An auditor can therefore watch a reading "depth = 3500" become a typing
"Gas-bearing" under a rule labelled "Gas-bearing well", but cannot tell that
the rule declared depth > 3000 on entities of class Well. The criteria
are not secret — MCP exposes list_rules / rule_matches precisely so an
agent can read a rule and its thresholds — but they reach an agent as
formatted prose, not the export's structured data. An auditor holding only
the file, which is the premise ADR 0020 was written for, has no way to
recover the operation the name abbreviates.
Why this belongs in the RDF contract
ADR 0020 made this mapping the supported machine-readable read contract so a
reader can "walk from a conclusion to the sentences underneath it without our
API", and web/src/docs/mcp.md already documents derivations as "linked to
their rules and premise statements". prov:wasGeneratedBy commits the
contract to exposing which rule produced a conclusion — that identity is
public lineage today. What is not yet true is that the identity resolves to
something an auditor can understand without database access: today it
resolves to a label, not to the operation it names.
This is not a request to export the whole ledger. It asks only that a
resource the contract already exposes as a generator carry enough structure
to be understood.
Scope of a first cut
Only the two rule families the current derived-fact export already
references, and only the minimum structured body that lets an auditor
understand the declared operation:
rules — kind as structured data, plus the bound predicate.
attribute_rules — subject class, conclusion kind, and what it concludes;
conditions to the extent they can be represented without committing to an
unstable operand/expression vocabulary.
Plus the small structural regression described below. A natural home is the
existing derived_page read and emit_derived; no new machinery is implied.
premise ordering (prov:used identifies premises, not their sequence —
already declared in ADR 0020);
open-statement completeness, evidence/chunk/document-version expansion,
entity descriptions, relation declared qualifiers, or any other ledger
surface;
schema or migration changes, SPARQL, import-of-export;
a generic metadata framework or a second serializer.
Design questions for maintainers
What is the minimum semantic body an exported rule identity should carry?
For axiom rules, kind + bound predicate seems to suffice. For attribute
rules the candidates are subject class, conclusion kind, the concluded
target, and the conditions — which of these are needed to understand the
rule, versus merely restating what each derived fact already shows?
Is this maintenance of ADR 0020 — a dated revision landing with the
implementation — or does the rule-semantic vocabulary constitute a new
decision record in its own right?
Which fields are stable enough to enter the permanent RDF vocabulary? op is a closed eight-value enum and condition grouping is one level,
but operand and conclude_expr embed expression trees whose attr
leaves are stored UUIDs — exporting them commits the contract to a public
expression vocabulary (or to IRI resolution inside literals). Migration 0070 already anticipated this seam: it exempts attr leaves from
column-level reference enforcement and assigns them to export-side
validation instead — a validation layer that does not yet exist because
the export never reads expressions today.
Should expression bodies be represented structurally now, or should the
first cut cover only the semantics expressible without a public
expression vocabulary — kind, bound predicate, subject class, conclusion
kind/target, non-expression operands?
Suggested acceptance criteria
For a representative derived fact, an auditor can follow derived fact → generating rule → enough structured semantics to understand that rule without database access, internal API calls, or
parsing a label.
Identical coverage in Turtle and JSON-LD.
Additive only: no existing triple removed or renamed.
No implication-rule or implied-fact support, no conflict state, no schema
or migration changes.
A small structural regression passes: every Utopia-local IRI appearing in
object position in the emitted set is either described by a resource body
or explicitly classified as intentionally opaque — and for rule resources,
the agreed minimum body is asserted, not merely non-emptiness.
Related
ADR 0020 (docs/decisions/0020-an-auditor-reads-it-without-us.md) — the
export contract this extends
fix(export): keep preflight coverage aligned with the schema #874 — keeps export preflight aligned with the schema; the reference edges
these rule fields would traverse (rules.predicate_id, attribute_rules.subject_type_id / conclude_type_id / conclude_predicate_id, attribute_rule_conditions.predicate_id) are
already inside the same-KB provenance invariant
Problem
The supported RDF export (ADR 0020, clarified in #550) links every derived
fact to the rule that produced it. Verified on
9f50e3c(
emit_derivedincrates/utopia-server/src/rdf.rs, fed byderived_pagein
crates/utopia-store/src/export.rs), a derived fact serializes as:The rule identity is not dangling: it is minted, typed, and labelled, and it
is stable across exports. What it does not carry is any machine-readable
account of what the rule does. A
prov:Activityplus a human label lets anauditor cite that something produced a conclusion, but not what operation
produced it — which is the question a derived conclusion actually raises.
(The
rule:{uuid}mint itself is also absent from the documented IRI list inweb/src/docs/mcp.md, which names only entity/fact/derived/document.)What is missing differs by rule family (the same
rule:{uuid}mint servesboth, so the body is also the only reliable way to tell which family a rule
came from — a business rule can perfectly well be named "transitive"):
Axiom rules (
rules) — one row per(kb_id, predicate_id, kind), kindstransitive | symmetric | inverse | sub_property:kindsurvives only asrdfs:labeltext; a consumer must parse a labelto recover a closed enum.
rules.predicate_id) is not emitted. Fortransitiveand
symmetricit coincides with the conclusion'srdf:predicateand canbe inferred. For
inverseandsub_propertythe rule is bound to thedeclaring predicate (
Derived.via:ceo_of ⊑ works_atconcludes onworks_at), which differs from the conclusion's predicate — recovering itmeans re-deriving the engine's compile convention from the exported
owl:inverseOf/rdfs:subPropertyOfdeclarations, and is ambiguous whenseveral predicates declare toward the same target.
Attribute rules (
attribute_rules+attribute_rule_conditions) —name, subject class, a conclusion (
typing | attribute | computed), andgrouped conditions
(predicate, op, operand):namereaches the export, as the label.subject_type_id,conclusion,conclude_type_id,conclude_predicate_id,conclude_value,conclude_expr, or theconditions is emitted.
attributeconclusion'svalue is the fact's
rdf:object, atypingconclusion's class sits insidethe object's
{"class": …}literal — but the rule's declared criterion isnowhere:
prov:usedsays which readings fired, never the thresholds, ops,or sets the rule was declared with. For a
computedconclusion theexpression itself is unrecoverable; only its result survives.
An auditor can therefore watch a reading "depth = 3500" become a typing
"Gas-bearing" under a rule labelled "Gas-bearing well", but cannot tell that
the rule declared
depth > 3000on entities of classWell. The criteriaare not secret — MCP exposes
list_rules/rule_matchesprecisely so anagent can read a rule and its thresholds — but they reach an agent as
formatted prose, not the export's structured data. An auditor holding only
the file, which is the premise ADR 0020 was written for, has no way to
recover the operation the name abbreviates.
Why this belongs in the RDF contract
ADR 0020 made this mapping the supported machine-readable read contract so a
reader can "walk from a conclusion to the sentences underneath it without our
API", and
web/src/docs/mcp.mdalready documents derivations as "linked totheir rules and premise statements".
prov:wasGeneratedBycommits thecontract to exposing which rule produced a conclusion — that identity is
public lineage today. What is not yet true is that the identity resolves to
something an auditor can understand without database access: today it
resolves to a label, not to the operation it names.
This is not a request to export the whole ledger. It asks only that a
resource the contract already exposes as a generator carry enough structure
to be understood.
Scope of a first cut
Only the two rule families the current derived-fact export already
references, and only the minimum structured body that lets an auditor
understand the declared operation:
rules— kind as structured data, plus the bound predicate.attribute_rules— subject class, conclusion kind, and what it concludes;conditions to the extent they can be represented without committing to an
unstable operand/expression vocabulary.
Plus the small structural regression described below. A natural home is the
existing
derived_pageread andemit_derived; no new machinery is implied.Explicitly out of scope:
implication_rules,implied_fact_sources,phrase_readings, andimplied-fact provenance generally — that lineage integrity is unresolved
in Implied-fact lineage is outside the same-KB provenance invariant #901 and this issue neither fixes nor depends on it;
prov:usedidentifies premises, not their sequence —already declared in ADR 0020);
entity descriptions, relation declared qualifiers, or any other ledger
surface;
Design questions for maintainers
For axiom rules, kind + bound predicate seems to suffice. For attribute
rules the candidates are subject class, conclusion kind, the concluded
target, and the conditions — which of these are needed to understand the
rule, versus merely restating what each derived fact already shows?
implementation — or does the rule-semantic vocabulary constitute a new
decision record in its own right?
opis a closed eight-value enum and condition grouping is one level,but
operandandconclude_exprembed expression trees whoseattrleaves are stored UUIDs — exporting them commits the contract to a public
expression vocabulary (or to IRI resolution inside literals). Migration
0070already anticipated this seam: it exemptsattrleaves fromcolumn-level reference enforcement and assigns them to export-side
validation instead — a validation layer that does not yet exist because
the export never reads expressions today.
first cut cover only the semantics expressible without a public
expression vocabulary — kind, bound predicate, subject class, conclusion
kind/target, non-expression operands?
Suggested acceptance criteria
derived fact → generating rule → enough structured semantics to understand that rulewithout database access, internal API calls, orparsing a label.
or migration changes.
object position in the emitted set is either described by a resource body
or explicitly classified as intentionally opaque — and for rule resources,
the agreed minimum body is asserted, not merely non-emptiness.
Related
docs/decisions/0020-an-auditor-reads-it-without-us.md) — theexport contract this extends
these rule fields would traverse (
rules.predicate_id,attribute_rules.subject_type_id/conclude_type_id/conclude_predicate_id,attribute_rule_conditions.predicate_id) arealready inside the same-KB provenance invariant