|
| 1 | += Architecture Decision Record: 0012-json-family-sidecar-storage |
| 2 | +<!-- SPDX-License-Identifier: PMPL-1.0-or-later --> |
| 3 | +<!-- Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk> --> |
| 4 | + |
| 5 | +# 12. Implement the JSON-family sidecar store (supersedes ADR-0011) |
| 6 | + |
| 7 | +Date: 2026-05-31 |
| 8 | + |
| 9 | +## Status |
| 10 | + |
| 11 | +Accepted (2026-05-31) — implemented in #148. Closes #146 (V-L2-F3). |
| 12 | +**Supersedes [ADR-0011](0011-sidecar-storage-is-relational.adoc)** (V-L2-F2, |
| 13 | +the json *drop*), reversing it at the maintainer's request. |
| 14 | + |
| 15 | +## Context |
| 16 | + |
| 17 | +ADR-0011 (V-L2-F2, shipped in #144) dropped the never-implemented `json` |
| 18 | +sidecar store on the grounds that the octad runtime is relational and no |
| 19 | +storage-abstraction layer existed to host a second backend. That reasoning |
| 20 | +was sound *at the time*, but it rested on an absence (no abstraction) rather |
| 21 | +than an impossibility. |
| 22 | + |
| 23 | +The maintainer subsequently asked for a real JSON sidecar capability (#146, |
| 24 | +V-L2-F3) — deliberately reversing the drop — scoped as a *family* of on-disk |
| 25 | +encodings with **full parity** to the runtime operations the SQLite path |
| 26 | +implements today. |
| 27 | + |
| 28 | +## Decision |
| 29 | + |
| 30 | +Accept `[sidecar].storage = "json"` again, backed by a real implementation |
| 31 | +(#148): |
| 32 | + |
| 33 | +* **`[sidecar].format`** selects the on-disk encoding — `plain` (default) | |
| 34 | + `ld` (JSON-LD) | `ndjson`. It is meaningful only for `json` and ignored |
| 35 | + for `sqlite`/`postgres`. |
| 36 | +* **One resolver.** `sidecar::StorageKind::resolve(storage, format)` is the |
| 37 | + single source of truth (`Sqlite | Postgres | Json(JsonFormat)`); |
| 38 | + `SqlDialect::from_storage` is folded into it, and `validate`/`doctor`, |
| 39 | + `generate`, `drift`, and `gc` all dispatch on it. |
| 40 | +* **Storage abstraction.** A single `SidecarData` model mirrors the |
| 41 | + `verisimdb_*` tables (provenance log + chain-head set, temporal versions, |
| 42 | + lineage edges, access policies). **Format is purely a codec** over that |
| 43 | + model — every octad operation is written once and is format-independent |
| 44 | + (plain = table→rows object; ld = `@context` + typed `@graph`; ndjson = |
| 45 | + one record per line). Writes are crash-safe (temp file + atomic rename). |
| 46 | +* **Octad parity.** Provenance (`append`, `append_fork`, `verify_chain`, |
| 47 | + `fork_points`) reuses `abi::ProvenanceEntry::compute_hash`, so hashing is |
| 48 | + identical across backends; temporal enforces monotonic versions and the |
| 49 | + exactly-one-current invariant in code; drift reuses the `tier1` temporal |
| 50 | + kernel; `gc` purges by age; `generate` emits a |
| 51 | + `sidecar_schema.{json,jsonld,ndjson}` scaffold instead of SQL DDL. |
| 52 | +
|
| 53 | +### Relationship to ADR-0011 |
| 54 | + |
| 55 | +ADR-0011's premise — *no abstraction; a JSON store would re-encode |
| 56 | +relational invariants in application code with weaker guarantees* — is |
| 57 | +answered rather than simply contradicted: #148 introduces the abstraction |
| 58 | +(`StorageKind` + the `SidecarData` codec) and re-encodes those invariants in |
| 59 | +Rust with documented parity. The one acknowledged difference is concurrency: |
| 60 | +the JSON store is single-writer (atomic whole-file rewrite) where SQLite |
| 61 | +serialises via `BEGIN IMMEDIATE` locks. The guarantees SQL enforced |
| 62 | +structurally (one current temporal row, monotonic versions, fork-aware |
| 63 | +chains, hash identity) are now enforced in code for the json path and |
| 64 | +covered by tests mirroring the SQLite suite, across all three formats. |
| 65 | + |
| 66 | +## Consequences |
| 67 | + |
| 68 | +### Positive |
| 69 | + |
| 70 | +- `json` / `jsonld` / `ndjson` sidecars with octad parity — human-diffable, |
| 71 | + portable, dependency-light stores, plus a genuine linked-data (JSON-LD) |
| 72 | + option. |
| 73 | +- A single backend resolver (`StorageKind::resolve`) for every storage value. |
| 74 | +
|
| 75 | +### Negative |
| 76 | + |
| 77 | +- The json path enforces invariants in application code rather than via SQL |
| 78 | + constraints, so it carries a heavier test burden and a documented |
| 79 | + single-writer limitation. |
| 80 | +- Two storage families to maintain in lockstep (the hash function is shared |
| 81 | + via `abi`, which mitigates the highest-risk drift). |
| 82 | +
|
| 83 | +## References |
| 84 | + |
| 85 | +- Issue #146 (V-L2-F3); PR #148. |
| 86 | +- Supersedes ADR-0011 (V-L2-F2; issue #112, shipped in #144/#147). |
| 87 | +- ADR-0004 (octad ontology), ADR-0010 (provenance forks are first-class). |
0 commit comments