ROADMAP — Cairn¶
Disposable working scaffolding, not a source of truth. The canonical what is the spec; the why is the ADR log. This file only orders the build. If it disagrees with the canonical docs, the canonical docs win.
Scope: the foundation that must exist before the policy and GUI layers. Ordered bottom-up by the four-layer model (ADR-0021): wire core → in-DB enforcement floor → sync → identity → security → federation → blobs → native API. Policy and UI sit above this line and are deliberately out of scope here.
Cross-cutting (applies to every phase)¶
- TDD — failing test first, then code (load-bearing on the §9 safety-critical surface).
- Language by defect blast radius (§9) — safety-critical = Rust or in-DB (SQL/PL-pgSQL/pgrx), optimized for reviewer-legibility; advisory/cosmetic = fit-for-purpose (Python/ML). The integration boundary is the PostgreSQL boundary (≥ 18); avoid FFI coupling.
- AGPL-3.0 for all code; every dependency AGPL-3.0-compatible (checked before adding).
- Each phase takes the relevant spike → production-grade; close honest gaps, don't re-spike.
Phase 0 — Proven foundations (done, as spikes)¶
- Event serialization + signatures — COSE_Sign1 + Ed25519 + SHA-256 (ADR-0015);
cairn-event, Bet A ✓. - In-DB floor spiked — validated
submit_eventdoor + recall, holds against a hostile agent (Spike 0002, C1–C5 ✓);db/001–008,cairn_pgxverify. - First federating node — admission/pairing/mTLS/set-union
node_eventsync (ADR-0017);cairn-node, floor ENFORCED proof. - Walking skeleton + WAN sync + replication/failover PoC.
Phase 1 — Event core to production (the wire contract)¶
- HLC ordering + incremental sync watermark — ✓ done at
cairn-nodelevel (issue #38, PR #42): real local HLC, per-peerseqcursor via advance-only door, full-sweep correctness floor. Promote the same discipline into the productioncairn-event/cairn-synccore. Clock-drift admission ceiling ✓ done (PR #133, closes the #102 ratchet finding): sharedcairn_max_hlc_drift_ms()(24h) bounds a remote event's asserted wall against our ownclock_timestamp()on BOTH remote-apply doors — node plane REJECTs (self-healing skip+re-offer), clinical plane ADMITs-but-CLAMPs thehlc_statemerge (a refusal would wedgecairn-sync's frozen watermark; the event's asserted wall is preserved verbatim, principle 1). Same PR added the CI Rust workspace + in-DB floor test gate (.github/workflows/rust.yml, #117). CI hygiene gates extended ✓ (PR #143):fmt(rustfmt-defaults, whole-workspace reformat + check on both cargo trees),deny(cargo-deny 0.19.9 — AGPL-compat license allow-list + RUSTSEC advisories + wildcard/source bans,deny.toml), andmatcher.yml(ruff + pytest for the advisory Python tier). Toolchain pinned ✓ (PR #147, merged; closes #144):rust-toolchain.tomlpins the exact channel (1.96.0) + rustfmt/clippy components for both cargo trees (stops fmt-gate drift),[workspace.lints]mirrors the CI-D warningsgate locally, honestrust-version1.74→1.96, and thetestjob now gates on PG18 (PGDG apt repo) matching the shippedpg18default. CI gaps closed ✓ (PR #149): the matcher DB-gated suite now runs in the floortestjob against the same PG18+cairn_pgxcluster (#145); CodeQL test-fixture crypto false positives fixed at the source — runtime-derived test seed/salt/nonce + a CLAUDE.md house rule (#146); the required-check set is documented inCONTRIBUTING.md(#117); and the stricter ruff ruleset (I/UP/B/E5 atline-length=100, Rust-parity) is now enforced inmatcher.yml— closing the last PR #143 deferral. - Legibility twin — mandatory signed mechanically-derived plaintext twin on every event; promote from skeletal (ADR-0012, §3.13). Author-materialised twin globalised to every event type ✓ done (ADR-0039, SCHEMA 13→14,
db/015): floor prefers authored twin; non-demographic types degrade honestly to a flagged, payload-rendering derived skeleton when absent; demographic types keep ADR-0034's hard requirement; authored-vs-derived is a derivable read-time projection, no stored flag. - Canonical identifiers + node-local surrogate keys (ADR-0031).
- Additive-only schema evolution discipline baked into the event format (ADR-0012).
Phase 2 — In-DB enforcement floor (unbypassable safety floor)¶
submit_eventvalidated write surface hardened to production (ADR-0022); RLS + constraints + append-only envelope; raw-SQL clients still cannot break the floor (principle 12).- Actor registry + version-pinning + key custody (ADR-0011); skill-epoch + served-model digest as pinned actor determinants (ADR-0029). Enroll collision floor now ENFORCED ✓ (ADR-0044, closes #152): since
actor_id = content-address(pinned set)alone (the key stays mutable acrossrotate-key), two distinct keys with an identical pinned set collided into oneactor_idandactor_currentsilently dropped the earlier — a silent identity-merge (principle 2).enroll_actornow fails closed on a distinct-key collision across the wholeactor_eventhistory (immortal even afterrevoke); idempotent same-key re-enroll passes. Single door (no actor-sync apply door yet); humans carry a person-distinguishing determinant (guidance). Now bidirectional ✓ (ADR-0046, closes #166): the A-direction (oneactor_id← two keys) is joined by the B-direction (one key → twoactor_ids), whichsubmit_event(db/005) would otherwise punish by NULLing that key's authorship node-wide. A new pure whole-history predicatecairn_key_actor_id_conflict+ a per-key advisory lock (key-lock-first → deadlock-free) refuse it; idempotent/distinct-key/matcher-per-epoch enrolls are unaffected. Both future doors that bind a key to an actor (rotate-key/supersede, actor-sync apply) must mirror both checks. - Deterministic overlay convergence now ENFORCED ✓ (closes #115 part 1): every standing-state overlay folds a new event in via one shared pure
cairn_hlc_overlay_wins()predicate that appends the eventcontent_address(BYTEA multihash — canonical, UNIQUE, collation-free) as the deterministic final tiebreaker after(hlc_wall, hlc_counter, origin). Before, two distinct events sharing an identical HLC triple (a Byzantine/broken signer reusing its own triple) settled by arrival order → silent cross-node divergence in the safety-critical projection layer (clinician-visible forchart_dispute). Applied to the five uniform state overlays —patient_chart(db/002),patient_link(db/018),chart_dispute(db/023),chart_identity_state(db/024),name_repudiation(db/025). Projection-read-side only (no wire/event-format/ADR/spec change). Demographic overlays (db/010–014) then closed their residual TEXT-collation gap — see the collation bullet below (#69). #115 part 2 (twin-ladder registry,cairn_require_uuid) still open. Byzantine collision now also SURFACED ✓ (closes #157): the tiebreaker resolved a genuine HLC-triple collision (proof of a broken/hostile signer) silently;db/029_hlc_collision_log.sqladds a shared purecairn_hlc_triple_collision()predicate + a convergent append-onlyhlc_collision_log(canonical unorderedcontent_addresspair as PK → one row per 2-way collision per node) + a structurally non-gating recorder (INSERT ... SELECTwith a null-guardWHERE+ON CONFLICT DO NOTHING→ can never raise, so it cannot gate the apply path by construction), and each of the five overlay triggers records the signal before its unchanged upsert. Advisory/observability only (accepted limits: a concurrent apply may miss the signal; a ≥3-way collision records a non-convergent pairwise chain — the §5.13 sweep is the backstop, the resolution stays correct regardless); the Python §5.13-sweep / human-worklist consumer is a documented future seam. - Collation-independent projection tiebreaks now ENFORCED ✓ (closes #69; ADR-0045, spec v0.46): every projection winner tiebreak over a TEXT key (
node_origin/asserted_origin+ the finalvalue/display/use_key) now compares underCOLLATE "C"(byte order of the identical-on-every-node UTF-8 bytes), so a(rank,wall,counter)tie converges to the same display winner across a federation of mixed default collations — before, the default (possibly locale/ICU) collation was a node-local property, so honest nodes could pick different winners (the cross-origin(wall,counter)tie needs no misbehavior; it was decided before #115's collation-freecontent_address). One sharedcairn_hlc_overlay_winsfix (db/002) covers the five overlays; inlineCOLLATE "C"onpatient_identifier(db/010),patient_demographic(db/013 both branches +cairn_demographic_backfill; db/011 superseded),patient_name(db/012 trigger +patient_name_currentVIEW and its db/025 re-definition),patient_address(db/014 trigger + VIEW). Projection-read-side only (no wire/floor/SCHEMA change). ADR-0045 makes the invariant binding on future projection slices. Drift follow-up ✓ (closes #159): thepatient_name_currentwinner ORDER BY is duplicated across db/012 + db/025 (db/025's copy is live), with nothing in SQL keeping them in lockstep (DISTINCT ON + the pre-winner anti-join preclude a shared base view). Guarded now by a no-DB source-level test (crates/cairn-node/tests/name_winner_order_drift.rs) asserting the two clauses stay byte-identical, catching drift in either direction; cross-reference DRIFT comments added to both migrations. - Authorship + attestation — compositional author set, separable responsibility; closed contributor-role enum (ADR-0007, ADR-0028); additive-vs-suppressing derived, not declared (ADR-0010). Suppression owner-gate now ENFORCED ✓ (ADR-0043, closes the last open sub-item of #99): a suppressing overlay of a human author's event is self-only (cross-human suppression refused — disagreement is additive; agent/un-owned advisories stay dismissable, principle 10), enforced identically at both write doors via one shared
cairn_suppression_author_okhelper (db/005+db/020, principle 12). §5.9 sensitivity-sealing +repudiatecarved out. - Twin-check dispatch de-risked ✓ (#173; ADR-0048, spec v0.49): the per-type structural-floor + legibility-twin dispatcher
cairn_event_twinwas re-declared in 11 migrations, each copying the whole growing IF/ELSIF chain — a stale copy could silently DROP a floor check (a safety-floor regression with no error). Replaced with a locked registry tablecairn_event_twin_check(event_type, check_fn, twin_required_msg)+ a fail-closed load-time validation trigger, a single stable dispatcher (db/005 only, dynamicEXECUTE %Iover the table), and all per-type check fns unified to(p_type text, b jsonb) RETURNS void. A new event type registers ONE additive row and never touches the dispatcher; the single-source invariant is enforced by the no-DB guardtwin_dispatch_single_source.rs. First dynamic SQL in the floor (bounded: migration-only locked table,%Iquoting, fail-closed, load-time validated,search_path-pinned definers). ZERO behaviour change (15 seed rows verbatim from db/033's chain; full suite green).event_type_classdeliberately not merged (future convergence). - Bitemporal time —
t_recorded(HLC ceiling) vs freely-backdatablet_effective; clashes flagged, never auto-resolved (ADR-0003). Tier-1 ceiling (t_effective ≤ t_recorded) now enforced at thesubmit_eventdoor (2026-07-02 review); the graded-interval / RTC-less-Pi refinement + the tier-2 clash flag are #103 / #91. - Acknowledged-uncertainty value types — first-class unknown / not-yet-asked / refused / ranges (§3.7).
Phase 3 — Sync engine (set-union + the two planes)¶
- Set-union sync with scope as prefetch hint, not authority (ADR-0004).
- Two-plane schema/code evolution — events sync forward-compatibly; code/DDL/pgrx travel a separate signed, per-architecture, sneakernet-capable distribution plane; version is a local node property (ADR-0012, §6.5).
- Record discovery + replicated essential tier (ADR-0016).
- Signing-context domain separation + honest-degradation seams (ADR-0040, issues #95/#108/#109): one signature per event, domain-separated by a registered signing context (content-type +
external_aad); durable clinical-plane pull quarantine with a re-offer floor (#108); the verify primitives wired into the doors — every signature door surfacescairn_verify_erroras exception DETAIL, cairn-sync fails fast on a stalecairn_pgx(cairn_pgx_version() >= 0.2.0) at startup, andevent_twin_provenanceexposes averifiablecolumn (#109). Node-event-plane quarantine sibling: #111. - Clinical-plane in-DB apply door — ✓ done (issue #91, review A2/A5b/M8/H4):
apply_remote_event(db/020), the sibling ofapply_remote_node_event, so a replicated clinical event faces the SAME floor as a locally-authored one (signature, enrollment, fail-closed classification, attestation gate, twin floor, substitution guard);cairn-syncnow does zero checks and zero raw DML on apply. Attestation tokens are stored (db/001additive columns) and travel on the sync wire so the suppress gate is re-runnable at every hop;t_effectivewire-pinned to an explicit UTC offset (cairn_t_effective, both doors); node-local projection guards clamp-and-flag at apply instead of vetoing (identity_projection_flag, db/018). Known residual: the actor registry does not replicate yet, so cross-node apply needs the operator enrollment ceremony (cairn-sync enroll) until ADR-0011 registry sync exists. - Durable pull-plane quarantine — ✓ done on both planes: clinical (
cairn-sync, #108/db/021) and node-event (cairn-nodesync.rs, #111/db/022). An UNVERIFIABLE pulled event is penned durably with a re-offer floor (never a silent skip-past), auto-releases when its cause is fixed, and fails the pull loudly until resolved or human-acked; a verifiable-but-refused event stays skip-and-swept (self-healing). No manual requeue on the node plane — the derived floor + full sweep re-offer, and success auto-releases.
Phase 4 — Identity & demographics subsystem¶
- Identity event algebra — closed link/unlink/reattribute/repudiate/identify/dispute set; immortal UUIDs; never merge/erase (§5.7, principle 2).
- Demographics assertion stream — per-field projection policy (§4). Address model specified (ADR-0032, §4.3): culture-neutral three-facet value (display legibility twin + optional geolocation + culture-tagged structured parts via a content-addressed locale profile reusing ADR-0014). Patient-identifier representation specified (ADR-0033, §4.4): namespace/profile split (stable veto key + versioned validator) + a normalized form materialised so the hard veto survives a profile-less node; advisory validation; professional licensure/registration IDs fixed in the §7.5 actor registry (billing/relational provider numbers split out to §4.6, below). Demographic legibility twin specified (ADR-0034, §4.5): every demographic assertion carries the §3.13 principle-11 twin, materialised profile-independently, with
display/valuereconciled as its value-core and a forward guarantee for future field shapes. Provider-number relational model specified (ADR-0035, §4.6): abstract entity (openkind) + reified relationships carrying their own identifier sets + subject-kind partitioning{patient, entity, relationship}as structural non-conflation. All demographics gaps now closed. Demographics IMPLEMENTATION underway (first production clinical surface, oncairn-node). Slice 1 — §4.4 patient identifiers (db/010_demographics.sql): culture-neutral structural floor + authored §4.5 twin carried through the reusedsubmit_event+ set-unionpatient_identifierprojection; purecairn-event::demographicsbuilders +EventBody.plaintext_twin. Slice 2 — §4.2 DOB + sex-at-birth (db/011_demographics_fields.sql): the provenance-precedence mechanic — genericdemographic.field.assertedevent +cairn_provenance_rankladder (incl. newfact-proventop tier; unrecognized→0) + winner-by-(rank,HLC,origin)patient_demographicprojection ("verified value locks"); floor stays open / projection gated (unknown field stored + legible but not projected — federation-forward per ADR-0012); §4.1 ladder prose extended. Slice 3 — §4.2 names (patient_nameretained-set projection +patient_name_currentdisplay-winner VIEW): recency-first within the legal-use tier (HLC wins; provenance/origin break ties); falls back to most-recent any-usewhen no legal name exists; all names retained as evidence; deliberately diverges from DOB's provenance-lock (ADR-0036). Slice 4 — §4.2 administrative-sex + gender-identity (db/013_demographics_sex_gender.sql): per-field winner policy via an IMMUTABLEcairn_demographic_field_policy(field)classifier; administrative-sex provenance-first (document-anchored; recency breaks equal-provenance ties); gender-identity recency-first (patient's current stated identity always wins regardless of provenance — the inverse of DOB's ordering; provenance still feeds the §5.2 matcher). Karyotype resolved (ADR-0037) as a distinct field — no karyotype code yet; spec/ADR only. Additive: no new event type, no floor change, nopatient_demographicschema change; db/013 supersedes db/011's trigger. Slice 5 — §4.3 address (db/014_demographics_address.sql): retained-setpatient_address+ per-usepatient_address_currentrecency-first VIEW (one current address peruse); additive floor branch; per-use recency-first winner — addresses are volatile, a fresh patient-stated move must displace a stale document-verified address (ADR-0038). Slices 6–12 — §5.2 matcher pieces A/B1/B2/B2b/B3 harness + compound key + generator (2026-06-28→07-01; condensed, full detail in git). Advisory Pythonmatcher/(cairn-matcher, AGPL-3.0, zero runtime deps, pure functions — fit-for-purpose §9 tier); no ADR/spec bump throughout (implements settled §5.2/§5.13/§4.1). Slice 6 — piece A (db/016_match_veto.sql, SCHEMA 14→15): the in-DB hard-veto floor —cairn_match_veto/cairn_has_hard_vetoimplement the closed hard-veto set (same-system identifier mismatch · verified-DOB clash · verified-sex-at-birth clash);hard_veto/degrade_holdverdicts, precision-gated DOB (no date parsing),system:unknownnever vetoes; 12 tests; deceased-status veto deferred (stub). Slice 7 — piece B1: the scoring core — comparator contract (PHONETIC/NICKNAMEreserved, never emitted — anti-cultural-capture) + in-house Jaro–Winkler + 4 culture-neutral comparators + positive-onlycompare_identifier_sets+ Fellegi–Sunter combiner (MatchScore); 55 pure tests; final review fixed one Critical (score symmetry, greedy name-pairingmax(a,b / b,a)). Slice 8 — piece B2 (db/017_match_proposal.sql, SCHEMA 15→16): the veto-gated pairwise pipeline — ISO-only DOB extraction, token-bag names,auto_candidate/review/Nonebanding (any veto caps at review, never auto-link/auto-reject);db/017an advisory worklist, not a safety gate; 92 tests with DB. Slice 9 — piece B2b (nodb/file): blocking/candidate-pair generation — 3-pass disjunction (shared identifier · exact DOB · shared name token), canonical-pair dedup, oversized-block guard skips+reports (never silently caps) +sweep()batch driver; 113 tests with DB. Slice 10 — B3 harness (cairn_matcher/eval/, nodb/file): scorer metrics (precision/recall/F1, zero-denominator→0.0) + DB-gated blocking-recall measurement (pair-completeness/reduction-ratio/dropped-true-matches) + culture-pluralgold_v1.json+ CLI; 146 with DB. Slice 11 — B3 compound key (pipeline/db.py): additivename+yearUNION ALLpass (birth-year CTE, first-4-digit-run culture-neutral degrade) partitions oversized name-token blocks — recall non-decreasing; 151 with DB; filed issue #84 (test-leak + harnessKeyError, theKeyErrorarm later fixed in slice 21). Slice 12 — B3 generator (eval/generator.py+generate.py, pure/stdlib): seed+corrupted-clone entity clusters recoverable by construction (a_repairstep guarantees ≥1 shared blocking key), drift-canary-pinned to_GROUPS_SQL; 200-entity volume test:pair_completeness == 1.0,reduction_ratio≈0.919. All pieces' whole-branch reviews READY-TO-MERGE/MERGE-READY (0 Critical outstanding per slice; findings fixed in-branch or in PR #83's post-review wave). - Point-of-care identity, possession semantics,
sign-assalvage (ADR-0008). - Locale-pluggable matcher comparators — advisory only (Python/ML); comparator-profile tag travels with each demographic assertion, degrades honestly to human review (ADR-0014).
Slices 13–35 — condensed (2026-07-02 → 07-16; full detail in git, the PRs and the linked ADRs). The identity/John-Doe/medication build-out and the review course's Priority-1 slice. What exists:
- §5.7 identity core C1–C5 (slices 13–18,
db/018/019/023/024/025, SCHEMA 16→18) — the closed identity algebra:linkassertions and the linkage projection (C1); thematch_proposal→apply seam with a human-accepted door (C2); auto-apply of theauto_candidateband viamatcher_actor.rs(C2b);dispute+ the chart trust-state projection (C3);identify+ the unconfirmed trust state (C4);repudiate+ the known-alias pool (C5, the first suppressing identity event). The confirmed/unconfirmed/under-review contract is COMPLETE. - §5.4 John-Doe subsystem (slices 20, 26–30) — registration front door (A+B, no new event type); photo
evidence carrying the day-one §3.14 attachment-reference shape (ADR-0042);
marks/belongings/EMS-context text evidence (three
kindvalues on the existing evidence type); finishers (node-local ordinal +--observed-year;identify→ optional link); theenroll-humanceremony CLI. - §5.2 matcher, advisory tier (slices 19, 21–25; Python
matcher/, nodb/change) — the alias-pool evidence pass; birth-year-range blocking + A/B pass toggle; administrative-sex scoring and the unconfirmed-chart REVIEW rule; the B3 eval mirror (generator range-DOB + sex representation); supervised Fellegi–Sunter weight-learning (a PoC on small/synthetic data — see the gold-set item below); compound blocking keys (dob+first-initial,name+sex). clinical.medicationslices 1–5 (slices 30b–34,db/031–db/035) — the first clinical-content stream: assert/cease + the E1 reconciliation flag; the bitemporal dose overlay/timeline; cross-thread reconciliation as a link (ADR-0047); the commitment-based attestation responsibility overlay (ADR-0049, plus a hardening/coverage follow-up); per-field dose effective-date/reason correction (ADR-0050). Twin-check registry: ADR-0048.- Slice 35 — the P1 floor-hardening slice (2026-07-16, PR #219; no ADR/spec/SCHEMA change) — the ADR-0030
hostile-enrolled-writer threat model re-run against the in-DB floor across eight issues
(#187/#207/#194/#191/#192[+#177]/#190/#193/#195), closing the local-door HLC drift ceiling, the widened-column
replay guard,
content_addressfinal tiebreaks, the fail-closed suppression-target gate, medication patient-consistency (resolving #177), the un-attestedidentity.linkveto, the restore-door drift ceiling and the responsibility↔attester binding. Follow-up #220 remains (the #190 veto is link-arrival-only).
Still open from these slices. Condensing 13–35 must not lose the open remainder, so it is enumerated in full here (a PR #271 review finding: the first pass dropped two open issues out of every tracked file).
- Filed and open. #141 — photo evidence has no size guard
on the local blob-store path (§6.6 byte-tier slice). #184 —
a non-array
contributorsyields a cryptic scalar-extract error at both submit doors, all event types. #163 (demographics currency), #168 (entity→role-actor 1:many), #185 (cross-thread dose-correction suppression vector — needs a PK/design decision), #79 (B2 Minors), #220 (the #190 veto is link-arrival-only), #172 (the future actor-write doors — rotate-key/supersede, actor-event sync apply — must mirror BOTH enroll collision checks; ADR-0054 makes this live work). - Identity C5+.
reattribute(§5.5 event-granular strike-through of clinical documentation) waits on a clinical-note surface; a reversal / de-repudiation event; a chart-history VIEW rendering struck names (the data is already present); an accept-at-cap boundary test for the oversize guard; the §5.2 coherence feedback loop; notification / contamination cascade on dispute; person-level trust aggregation. The §5.12 push-alert and the §5.3/§5.8 search-before-create funnel are the non-structural John-Doe remainder. - Matcher (advisory tier). A large hand-crafted gold set to re-run the learner for authoritative magnitudes;
full §7.5 matcher actor registration (the matcher's contributor identity lives in a provenance string for
now); no recovery escrow for the sealed matcher key (regenerable today, so this is a convenience gap, not a
data-loss one); no background scheduler (operator-invoked CLI only); locale comparator packs; the hub-tier
aggressive duplicate sweep; a veto-aware scorer mode; fuzzy/edit-distance alias recognition and a dedicated
aliasblocking pass; fuzzy near-window softening; variable cluster size / hard negatives in the volume generator; acompare_addresscomparator; a CLI sweep entry; the B3 mirror still ignores the block-size cap. - Medication (slices 30b–34). Automated reconciliation detection — the human-driven resolution exists,
fuzzy/automatic detection plus a Tier-A dictionary is the gap; a partially-attested-group read surface (which
member is stale); a whole-list sign-off summary event; statement-level
started-date correction and per-field merge across corrections of the same point; a rendering-suppression visibility overlay fordelete; structured sig/frequency; a separateroutefield; prefer-INN display term. - Attachments (slice 26). Bytes are local only — cross-node fetch deferred; the residual DO-UPDATE
overwrites a caller-supplied
media_type(benign). - Accepted risk with a named remedy. The
enroll_actordual-mapping guard's TOCTOU window (#166, closed as accepted): the durable fix is a floor-level per-key guard indb/004. Recorded here so the accepted risk keeps its remedy attached.
Done, not open (called out because an earlier condensation listed it as outstanding): stale forced-REVIEW proposal retraction — #135, closed by PR #151.
Slices 36–56 — condensed (2026-07-16 → 07-27; the 2026-07-15 whole-project review course, its Priority-6 design queue, and the first medication-coding slices. Full detail in git, the PRs and the linked ADRs; the why is in each ADR and must not be restated here). The review course is fully closed. What exists:
- P2 sync-convergence integrity (slices 36–40, PRs #221–#225) — the flagship A→B convergence test
driving the real binaries over TCP (#199); the cairn-sync SCHEMA subset standing alone (#198); the
clinical-plane
seqcursor + periodic full sweep (#196,db/036); acked rows freed from the quarantine quota (#197); cairn-sync wire hygiene + thenode.supersededapply arm (#202/#201). Open: #227, #228. - P3 — both wire windows shut (slices 41–43). ADR-0051
contributor-role vocabulary floor (#203+#96):
recordedratified,{held_by, on_behalf_of?}responsibility objects, partition-prefixed future members, strict-submit/lenient-apply. ADR-0052 born-sealed clinical bodies (#189+#92,db/037): every clinical JSONB body sealed at write under a per-event DEK the node itself holds — an erasability substrate, not confidentiality — with a custody plane, both doors enforcing sealed⇒clinical scope, and a rung-3 shred CLI. ADR-0053 per-write human authorship (#204):{human,authored}+{node,recorded}, human signs while the node seals;cairn_authorship_boundstrict-door binding;--author-as. - P4/P5 process + tech debt (slices 44–45, PRs #251/#253/#255) — the #188 schema-version downgrade
guard in both loaders (repo-wide
SCHEMA_GENERATION+ fs-derived guard tests + theSCHEMA_LOAD_LOCKTOCTOU close);scripts/run-db-sql-tests.shrunning thedb/tests/*.sqlmirrors in CI (#212); the registryDO UPDATEconvergence arm (#214); HANDOVER staleness (#215). #212's first property suite (proptest) immediately caught a real grading defect before any read path shipped. - P6 design queue → four ADRs (slices 46–50). ADR-0054
actor-registry federation is admit-and-dispute (#205; closes #154 structurally).
ADR-0055
chained trust-root document (#206). ADR-0056
unknown event types admitted uninterpreted (#200 — the filed premise was inverted: the spec was
right, the code was wrong). ADR-0057
generic reprojection (#208, PRs #274/#278): one registered
cairn_projection_applyfn per projection - one dispatcher replacing ~15 per-type triggers;
cairn_reprojectis the generic heal/rebuild replay both loaders run on a schema-generation change;cairn_replay_eligibleis the #265/#266 seam. ADR-0058 grade-gatedt_effectiveceiling (#216, PR #285): a bornclock_gradebounds the ceiling's rejecting power — atself-asserted/unknown(every node today) it flags-never-rejects, and the remote door admits-and-flags, closing a latent one-event sync-wedge DoS. - Matcher, advisory tier (slices 51/53/54; Python
matcher/only, no spec/SCHEMA/wire/ADR change) — #209derive_thresholdsfails closed on an empty non-match set (no impostor ⇒ no safe auto anchor); #210 a sweep-level pass retracts proposals orphaned when a pair leaves the blocking universe; #211 the E3 four-gap batch (alias-map canonicalization, inverted-threshold refusal, alower()-vs-casefold()doc-honesty fix,repaired: Truemarking); #290 eval consumers REPORT the repaired-pair count so a reader discounts the optimistic recall/F1. Open: #287. - Slice 52 — the #217 paper-parity plan-section rule — every clinical-surface slice plan now carries a
## Paper-parity benchmark (§1.2)section or a forced-rationale escape, enforced by a no-DB source guard and stated in CONTRIBUTING.md + CLAUDE.md house rule 7. First live entry: #288. - Slices 55–56 — medication drug coding (ADR-0059 + its first code slice). ADR-0059
(design-only, spec v0.61) anchors drug identity on drugref's immortal
moiety_uuid(INN is display, never key), assubstance.coding {system, code, display}, advisory + honest-degrading. Slice 6a (PRs #297/#298,db/041,SCHEMA_GENERATION40→41) shipped the inline shape: themedication_coding_systemregistry + a two-tier floor (structural refuses at both doors, registry-derived is strict-submit/lenient-apply),medication_codingas its own projection table, the(system, code)-pair dup-key, a prefer-coded group display, an advisory anchor-conflict view, and honest degradation proven by construction via a source guard that no.sql/.rsunderdb/,crates/orextensions/references drugref executably. Three findings changed shipped behaviour: db/020'scairn.remote_applymarker moved to precede the floor dispatch; the canonical UUID spelling is pinned at the strict door; andcairn_execute_shreddid not scrubmedication_coding— a shred reporting success left the drug's preferred name and its immortal anchor readable besidepatient_idin acairn_agent-readable table (the ADR-0005 rung-3 / #92(b) failure). Open: #294.
Still open from slices 36–56. Enumerated in full, because condensing must not lose the open remainder (the PR #271 review finding).
- Sync/convergence. #227, #228, #284 (cairn-node's full SCHEMA list vs cairn-sync's subset staying consistent).
- Born-sealed / erasure (ADR-0052 follow-ons). #230–#237:
notably #231 (unwrap-cert kid pinning — until it
lands, born-sealed is an erasability substrate, NOT confidentiality),
#232 (sequester + the sensitivity stream + §5.9
safety-projection emission), #233 (unwrap-key
rotation ceremony), #234 (blob-byte born-sealing),
#235 (shred authorization policy hooks),
#236 (FTS/RAG must build on the
event_clearshadow with shred-triggered invalidation), #237 (code hygiene). - Authorship (ADR-0053 follow-ons). #242 (the
assertedgrade + token-backed author — verbal orders, AI-scribe, dictation), #243 (point-of-care durable session-decoupled drafts +sign-assalvage — the ADR-0008 UI half), #244 (authorship + responsibility on one clinical event — collapse the self-vouch case), #245 (the SQL mirror ofclassify_authorship_confidence+ the §5.10 authorship-confidence projection), #247. Standing notes: grading stays half-live until #245 wires a read path forclassify_authorship_confidence; authorship in a contributor set is key-scoped and does not survive key rotation (#247, which constrains #245); a--author-asevent is owned under the ADR-0043 suppression gate where a device-signed equivalent was dismissable by anyone. - ADR-0054/0055/0056 code work (all design-settled, none built). ADR-0054: #94, the key-loss-ceremony ADR, the rotate-key local door. ADR-0055: #257 (root-chain verifier + load gate), #258 (transparency-log role), #259 (reproducibility CI), #260 (freshness rung), #261 (sync-auth onboarding UX design session). ADR-0056: #265 (door admits uninterpreted), #266 (re-adjudicate the deferred gates, then reproject — reprojection alone would grant power that never passed the attestation / target-exists / cross-author-suppression gates), #267 (pen door refusals verbatim), #268 (align the node-plane skip), #269 (node-plane heal test gap), #270 (a frozen watermark must fail loud). The posture triad: the content plane admits-and-disputes (0054) and admits-and-defers (0056), while the code plane verifies-or-refuses (0055).
- Reprojection (ADR-0057 follow-ons). #272 (the
authoritative Pi5/NVMe same-rig re-run — the shipped Bet-B numbers are cross-rig),
#275 (per-row logic-generation watermark),
#276 (registry governance at scale),
#277 (the loader's heal cannot re-derive
ON CONFLICT DO NOTHINGprojections after an extraction-logic fix). - Trusted time (ADR-0058 deferred). #279
(anchor/notary planes + grade-upgrade tokens), #280
(causal lower-bound tightening), #281 (clock-sanity
UI alert), #282 (auto-downgrade a failed clock),
#283 (render
clock_gradein the twin). - Registry hygiene. #254 — 8 twin-check
registrations still use
ON CONFLICT DO NOTHING; unify with the #214DO UPDATEarm or record why not. - Deps. #252 —
quick-xmlRUSTSEC-2026-0194/0195 viawayland-scanner(cairn-gui), upstream-blocked. - Medication/matcher. #287 (hub-scale sweep re-scoring cost), #288 (med-list whole-list sign-off must collapse to ONE human gesture — owed by the future Tauri med-list slice), #294 (the §5.9 safety projection must carry the coding-derived drug class rather than re-derive it; blocked on #232).
Operational caveats that outlive these slices. Pre-ADR-0051 event logs (old role:"author"-without-
actor_id, flat-string responsibility) and pre-ADR-0052 plaintext clinical.* bodies REFUSE at db/020 —
wipe dev/PoC rigs (the replication-failover demo, the spike rigs), never sync them through. Pre-wire
unsigned actor rows never sync. Test DBs need cairn_pgx ≥ 0.3.0.
Slice 57 — clinical.medication slice 6b: the coding-overlay event types (2026-07-28; branch
feat/medication-coding-overlay-slice-6b-0059; completes
ADR-0059 decision 3; no spec/ADR
change; SCHEMA_GENERATION 41→42). Coding becomes a separately-authored act: db/042 adds
clinical.medication-coding.asserted and -correction.asserted, both ('additive', FALSE) — a
correction ADDS a claim, and targets_other_author = TRUE would route it through the ADR-0043 owner gate
and refuse a pharmacist correcting someone else's coding. Why the strike exists (the decision the
slice turns on): a reviewer who establishes a medication is NOT metformin but cannot say what it is would
otherwise have to leave a known-wrong anchor standing or invent an identity they cannot vouch for — the
fabrication principle 4 forbids. The correction event must be able to say "not that, and I don't know."
The slice stayed additive — 6a's table-not-columns payoff: both apply fns write the existing
medication_coding table under the existing overlay-winner rule. A strike NULLs the anchor rather than
deleting the row (deleting breaks arrival-order independence — a lower-HLC coding arriving later would
have nothing to lose the race against). patient_medication_uncoded is the coder worklist, with
previously_struck separating "nobody has coded this" from "a reviewer established this is NOT what it
was coded as". CLI: medication-code / medication-code-correct, both --author-as (ADR-0053) but
deliberately no --attest-as — coding a drug identity is not a sign-off of the medication list.
Also closed #295 (anchor-conflict collation pin — the behavioural test cannot catch a future
unpinning on a deterministic-default cluster, so the real gate is a no-DB source guard) and #296 (a
cairn-sync test dropped event_log.seq, letting the migration re-add it at the END and permanently
reorder a SHARED test database — the root cause of the long-carried "recreate the test DBs" gotcha).
Four lessons worth keeping (full detail in git): (1) a redundant projection column is a convergence
hazard — struck duplicated coding_code IS NULL and only two of three writers set it, so arrival
order decided what a node read; it is now GENERATED ALWAYS AS … STORED, deleting the writer rather than
correcting it. Deliberately not a CHECK: a violated CHECK aborts the apply and wedges that event
forever. (2) Nullable-widening a column means re-reading every aggregate over it — unlike
count(DISTINCT …), array_agg KEEPS NULLs, so the anchor-conflict view emitted a blank entry.
(3) A passing test can be worthless — the group-display test asserted only coding_display and went
green against a live defect; asserting the term alongside made it discriminate. (4) Only
cargo test --workspace catches guard-scope gaps — 6a's drugref guard had never met a #[cfg(test)]
module inside a src/ file. Workspace 916/0. Filed
#300: the worklist lists every uncoded member of an
already-coded reconciled group — a design question (hiding them could suppress the mis-reconciliation
signal 6a built), not a defect.
Deliberately NOT done: no drugref code anywhere in the tree; the coded↔uncoded duplicate case remains open (needs term→anchor resolution — ADR-0059 decision 5 is explicit the key does not close it); the §5.9 safety class is still owed (#294, blocked on
232); the coding UI and its §1.2 time budget are owed by the med-list UI slice¶
(#288 neighbourhood).
Slice 58 — the ADR-0056 floor: admit uninterpreted, re-adjudicate before power (2026-07-29; branch
feat/adr-0056-admit-uninterpreted-floor-265-266; closes
#265 +
#266 — decisions 1 and 4 of
ADR-0056, ratified 2026-07-20 with no
code; six tasks, TDD throughout; SCHEMA_GENERATION 42→43). apply_remote_event used to RAISE on an
event_type absent from event_type_class, so the event was never stored at all. A phone-tier node
carrying a chart between two upgraded facilities — the §6.1 sneakernet path, the case Cairn exists for —
acquired nothing past the first unknown-type event: not unrendered, absent. sync.md §6.5's
lossless-forwarding invariant was therefore false for unknown types, and the spec was right while the code
was wrong. The door now admits verbatim, projects nothing, confers nothing, and records an explicit marker.
event_deferred (in db/001, next to event_log) is that marker — node-local, never on the wire. It
lives there rather than in this slice's own db/043 because db/005's cairn_replay_eligible and
cairn_suppression_author_ok read it and both are LANGUAGE sql, whose bodies resolve table names at
CREATE time, unlike PL/pgSQL's late binding. Its presence IS the invariant ("powerless; the
classification-gated checks have not been passed"); promotion deletes the row rather than marking it
resolved, so there is one source of truth. adjudication_error is decision 4's flagged legibly, surfaced
by a new cairn-node deferred listing.
Why re-adjudication is load-bearing rather than bookkeeping. Admitting uninterpreted necessarily SKIPS
every floor check derived from the type's mode or its target relationship — in db/020 the
suppressing⇒attestation gate, the overlay-target-exists refusal and the ADR-0043 cross-author refusal all sit
downstream of the classification lookup. Those are deferred with the interpretation, not waived by it, so
cairn_readjudicate_deferred (db/043) re-runs all three before anything reprojects; that ordering is what
makes "no unattested suppression" hold at every instant rather than being violated-then-repaired. The
envelope is re-derived with cairn_body(signed_bytes), never reconstructed from projection columns, so the
predicates see exactly what the door saw. Failures are captured per row, never raised — the pass runs
inside connect_and_load_schema, and a raise would wedge the node on one bad event, the very failure mode
the ADR removes. Candidates are ordered by HLC so a deferred overlay is adjudicated after the deferred target
it points at. cairn_replay_eligible — the constantly-TRUE stub ADR-0057 built for this slice — becomes
"carries no marker", so no reprojection path can grant unadjudicated power.
Two decisions the ADR did not force. (1) The pass runs on every connect, not only on a
schema-generation change. Classification arrives only with a code-plane update, but re-adjudication can FAIL
for a reason that resolves without one — overlay targets unknown event, where the target is still in flight
from another peer. Generation-gated, that event would stay powerless until the next code update, potentially
months; event_deferred is empty on a healthy node, so the pass costs one indexed probe. A generation change
still reprojects everything; otherwise only the promoted types are healed, in heal mode (a narrow rebuild
would hit db/039's shared-table refusal, which since slice 6b is the normal case). (2) The projection
registry gained a classified-before-projected guard: the marker is written after the event_log
INSERT while the AFTER-INSERT dispatcher fires during it, so an unclassified-but-registered type would be
projected at admission. Honest residual, found by a failing test and recorded at the guard site: the check
runs at registration time, so unreachability rests on two premises — the guard, plus the fact that
classification and registration arrive in the same migration and no migration ever DELETEs a class row.
The security finding, and the trap under it. A suppressing event's attestation token travels on the sync
wire and was stored only where the gate passed — so skipping the gate naively drops it, and
re-adjudication would then have nothing to verify, silently turning admit-and-defer into a slower
fail-closed. The deferred arm therefore stores it unverified: carried, not vouched. Auditing every reader
of event_log.attester_key against that state found one that breaks.
cairn_suppression_author_ok reads the target's attester_key into the ADR-0043 owner-gate's
human-author set, and unlike the two projection apply fns that read the same column (db/018, db/034 — kept
unreachable by the registration guard and cairn_replay_eligible) it is reachable for a deferred row. A
hostile peer attaching a forged token to an unknown-type event would put any key it liked inside that
event's permitted-suppressor set — over-permission on a floor whose own header says "wrong direction is
over-refusal, never over-permission." The gate now ignores a deferred target's token entirely. The fix is
deliberately neutral, not merely stricter: for an agent-signed deferred target it empties the author set
and the gate OPENS (the agent-advisory-is-dismissable rule), because an unverified token must not move the
gate in either direction. Pinned by the slice's security test, which asserts the hazard's precondition
before asserting the fix.
The review round — PR #302 found three findings; seven tasks fixed them; promotion must now be
proven (2026-07-30; same branch; no ADR change; SCHEMA_GENERATION stays 43 — no new db/*.sql
file). Full design in
promotion must be proven.
F1 (the serious one): cairn_readjudicate_deferred promoted a deferred event without checking it
could actually project. The marker delete committed, the loader's heal then raised on the event, and
because event_log is append-only nothing could undo it — three consecutive connect_and_load_schema
calls failed and node_schema.version never advanced. Fixed by gate 0 (re-run the per-type
structural floor db/020 step 8 skipped) and gate 4 (run the type's heal-safe apply fns inside the
promotion subtransaction): a promoted event is now, by construction, one that has already projected
cleanly, so the loader's targeted reproject became redundant and db.rs got smaller. F2: the
carried attestation token was never verified when the type is additive and bears no responsibility,
yet promotion deleted the event_deferred marker that cairn_suppression_author_ok keyed its
exclusion on — a garbage 64-byte blob naming an unrelated enrolled human put that human inside another
clinician's author set. Fixed by naming the carried-not-vouched state, event_attestation_unvouched
(db/001) + cairn_attestation_vouched(uuid), with three readers switched onto it: db/005's owner-gate
(replacing the event_deferred proxy), db/018's link-veto flag lifecycle, db/034's attester_kid. F3:
cairn-sync embedded db/043 but never called it — fixed last, deliberately, since landing it before gate
4 would have spread the wedge to the sync daemon.
Five of the seven tasks' review findings were plan-mandated — every one a defect in the plan's own
text, not implementer error, and every implementer surfaced rather than silently absorbed it: a db/005
comment claiming in the present tense that two other readers already carried an exclusion they did not
yet carry; a global count(*) on a table setup() never truncates, order-dependent under parallel test
binaries; a {"nonsense": true} fixture that refused on a missing medication_id before reaching the
twin check the test existed to pin; a cairn-sync probe that could never be promoted, because the pass
re-derives every envelope via cairn_body and refuses unparseable bytes; and cleanup placed after an
assertion, so a future regression would strand rows in a shared database. The generalisable rule:
mandated test code is not exempt from review, and declining to silently deviate from a flawed mandate is
the right call.
A discovery worth recording. Once gate 0 exists, there is no longer any reachable event that
passes gates 0–3 and then fails a heal-safe apply fn: cairn_check_medication_assertion validates
exactly the payload-derived NOT NULL columns of medication_statement, medication_cessation_apply
needs only medication_id, and the only three types with a projection but no check_fn
(note.added, patient.amended, patient.created) have no payload-derived NOT NULL columns either
(note.added is additionally heal_safe = false, so gate 4 skips it by design). That makes gate 4
pure defence-in-depth against a future type that violates the pattern — which is why its test is
white-box rather than waiting for a fixture that exercises it black-box.
The predecessor design doc's two false claims — "the twin needs no work" (true at admission, never
re-asked for promotion) and "promotion deletes the marker, at which point the now-verified token
counts normally" (assumed a verification that only happens when the mode demands a token) — are
corrected in place, with the reasoning failure recorded rather than quietly overwritten. Measured
evidence: cargo fmt --all --check, cargo clippy --workspace --all-targets -- -D warnings, and the
batched workspace suite — cargo test -p cairn-event; -p cairn-sync -- --test-threads=1; -p
cairn-node --lib --bins and --doc separately (cargo refuses to mix --doc with target-selecting
flags); and the 77 crates/cairn-node/tests/*.rs integration targets split into eight --test groups
so no single command risks the ten-minute cap — run twice against the same databases to prove
replay safety (a second green run is what shows no test poisons a shared database for its successors).
Both runs: 935/0, up from 927/0 (2 new tests naming the unvouched state in Task 1, one apiece for
each of Tasks 2–7). ./scripts/run-db-sql-tests.sh and mkdocs build both clean.
The count alone is not the whole proof: without CAIRN_TEST_PG2/PG3 the multi-node convergence
suites self-skip and cargo still counts them as passed — 10 of 11 in clinical_pull, 2 of 6 in
sync_watermark, both in federation. Those were re-run separately with all three databases set
and confirmed to execute (twice, green), which is what makes the cross-node half of "promotion
must be proven" actually verified rather than merely counted.
Deliberately NOT done, stated honestly. The node/actor plane still fail-closes on an unmappable type
(db/007) — filed as #301 rather than left silent: the
carrier-forwarding argument transfers, but node_event is type-shaped (four hardcoded ops, bespoke INSERTs,
per-type trust logic), so it needs a carried-not-interpreted row shape and a reader audit of its own. So
§6.5's invariant is now true as written for clinical events only, and that asymmetry is a known gap, not
a design. ADR-0056 decision 5 — the residual refusal contract — is untouched and is the next slice:
#267 (door refusals on verifiable bytes pen nothing),
#268 (node-plane skip-and-advance vs clinical freeze),
#269 (no test of a skipped event healing via full
sweep), #270 (a frozen clinical watermark exits
success). This slice shrinks their blast radius — with unknown types no longer refusing, those paths are
now exercised only by genuine refusals. No paper-parity time budget: the slice takes the §1.2
forced-rationale escape (no human act changes at any layer; it changes only what a node retains and when
power is granted).
Phase 5 — Security & compliance core¶
- Erasure = key-custody redistribution / crypto-shred on the severity ladder (ADR-0005, principle 9).
- Visibility-scope ≠ replication; the safety projection — sealed bodies emit de-identified, severity-graded safety projection; sensitivity is a graded append-only stream (ADR-0006).
- At-rest seal — ✓ done at node level (ADR-0026 slice A): signing key sealed with a dual-recipient
envelope (Argon2id KEKs from an operational passphrase + a one-time off-node recovery code; XChaCha20-Poly1305),
recovery escrow minted at
init,seal-keymigration. - Backup-as-cold-peer (export + health) — ✓ done at node level (ADR-0026 slice B):
backup/verify-backupCLI +last_backupstatus; signed-event medium, self-verifying via the existing signature invariant; fail-safe node-local health sidecar; sharedfsioatomic-write. - Restore-apply + new-identity
supersede— ✓ done at node level (ADR-0026 slice C, issue #50):cairn-node restorerehydrates thenode_eventlog into a fresh DB via a self-trustingrestore_node_eventdoor (empty-genesis fenced — a no-op on a live node), mints a fresh key, records asupersede(dead→new);db/009opsupersede+node_lineage;statussupersedesline. Cold-medium self-identification (#53, 2026-06-26): a federated medium can't be self-identified from its (convergent) events, so the backup writes a container-level self-marker —crates/cairn-node/src/medium.rs,CAIRNB2format; a signednode.self_attested(unforgeable + event-set-bound viaevent_set_commitment, rejecting a different-set splice) or unsigned (operator-error-safe).restore::resolve_dead_noderejects a peer/off-medium--superseded-nodefail-closed. Known residual (code review): the commitment binds to set content, so it can't reject a peer's genuine marker spliced between byte-identical converged media; impossible on a sole-enroll medium, so multi-enroll restores reportProvenance::SignedFederated→ confirm-on-restore. Net: forgery-proof always; misdirect-proof for sole-enroll + different-set splices; converged-peer splice is confirm-on-restore. - Sealed local-state export — ✓ done at node level (ADR-0026 slice D): a long-lived local-state DEK dual-wrapped
once at provisioning (op-pass + recovery code, point-5 compliant);
CAIRNL1export co-located with the backup medium +CAIRNX1.lsksidecar; additive-CBORLocalStatewith typed-empty slots + DB read/apply seams the clinical tier extends; signing key never in the bundle (point 4);establish-local-state-key+statusline; honest-degrades on absent/corrupt export.localstate.rs(no schema change). All ADR-0026 slices (A–D) complete. - Uniform key-material zeroization — ✓ done (#54, 2026-06-26):
every transient KEK/DEK/seed/LSK held in
Zeroizing(wiped on drop) acrossseal.rs+localstate.rs; key-yielding functions returnZeroizing<[u8;32]>. Remaining optional follow-on: escrow rungs (Shamir M-of-N, QR, TPM/keyring) (ADR-0026). - Trusted-time anchoring — graded-interval
t_recordedwith clock-confidence grade; transparency-log multi-anchor existence proof (ADR-0027). - Audit-log integrity, offline auth, mTLS (§7).
Phase 6 — Federation hardening¶
- Revocation cascade; anchor-as-power (ADR-0018).
- DR / recovery escrow — ✓ done at node level (ADR-0026 slices A–D, see Phase 5); uniform key zeroization (#54) ✓ done. Federation-tier follow-ons: peer-quorum (social) recovery + escrow rungs (Shamir M-of-N, QR, TPM/keyring).
- Node-identity
supersede— ✓ done (ADR-0026 slice C). Signing-key rotation (rotate-keyactor event) — still reserved, not built.
Phase 7 — Attachments / byte tier¶
- Content-addressed lazy blobs referenced by the signed event, never inlined; day-one attachment-reference shape (ADR-0013). The concrete shape is FINALIZED (ADR-0042, 2026-07-08, slice 26):
Attachment{descriptor, renditions:[Rendition{…, inline?, seal?}]}+SealRefincairn-event/src/attachment.rs(all five §3.14 reserves; field order frozen),EventBody.attachments: Vec<Attachment>, and reference-eager per-rendition learning in both doors via the sharedcairn_learn_attachment_refshelper (db/027; db/005 + db/020). Byte tier (db/003 +cairn-syncblobd) is chunked/resumable/windowed. First real consumer: §5.4 photo evidence (slice 26). Deferred: cross-node byte fetch wired intocairn-node; per-blob DEK sealing; preview/extracted-text renditions. - Blob self-verification in-DB floor — ✓ done 2026-07-05 (
db/026_blob_verify_floor.sql+cairn_pgx0.3.0cairn_blob_verify/cairn_blob_verify_error, thin wrappers over the samecairn_event::blob_addressL2 uses — one hashing implementation, never two): the BLAKE3-vs-address checkcairn-syncperforms before flippingpresent := TRUEis restated in-DB as a trigger floor onblob_store(INSERT arriving present; column-level UPDATE OF content/address/present that flips into present, swaps content under a present row, or re-keys it — metadata-only updates neither re-pay the hash nor detoast the content for the WHEN comparison), closing the honest gap db/003 recorded since the walking skeleton: a raw-SQL client could store arbitrary bytes as any named blob (the exact "wrong-hash blob served as the named one" failure ADR-0013 point 11 designates as this tier's safety-critical seam; principle 12 requires the floor below every client). Stale-.solegibility is two-layered: db/026 itself refuses to load whencairn_blob_verifyis absent (ato_regproceduregate binding every loader, cairn-node included — the guard is late-bound PL/pgSQL, so without this the load would succeed and the illegibleundefined functionwould surface only at the first present-flip), andcairn-sync'sREQUIRED_PGX_FLOOR0.2.0 → 0.3.0 connect gate (now also onput-blob/gen-blob/blobd, the commands whose writes fire the trigger) catches.soskew after init. TDD: 7 DB-gated hostile-client tests (crates/cairn-node/tests/blob_floor.rs) + acairn_pgxpg_test (fail-closed on tampered bytes / truncated / wrong-prefix / empty addresses). Honest limits (recorded in the design doc):blob_chunkrows andoutboardare NOT in-DB verified — wrong chunks can only assemble into a whole-blob flip that FAILS the floor (space waste, never wrong bytes served), and a wrong outboard yields slices the fetching peer's bao decode rejects against the signed address root (availability degradation, never an integrity hole). No event-format change, no ADR/spec change (implements settled ADR-0013). - Resource-isolated byte tier — chunked/preemptible/separately-budgeted; can never starve clinical sync; opt-in byte replication; self-verifying swarm fetch.
- Rendition set — the binary's legibility twin (retrievability axis); per-blob DEK crypto-shred inherits.
Phase 8 — Native API contract (the boundary below the application)¶
- Native API: capability-described + conformance-tested, evolves additively (ADR-0023); the four-layer boundary sits below policy/UI (ADR-0021).
- Author-scoped export — the medico-legal copy (ADR-0019).
- FHIR interop façade — distinct from the native API (§9.7).
Phase 9 — Terminology services¶
- ICD-11 canonical interlingua + local-terminology overlay (ADR-0025).
Above the foundation line (NOT in this roadmap)¶
- Policy layer — hard policy as a signed policy-assertion stream + effective-policy projection (ADR-0024); soft policy in UI.
- GUI / reference UI — built only on the same public native API everyone else uses (principle 12); paper-parity is the governing law, no confirmation dialogs as a safety mechanism.
- Active-write thin encounters and clinical workflow surfaces (ADR-0020).
Parallel build-prep (not blocking the critical path)¶
- Bet B — Pi compute-cost run — Ran 2026-06-25 on Pi 5 / 8 GB → PASS (PR #57): all §6 gates green with headroom; B4 confirms ADR-0015's BLAKE3 blob-digest default (BLAKE3 ~4× SHA-256 on Cortex-A76).
cairn_pgxnow PG-18-capable (pgrx 0.18.1, PR #56). Open follow-ups: clean re-run on PG 18 + USB-3 SSD + 27 W PSU for authoritative precision numbers; drop "provisional" from the ADR-0015 blob-digest line. - Spike 0003 — Postgres on Android — Ran 2026-06-25, G0–G3 PASS: native PG 18.2 + a cross-built pgrx extension (incl. SPI) on a stock Android 16 phone; validates the fractal-topology invariant at the phone tier. Runnable kit at
poc/pg-android-kit/. Remaining gaps (from-source PG build, APK packaging) are non-load-bearing. - Continued clinical case-mining — the highest-signal mode for stress-testing the primitives before product build.