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 and logs what each slice built. If it disagrees with the canonical docs, the canonical docs win. Keep it under 500 lines (#368): condense a slice once it is behind us — its why belongs in its ADR — but never drop an open issue number while condensing (the PR
271 review finding).¶
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). AGPL-3.0 for all code; every dependency AGPL-3.0-compatible (checked before adding).
- 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.
- 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 — validatedsubmit_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:link+ the linkage projection (C1); thematch_proposal→apply seam with a human-accepted door (C2) and auto-apply of theauto_candidateband (C2b);dispute+ the chart trust-state projection (C3);identify+ unconfirmed (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); photo evidence carrying the
day-one §3.14 attachment-reference shape (ADR-0042);
marks/belongings/EMS-context text evidence; finishers; the
enroll-humanceremony CLI. - §5.2 matcher, advisory tier (slices 19, 21–25; Python only) — the alias-pool evidence pass; birth-year-range blocking + A/B toggle; administrative-sex scoring and the unconfirmed-chart REVIEW rule; the B3 eval mirror; supervised Fellegi–Sunter weight-learning (a PoC on small/synthetic data); compound blocking keys.
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 timeline; cross-thread reconciliation as a link (ADR-0047); the commitment-based attestation overlay (ADR-0049); per-field dose correction (ADR-0050); twin-check registry (ADR-0048).- Slice 35 — the P1 floor-hardening slice (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). #220 remains.
Still open from these slices — enumerated in full (see the header rule).
- Filed and open. #141, #163, #168, #184, #220. Two that carry
standing consequences: #185 (cross-thread dose-correction suppression vector — needs a PK/design
decision, so it cannot be picked up as routine tech debt) and #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). #79 (B2 Minors) is matcher-side. - Identity C5+.
reattribute(§5.5 event-granular strike-through) waits on a clinical-note surface; a reversal / de-repudiation event; a chart-history VIEW rendering struck names (data already present); an accept-at-cap boundary test; the §5.2 coherence feedback loop; contamination cascade on dispute; person-level trust aggregation. The §5.12 push-alert is 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 (its contributor identity is a provenance string for
now); no recovery escrow for the sealed matcher key (regenerable — a convenience gap); no background
scheduler; locale comparator packs; the hub-tier duplicate sweep; a veto-aware scorer mode; fuzzy alias
recognition + an
aliasblocking pass; near-window softening; variable cluster size / hard negatives in the generator; acompare_addresscomparator; a CLI sweep entry; the B3 mirror ignores the block cap. - Medication (slices 30b–34). Automated reconciliation detection (human-driven resolution exists;
fuzzy detection plus a Tier-A dictionary is the gap); a partially-attested-group read surface; a whole-list
sign-off summary event; statement-level
started-date correction and per-field merge across corrections of one point; a rendering-suppression 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.
Slices 36–56 — condensed (2026-07-16 → 07-27: the 2026-07-15 whole-project review course — fully closed — its Priority-6 design queue, and the first medication-coding slices; detail in git, the PRs and the linked ADRs).
- P2 sync-convergence integrity (36–40, PRs #221–#225): the A→B convergence test over the real binaries and 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); wire hygiene +node.superseded(#202/#201). - P3 — both wire windows shut (41–43): ADR-0051 contributor-role vocabulary floor (#203+#96);
ADR-0052 born-sealed clinical bodies (#189+#92,
db/037) — every clinical body sealed at write under a per-event DEK the node holds, a custody plane, sealed⇒clinical at both doors, a rung-3 shred CLI; ADR-0053 per-write human authorship (#204,cairn_authorship_bound). - P4/P5 (44–45, PRs #251/#253/#255): the #188 schema-version downgrade guard in both loaders
(
SCHEMA_GENERATION,SCHEMA_LOAD_LOCK);scripts/run-db-sql-tests.shin CI (#212); the registryDO UPDATEarm (#214); HANDOVER staleness (#215). - P6 design queue → five ADRs (46–50): ADR-0054 actor-registry federation is admit-and-dispute (#205, closes
#154 structurally); ADR-0055 the chained trust-root document (#206); ADR-0056 unknown event types admitted
uninterpreted (#200 — the filed premise was inverted); ADR-0057 generic reprojection (#208, PRs #274/#278; one
apply fn per projection,
cairn_replay_eligiblethe #265/#266 seam); ADR-0058 the grade-gatedt_effectiveceiling (#216, PR #285 — closing a latent one-event sync-wedge DoS). - Matcher (51/53/54, Python): #209 fail closed on an empty non-match set; #210 retract orphaned proposals; #211; #290. Slice 52 — the #217 paper-parity plan-section rule (house rule 7; first live entry #288).
- 55–56 — drug coding. ADR-0059 anchors drug identity on drugref's immortal
moiety_uuid(INN is display, never key) assubstance.coding, advisory + honest-degrading. Slice 6a (PRs #297/#298,db/041, generation 40→41): themedication_coding_systemregistry,medication_codingas its own projection, the(system, code)-pair dup-key, and a source guard that nothing executable references drugref. Sharpest review finding: a shred leftmedication_codingreadable besidepatient_id(ADR-0005 rung-3 / #92(b)).
Still open from slices 36–56 — enumerated in full (see the header rule).
- Sync: #284. Born-sealed / erasure (ADR-0052): #230 · #231 · #232 · #233 · #234 · #235 · #236 · #237. #231 (unwrap-cert kid pinning) landed as Slice 66, so custody follows admission; #232's parts A/B shipped (Slices 65/67, discharging #294), the authority floor as Slice 68, and parts C/D are DESIGNED (ADR-0065, v0.67), not built (#376 answered, #377 merged into it; rung 2 #496, chart-wide narrowing #499, rung-1 offline glass #498).
- Authorship (ADR-0053): #242 · #243 · #244 · #245 · #247. Grading is half-live until #245; contributor-set
authorship is key-scoped and does not survive rotation (#247 constrains #245); a
--author-asevent is owned under the ADR-0043 suppression gate. - ADR-0054/0055/0056 code (design-settled, none built): ADR-0054 — #94, the key-loss-ceremony ADR, the rotate-key door; ADR-0055 — #257 · #258 · #259 · #260 · #261; ADR-0056 — #268 (#265/#266/#267/#269/#270 closed by Slices 58/60). The posture triad: the content plane admits-and-disputes (0054) and admits-and-defers (0056); the code plane verifies-or-refuses (0055).
- Reprojection (ADR-0057): #272 (the authoritative Pi5/NVMe same-rig re-run), #275, #276, #277 (heal cannot
re-derive
DO NOTHINGprojections). Trusted time (ADR-0058): #279 · #280 · #281 · #282 · #283. Registry hygiene: #254 (8 twin-check registrations stillDO NOTHING). Deps: #252 closed by retiring iced; #317; #389. - Medication/matcher: #287 (hub-scale sweep cost), #288 (med-list sign-off as ONE gesture — the human measurement remains), #294 (discharged by Slice 67), #334 (repaired by R1, PR #688), #331 · #333 · #335 · #336 · #337 (Slice 61 follow-ons).
Operational caveats that outlive these slices. Pre-ADR-0051 event logs and pre-ADR-0052 plaintext clinical.*
bodies REFUSE at db/020 — wipe dev/PoC rigs, never sync them through. Pre-wire unsigned actor rows never sync.
Test DBs need cairn_pgx ≥ 0.3.0.
Slices 57–69 and the August tech-debt passes — condensed (2026-07-28 → 08-22; full detail in git, the PRs and the linked ADRs — the why is in each ADR and is not restated here).
- 57 — medication 6b: the coding-overlay event types (ADR-0059
decision 3;
db/042; closed #295, #296). A strike NULLs the anchor ("not that, and I don't know"). Lessons: a redundant projection column is a convergence hazard;array_aggKEEPS NULLs. Open: the coded↔uncoded case; #294; #300. - 58 — the ADR-0056 floor: admit uninterpreted, re-adjudicate before power (PR #302; closes #265, #266;
cairn_readjudicate_deferred, db/043). Refusal hides, admission cannot. Open: #301 (the node/actor plane still fail-closes), #308, #309. - 59 — floor determinism (PR #311 closes #75): the twin blank-test was collation-dependent;
cairn_twin_is_presentspells the 25 UnicodeWhite_Spacepoints. - Interlude — the loop ran unattended (07-31 → 08-01). Nine PRs; closed #79, #11, #100, #119, #120; loop fixes PRs #316, #321, #325. Open: #312 · #314 · #315 · #317 · #322 · #326 · #327.
- 60 — the residual refusal contract, clinical plane (closes #267/#270): a deliberate refusal on verifiable bytes is
penned by digest and auto-released.
P0001is a contract with the pull loop (PR #371 node plane, #370 clinical). Symmetry between two planes is a hypothesis, not a goal — the naive #268 alignment would be a defect. - 61+62 — the med-list node tier and WINDOW (ADR-0060,
v0.62): the first clinical READ path; iced retired for
cairn-gui-tauri; write cost median 222 ms; sign-off per LINE (#339), reaching the transaction layer (#342). A unit-tested safety control can still be defeated by the surface that calls it; a compensating control outside CI is not a control (#444). Owes #288. Open: #331 · #332 · #333 · #335 · #336 · #337 · #340. - 63 — the search-before-create funnel (ADR-0061,
v0.63,
db/045/db/046): the attestation NAMES the displayed candidates. Open: #346–#357, #359–#362; worth naming #349, #351, #352, and the §1.2 write-cost half #360. - 64 — closing the funnel's bypass (closes #345): db/005 step 8b, a chart's first event is its registration;
retiring
patient.createdwas the load-bearing half. Unfloored: #364, #365. - 65 — the §5.9 sensitivity stream, part A (ADR-0062,
v0.64,
db/048): effective grade = the max of event/thread/chart; unknown ranks MAX; erratum E6. Open: #374 · #378 · #379 · #436; parts C/D #376/#377 (ADR-0065). - 66 — custody follows admission (closes #231): the unwrap-cert
kidpinned totrust_peer; withhold the key, never the bytes.unsound = "all"in bothdeny.tomltrees; #389's review date lives in its reason. - 67 — the §5.9 safety projection, part B (closes #375; ADR-0063,
v0.65): the seal boundary is the coarsening boundary;
safety_class_mapships EMPTY. Fixed #404. Open: #394 · #395 · #397 · #398 · #399 · #400 · #401 · #402 · #406 · #407. - 68 + two interludes (ADR-0064, v0.66; closes #380,
#412, #405, #426): one
cairn_claim_authoritypredicate gates effect, never admission; 7 of 11 production mutations had survived a green suite. A columnREVOKEcannot narrow a tableGRANT(db/049 §8 — #425, #427, #432);classify_authorship_confidencegraded a forgeryAttested(#428); 21 headers gained, pg_temp. Open: #408 · #409 · #413 · #414 · #415 (expect it to fire on routine care) · #416 · #417 · #418 · #419 · #420 · #422 · #430 (~100 unpinned invoker-rights functions) · #431. - 69 + two passes — the §5.9 operator surface (closes #388, #383, #421, #435, #387, #439, #382, #385, #381):
patient-sensitivity— NAME, NEVER COUNT;TargetState::OnAnotherChartmust never collapse (residual #436);cargo docblocking (#439); a NULLproaclis the PERMISSIVE case (#382). Residual: #441. - The silent gates (08-20 → 08-21; closes #446, #442, #443, #449–#453, #386; opens #447).
Nine gates that could pass without running (
cargo_lockfiles_tracked.rs,db_gate_actually_ran.rs). Not done: unifying the 342 bare skip sites (#327). - The freeze that hid and the flake that lied (08-21; closes #370, #457; opens #458).
cairn_learn_attachment_refshad nine freeze paths on malformed signed bodies — a signature proves authorship, not well-formedness. - The db-error legibility sweep (08-22; closes #460, #465, #467, #469, #471, #473, #474, #475;
db/050; opens #461 · #463 · #464 · #468 · #470). Constrain an envelope field where it is MINTED, read it permissively where it ARRIVES.tokio_postgres::Error'sDisplayIS"db error"—legible_db_errorrendersmessage [SQLSTATE] — DETAIL — HINT; never "tidy"LocalDbFaultinto ananyhow!;EXCEPTION WHEN OTHERSdoes not catch 57014; force a write failure in a shared test DB with a LOCK under a shortlock_timeout.
Open-issue index — every open number the narrative above does not name. The convention is never drop
an open issue number (the PR #271 review finding); prose cannot hold ~145 of them, and this index only
guarantees nothing is orphaned here. Live list: gh issue list --state open.
93 #98 #283 — spec/ADR debt (revocation cascade trusts the authoring node's clock; ADR-0016 vs ADR-0001¶
and the node-compromise threat model; the clock_grade still unrendered in the legibility twin). #97 #347 #348 #353 #354 #355 #356 #361 — demographics + Slice 63 follow-ons.
101 — sync/blob; item 1 CLOSED by DR slice 2b — the issue's title says "paginate EventsAfter", but it is EventsAfterSeq that gained paging: the legacy EventsAfter arm is unpaginated by construction and stays that way, and it is one of the two paths that can still reach the 64 MiB frame cap. Items 2 (blob byte_len wedge) and 3 (BLAKE3 verify in-DB) keep #101 open. #318 #329 #373 #411 #531 #532 #534–#538 — test + tooling hygiene (cairn-sync/src/main.rs: #329 filed it at 5.3k, #531 at 11.6k, and it has grown again since — read the file, do not quote a figure that is stale by the end of the slice that wrote it; no frozen wire fixture across crypto-library bumps; run-db-sql-tests.sh wrong-cluster + dropdb before the #169 marker check; a multi-page pull that fails late reports nothing about the pages that landed).¶
458 — a non-object attachment element admitted silently (re-scoped 2026-08-22 to the #460 ledger + a loud¶
UI, NOT a floor rule). #392 #393 — federation (peer_pubkey hex case; custody grants leave no audit trail).
303 #304 #305 #306 — the tech-debt loop's own tooling, stopped.¶
602 — any client can set cairn.remote_apply before submit_event, turning the strict door's refusals into flags (a decision; found in #584's review, see the ADR-0070 entry under Phase 8/9).¶
603 — a late key racing connect-time re-adjudication of a deferred event could leave the promoted record off the chart, and requeue then exits 0 (a cross-transaction race; ADR-0070 residual); fixed in PR #774 (the heal-safe dispatch takes the event's custody lock). Same PR: #773 (a heal pass or re-adjudication racing a shred re-projected the shredded record; cairn_lock_erasure_heal, shared by shreds, exclusive by cairn_reproject), #346, #553, #616, #744, #704.¶
604 — a shred racing a late key could resurrect custody (and, since ADR-0070, the projection); fixed 2026-10-13 by cairn_lock_custody, a per-event xact lock both doors' step 9 and the shred executor take (tests/shred_custody_race.rs reproduces both interleavings). Same session: #608's late-custody half, #431, #464 (db/050 writes once per event, not per rendition), #768 (a misfiled peer vouch is named and reads stale).¶
708 · #713 — a human identity judgement could silently overrule a colleague's: a link/unlink made from a comparison was signed over whatever the records had become (a peer link landing mid-judgement), and a judgement whose COMMIT was interrupted was offered as a live retry over a chart that looked unchanged; fixed in PR #776 (RecordsShown, re-read under CARNLK; CommitOutcomeUnknown → Retry::AfterReread). Same PR: #569 (a restore kept a conflicting registry row — a carried revoke could fail to land), #560 (export coverage could claim a seq whose DEK the export lacked; the issue's one-snapshot fix would not have closed it — the export now waits out in-flight event_log writers), #682 (NFC before tokenising), #509. Filed #775 (Turkish İ: Rust vs a C.UTF-8 cluster's lower()). #775 fixed in PR #779 (option A: every name-lowercasing site, search and sweep, is COLLATE pg_unicode_fast; matcher 0.2.1); follow-on #780 (casefold()).¶
2026-08-23 → 09-12 — the sweep's tail, the misclassification cluster, §5.9 C+D designed, the DR audit, DR slices
1 → 2d, the restore's §1.2, #511, the closing-keyword guard, #527 and the CodeQL pack — condensed. The why of each
lives in its ADR (0065–0068) and design doc under docs/superpowers/; docs/developers/durable-rules.md's traps hold the rules that still bite.
- The sweep's tail (08-23; closes #481, #479, #477).
A guard runs only when its crate is tested (
db_gate.rs); one pureoperator_chain. Opened #485 (89 postgres call sites naming no operation) and #487–#492. - The misclassification cluster (08-23; closes #489, #482, #480, #490 items 1–2) —
a failure wearing another subsystem's clothes (
PullIntegrityError; a type outranks a kind). Open: #490 item 3 · #483 · #484 · #487 · #488 · #491 · #492 · #485 · #476. - §5.9 parts C+D designed — narrow the custody, never the reach (ADR-0065,
v0.67; answers #376, merges #377). Opened #494 (ADR-0052's
event_dekerratum), #495, #496, #498, #499. A control a faithful peer defeats by computing correctly is incoherent; "conservative" is a property of a direction; refuse at a door only what that door can drop whole. - The DR-guarantee audit (08-23; confirms #495, opens #500, #502). ADR-0026 decision 1's three clinical promises were all false while every surface reported honestly — the composite was a precise untruth. A deferral is honest only while its precondition holds.
- DR slice 1 — identity dies with the disk; custody must not (ADR-0066,
v0.68; closes #495): an independent X25519 unwrap keypair in
<key>.unwrap; custody ridesCAIRNL1;restoreadopts, never mints (traps 1–2). Open: #504 (dead_node_sk— a decision) · #505 (a second recovery code) · #506 · #507 · #508 (CBOR leaves unwiped copies of the unwrap secret) · #509 · #512 (theM > Npaper-parity defect) · #513. - #503 —
cairn-keystore:cairn-syncloads the node's custody key (08-30; closes #503) through a pure decision table (trap 3). Opened #514 (retire the fallback) · #515 (the binaries disagree on the signing-key file format) · #516 · #517 (no test starts the daemon from a PROVISIONED key) · #518 · #520 · #521. Breakage hid from a gate three ways →scripts/run-db-gated-tests.sh; run a plan's verbatim code against the gates first. - DR 2a + 2b — the medium format and the transport seam (08-31 / 09-02;
cairn-medium,cairn-wire; #101 item 1 only).CAIRNB3;Transportas the one seam. 19 of 19 mutations survived a round-trip suite →wire_pins.rsgolden bytes. Open: #531 · #532 · #534 (a freeze stops content convergence) · #535 · #536 (an unopenable DEK is counted nowhere on sync) · #537 · #538 · #556 (segment_commitmentdid not bind the attestation — fixed in PR #779, re-frozen before any CAIRNB3 writer shipped; the sync-wire half is #778) · #557 · #558 · #559 (operator messages naming the wrong remedy) · #560 · #561 · #562 · #563. - #511 — the custody newtypes (09-04; opens #541, #543, #544, #545).
Secret32/PublicKey32make public-for-secret a compile error; they do NOT separate secret roles (trap 5). - The closing-keyword guard (09-04; reopens #101, #115, #434, #441, #468, #500, #534). GitHub closes on ADJACENCY;
scripts/check_closing_keywords.py+ the workflow scan title, body and commits. Residual #444, #547, #548. - DR slice 2c — the medium carries the clinical record (09-06;
db/051; closes #524, #500 as titled — its READ half became #554 — and #523, whose merge also closed #182, #404, #430, #431 and #441 — the last three open again, as is #522; #550 opened and closed in-branch). A backup reproduces the state at CAPTURE TIME (trap 7); the shred predicate has ONE home;seqholes are routine (#549 owes the operator surface); a torn tail must NOT refuserestore. Open: #551 · #552 (a capture is O(whole medium)). #553 (an unmarked foreign legacy medium could be destroyed by succession) is fixed in PR #774: succession now compares event sets, not counts. - DR slice 2d — the record comes home (ADR-0067, v0.69;
db/052; closes #554; opens #567–#571). The headline test DECRYPTS a body; the pen moves into the database and gains custody; the gap pin inverts toa_clinical_event_restores_from_a_medium. 2e is retired as a label. - The restore's §1.2, measured (ADR-0068,
v0.70 — provenance warns, never gates; closes #571; opens #572; PR #573):
100 003 events in 116.7 s against 600 s; #552 confirmed;
M > Nstands (#512). - CodeQL advanced setup + a model pack (09-12; PR #576; corrects #562):
one
barrierModelrow per NAME-heuristic source, 44 → 3. Not touched: #575 (a real secret on stderr); required is #444. - #527 — a discriminator is not a salt (09-02; opens #529, #530). CodeQL picks its sink by the binding's NAME;
renamed to
lineage,crypto_sink_names_are_genuine.rs, house rule 6b. #530:PairingBundle.nonceis signed and never read (a decision). #529: no daemon path prints a patient id — by accident, not by rule.
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 (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 — ✓ COMPLETE (ADR-0026 slice B):
backup/verify-backup+last_backupstatus. Since DR slice 2c the medium is a CAIRNB3 image carrying both planes; since 2d a restore applies the clinical plane through thedb/020door, installs the carried custody and pens what it cannot key, so ADR-0026 decision 1's "the clinical event log survives" and decision 2's "clinical events back up as a cold peer" may be cited as met (#500 → #554;dr_clinical_guarantee_gap.rs::a_clinical_event_restores_from_a_medium,medium_carries_both_planes). Since then (Phase 8/9 below): ADR-0069, #568, #567, #593, #584 (ADR-0070), #594 (ADR-0071), #614/#615 (ADR-0072), #619 (ADR-0073), #621 (ADR-0074). What remains is not slices: #575 (re-deferred), #611, #602, the races #603/#604, #589 · #590 · #591 · #592, #596 · #597 · #598 · #599, #605–#610, #616 · #617, #608's late-custody half, and node-plane completeness accounting (a slice of its own). - Restore-apply + new-identity
supersede— ✓ done at node level (ADR-0026 slice C, issue #50):cairn-node restorerehydrates thenode_eventlog through a self-trustingrestore_node_eventdoor (empty-genesis fenced), mints a fresh key, records asupersede(dead→new);db/009. Cold-medium self-identification (#53): a container-level self-marker (CAIRNB2). Live residual: a peer's genuine marker spliced between byte-identical converged media is not rejectable — multi-enroll restores reportProvenance::SignedFederated(warn, never gate — ADR-0068). - Sealed local-state export — ✓ CUSTODY travels (ADR-0026 slice D, completed by DR slice 1 /
ADR-0066):
CAIRNL1+ aCAIRNX1.lsksidecar carrying the independent unwrap secret, the survivingevent_dekrows (a shredded event's excluded by construction) and, since 2c, the enrolled actor registry; verified after write; installed by 2d. ADR-0026 decision 1's promise 2 ("node-default data-at-rest keys survive") has no subject in the built system — neither honoured nor violated. Uniform key-material zeroization ✓ (#54), residual #508. Optional follow-on: escrow rungs (Shamir M-of-N, QR, TPM). - 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 — ADR-0026 slices A, C, D done at node level; slice B is COMPLETE — the medium carries the clinical plane (2c) and a restore reads it back (2d, #554), see Phase 5. Federation-tier follow-ons: peer-quorum (social) recovery + escrow rungs (Shamir M-of-N, QR, TPM/keyring). Node-tier residual #505 (the ADR-0066 migration path mints a second recovery code).
- 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, thin wrappers over the samecairn_event::blob_addressL2 uses — one hashing implementation): the BLAKE3-vs-address check is restated in-DB as a trigger floor onblob_store, so a raw-SQL client can no longer store arbitrary bytes as a named blob (principle 12). Honest limits:blob_chunkrows andoutboardare NOT in-DB verified — wrong chunks can only assemble into a whole-blob flip that FAILS the floor, and a wrong outboard is rejected by the fetching peer's bao decode (availability degradation, never an integrity hole). - 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) · Phase 9 — Terminology¶
- 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 — ICD-11 canonical interlingua + local-terminology overlay (ADR-0025).
2026-09-11 → 09-20 — the DR close-out and the node-plane doors, condensed. Each ADR (0069–0074) and plan
(docs/superpowers/plans/2026-09-1*) carries the argument and review ledger; docs/developers/durable-rules.md's traps 8–14 carry the rules.
- The restore takes its recovery code from a file (ADR-0069,
v0.71; closes #572 and #570; PR #574):
--old-recovery-code-file <PATH>, read in the step-0 pre-flight. The refusal the design started with was dropped because "no cron-run command reachesprint_recovery_code" was already false → #575. requeuereleases custody (09-12; closes #568) — five DB tests drive the binary and assert a body OPENS; arms are indexed by input pair, not by outcome.requeuenever counts a release it did not get (closes #578–#581; PR #582): released only when custody is SETTLED (trap 8), in the EXISTINGdb/052. Filed #585 (nothing reads Postgres notices) · #586 (two older source guards skip after a file's first#[cfg(test)] mod) · #587 (cairn-syncdoes not replay schema); a predecessor's fixture failed the gate (#583).verify-backupasks the clinical-plane question (09-13/14; closes #567; PR #588) —backup SHORTonly on evidence. Residuals #551 · #589 · #590 · #591 · #592; commented on #559 and #549. Operators: run it AFTERbackup.- 2d's last six design tests (09-14; closes #593 and, by a lockfile bump, #600 — RUSTSEC-2026-0285, both lockfiles; PR #595). Thirteen mutations run, thirteen killed. Opened #594 and #596 · #597 · #598 · #599.
- A late key reaches the chart (ADR-0070, v0.72; builds #584;
PR #601): both doors call
cairn_project_late_custodyafter the substitution guard (traps 9/10). Filed #602 (any client can setcairn.remote_applybeforesubmit_event) · the races #603, #604 · #605 (an in-placedb/edit is unprotected by the #188 guard) · #606 · #607 · #608 (the substitution guards'<>fail-open) · #609 · #610. - A restore that left records behind exits INCOMPLETE (ADR-0071, v0.73; builds #594; PR #612): five causes share exit 3; exit 1 = BLOCKED (trap 11). Filed #611 · #613 · #614 · #615 · #616 · #617. A fix written under the pressure of a finding is itself unreviewed code.
- A restore loses no record silently (ADR-0072, v0.74,
errata E1–E2;
db/053; builds #614 + #615; PR #618): onecairn_refuse_substitution,IS DISTINCT FROM(trap 12). Its census error became #619. - The node plane refuses a substitution and pens it (ADR-0073,
v0.75; builds #619; PR #623): trap 13; the declared survivor M10
was killable via a
SET ROLEseam — "untestable" is a claim. Filed #620 (the COSE unprotected header lies outside the signature — wire core, a decision) · #621 · #622 (catalogue guards blind toBEGIN ATOMIC) · #624 · #625. Left open: #605 · #268's other classes · #301 · #569 · node-plane completeness accounting · #608's late-custody half. - A deterministic door failure is a refusal, not a fault (ADR-0074,
v0.76; builds #621): the three node doors are total; the puller pens a non-local non-
P0001failure (trap 14). Filed #626 (the clinical plane has the identical shape; db/020 is the 100k-event hot path) · #628 (cairn_body's22P05on a NUL) · #629 · #631 · #632 · #633 · #634 (#630 closed as a duplicate of #625).
2026-09-21 → 09-26 — search fragments and speed (#636 slice 1, #639); the funnel UI 2a → 2c (PRs #646, #653, #661, #674); #671 (ADR-0075, PR #678)¶
- #636 slice 1 widened
db/046pass 3 in the safe direction (punctuated tokens project their PARTS; a ≥3-byte prefix viastarts_with, neverLIKE); monotonicity proved (0 lost, 9 gained);patient_search_drift.rsmakes sweep-paired ⊆ search-found executable. #639 took the Pi floor 1528.4 → 883.3 ms and the spread 777.9 → 14.6 ms at 50,000 real names (traps 15–17;scripts/measure_patient_search.py). A zero-row search measures nothing. Filed #637 (the materialised token table — the fix for the ~860 ms floor, its own slice) · #638 (closed) · #640 · #641 (the parts branch cuts a Devanagari or Thai name at its marks) · #643 (the rig timescount(*)). §1.2: paper 3 → forced 2 → target 2. - The funnel UI (design
docs/superpowers/specs/2026-09-20-registration-search-funnel-ui-design.md; no ADR, no migration). 2a the pure core (cairn-gui-funnel: the advisory trigger,PROMPT_CAP = 5, the counted-custody tokenAttestedSearch; 21/21 mutations killed). 2b the live ports (DataError::Refused, #648; #658). 2c's prerequisites (#661):TokenStore::settle(#659),MockData::fail_next(#660),DeliberateRefusal(#651), one enrolment rule (#654). 2c — the window (#674): the search now RANKS (it showed the five OLDEST charts);FunnelSession, thefunnel/module, runbook §8; identifier entry → #672; the header shows age (#673). Review: a sign-off signed whichever chart was OPEN → every chart command names the displayed chart; #675 fixed; #677 decided. Still open: #355 · #645 · #647 · #649 · #650 · #652 · #655 (42501/42P01/class-23 land inUnavailable) · #656 · #657 · #658 · #662 (seveniniteffects unpinned) · #663 · #664 / #666 · #665 · #667 · #668 · #669 · #670 · #672 · #673 · #676. Also cited: #442, #450, #583, #636, #638. §1.2: register 5 → 4 → 4; find 3 → 2 → 2. - #671 — the step-3 prompt is a nudge, not a completeness claim (ADR-0075,
v0.77). Maintainer's decision: duplicates are common and the desk cannot be made to browse, so make repair by
linkeasy — the safety measure is how fast a duplicate is FOUND.search.incomplete= the SEARCH was partial; the cut isPromptList::withheld, never signed; seven-key ranking (#349). Measured: every single slip in the five (500/500); a surname typo AND a wrong DOB 203/500 — the repair path's residue (representative re-run #685). Filed #679 · #680 · #681 (the repair path, below) · #682 (an NFD word-final accent is lost) · #683 (CandidateList's partiality as one sum type) · #684 (1980-3-7misses1980-03-07— a SET gap) · #685 · #686 · #687 (fixed). §1.2: 1 → 1 → 1; a warning that fired on 92% of registrations is gone.
2026-09-27 → 10-15 — the duplicate repair path, R1 → R7 (#679 · #680 · #681)¶
ADR-0076
(spec v0.78); design docs/superpowers/specs/2026-09-27-duplicate-repair-path-679-680-681-design.md (every slice
designed + as built, with its plan under docs/superpowers/plans/ and its §1.2 section); durable rules
docs/developers/durable-rules.md; human passes RUNBOOK §9–§13. Everything through R7 is merged (R7: PR #788, 2026-10-15).
| Slice (PR) | What it built | Schema / spec | Filed |
|---|---|---|---|
| R1 (#688) — a linked chart reads as one | cairn_person_charts, set-wide cairn_medication_duplicate_groups, ChartSet, membership read (#334 fixed), per-thread sign-off over a named set, the doubted-link hold (containing #220 for this read) |
db/054, gen 54 (#284) | #689 · #690 (a decision) · #691 · #692 · #693 · #694 · #695 · #696 · #697 (built in R1b); commented #333 |
| R2a (#698) — a human's judgement outranks a machine's | patient_link.attested, attested-first winner order, db/055's heal + re-fold (#703); chart_link with a human Reviewer; one lock order |
db/055, gen 55 | #699 (built in R2b-2) · #700 · #701 (fixed in R1b) · #702 · #703 · #704 · #705 · #706 |
| R2b-1 (#707) — "Same person as…" | set-against-set compare, marked refusals, the compare-and-link panel | — | #708 · #709 · #710 |
| R2b-2 (#711) — "Not the same person…" | an unlink filed under the record it was judged from, the per-LINK list, unlink.js |
ADR-0077, v0.79 | #712 · #713 · #714 · #715 |
| R1b (#717) — a doubted set withholds every line not on the opened chart | WrongChartReasons, the A–C–X bridge as a doubt, per-reason wording |
— | #716 · #718 · #719 · #720; commented #335 |
| R3 (#721) — the front door collapses by person | group_by_person, displayed_charts() (the ONE signed list), rows capped by person, one open per member |
— | #722 · #723 |
| R4 (#724) + #725 (#733) — the commit-time duplicate check | db/056's notice log + non-raising hook, cairn-matcher watch, cairn-node duplicate-check; anchored range blocking, per-chart p95 9 651 → 1 398 ms at 10 000 charts |
db/056, gen 56 | #725 (Pi figure #728) · #726 · #727 · #728 · #729 · #730 · #731 · #732 (fixed, PR #794) · #734 |
| R5a (#735) — the possible-duplicate banner | db/057 match_proposal_open, duplicate_review, "Different people", a fresh veto note |
db/057, gen 57 | #736 (built in R5b) · #737 + #796 (PR #795) · #738 · #739 · #740 + #745 (fixed, PR #798) · #741 (fixed in R5b) · #742 · #743 · #744 · #746 · #747; commented #708, #722 |
| R5b (#749) — the possible-duplicate tray | the matcher skips what db/057 holds closed (#741), re-assesses review rows (#743 part 1), DISPUTED_SQL, the tray |
ADR-0078, v0.80 | #750 · #751 · #752 (fixed, PR #798) · #753 · #754 · #755 · #756 · #757; commented #738, #332, #728 |
| DOB near miss (#758) | a DOB one keying error away grades NEAR, never auto-links; name+monthday blocking; missed duplicates 351 → 266 of 2 000 |
ADR-0079, v0.81 | #759 · #760 · #762 · #763 · #764 · #765 · #766 |
| R6 (#777) — confirm a doubted link; name the doubt at the front door | cairn_chart_set_doubts (one per-link spelling of "doubted"), one door per line, both verdicts signed only over the pair shown, the doubted person row (#723) |
db/054 in place, gen 58 | #781 (fixed, PR #794) · #782 (fixed, PR #792) · #783 (fixed, #792) · #784 · #785 · #786 (fixed, #792) · #787 (fixed, #792); commented #738 |
| R7 (#788) — a "running" line on a checked chart; a note on an unverified link | charts_changing / change_waiting (#742); cairn_chart_set_unverified_links, a non-holding note (#743 part 2) |
db/054 + db/056 in place, gen 58 | #789 (fixed, PR #794) · #790 · #791 |
- §1.2 (each slice's own section carries the detail): reading a linked chart, signing off a combined list, find / open / register at the front door: 1 → 1 → 1 each. Link judgement 3 → 3 → 3 (2 from the banner), ≤ 20 s (RUNBOOK §9, §11); unlink 2 → 1 → 2, ≤ 15 s (§10); the tray: paper 4 → 5 closed / 4 open (#754), ≤ 25 s per entry, the find ≤ 5 s with the tray open (§12); confirm 2 → 2 → 2 and bridge re-link 3 → 2 → 2, ≤ 15 s (§13); the commit-time check and R7's two lines 0 → 0 → 0. Every figure is a human act still owed.
- 2026-10-16/17, merged: PRs #794 (#781 · #789 · #732), #795 (#793 + #712 item 3: every
cairn-gui-taurifile under 500 lines; #737 + #796: entries headed by whose record(s) they are), #798 (#745 · #752 · #740: the banner survives a failed read and hides only on Rust'ssilent; a decided refusal re-reads the banner, not the chart; the tray's reads ordered) and #802 (#799 · #800 · #801: a re-read keeps typed stop reasons, drops overtaken answers, and never leaves a partial list under a live sign-off). Filed #797 from #798. - 2026-10-17 (#797, PR #805, merged 2026-10-18): a refusal because the record changed under the clinician
is typed (
RefusalScope::Stale→DataError::Stale→Retry::AfterReread); the window re-reads the whole chart, never retries, and names only changes from ANOTHER source (the maintainer's rule; a held chart set moved only by being shown or by the clinician's own act,charts_afterread inside the act's transaction); an always-visible "Read this chart again" replaces every "reload the chart". RUNBOOK §14 owes the human timing. Filed #806–#810. 2026-10-18: a second five-reviewer pass, every finding fixed on the branch — sign-off's target-set mismatch typedStale(ensure_same_targets), "the list is behind the record" made window state (behindAfter), a partly-recorded "Different people" carries its stale pair (behind_the_record), the CLI prints a remedy line after a stale refusal, a node-side guard against sentences naming window acts — closing #806 too. Filed #811 (a decision:AfterRereadlumps "not recorded" with "may have been", #713) and #812 (type/test tightening). - 2026-10-18 → 10-20 (PRs #813 · #816 · #819 · #821 · #824 · #827 · #829 · #830, all merged): an interrupted commit
says only what is known. #811 option (b):
Retry::AfterReread(Recorded::{No, Unknown}), window state with its own end conditions. #814: the medication writes' commits go through onecommit_failuremarker; only an ERROR answer proves a rollback; a tree inventory guards every commit. #815 + #818 (maintainer: mark + search): registration and sign-off marked, a maybe-recorded outcome settles like a success. #817: every identity-side commit marked, autocommits detected; the window never reconnects after a lost connection (#807). #820: a real lost COMMIT driven end to end (tests/identity_commit_lost.rs); filed #825 (window operator diagnostics — a decision). #822 + #823:cairn-node event <id>; auto-apply stops at an unknown outcome with its summary. #826 + #828: every CLI write verb prints its event ids, through pure line builders (the RUNBOOK's first-line parse contract pinned bythe_runbook_sed_still_reads_the_chart_id_alone). - 2026-10-21 (#803 slice A, PR #831): medication withhold "W" — ADR-0081
(spec v0.83): two additive born-sealed events (withhold with a reason and
untila time or "reviewed"; release naming withholds by event id),db/059(SCHEMA_GENERATION 59), a read-time fold (expiry writes no event; the default is resume), holds BESIDEMedicationStatus, both types in the sign-off commitment; CLImedication-withhold/medication-release. A hold's start and a release's instant are the authoring node's PHYSICAL clock, and the floor refuses a body without one (a fast peer's HLC would start "W from now" hours late). Filed #832 (slice B), #833 (ward-level new-withholds list), #834 (undecryptable-event marker), #835 (a future-dated release is invisible), #836 (the #192 conflict flag is never read), #837 (test gaps). PR #831 merged 2026-10-21. - 2026-10-21 (#832, PR #838): the W in the window — each hold a button in the status cell, local time
("W until 20:00", a date when not today, a year when not this year), reason revealed on click/focus (hover a
convenience; Escape/outside click closes), a ceased line's reason the same way; Withhold (reason + "until
reviewed" default / a time) and Release (sends only the hold ids drawn; says when a hold it never saw is still in
force) always visible on the line (maintainer's call, K = 2). New
holdmodule (purewithhold_target/release_plan,cease-shaped commands, unknown commits never "failed"),holds.js, db/044 timeswithhold/release(paired ALTER + a replay-convergence test). RUNBOOK §15 owes the human timing and VoiceOver pass. Filed #839; #809 gained item 5. Second review: the list re-reads at a hold's start or end (next_hold_change); filed #840, #841. PR #838 merged 2026-10-21. - 2026-10-22 (#839 + #840 + #841, PR #842): #838's follow-ups. A Release names unseen holds (on its all-gone
refusal too) as "still on this line"; a cease or release that wrote nothing returns
Errwith one class (stale wins); Withhold and Stop are drawn only where their command can write; an empty end is "missing or incomplete". Review filed #843 (a release of an already-ended hold is admitted, never a stale refusal) and #844 (Release is not yet offered only where it can write). PR #842 merged 2026-10-21. - 2026-10-22 (#844 + #836, PR #845): Release is drawn only where
release_plancan write (the #841 rule, finished; the line then says where to release it). A synced medication event naming another person's chart (db/031'smedication_patient_conflict_flag, never read before) makes its line a wrong-chart hazard, a third reasonpatient_conflictwith its own remedy: stop the line and record it again. Review filed #846 (db/031 flags a stray dose or release only if a claim on its thread arrived first). - Next: run #838's RUNBOOK §15; #804 (needs #690), #825 and #843 (decisions), #846, #835, #807; then the open issues above (HANDOVER's ⇒ NEXT orders them). #699 stays open until the maintainer closes it.
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 — PASS twice on Pi 5 / 8 GB: 2026-06-25 (PR #57, caveated by a USB-2 dock + PG16) and the clean 2026-07-07 re-run on PG 18.4 + a PCIe NVMe HAT with both caveats resolved — B1 p95 3.99 ms @ 2,004,000 events, B2 p95 4.5 ms/374-note chart; B4 confirms ADR-0015's BLAKE3 blob-digest default (~4× SHA-256 on Cortex-A76).
cairn_pgxis PG-18-capable (pgrx 0.18.1, PR #56). Only remaining follow-up: fold the now un-caveated B4 number into the ADR-0015 follow-up to drop "provisional" from the 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 stays the highest-signal mode for stress-testing the primitives before product build.