The versioned artefact is the operation dump, not the database that indexes it
Proposed
Context
The substrate's durable state has been committed to git as a redb database file — `.ontoref/ontologies/<name>.oplog` — since ADR-023 introduced it. The file is the log; the log is the truth; committing it looked like committing the truth.
Three measurements taken on 2026-07-30 say it is committing something else.
SIZE. The project's own ontology oplog was 6.85 MB, of which 86% was zero bytes over 940 KB of live content. redb allocates in pages and never returns freed ones, so every reconciliation grows a tail of dead space. git's zlib collapses that tail in the object store, which is exactly why nobody noticed: the repository looked fine while every working tree and every consumer that materialised the file paid for it in full. A compaction pass took it to 3.81 MB — a 44% reduction that recovered nothing but padding.
FRAGILITY. The substrate freshness gate (`just check-substrate`, wired as the `substrate-freshness` pre-commit hook) rests on one premise: a dry run opens the log with `OpLog::open_existing` and leaves the tracked file byte-identical, so a gate can check staleness without dirtying what it inspects. An upgrade from redb 2 to redb 4 silently voids that premise. Isolated and measured in a standalone probe: redb 2's `Database::open` leaves the file byte-identical; redb 4's rewrites it — same length, different bytes. Nothing in the workspace needed redb 4, and the regression surfaced through a unit test at commit time, not through the gate whose premise it destroyed. The pin now carries the reason beside it, but a versioned artefact whose integrity depends on a transitive library's internal file handling is not an artefact the project controls.
REACH. ADR-028 makes cross-project verification by witness rather than by clone. A consumer verifying a slice today must therefore be able to read a redb database file at whatever format version the producer happened to build with. That is a far larger demand than the protocol makes anywhere else, and it is invisible in the schema — nothing declares that the reader needs redb, or which redb.
What the three share is one confusion: the operations are the substrate's content, and the database is an index over them. Committing the index and calling it the log is versioning a derived artefact.
The material for the alternative already exists and was not built for this. Operations are stored as canonical CBOR (`ontoref_types::canonical`), which is the encoding each operation's signature is taken over. `OpLog::all_ops_in_hlc_order` returns every stored operation in a deterministic total order — HLC, with op id as tie-break — and `OpLog::append` replays one. Dump and restore are those two functions with nothing between them.
A first step was taken ahead of this decision and is what prompted it: the oplog was untracked and gitignored (commit d3f079c) to stop paying for the wrong artefact immediately. That leaves the repository with no substrate artefact at all and `_refs.ncl` pointing at a file a clone does not have — a deliberately visible hole, which this ADR fills.
Decision
The artefact of record for an ontology's substrate is a DUMP of its operations. The redb database is a local index, regenerable from the dump, and is never versioned.
FORM. The dump is the canonical CBOR encoding of every operation, in `all_ops_in_hlc_order` order. This is not a format choice with alternatives of equal standing: each operation's signature is taken over its canonical CBOR encoding, so any other serialisation would have to be re-encoded before verification, and the round-trip is exactly where a signature guarantee is lost. A human-readable projection (`--format json`) is an inspection convenience and never the artefact.
LOCATION. `.ontoref/ontologies/<name>.ops` alongside the gitignored `.ontoref/ontologies/<name>.oplog`. The dump is tracked; the database is not.
RESTORE. When a command needs the database and it is absent, the database is rebuilt from the dump, and the rebuild is REPORTED, never silent. When the database is present and diverges from the dump, the command REFUSES and names the divergence; reconciling the two is an explicit act, not a side effect of the next read. Absent-so-rebuild is safe because it has one possible outcome; present-and-divergent has two, and choosing between them is not a decision a read should make on the operator's behalf.
FRESHNESS. The gate compares the declaration against the dump. With the database no longer tracked, `OpLog::open_existing`'s byte-identity contract stops being load-bearing for the gate, and the redb version returns to being an implementation detail. The pin at redb 2 becomes a preference to revisit rather than a constraint the gate depends on.
RECORD. `_refs.ncl` records the dump's path. The database's location stops being a public fact about an ontology.
SCOPE. This is a protocol contract, not an ontoref-self convenience: it changes which file is of record in any onboarded project, so it ships with a migration. The protocol version bump that migration may warrant is a release decision and is not taken here.
STATUS. Proposed, and deliberately not Accepted. None of the mechanism exists yet, so all but one of the constraints below carry checks that have never been observed failing. This corpus has already paid for the opposite habit — sixteen Hard constraints invoking validators that were never built, reading as enforcement for months (ADR-070, ADR-078). Acceptance is gated on each check being falsified against a deliberate fixture and then passing.
Constraints
- Hard No `.ontoref/ontologies/*.oplog` file may be tracked in any repository. The database is a local index; a tracked one is the artefact this decision removes.
- Hard The tracked dump MUST be the canonical CBOR encoding of its operations, the same encoding each operation's signature is taken over. A dump in any other serialisation MUST NOT be the artefact of record.
- Hard Rebuilding the database from the dump MUST occur only when the database is absent, and MUST be reported. When a database is present and diverges from the dump, the operation MUST refuse and name the divergence. It MUST NOT pick a direction.
- Hard The substrate freshness gate MUST compare the declaration against the tracked dump, never against the untracked database. A gate reading an artefact a clone does not have cannot run for anyone but its author.
- Soft Once the gate reads the dump, no ADR, manifest comment or gate may justify a redb version constraint by appeal to the freshness gate's byte-identity premise. Reported, not gated: the direction of motion is that the storage engine becomes replaceable.
Alternatives considered
- Keep versioning the database, and compact it before publishing — rejected: This was implemented and measured before being rejected: compaction took 6.85 MB to 3.81 MB, and the remaining file is still a redb database at a specific format version, still requiring a redb reader to verify, still hostage to whether `Database::open` writes. It treats the symptom that is cheapest to see (size) and leaves both others (fragility, reach) exactly where they were. The compaction API survives as a local-index tidy; it is no longer a publication step.
- Keep versioning the database and pin redb permanently — rejected: It makes a dependency's internal file handling into a protocol invariant, enforced by a comment. The pin is currently correct and is not the disagreement — the disagreement is that a versioned artefact should not have a property that only holds while a transitive library chooses to preserve it. It also freezes the project out of redb 4 for a reason unrelated to what redb 4 offers.
- Version nothing — the oplog is always rebuilt from core.ncl — rejected: It discards history, which is the point of an append-only log. Reconciliation appends a delta and each operation carries an HLC, so the log answers what the ontology said at a past moment (ADR-041 bitemporal queries). Rebuilding from the current core.ncl produces one moment and destroys every prior one. It would also make the ontology_id a function of the present declaration rather than of an accumulated DAG, which changes what the address means.
- Reuse the reflection migration system for the restore trigger — rejected: reflection/migrations/ propagates protocol-surface changes to consumer projects — a declared, ordered, once-only sequence a project advances through. Restore is none of those: it is idempotent, derived entirely from present state, and carries no ordering. Borrowing the mechanism would put a data-materialisation step into the surface that tells adopters what changed in the protocol, and the two would drift into each other's meaning.
Anti-patterns
- Dump as a second source of truth — Treating dump and database as two records to be kept in sync, so writes go to one and are copied to the other. Sync is then a step that can be forgotten, and the two disagree while both look authoritative.
- Silent rebuild over divergence — Making restore convenient by rebuilding whenever the database does not match the dump. It removes a refusal the operator would have wanted, and destroys whichever side happened to be ahead.
- Compaction as a publication step — Reviving compaction as something run before committing, on the reasoning that a smaller tracked file is better. It reintroduces the database into the publication path this decision removed it from.
Related ADRs
ADR-023 · ADR-025 · ADR-028 · ADR-029 · ADR-036 · ADR-041 · ADR-070 · ADR-078