Typed Warrant and Reconciled Edge Contract — Provenance and Graph Integrity the Validator Can Prove

Accepted

ontoref
The `context-as-property` work (2026-07-09, from Jessica Talisman's essay naming,

Context

The `context-as-property` work (2026-07-09, from Jessica Talisman's essay naming, from library science, the category ontoref occupies) recorded field-for-field the convergence between the glossary term schema and ANSI/NISO Z39.19 — and, with ondaod honesty, the three gaps the lineage does NOT certify. Two of them are structural claims the ontology graph could not yet back:

1. WARRANT — "on whose authority does this node exist?" — was typed only on glossary terms (`origin`) and value-props (`evidence_adrs`); inside the ontology's own node descriptions it was prose. A node's provenance could be asserted in a paragraph and never queried.

2. Z39.19 §8.1.1 reciprocity ("every relation A→B carries B→A, for all types of relationships") was stricter than any validator ontoref ran on its own graph.

Verifying the edge surface to act on gap 2 surfaced a dormant third-party defect: a THREE-WAY contract divergence on edges, alive but asleep. The schema enum (`code/ontology/schemas/core.ncl`) listed four kinds never used (ValidatedBy, FlowsTo, CyclesIn, LimitedBy) and omitted four in active use (Contradicts, DependsOn, Implies, Resolves); it typed `weight` as `Number` while the data has always carried symbolic `'High/'Medium/'Low`; and the Rust loader (`types.rs::EdgeType`, `weight: f64`) mirrored the stale schema. `nickel export` passed anyway because the edge records in `core.ncl` are BARE — no `make_edge`, no `CoreConfig` on the top level — so no contract ever bit them. The Rust typed path (`Core::from_value`) would REJECT the real `core.ncl` (`kind:"Resolves"`, `weight:"High"`), but it has zero live callers: the daemon seeds edges from raw JSON. A latent lie, not an active break.

Decision

Close both admitted gaps and reconcile the dormant divergence, in one move, with a validator as the enforcement surface where the NCL contract cannot reach.

WARRANT (gap 1) — additive, audited, never required: - `WarrantRef = { kind | [| 'Adr, 'Emergence, 'Interview, 'Session, 'External |], ref | String, note | String | default = "" }` and `Node.warrant | Array WarrantRef | default = []` in the core schema. The default makes every existing node validate unchanged (no migration: the trigger rule fires only for REQUIRED fields). - The evidence itself lives in reflection (emergence.log, interviews); the node CITES it, never duplicates it. The Rust `Node` struct is left unchanged — serde drops the field exactly as it already drops `axis` — until a surface needs to expose it. - Enforcement is an AUDIT, not a contract: `validate ontology --warrant` flags a non-Axiom node (Practice / Tension / — in a consumer — Project / Moment) that carries neither `adrs` nor `warrant`. Soft, direction-reporting; a REQUIRED warrant would be ceremony-capture. Axioms are self-warranting (they are the Yang floor, invariant = true).

EDGE RECONCILIATION (gap 2 + the dormant bug) — schema and Rust follow the data: - `edge_type` becomes the nine kinds ACTUALLY used (Complements, Contains, Contradicts, DependsOn, Implies, ManifestsIn, Resolves, SpiralsWith, TensionWith); `weight` becomes the symbolic `weight_type = [| 'High, 'Medium, 'Low |]`. Rust `EdgeType` and `Weight` mirror them exactly. The typed path now tells the truth about the data. - Reciprocity is DERIVABLE, not stored (the correct DAG adaptation of §8.1.1): `code/ontology/defaults/edge-inverses.ncl` freezes, per kind, an inverse label for reverse-direction rendering, and the set of symmetric kinds (Complements, Contradicts, SpiralsWith, TensionWith) whose inverse is the kind itself. - `validate ontology` is the graph-integrity gate the bare-record edges left open: dangling endpoints (Hard), every kind present in the inverse-map vocabulary (Hard), symmetric reciprocity consistency (Soft). It reads `core.ncl` via `nickel export`, never through the typed Rust path, so it works daemon-free.

The nine-kind vocabulary is now single-source across three surfaces (schema enum, Rust `EdgeType`, `edge-inverses.ncl`); a new kind must be added to all three, and `validate ontology` fails until it is.

Constraints

  • Hard `Node.warrant` MUST carry `default = []` so existing nodes and consumer graphs validate unchanged; no schema may make warrant a required field.
  • Hard Warrant provenance MUST be enforced as a Soft audit (`validate ontology --warrant`) that reports the gap, never as a Hard reject of un-warranted nodes.
  • Hard The edge-kind vocabulary MUST stay identical across the schema enum, the Rust `EdgeType`, and `edge-inverses.ncl`; a new kind added to one MUST be added to all three.
  • Hard Edge `weight` MUST be the symbolic `weight_type` ('High/'Medium/'Low) in both the schema and Rust — never a Number — matching the data the graph has always carried.
  • Soft `validate ontology` MUST check dangling endpoints and kind-in-vocabulary against `core.ncl` via `nickel export`, working without a running daemon (local-fallback, ADR-029).
  • Soft Reciprocity MUST be derivable, not stored: symmetric kinds (their inverse is the kind itself) are frozen in `edge-inverses.ncl`, and a stored reverse edge of a symmetric kind carrying a DIFFERENT kind is a reciprocity inconsistency.

Alternatives considered

  • Leave warrant as prose in node descriptionsrejected: The status quo the essay named as a gap: provenance asserted in a paragraph is not queryable, not auditable, and indistinguishable from an unsupported claim. The whole point of the context-as-property lineage is that warrant is a typed property, not narration.
  • Make warrant a required Node fieldrejected: Ceremony-capture (adr-029): every node authored under a Hard gate that may not fit, and a mass migration of 59 existing nodes. Collapses formalization-vs-adoption to the Yang pole. The audit reports the gap without forbidding the node.
  • Migrate the edge DATA to the stale schema (numbers, drop the four extra kinds)rejected: Inverts the direction of truth: the data is the ground reality (168 edges across nine kinds with symbolic weights), the schema was the aspirational fiction. Schema and Rust follow the data, not the reverse — and rewriting 168 records to fit a lie is pure churn.
  • Store both edge directions (literal Z39.19 §8.1.1)rejected: In a directed DAG with a fixed kind vocabulary, storing B→A for every A→B is redundant noise that doubles the edge set and invites the two directions to drift. Derivable reciprocity (an inverse label per kind + a symmetric set) renders both directions without persisting either.
  • Wrap every edge in make_edge so the per-record NCL contract bitesrejected: 168 mechanical edits, high churn and merge risk, and it still would not catch dangling endpoints or cross-edge reciprocity (a record contract sees one record). A graph-level validator is one surface with strictly greater reach.

Anti-patterns

  • A Typed Contract That Diverges From Its Data Because Nothing Enforces It — A schema, a Rust type and the data drift apart while every path that would surface the mismatch is dead (bare records skip the contract; the typed loader has no callers). The crispness of the types lends unearned credibility to a lie nothing checks — tier-0 false certainty at the contract layer.
  • Provenance Asserted in a Description, Never Queryable — A node's authority ('surfaced in the interview', 'per ADR-055') lives only in its prose description — not typed, not auditable, indistinguishable from an unsupported claim. The Z39.19 warrant gap the context-as-property essay admitted.
  • Making Provenance a Hard Gate — Turning warrant into a required field or a Hard reject — every node authored under a gate that may not fit, a mass migration forced on consumers. Enforcement as the product; the Yang collapse of formalization-vs-adoption.
  • An Enum That Lists What Was Imagined, Not What Is Used — A type enumerates kinds that never appear in the data while omitting kinds the data actively uses. The schema reads as a governed vocabulary but describes a fiction; a graph validator built on it checks the wrong set.
  • An Agent Moves the Protocol Version on Its Own Criterion — An agent edits reflection/version.ncl protocol_version, or adds a version-bumping protocol_version delta to a migration, because a migration 'implies' a bump. The protocol version is an outward release act (⛓G3 single-source in reflection/version.ncl); coupling it to each migration delta lets an agent move the version by its own judgment. check_chain (audit C2) enforces version↔chain CONSISTENCY, not AUTHORSHIP — so a consistent-but-unauthorized bump passes the audit while still being a decision the agent had no standing to make. Surfaced 2026-07-09 when this very session bumped 0.1.19→0.1.20 unprompted while authoring migration 0043.

Related ADRs

ADR-029 · ADR-035 · ADR-050 · ADR-001 · ADR-028

Was this useful? Rate it
Got something to add? Tell me what you think, what you'd suggest, or whether we should keep exploring this topic.
· reads

We use cookies to help this site function, understand service usage, and support marketing efforts. Cookie Policy for more info.