Relationship-emission walk + object-property coverage gate
Context and Problem Statement
ODR-0032 ratified the ontology’s relationship layer — the inter-entity OWL object properties ODR-0006/0007 specified — and a relationship-completeness criterion (every inter-entity association reified as an owl:ObjectProperty with rdfs:domain + rdfs:range). The generator does not implement it. Today opda-gen (emitters/modules/agent.py, transaction.py) emits the relator-spine properties opda:founds/opda:mediates as rangeless owl:ObjectProperty stubs (“design-time, never reasoned”) and never emits opda:playedBy/opda:plays, the opda:SellerShape role-play SHACL shape, the structured opda:Name class, opda:Person/opda:Organisation → opda:Address, Transaction → participant/property, or the chain predicates opda:dependsOnTransaction/opda:chainMembers. The corpus therefore has only 7 class→class object properties; ~30 classes are unconnected.
Crucially, the omission was never caught because the only completeness gate over the corpus — ci-category-g-coverage (ADR-0032) — measures descriptive datatype-leaf coverage, not object properties. The descriptive layer got a walk and a gate; the relationship layer got neither.
This ADR is the engineering that realises ODR-0032: a relationship-emission pass in the generator, a new CI gate over the object-property layer, and the regeneration that re-emits the corpus. It is the object-property analogue of how ADR-0031/ADR-0032 realised ODR-0022 for the datatype layer.
Decision Drivers
- Parity with the descriptive layer — that layer is walked and gated; the relationship layer must be too, or it silently regresses.
- Generated, not hand-edited — every TTL header says
DO NOT HAND-EDIT; the fix MUST live in the emitters + be regenerated, not patched into the corpus. - Determinism / byte-identity — the corpus is byte-identity-pinned; new triples must be emitted deterministically and re-pinned (TTLs,
ontology-model.json, graph, exemplar expected-reports). - Separation of graphs — object properties land in the class graph; the role-play/relator constraints land in the shapes graph (ODR-0013), never conflated.
- No new modelling debate — the what is settled by ODR-0032 §Rules; this ADR is purely how.
Considered Options
- Option A (chosen) — Emitter relationship-pass +
ci-object-property-coveragegate + regenerate. Implement the ODR-0032 R2 inventory in the module emitters with domain+range and the SHACL shapes; add a CI gate that fails on rangeless object properties and unreified associations; re-emit and re-pin. - Option B — Hand-edit the TTL corpus to add the properties. Rejected: the corpus is generator output (
DO NOT HAND-EDIT); a hand-edit breaks byte-identity on the next emit and is immediately overwritten. - Option C — Emit the properties but add no gate. Rejected: repeats the original failure mode — without a coverage gate the relationship layer drifts again the moment a new module is added.
- Option D — Express the relationships only as SHACL shapes, not OWL object properties. Rejected per ODR-0032 Option D: SHACL constrains, the object property is the navigable data; a
sh:pathto an undeclared predicate is not an edge.
Decision Outcome
Chosen option: “Option A — emitter relationship-pass + coverage gate + regeneration”, because it fixes the underlying cause (the emitters never produced the layer; nothing gated it) rather than patching individual properties, and brings the relationship layer to parity with the gated descriptive layer.
Consequences
- Good, because the agent/transaction/role classes become connected and the model answers the domain’s competency questions (parties-of-transaction, plays-role, has-address, binds-title).
- Good, because
ci-object-property-coveragemakes relationship completeness a counted, enforced invariant — the failure can’t silently recur. - Bad, because it is a large byte-identity-affecting regeneration (new object properties + SHACL shapes across
opda-{agent,transaction,property,descriptive}*.ttl, regeneratedontology-model.json+ graph + exemplar expected-reports) and requires a generator version bump. - Bad, because new SHACL role-play/relator shapes may surface conformance violations in existing diagnostic exemplars that must be reconciled.
- Neutral, because the participant attribute backlog (
phone/email/structuredname/verification) is explicitly out of scope (ODR-0032) — this changes only the object-property layer.
Confirmation
- The new
opda-gen ci-object-property-coveragegate passes (and is wired intomake ci+ the three-graph CI surface + the AWS deploy gates). - Byte-identity holds after re-pin (
make verify-ontology), repo-roottests/baspi5_round_trip/passes, andemit-exemplar-reportsis byte-identical. - The diagnostic exemplars gain participant-relationship facets that validate against the new role-play/relator shapes.
- Post-regeneration, a count of class→class object properties rises from 7 to cover the GATED ODR-0032 §R2 inventory; every
owl:ObjectPropertyis type-pinned in OWL or SHACL (none rangeless-AND-shapeless),rdfs:domain/rdfs:rangeappears only on single-domain edges with no never-reasoned commitment (founds/mediatesare SHACL-typed), and each GATED edge has a passing worked competency query (Council session-047).
Implementation Plan
Phased; each phase regenerates + keeps gates green before the next.
- Relator spine made functional. In
emitters/modules/agent.py/transaction.py, type-pinopda:founds(Relator → Role) andopda:mediates(Proprietorship → Proprietor) in SHACLsh:class, NOT inrdfs:domain/rdfs:range(Council session-047): addsh:class opda:Proprietorto the existingProprietorshipMediationShape(opda-agent-shapes.ttl:59, today range-unpinned —sh:minCount 2only), a Relator→Rolesh:classshape forfounds, and a SHACL subject-guard confining their subjects to the relator kinds (conservative-by-construction). Keep their"Design-time, NEVER reasoned"rdfs:comments (ODR-0029/0030/0031) — nordfs:rangeis added, so the comment stays true. (Assertingrdfs:domain+rdfs:rangeonfounds/mediatesis a deferred optional future decision, electable only by deleting those comments + overriding ODR-0029/0030/0031 — out of scope here.) Emitopda:playedBy/opda:plays(Role → Person/Organisation) as an OPTIONAL, distinct-node-only edge (never a self-edge; bearer disjunction in theopda:SellerShape/role-play SHACLsh:or ([sh:class Person][sh:class Organisation]), ODR-0013 — not anrdfs:range/owl:unionOf); emitopda:hasRegisteredTitle(Proprietorship → RegisteredTitle) withrdfs:domain+rdfs:range(single-domain, no never-reasoned commitment). - Kind-layer joins (Council session-047 revised). Do NOT mint an
opda:Nameclass: emitopda:hasNameas a structured datatype/value path (nordfs:domain; component structure + bearer-typing in a SHACLNameShape). A dependentopda:Namenode is promoted only on a named consumer that dereferences to it — not met today (opda:NameChangeEventpoints at thePersonwith string-literal names). Extend theopda:hasAddresspredicate to Person/Organisation by dropping itsProperty-onlyrdfs:domain(bearer-typing → SHACL; keeprdfs:range opda:Address); do NOT manufacture or re-settle theopda:Addressclass/IC — Mode-vs-Resource is ODR-0015’s open question, so Address stays RESIDUE-PENDING. - Transaction joins + chain. Emit
opda:hasParticipant(Transaction → Seller/Buyer, bearer disjunction via SHACLsh:or) andopda:concernsProperty(Transaction → Property,rdfs:domain+rdfs:range). The chain predicatesopda:dependsOnTransaction/opda:chainMembersare DEFERRED (Council session-047 Q2 — no chain exemplar + worked query yet); recorded in the residue register, not emitted, until a concrete consumer lands. - The gate (Council session-047 rule — replaces the absolute domain+range mandate). Add
opda_gen/ci/object_property_coverage_test.py+opda-gen ci-object-property-coverage: FAIL on (a) anyowl:ObjectPropertythat is rangeless AND shapeless (nordfs:rangeand no SHACLsh:class/sh:nodevalue-type shape) — not on rangelessness per se; (b) anyrdfs:domain/rdfs:rangethat is not universally true (catches a singlerdfs:domain opda:Personon a multi-bearer predicate likehasName/hasAddress/playedBy); (c) any GATED ODR-0032 §R2 association lacking one worked SPARQL competency query that traverses it; (d) any residue-register entry (RESIDUE-PENDING / DEFERRED / VALUE-SLOT) with an empty/“TODO” disposition. Keep the class-graph dead-edge check separate from the shapes-graph bearer check (ODR-0013 open/closed-world guard). Wire into the three-graph CI +make ci+deploy-aws.yml. - Regenerate + re-pin.
opda-gen emit→ re-pin byte-identity (24 TTLs + shapes);emit-exemplar-reports→ re-pin expected-reports; regeneratesrc/data/ontology-model.json+public/data/ontology-graph-elements.json(deploy artefacts, per ADR-0044); bumpopda_gen.__version__and the affected modules’owl:versionIRI; verifymake ci+ repo-root pytest + a cleanbuild:data.
More Information
- Realises: ODR-0032 (the relationship-layer modelling decision + §R2 object-property inventory + the relationship-completeness criterion).
- Designed-but-unbuilt source: ODR-0006 (
playedBy, role-play SHACL,opda:Name, Address joins), ODR-0007 (participant/chain joins). - Engineering precedent: ADR-0031 + ADR-0032 (the descriptive-layer walk +
ci-category-g-coveragethis mirrors); ADR-0011 (module TBox emission — where the new triples are produced). - SHACL home: ODR-0013 (shapes graph separate from the class graph).
- Evidence of the gap:
source/03-standards/ontology/opda-agent.ttl(opda:founds/opda:mediatesrangeless; noplayedBy);ci-category-g-coveragecovers datatype leaves only. (The handover’s “only 7 class→class object properties” was a narrow hand-picked count that understated the baseline — see §As-built for the measured figures. The verdict is unaffected: the specific gaps — rangelessfounds/mediates, absentplayedBy/hasParticipant/concernsProperty/hasRegisteredTitle— were each verified real.) - Shaped by Council session-047 (2026-06-17): endpoint-IC §R1 + three-bucket gate-checked residue register (GATED / VALUE-SLOT / PENDING-upstream-IC); type-pinned-OWL-or-SHACL gate (replaces the absolute domain+range mandate) + per-edge worked query + two-graph separation;
opda:Nameemitted as a datatype/SHACL value not a class;playedByoptional/distinct-node;hasAddresspredicate-only (Address class PENDING ODR-0015);founds/mediatestype-pinned in SHACLsh:class(preserving “never reasoned”). Full convergence — no held-as-live dissent; assertingrdfs:domain+rdfs:rangeonfounds/mediatesis a recorded deferred option (overrides ODR-0029/0030/0031). - Status
proposed; the operator ratifies before implementation.
As-built (2026-06-17 — implemented)
Implemented via a 3-agent swarm (emitter / gate / adversarial reviewer) + convener integration. All gates green; byte-identity deterministic; the operator still ratifies adoption (records stay proposed).
Measured baseline + delta (corrects the inherited “7”). Pre-implementation the corpus had 24 opda-class→opda-class object properties by rdfs:domain+rdfs:range (not 7 — the handover’s “7” was a narrower hand-picked subset). Post-implementation: the gate counts 68 GATED object properties type-pinned (OWL rdfs:range or SHACL value-type shape); OWL-class-ranged rose 25 → 27; the §R2 relationship inventory is emitted (playedBy/plays, hasParticipant, concernsProperty, hasRegisteredTitle; founds/mediates SHACL-pinned). ci-object-property-coverage --strict = PASS (9/9 competency-covered, residue register well-formed, baselineCategory exempt-with-reason).
Beyond the plan — two latent defects the adversarial review surfaced + fixed:
profiles.pyenumerator silent-loss (D4). Droppingopda:hasAddress’srdfs:domain(phase 2) exposed a pre-existing bug:_build_enumerated_shapes_forbound a domainless predicate tosh:targetClass None(rdflib drops it) → a shape silently lost and an over-counted “bound” header, across 31 overlay forms. Fixed: a domainless predicate is now GAPped (named in the per-form gap register), per the S034 “bind only an emitted predicate with a singlerdfs:domain; else GAP — never fabricate a target” rule. Not a validation loss —hasAddressbearers are typed corpus-wide by the globalopda:HasAddressBearerShape. (Byte-identity could not catch this — the loss was deterministic.)opda:baselineCategory(a 3rd rangeless object property the records never named). Pre-existing governance predicate whose object is an external DPV-PD category IRI (reference-not-import, ODR-0012/0018). Recorded in the residue register as aREFERENCEdisposition (out of the §R2 relationship inventory) — flagged-then-exempted-with-reason, never silently skipped; the gate FAILs if its disposition is removed.- Vacuous SHACL range shapes on
founds+plays(D6) — found in convener pyshacl audit, NOT by the swarm. The emitter builtFoundsRangeShapeandPlaysRangeShapeassh:targetObjectsOf <pred>+sh:property[sh:path <pred>; sh:class/sh:or]— which re-traverses<pred>from its own object node (which has no outgoing<pred>), so the constraint was vacuously satisfied and enforced nothing. Sincefounds/mediatesare rangeless-in-OWL by council design, SHACL is their only type-pin — sofoundswas effectively rangeless-AND-shapeless, and the gate false-passed it (its Pattern-2 detection matched thesh:path+sh:classsyntactically). pyshacl proof:Transaction founds aPropertyconformed=True (wrong co-domain uncaught). The adversarial reviewer caughtplays(D6) but missedfounds— the convener’s independent pyshacl sweep over all five edges caught it. Fixed: both shapes now put the value-typesh:or [Role, RoleMixin]directly on the node shape (the correctHasParticipantRangeShapepattern;sh:ornotsh:class opda:Role, sincefounds/playsrange over a Role or a RoleMixin — siblings). Verified by pyshacl (negative: wrong co-domain now caught; positive:founds→Seller/Proprietor still allowed) + the exemplar expected-reports. Gate hardened too: Pattern-2 detection now skips the vacuous re-traversal (ash:path Pproperty shape whose enclosing nodesh:targetObjectsOf P), so a future regression of this class FAILs the gate instead of false-passing (+2 unit tests).
As-built findings — two undeclared orphan predicates in exemplars (out of §R2; flagged for a future council, not migrated): opda:concerns (Transaction → LegalEstate — the legal interest conveyed; distinct from concernsProperty Transaction → Property, which was added alongside it) and opda:partOfTransaction (Milestone → Transaction — a perdurant milestone-part relation). Both are candidate inter-entity edges the council’s §R2 inventory did not include; left as-is rather than force-mapped onto founds/hasParticipant. A future council may ratify Transaction → LegalEstate and a milestone-part edge.
Carrier (as-built): founds/mediates are SHACL sh:class-pinned (Role / Proprietor) with a relator-subject guard, keeping their “Design-time, NEVER reasoned” comment true (no rdfs:range added). concernsProperty/hasRegisteredTitle carry OWL rdfs:domain+rdfs:range; playedBy carries rdfs:domain opda:Role (universally true) with its Person∪Org bearer in SHACL sh:or; plays is owl:inverseOf playedBy (covered-via-inverse, no exemplar denormalisation).
Verification (convener, at file:line): determinism (fresh emit == working tree, generated files); ci-ontology all gates; ci-object-property-coverage --strict; opda-gen pytest 391; repo-root baspi5 27; JS 29; graph-drift PASS; doc-drift PASS (custom /ontology reference regenerated: 69 object props / 379 SHACL shapes); build:data regenerated ontology-model.json + ontology-graph-elements.json (deploy model-drift gate). Gate wired into make ci-ontology + ontology-byte-identity.yml + deploy-aws.yml.
Comments
Loading comments…
Sign in to post a comment