The daemon carries ONE storage substrate, and it is embedded — a projection nobody reads is capability without a witness
Accepted
Context
ADR-002 (2026-03-12) wired stratum-db, and through it SurrealDB, into ontoref-daemon to enable «cross-session impact analysis and audit history». On 2026-08-20 that integration was measured, end to end, and it is WRITE-ONLY.
seed.rs:147 db.upsert nodes / dimensions / membranes seed.rs:174 db.upsert edges main.rs:1804 health_check startup probe
Three call sites in the entire workspace. Zero `query`. Zero `select`. Every graph read the daemon actually serves goes elsewhere: `store nodes` and `store dimensions` resolve through `daemon-export-safe` to the NCL cache; search.rs walks NCL; federation.rs answers cross-project questions by asking the daemon that OWNS the data over HTTP; memory.rs states in its own `//!` that there is one data path and four display surfaces, and that path is the cache.
So the projection had no reader. That is not an unused feature — it is a structure that CANNOT DRIFT DETECTABLY, because nothing observes it. It was also off: `db.enabled = false` in the shipped config, on a daemon the FSM still records as `local` rather than run as a service.
WHAT IT COST WHILE ANSWERING NOTHING:
daemon dependency tree 618 -> 454 crates once removed (-164) build artefacts ~69 MB for the surrealdb subtree alone advisories RUSTSEC-2026-0044 and -0048 (aws-lc-sys), ignored PERMANENTLY because the pull arrived through a peer repo and deny.toml recorded that ontoref could not fix them from here
MEANWHILE THE CAPABILITY ADR-002 WANTED HAD BEEN BUILT, EMBEDDED, IN-HOUSE. ADR-023 opened the verifiable substrate; ADR-025 and ADR-036 landed it on redb: ontoref-oplog (append-only DAG), ontoref-triples (bitemporal EAV with `as_of`/`valid_at`), ontoref-query, ontoref-commit. Audit history and cross-session impact analysis — ADR-002's stated payoff — acquired an owner that needs no service, no port, no credentials. Nobody went back to retire the store that had been superseded, and the superseded store kept being compiled by default for months.
REDB WAS ALREADY THE STORAGE DEPENDENCY BEFORE THIS DECISION, which is the fact that makes this a retirement rather than a swap. It enters through ontoref-ontology-content, a NON-optional dep of the daemon, and is present even under `--no-default-features`. Its footprint is 2 crates (redb + libc), 2.2 MB, zero advisories. And it never reaches ADR-001's minimal adoption surface: ontoref-ontology declares serde, serde_json, anyhow, thiserror, tracing and two optional in-workspace crates, and nothing else.
THE SECOND HALF — MAKING THE SURVIVOR FULLY OPERATIVE — WAS BLOCKED BY A PIN WHOSE STATED REASON WAS HALF THE PROBLEM. `redb = "2"` carried a comment: redb 4 rewrites the file on `Database::open`, which voids `OpLog::open_existing` and with it the substrate-freshness gate. True, and measured to be worse than recorded — under redb 4 `Database::open` rewrites on EVERY open, not merely the first. The comment named the fix (`ReadOnlyDatabase`) and called it «a refactor, not a version bump».
The half it did not name is larger and is a DATA fact, not a code one. Measured on this project's own 3.8 MB oplog:
redb 4 refuses a v2 file on BOTH open paths, `UpgradeRequired(2)`, no auto-upgrade — a failure that appears at `open`, in runtime, for everyone who already holds an oplog, and never at compile time redb 2 writes v2 by default; v3 requires `create_with_file_format_v3(true)` a v3 file written by redb 2 still answers `RepairAborted` to redb 4's read-only open — it lacks the allocator state table, and a read-only handle cannot repair; ONE writable open under redb 4 normalises it after that normalisation redb 2 PANICS on the file — `unreachable!()` at redb-2.6.3/src/transactions.rs:216. Not an `Err`. An emergency downgrade would not print a diagnosis; it would drop the process
The door is therefore one-way, and the migration is lossless across it: 501 ops / 501 heads, content digest `f5cb868f885ef9d5` identical before and after, 3809280 -> 3690496 bytes.
Decision
The daemon carries exactly ONE storage substrate, and it is embedded.
WHAT LEAVES. SurrealDB and its entire projection surface, as one mechanism rather than as a backend swap: the `db` feature and its 36 cfg sites, the `stratum-db` peer path dep (also from `[workspace.dependencies]`), `seed.rs`, the `POST /sync` endpoint, `DbConfig` and the `db` section of the shipped config, `store sync-push` and its three callers, the SurrealDB entry in `manifest.ncl::requirements`, the SurrealDB questions in the new-project and config forms, and the two RUSTSEC ignores that existed only to carry it.
Retiring three of those four surfaces and keeping one would leave a catalogued route that can never answer — the failure ADR-095 names one level up.
WHAT STAYS AND BECOMES FULLY OPERATIVE. redb, at 4.2, with the read/write split moved OUT OF PROSE AND INTO THE TYPE. `OpLog` now holds a `Backing` enum over `Database` and `ReadOnlyDatabase`; `open_existing` returns the read-only arm, whose `append` and `compact` answer `Error::ReadOnly` instead of succeeding. Until today that rule was a doc-comment observing that «append still works but defeats the purpose». Under redb 4 it became load-bearing for a gate that runs on every commit, and a rule that load-bearing does not stay a comment.
HOW THE FORMAT MOVES. `crates/ontoref-oplog-migrate` — a workspace member that pins BOTH redb majors ITSELF and deliberately does not follow the workspace pin, now or later. It performs v2 -> v3 (redb 2), normalisation (redb 4), and verifies losslessness by content digest BEFORE installing the result over the original. It refuses without `--yes`, because what it does cannot be undone.
The pin is the point: phase 1 can only be done by redb 2 and phase 2 only by redb 4, so a migrator following the workspace could do exactly one half of its own job — and would stop being buildable the moment the workspace moved, stranding whoever upgraded the binary before migrating, with no tool left that can read their file.
ORDER OF ADOPTION, MEASURED AND NOT ASSUMED: upgrade the binary FIRST, migrate SECOND. Both orders have a window in which the daemon cannot read the oplog, and the windows are not equally survivable — new binary on an un-migrated file is a clean `UpgradeRequired` error, old binary on a migrated file is a panic. This inverts the order stated in the ratified SOW's prose, which reasoned from the data and not from the failure modes; the measurement decides, and the migration carries the measured order.
Constraints
- Hard ontoref-daemon's compiled dependency closure MUST contain no external database client. Cargo.lock names no surrealdb package and the daemon declares no stratum-db peer dependency.
- Hard `OpLog::open_existing` MUST return a handle backed by redb's `ReadOnlyDatabase`, and its write methods MUST refuse rather than succeed.
- Hard `ontoref-oplog-migrate` MUST declare its own redb versions and MUST NOT use `redb.workspace`. It must stay buildable after the workspace moves to any later redb.
- Hard The migrator MUST compare content digests and row sets between source and result, and MUST refuse to install the migrated file when they differ. It MUST NOT normalise without explicit operator confirmation.
- Soft Adoption guidance MUST state that the binary is upgraded BEFORE the oplog is migrated, and MUST NOT recommend the reverse order.
Alternatives considered
- Keep SurrealDB and finally BUILD the read path — resolvers that answer from the projection instead of the NCL cache — rejected: That is not completing ADR-002, it is a new architectural decision: it would move the authoritative read path off the NCL the repo versions and onto a derived store, and require a provisioned service for a daemon that Protocol-Not-Runtime says must be optional. It also duplicates ontoref-triples, which already answers those questions bitemporally and embedded.
- Replace SurrealDB with another external store — Postgres, or embedded SurrealDB — rejected: There is no gap to fill. Graph-with-history has an owner (ontoref-triples / ontoref-query); cross-actor notification has one (NATS JetStream, already a dependency); multi-instance divergence has a declared design (ADR-027, CRDT per domain). A replacement would answer a question none of the three asks.
- Retire the dependency but keep POST /sync answering 410 Gone — rejected: A catalogued route that answers an error forever is a declaration without a witness — the same thing this ADR retires, wearing an HTTP status. The three Nushell callers already swallow failure silently, so the route would have decayed unobserved.
- Stay on redb 2 and skip the format migration entirely — rejected: Defensible on its own terms — nothing FORCED the bump — but it leaves the read/write split as a comment on a rule the freshness gate depends on, and defers a mandatory data migration to a moment chosen by whoever next bumps a dependency rather than by a ratified decision. The gain taken here is redb 4's shared read lock: redb 2 takes LOCK_EX unconditionally, so no second process can even READ the oplog while one holds it.
- Put the migrator inside ontoref-oplog, behind a feature flag — rejected: It could then perform only one half of the migration: phase 1 needs redb 2, phase 2 needs redb 4, and the crate follows one workspace pin. Worse, it would stop building the moment the workspace moved, which is precisely when a stranded consumer needs it most.
Anti-patterns
- An external store re-enters through an unrelated peer — A path dep is added for some other capability, that peer carries a database client, and the daemon's closure grows a store nobody chose. This is exactly how SurrealDB arrived and persisted: through stratum-db, optional, transitive, and invisible to any check that read the manifest.
- A derived store with no reader — A projection is maintained — seeded, versioned, locked against concurrent writes — and nothing ever queries it. It cannot drift detectably because no reader exists to disagree with it, so it reads as capability on the dependency list while answering no question. It survives precisely because nothing fails when it is wrong.
- A version pin whose recorded reason covers only the code half — A dependency pin carries a careful justification about API behaviour and says nothing about the on-disk format its data already uses. The bump then looks like a refactor, passes CI green, and fails at `open` on every machine that holds existing data — a failure class no compile-time check can reach.
- The rescue tool pinned to the thing it rescues from — A data migrator declares `dep.workspace = true` for the library whose version boundary it exists to cross. It can then perform only the half its pin permits, and stops building entirely once the workspace moves — removing the only tool that can read the old format at the exact moment an upgraded consumer needs it.
Related ADRs
ADR-002 · ADR-036 · ADR-023 · ADR-025 · ADR-029 · ADR-027 · ADR-001 · ADR-074