Ontology Layer Separation from Project Layer — Content-Addressed Decentralized Ontology with Classification
Accepted
Context
Through this session's evolution, the project's ontology lived inside the project's filesystem (.ontology/, adrs/, reflection/), git-versioned alongside the project's code. This pattern made sense when the ontology was specific to a single project; it became structurally problematic as the ecosystem grew to 19 onboarded projects sharing the same ontoref protocol, the same axioms, the same Practice nodes for common patterns, and cross-project federation became a recurring need.
The session's load-bearing insight (your formulation): when the ontology is coupled to the project's filesystem, cross-project operations or verifications require dragging the entire other project into local scope. A project A trying to validate against a constraint declared in project B has no choice but to clone B, parse B's NCL, materialize B's state — and then perform a single check that touches one node of B. This is the H7 saturation pattern at the storage layer: as agents saturate when loaded with full project context, verifiers saturate when loaded with full foreign-project context.
The substrate G1-G10 (G5 in particular: Merkle commitments + witness API) was designed to escape this — a verifier holding only state_root + witness can validate a claim without holding the source. But the witness mechanism is useless when the verifier still must clone the source to know what state_root applies, what schema is in force, what validators bind to which ops. The asymmetry between (small witness) and (huge cloned context) breaks the local-verifiability promise.
This ADR closes the asymmetry by separating two layers that have been conflated:
Project layer — the consumer codebase. Rust crates, Nushell scripts, config files, project-specific NCL extensions. Lives in Radicle / jj repositories. Managed by the existing VCS abstraction (ADR-013).
Ontology layer — protocol-level and shared-domain ontologies. Lives as content-addressed decentralized streams: each ontology is an append-only oplog (G3 primitive), with Merkle commitments (G5) over its current state. Reachable by content-address, not by filesystem location. Multiple projects reference the same ontology by its content-address; none of them owns it.
Cross-project verification under this separation: project A's operation that requires validating against an ontological entity declared in shared ontology X resolves the reference by content-address, fetches only the witness for the specific cell, verifies against the ontology's state_root. No project is dragged; only the witness travels.
Decision
Separate the operational world into two layers with distinct sovereignty, sync mechanisms, and discovery patterns:
Ontology layer (decentralized, content-addressed):
An ontology is an append-only oplog (G3 primitive from substrate G1-G10) containing operations that build the ontological knowledge: axioms, tensions, practices, ADR entries, validators, schemas. Each ontology has: - An identity (Ed25519 keypair for its maintainers; multi-maintainer via CRDT per-domain from ADR-027) - A content-address: blake3 hash of the head OpId chain root - A current state_root (G5 Merkle commitment) - A discovery surface: pubkey + recent head OpId, broadcast via SyncBackend (ADR-027) — peer projects subscribe to follow updates
Ontologies are independent of any project. They exist as their own data structures with their own sync, their own maintainers, their own evolution. A project consumes ontologies by reference; cannot mutate them locally; cannot fork them without explicit forking semantics (which is itself an ontology operation — fork is a recorded act).
Project layer (Radicle + jj, existing infrastructure):
Project codebases continue to live in Radicle repos managed via jj (ADR-013 VCS abstraction + the existing agent-workspace-orchestration pattern with jjw). What changes is the content: - Source code (Rust, Nushell, etc.) — unchanged - Project-specific NCL extensions — minimized; most ontology content moves to the ontology layer - Local oplog of the project — records ops the project emits (proposed ADRs, FSM state moves, backlog ops); references the ontology layer for the schema and validators that apply - Witnesses + state_root snapshots — for cross-project verification
Ontology classification with granularity:
Type 1 — Protocol ontology (mandatorily decentralized): The ontoref-core itself — axioms (voluntary-adoption, internal-coherence-enforced, protocol-not-runtime, self-describing, dag-formalized), named tensions, base practices, base validators. Every adopting project references this by content-address; no project embeds it. The protocol ontology's maintainers are the ontoref project's contributors (this repo's current authors); its content-address is the protocol's published identity.
Type 2 — Shared domain ontology (granular: federable to mandatorily decentralized): Domain ontologies used by multiple projects: provisioning (cluster ops, component catalog), persona (career, opportunities), lian-build (image layers), forge-fleet (fleet management). The granularity: - Type 2a — Federable: shared between this author's projects; decentralization recommended but not mandatory; can live as a single oplog accessible to a known peer set - Type 2b — Mandatorily decentralized: shared with external consumers (any third party may consume); MUST be content- addressed and discoverable via SyncBackend; maintainers may be broader than the originating author
Type 3 — Project-specific ontology (optional decentralization): Extensions unique to a single project that have not (yet) graduated to shared-domain status. Lives as project-local oplog initially; promotable to Type 2 when consumption emerges. The current .ontology/ content of the 19 onboarded projects predominantly falls here.
Type 4 — Private ontology (local, tier-0): Projects that have NCL files but no substrate, no oplog, no operations layer. The pre-ADR-024 baseline. Untouched by this ADR.
The classification is metadata on every ontology declaration. The ontoref-ontology-content crate (new, see below) reads the classification and applies the corresponding sync / sovereignty rules.
New crate: ontoref-ontology-content
Hosts the per-ontology oplog primitives, content-addressing, and fetch-by-witness logic. Distinct from ontoref-ontology (which becomes the bridge between state and NCL manifestation, per ADR-025). Roles: - OntologyId(blake3) — content-address - fetch_ontology(id, sync_backend) — pull ops from peers - fetch_cell_witness(id, cell_query) — fetch only the witness needed to verify a specific cell, not the whole state - verify_cell(ontology_id, cell, witness, state_root) — local check - publish_op(ontology_id, op) — append to the ontology's oplog if authorized; broadcast via SyncBackend
Cross-project verification via witness (the asymmetry-closing property):
Project A invokes an operation whose precondition includes "this entity is in the pool declared by shared ontology X". The runtime: 1. Resolves X by content-address (ontology_id) 2. Determines the cell-of-X the precondition reads (e.g. the pool entity) 3. Calls fetch_cell_witness(X, pool_query) — receives only the witness for that cell + the current state_root of X 4. Verifies the cell content + Merkle proof against state_root 5. If valid, the precondition is satisfied; runtime proceeds
No clone of X. No materialization of X's state. Only the witness travels. The asymmetry is closed.
Migration path:
The existing 19 onboarded projects do not migrate immediately. ADR-028 is a structural commitment with progressive realization: - The ontoref-core ontology (Type 1) is extracted first into its own content-addressed oplog; the existing .ontology/core.ncl content becomes the bootstrap content of that oplog - Each shared-domain ontology (Type 2) is extracted when its multi-project usage is operationally evident (provisioning is the most concrete candidate — used by provisioning + forge-fleet + libre-* projects) - Project-specific (Type 3) extensions stay in their projects until they need cross-project consumption - Private (Type 4) projects remain unaffected
Constraints
- Hard A new crate ontoref-ontology-content MUST exist with the following surface: OntologyId, fetch_ontology, fetch_cell_witness, verify_cell, publish_op. The crate depends on ontoref-types (for OpBody), ontoref-oplog (for log primitives), ontoref-commit (for state_root and witnesses), and optionally an SyncBackend impl.
- Hard Every ontology (in this project: ontoref-core, the existing .ontology/) MUST declare its classification (Type 1 | Type 2a | Type 2b | Type 3 | Type 4) in its metadata. The classification field is part of the OntologyMetadata struct surfaced via ontoref-ontology-content::metadata(ontology_id).
- Hard When ADR-028 transitions to Accepted (post-implementation), the ontoref-core ontology (current .ontology/core.ncl content) MUST be addressable via its content-address (blake3 of its head OpId chain root) and discoverable via at least one SyncBackend (FilesystemSync acceptable initially). The transition records the content-address as the canonical reference in CHANGELOG.md.
- Hard An integration test in crates/ontoref-ontology-content MUST demonstrate: project A invokes an operation whose precondition references a cell in ontology X, the runtime fetches only the cell witness (not the full ontology), verifies via state_root, and the operation proceeds without materializing X locally. The test fails if X's full state is loaded.
- Hard This ADR engages and synthesizes the protocol-not-runtime + self-describing axioms and the formalization-vs-adoption tension. The synthesis preserves all three: the ontology layer is reference content addressable by content-hash (not a runtime); self-description moves to its own layer (preserved structurally); classification with granularity absorbs the adoption shock per ontology.
Alternatives considered
- Keep ontology inside each project; build cross-project federation that smart-loads partial ontology data — rejected: This was the current federation.rs design (BFS depth 5 over HTTP). It has been unused for 419 lines of code precisely because the cost of operating it (every project must expose endpoints, every cross-project query is a network call to a project's filesystem) exceeds the benefit. ADR-028's content-addressing inverts the model: the ontology IS the addressable unit, not 'a slice of a project'. The cost of operation drops because the sync target is purpose-built.
- Single global ontology repo (one Radicle repo for all shared-domain content) — rejected: Reproduces the centralization anti-pattern at the content-management layer. A single repo for all shared ontologies creates a coordination point (who can merge to it?) and a single failure mode. The per-ontology oplog model gives each ontology its own sovereignty — provisioning ontology and persona ontology have different maintainers, different update cadences, different consumers; collapsing them into one repo would force false coordination.
- Move all ontology to a decentralized layer immediately; no Type 3 (project-specific) or Type 4 (private) tier — rejected: Crushes adoption. A new project trying ontoref for the first time would be forced to publish its ontology to a decentralized channel as a precondition of using the protocol at all. The classification with optional decentralization for Types 3 and 4 lets projects start where they are; the decentralization happens when consumption justifies it, not as an entry tax.
- Keep ontology in project but use IPFS-style content addressing for cross-project refs — rejected: Hybrid that does not resolve the core asymmetry. The ontology still lives in project filesystem; the content-address is a thin convenience layer. Cross-project queries still require the responding project to materialize its full ontology (because the content-address points to a file the consumer might not have, hence needs full materialization to serve the content). The asymmetry is unresolved.
- Use Radicle for ontology layer too (one Radicle repo per ontology) — rejected: Radicle is a git-protocol-based system; its strengths are code review, branching, social collaboration. Ontology evolution is operationally distinct — append-only oplog with CRDT merge, not git-style branching. Forcing ontology into Radicle would impose the wrong semantics. The ontology layer needs its own primitive (oplog from G3); Radicle stays for project code.
Anti-patterns
- Drag Whole Project For Cross-Project Verification — Verifying a single ontological assertion declared in another project by cloning that project and materializing its entire NCL state. Inverts the substrate's G5 witness asymmetry: a Merkle witness is hundreds of bytes, a project clone is megabytes-to-gigabytes plus parse cost. Saturates verifiers the way unbounded context saturates agents (H7 pattern).
- Centralized Shared-Ontology Repo — Collapsing every shared-domain ontology (provisioning, persona, lian-build, forge-fleet) into a single global repository. Creates one merge gatekeeper, one failure mode, and forces false coordination between ontologies whose maintainers, update cadences, and consumer sets are independent.
Related ADRs
ADR-001 · ADR-005 · ADR-009 · ADR-013 · ADR-018 · ADR-020 · ADR-023 · ADR-024 · ADR-025 · ADR-026 · ADR-027