Skip to content

Decision Log (ADRs)

This directory holds Cairn's Architecture Decision Records. Each ADR captures why a decision was made — the context, the choice, and its consequences. Aspect documents in ../ describe what the architecture is; ADRs explain why it is that way.

Rules

  • Append-only and immutable. An ADR is never edited to reverse it. To change a decision, write a new ADR that supersedes the old one; mark the old one Status: Superseded by ADR-NNNN and the new one Supersedes: ADR-NNNN. This is the project's own "never erase, always overlay" (identity §5.1) applied to its documentation. (Typo/clarity fixes that don't change meaning are fine.)
  • Errata are appended, never substituted. An accepted ADR sometimes turns out to state something factually false about the code — describing a parameter that was never built, a count that was miscounted, a measurement that was never taken. That is neither a reversal (no decision changes) nor a typo, so neither rule above covers it. Correct it by appending a marked erratum: a blockquote headed Erratum EN (date) — factual; the decision is unchanged, placed immediately below the passage it corrects, naming the false claim, stating the fact, and pointing at the tracking issue. Leave the original wording standing above it and list the errata in the ADR header. Never silently substitute the corrected text — a reader must be able to see what the record actually said when it was accepted, which is the point of immutability. Anything that changes a decision remains a superseding ADR, never an erratum. First applied in ADR-0061 (E1–E3).
  • Numbered and dated. NNNN-short-slug.md, zero-padded, allocated in order.
  • Read before reopening. Before reopening a settled question, read its ADR — the rationale is there on purpose.
  • The spec carries no in-file changelogs; git is the line history and ADRs are the rationale.

Index

ADR Title Status Date
0000 Pre-ADR changelog history (spec v0.1 → v0.6) Imported (historical) 2026-06-13
0001 Postgres-intelligence cluster: fat Postgres, thin Rust daemon Accepted 2026-06-13
0002 In-database Rust (pgrx) as the projection escape hatch Accepted (refines 0001) 2026-06-14
0003 Bitemporal event time and acknowledged uncertainty Accepted 2026-06-14
0004 Dynamic sync scope: a prefetch hint, not an authority Accepted 2026-06-14
0005 Erasure as key-custody redistribution: crypto-shredding and a policy-neutral severity ladder Accepted 2026-06-14
0006 Visibility-scope vs. sync-scope: replication is not the confidentiality boundary; the safety projection and graded sensitivity Accepted 2026-06-14
0007 Authorship is compositional; accountability is separable Accepted 2026-06-15
0008 Point-of-care identity: possession binding, fast authentication, and work-salvage Accepted 2026-06-15
0009 The notification economy: unbundling priority, responsibility-routing, and the acknowledgment floor Accepted 2026-06-15
0010 Additive-vs-suppressing classification: structural derivation, the demotion boundary, and automation-complacency detection Accepted (refines 0007) 2026-06-15
0011 The actor registry: version-pinned immutable identity, behavioral-configuration granularity, and key custody Accepted (refines 0007) 2026-06-15
0012 Schema evolution, event-format migration across the offline fleet, and legibility across time Accepted 2026-06-15
0013 Attachments: content-addressed blobs, the lazy byte tier, and reference-eager replication Accepted 2026-06-15
0014 Locale-pluggable matcher comparators: content-addressed profiles that travel with the data Accepted 2026-06-16
0015 Event serialization, signatures, and content addressing: tagged, migratable primitives over three structural moves Accepted 2026-06-16
0016 National-scale record discovery: the replicated essential-state tier and federation admission Accepted 2026-06-16
0017 Federation admission: the sovereignty floor, mutual peering, pluggable trust anchors, and the custodian contract Accepted 2026-06-16
0018 Federation revocation: counterparty enforcement, cascade, and the anchor as a position of power Accepted (refines 0017) 2026-06-16
0019 Author-scoped record export: the clinician's medico-legal copy Accepted (refines 0007) 2026-06-16
0020 The active-write model: thin encounters, type-through authoring, and the delete-vs-erase distinction Accepted 2026-06-17
0021 Layering, the node API, and UI pluralism: uniform core, plural edges Accepted 2026-06-17
0022 The validated submit surface: the node's write path Accepted (refines 0021) 2026-06-17
0023 The native API contract: capability description and executable conformance Accepted (refines 0021) 2026-06-17
0024 Hard policy expression: the policy-assertion stream and the effective-policy projection Accepted (refines 0021) 2026-06-17
0025 ICD-11 as the canonical classification interlingua and the local-terminology overlay Accepted 2026-06-19
0026 Node durability and disaster recovery: backup-as-cold-peer, new-identity restore, and shred-aware backups Accepted (refines 0005) 2026-06-20
0027 Trusted-time anchoring: the clock-confidence grade, the bracketed t_recorded, and the pluggable multi-anchor Accepted (refines 0003) 2026-06-20
0028 The finalized closed contributor-role enum Accepted (refines 0007) 2026-06-20
0029 Skill-epoch (and served-model digest) as a pinned determinant of an agent actor's identity Accepted (refines 0011) 2026-06-21
0030 The advisory-actor integration contract: L2/L3 attachment and authorship through the in-DB floor Accepted (refines 0021) 2026-06-21
0031 Canonical identifiers and node-local surrogate keys: the dual-identifier discipline Accepted (refines 0021) 2026-06-22
0032 Culture-neutral address representation: the three-facet address value Accepted (refines 0014) 2026-06-27
0033 Patient-identifier representation: namespace/profile split and the matching-survivable normalized form Accepted (refines 0014) 2026-06-27
0034 The demographic legibility twin: every demographic assertion stays human-readable without its profile Accepted (refines 0012) 2026-06-27
0035 Entities, relationships, and the provider-number relational model Accepted (refines 0033, 0014) 2026-06-27
0036 Demographic name display: recency-first within the legal tier Accepted (refines §4.2, 0014) 2026-06-28
0037 Administrative-sex provenance-first + per-field winner-policy selector; karyotype = distinct field Accepted (refines §4.2, 0036, 0014) 2026-06-28
0038 Demographic address display: per-use recency-first (volatile field; follows 0036) Accepted (refines §4.3, 0032, follows 0036) 2026-06-28
0039 Globalise the authored legibility twin; honest-degradation floor for non-demographic types Accepted (refines 0012, 0034) 2026-06-28
0040 Signing-context domain separation (content-type + external_aad); one signature per event, co-signing by overlay Accepted (refines 0015, 0007, 0030) 2026-07-02
0041 The progress-note narrative format: one signed event, markdown narrative, manifest-keyed media anchors Accepted (refines 0012, 0013, 0020, 0039) 2026-07-04
0042 The concrete attachment-reference shape (Attachment / Rendition / SealRef) Accepted (refines 0013) 2026-07-08
0043 Suppression is self-only (human-authored content); disagreement is additive; agent advisories dismissable Accepted (refines 0010, 0022) 2026-07-09
0044 Enroll fails closed on actor_id collision; human actors carry a person-distinguishing determinant Accepted (refines 0011, 0029) 2026-07-09
0045 Collation-independent projection winner tiebreaks Accepted (refines principle 1; relates to 0031, #115) 2026-07-10
0046 Enroll fails closed on key→actor dual mapping (the B-direction whole-history guard) Accepted (refines 0044, 0011) 2026-07-12
0047 Medication reconciliation is a link, not a cessation; symmetric collapse; latest-effective group status Accepted (refines principle 2, 0010) 2026-07-13
0048 The per-type twin/floor-check registry: one stable dispatcher, register-by-row, unified check-fn signature Accepted (refines 0039/0022) 2026-07-13
0049 Commitment-based sign-off currency: separable per-thread attestation overlay; staleness by set-commitment compare; supersede, never retract Accepted (refines 0007, principle 10) 2026-07-14
0050 Dose correction is a per-field patch: explicit strike, corrected effective drives the winner Accepted (refines principle 4, §3.6) 2026-07-15
0051 Contributor-role vocabulary floor: recorded ratified (12th member, contributory); responsibility = {held_by, on_behalf_of?} object; future members partition-prefixed; strict-submit/lenient-apply Accepted (refines 0028/0007/0049/0012) 2026-07-16
0052 Born-sealed clinical bodies: erasability as the shipped default; erasable vs. sequestered; seal-then-sign container + twin-under-seal; the event_dek/event_clear/erasure_shred_log custody plane; strict-submit refuses unsealed clinical.* Accepted (refines 0005/0006/0026/0048/0051) 2026-07-17
0053 Per-write human authorship: authoring signature + authorship binding + authorship-confidence grade Accepted (refines 0008/0007/0051/0052/0012) 2026-07-18
0054 Actor-registry federation: admit-and-dispute; signed actor-event wire shape on the node plane; derived live-bindings disputed state — content never waits, permissions always wait; adjudication = supersede by human ceremony Accepted (refines 0011/0044/0046) 2026-07-19
0055 Distribution trust root: channels with the steward as default anchor; chained threshold-capable root document (N=1 first-class, no expiry); root/release role split; fork-freeze; transparency by ADR-0027 reuse; one root shape for §7.6/§7.9/§7.7 Accepted (refines 0012/0024; applies 0017/0018) 2026-07-20
0056 Unknown event types are admitted uninterpreted: custody total, interpretation deferred, power earned; strict door still fail-closes; refusal + durable re-offer kept as the residual contract Accepted (refines 0012/0022; extends 0054; upholds 0010/0051) 2026-07-20
0057 Generic reprojection: a projection lives only in its registered apply function; healing is generic replay run by the loader on generation change; heal/rebuild semantics; the cairn_replay_eligible #266 seam Accepted (refines 0048/0045; upholds 0056 decision 4) 2026-07-21
0058 Grade-gated t_effective ceiling: the born clock-confidence grade bounds the ceiling's rejecting power; flag-never-reject at self-asserted/unknown; remote door never rejects on the ceiling; ADR-0027 §6 upper = RTC corrected to RTC + W(grade) Accepted (refines 0003/0027) 2026-07-22
0059 Medication drug-identity coding: anchor on drugref's immortal moiety_uuid (INN is display, never key); structured substance.coding {system, code, display} replacing the reserved inn_code slot; separately-authored inline-or-overlay coding act; advisory + honest-degrading — drugref-absent nodes still read/sync/reconcile, and the §5.9 safety class is captured pre-seal on the coding node and carried, never re-derived by the reader — the drug-axis companion to ADR-0025 Accepted (refines 0025/0047; applies 0007/0014/0052/0057) 2026-07-25
0060 Partial validity: a defect on one element of a composite clinical object never invalidates the others — never refuse the whole for a defect in a part; partial completion must be REPORTED, never implied (the line stays visibly unsigned, paper's blank signature box); uncertainty may only widen the account of what was not done; the exclusion must name what would repair it; the sole admissible whole-operation refusal is "is this the same object the human reviewed?", never "is it perfect?"; cancellation needs an owner and a rationale and only a clinical actor may author one (local-authoring rule, never a wire rule); and transaction scope must match clinical atomicity — independently-actionable lines never share a transaction, so a rollback does no collateral damage. Framing: the system may fail to record an order; it may never cancel one Accepted (corollary of principle 3 / §1.2; applies principle 4 + 0007/0047/0049) 2026-08-03
0061 Registration is an act that carries its search: one identity.registration.asserted type with §5.3's three classes so the precedence rule needs no "unless"; the attestation NAMES the displayed candidates rather than counting them (six months later, "was it on the screen?" has two opposite fixes a count cannot distinguish); strict local submit / lenient remote apply, with enforcement deliberately deferred to #345 — the funnel is complete but not yet unbypassable; gating a standard registration on a bound human author is REJECTED (it blocks care documentation, pushes named patients through the John Doe path, and produces no forensic record at all — §5.11: a grade, not a gate); the body is not born-sealed, so rung-2 erasure must reach attestations naming a candidate; a registration asserts the typed name/DOB as registrar-entered (patient-stated would often be a precise untruth); John Doe asserts a callsign and no name/DOB — symmetric rule, asymmetric content Accepted (applies principles 2/3/4 and 0014/0022/0048/0051/0052/0053/0058/0060; refines none) 2026-08-05
0062 The sensitivity stream and the inverted unknown: an assertion names an event / thread / patient and the effective grade is the max over standing assertions on all three (a grow-only CRDT — converges with no ordering; inheritance computed at READ, so no backfill); an unrecognised grade ranks MAX, deliberately inverting cairn_clock_grade_rank's ELSE 0 (there rank 0 withholds reject power, here it would withhold protection) while absence still ranks 0; declassification is withdraw-by-reference with standing as a set difference evaluated at read, so a withdrawal may arrive first; assertions are plaintext by necessity (a node must read the grade to coarsen — extends ADR-0052 §2) and the matched blacklist category never travels; ADR-0043's dismissable-by-anyone does not reach a protective auto-tag (dismissing is a lowering); raising is frictionless, lowering is a ceremony — at the LOCAL door only, the remote door tested to admit what the local door refuses (a refused raise would itself be a disclosure), deliberately asymmetric with ADR-0061 decision 4 because a withdrawal blocks nothing clinical; chart-wide grading is expressible but effortful and never automatic (rationale required, the automatic lookup cannot express it, the read surface names the winning subject) with no cap — capping below sequestered REJECTED as Cairn taking a policy stance; the effective grade is node-relative (thread membership needs custody, so less custody deliberately computes higher — the ADR-0052 §9 pattern; gaining custody can LOWER a grade); and the conservative bound is scoped to thread-bearing types (clinical.% + unknown), because a note having no thread is a fact, not uncertainty. Enforces nothing: parts B (safety projection) / C (sequester, blocked on #231) / D (break-glass) remain Accepted (decides the six questions ADR-0006 decision 3 left open; applies principles 2/4/9/11 and 0043/0045/0048/0052/0053/0056/0057/0060/0061) 2026-08-10
0063 The safety projection and the seal as coarsening boundary: the seal boundary IS the coarsening boundary — the precise {class, severity} is captured pre-seal and travels INSIDE the sealed payload while a rung chosen from the then-effective §5.9 grade travels in the clear, so every degradation (custody-but-no-coding-authority → precision; no custody → the rung; crypto-shredded → the rung, permanently) falls out of the seal instead of needing machinery; precise-in-the-clear REJECTED as the leak the mechanism exists to prevent ("Rh-sensitizing event" in the clear reads as "this patient had a termination"), sealed-only REJECTED as no signal on the node §5.9 is written for; coarsening binds at emission AND is re-applied at read, and both are load-bearing for DIFFERENT reasons — emission is the only coarsening that binds a peer's raw-SQL client, read is the only answer to a peer that legitimately emitted a finer rung (the grade is node-relative, ADR-0062 decision 9) — so deleting either reopens a distinct hole; an unrecognised severity ranks MAX (assume the worst) and an unrecognised rung is the coarsest (disclose nothing), inheriting ADR-0062 decision 2's inversion of db/040's ELSE 0; the signal rides the append-only event_log row, not a projection table, so coarsen-but-survive is structural rather than a scrub exemption a future reviewer will "fix"; safety_class_map ships empty (mechanism, never the drug knowledge); the floor is at the LOCAL door only and the read model is total — the deciding argument is blast radius, not ADR-0062 E2's structural-vs-ceremony category: a sensitivity assertion is an event, but the safety signal is a field on a clinical event, so refusing it at apply would drop the medication assertion it rides on (ADR-0060: the system may fail to record an order; it may never cancel one); an uncoded medication, and a coding absent from the map, emit NOTHING — not an existence marker, which would manufacture a signal from an absence of knowledge and reproduce §5.12's alert fatigue on day one; and, forced by the build, a half-formed class claim emits no signal rather than a malformed one (unwrap_or_default() would have built a blank-class precise rung that the strict door refuses via a bare PERFORM, so one blank map row would have cancelled every medication assert naming that drug) — an advisory field must never be able to fail a clinical write, not at a door and not by constructing a body the door will refuse. Still enforces nothing: parts C (sequester) / D (break-glass) remain Accepted (decides ADR-0006's safety projection into a concrete shape; discharges #294/ADR-0059 decision 4; applies principles 3/4/9/11/12 and 0052/0056/0058/0060/0062) 2026-08-14
0064 Admit the claim, withhold the power: three findings (#231's unwrap-cert kid, #380's un-attested grade withdrawal, #405 part 2's un-licensed rung) are one defect — the floor established that a claim was well-formed and treated well-formedness as authority — so ADR-0056's power is earned and #231's resolution (ADR-0052 E1: the serve door withholds custody, never the events) become one rule with one mechanism, compressed as withhold the key, never the bytes. Authority is a human actor this node can hold responsible, by either of two routes (R1 a vouched attestation resolving to exactly ONE enrolled kind='human' actor — stricter than db/020's EXISTS, because uncertainty withholds power; R2 human self-withdrawal of one's own claim, whose kind='human' clause enforces ADR-0062 decision 6 structurally); it gates effect, never admission, and only in the withholding direction — a claim below the bar may always RAISE protection and never lower it, so ADR-0062 decision 7 is not reopened (no refusal ⇒ no fork; a raise is never impeded) and it cannot deadlock (the local door already demands a bound human author, so every locally-authored withdrawal clears the bar by construction). The bar is a fixed floor, not a deployment threshold (principle 9 governs what is confidential, not whether removing protection must be accountable); the verdict is computed at read, never stamped at apply, because the answer improves as the target replicates; and it is consulted at exactly one site per dial — one clause in cairn_sensitivity_standing — so display coarsening, safety-rung emission, the CLI path and part C's future custody dial inherit it structurally (a per-dial check is the #404/#399 drift shape). Flag what cannot self-heal; view what can: the withdrawal worklist is a VIEW (inert, self-clearing, and gated on the target STILL STANDING so a superseded row does not linger forever; stranger-attested, permanent) while the safety-overclaim ledger is a FLAG, because a published byte can never improve — recorded at the LOCAL door only, deliberately breaking the ADR-0058 precedent it copies, since remotely the identical bytes arrive routinely and honestly. Two corrections the build forced on the design: stranger-attested asks about presence on the CHART, not node of origin (trust_peer/local_node are absent from the cairn-sync subset and a reference would have taken clinical sync down; node_origin is a self-asserted client string — so a LOCAL clinician who is a stranger to the chart is listed too, and that is the signal), bounded by HLC to events at or before the strip (else the flagged actor clears their own row by continuing to work — a record the flagged party can erase is not a control); and "over-coarsening is safe" is an emission-side frame that INVERTS on a detector — coarsening a disclosure withholds information, coarsening the licensed rung makes the test stricter and manufactures false accusations against the daemon's own correct output (a passing test was already writing one), so a detector must reproduce the emitter's inputs exactly. Declared: it buys accountability, not authorization — the record is the control and the gate is only the forcing function — prior chart standing REJECTED as authority (it fails the locum, the night-cover registrar and the receiving ED, and is replication-relative; retained as salience only), trust-set origin REJECTED (any admitted peer satisfies it — #380's own shape one layer up); and computing the verdict at read cuts both ways — because both routes resolve the actor through actor_current, which excludes a revoked actor, revoking an actor re-raises every grade they lawfully declassified (safe in direction, but a state change with no author; whether it is RIGHT is #409, undecided). Accepted (gives ADR-0056/#231's power is earned a mechanism; closes #380 and #405 part 2; applies principles 2/3/4/9/12 and 0011/0051/0052/0053/0058/0060/0062/0063) 2026-08-15
0065 Narrow the custody, never the reach: §5.9 part C, unblocked by #231's close (ADR-0052 E1). A custody ladder — rung 0 custody-follows-admission, rung 1 named NODES (serve withholds the DEK from non-holder peers), rung 2 named ACTORS (the floor gates QUIET unwrap at a holder node) — under one invariant: narrowing changes the cost and the noise of reading, never whether the content can be reached at a node that holds the key or can reach one, because an audited break-glass is available at every rung — bounded, because a rung-1 break-glass is a NETWORK act, so a partitioned non-holder falls to honest disclosure and cannot reach it (#498, the one row where the paper-parity table shows a defect: paper's envelope travels inside the file). Node custody is the NORM and per-clinician the EXCEPTION (a blanket per-clinician policy makes normal work inside a location impossible), which is also what keeps break-glass RARE — making it the route for the normal case is §5.11's confirmation-dialog disease and §5.12's alert fatigue in one gesture. The node's own DEK is the keyring and the floor is the glass: event_dek is already REVOKEd from PUBLIC/cairn_agent, so rung-2 break-glass is the in-DB floor writing its audit row in the same transaction and rung-1 break-glass is ADR-0004's acquisition trichotomy — no new key material, no escrow tier; the keyring is LOCAL, never a remote provider (one reached over the network fails at 3am under partition). Hence parts C and D are not separable and #377's dependency direction is reversed — the glass must exist before anything is sealed behind it. Custody is an additive field on the sensitivity assertion, not a new event type, so one gesture sets both dials (two would be independently forgettable — #376's own worse than shipping nothing, M > N against paper) and ADR-0064's authority floor is inherited free (widening is protection-removing, expressed as withdraw-by-reference) — which in turn forces composition to be INTERSECTION (a union rule would let a frictionless raise widen the holder set), and an intersection can EMPTY: two honest chart-wide narrowings collapse custody to nobody and make every read loud, destroying the rarity decision 1 rests on (#499; chart-wide narrowing is blocked on it). ADR-0064's handoff argument is CORRECTED while its conclusion is adopted: the thread-resolution case has the well-custodied node computing the TRUE grade and the custody-less one over-protecting (decision 9 working); the real quiet leaks are registry divergence (A revoked actor Z, B has not → the same withdrawal is inert on A and authorised on B, so B serves the DEK — both HONEST) and replication lag. Custody narrows on event and patient, never on thread — a custody-less node cannot resolve membership, and applying decision 9's bound would make break-glass routine on the nodes that see the patient least: the bound is right for disclosure and wrong for custody, the second ADR to hit that asymmetry, so "conservative" is a property of a direction, not of a value. Unparseable custody holds NOBODY and the grade still STANDS — fail-closed is affordable ONLY because the keyring guarantees reachability, and refusing the assertion would destroy protection with a malformed protection field; this names the three-implementations-no-name rule at last: refuse at a door only what that door can drop whole — the question is never how defective the bytes are, it is what else dies with them. Unknown ranks MAX as in db/048/049 but for a DIFFERENT reason (it withholds quiet access, not protection), flagged so nobody carries the wrong justification into a fourth site. Rung 2 is floor-enforced, not cryptographic: per-actor wrapping is available (--author-as is a passphrase-sealed key file the node never holds) but buys only NOISE against node-level DB access — which can break glass anyway — while creating a silent unrecoverable loss mode with no escrow (ADR-0026 decision 4: the signing key is never backed up) and no erasure_shred_log row to say so; deferred with its threat named. Rung 2 is blocked on a READER identity that does not exist (§5.11). Break-glass is loud in three directions with different jobs — location immediate and in-chart (the torn envelope colleagues see; the only one that actually restrains, and it ships with C), custodian and patient as §5.12 discharging obligations (part D). Declared: narrowing is forward-looking (nothing un-knows a fetched DEK, and a rendered holder list would be a precise untruth — #436's shape one dial over), enforcement is schema-generation-local (ADR-0012's two-plane model, not a hole), a node is not a person, and notification can itself be the disclosure — in the DV case the danger. Accepted (decides #376; merges #377 and reverses its stated dependency; applies principles 1/3/4/5/9/12 and 0004/0005/0012/0026/0052/0060/0062/0063/0064) 2026-08-23
0066 Identity dies with the disk; custody must not: ADR-0026 decision 4 (the private signing key is never backed up; a restored node mints a fresh keypair) and ADR-0052 decision 4 (the node unwrap key is X25519, derived from the node's Ed25519 signing seed by HKDF) are each sound alone and jointly fatal at the seam, which is code — a fresh seed derives a different X25519 secret, so every inherited event_dek row on a restored solo node is permanently unopenable, and since ADR-0052 made every clinical body born-sealed the blast radius is the whole clinical record: ADR-0026's own "the exact outcome crypto-shredding is designed to produce, by accident." The unwrap key becomes an independent X25519 keypair with its own lifecycle, superseding ADR-0052 decision 4's derivation clause only while preserving its rationale (no new key type, store or escrow mechanism — the same op-passphrase and recovery code seal it; one printed code holds for a node provisioned by init and explicitly NOT for the decision 5 migration, which mints a second code because the signing key's is off-node by design — scoped in decision 1 rather than left to an erratum, #505); ADR-0026 decision 4 stands unchanged and gets STRONGER, since nothing now depends on the seed surviving, so its flat sentence needs no narrowing erratum — and escrow the signing seed is REJECTED as trading a data-loss hole for an impersonation hole. The secret rides the CAIRNL1 sealed export ADR-0026 point 3 already defines for non-event, non-signing-key material, so it still yields read access, never a signing identity (ADR-0026 point 3's own test); a restored node ADOPTS the exported key rather than minting one, forced by node_unwrap_key being a singleton whose registrar refuses a differing key (rotation is a separate ceremony) — which deletes the keyring a mint-on-restore design would have needed; an existing node adopts its currently-derived secret as its first independent key, lossless (no rewrap, no row migrated, the registered public half unchanged) but workable only while the derived secret is still reconstructible, so the derivation survives as the migration path alone, contained by a pinned production call-site list. Registering the public half becomes a provisioning act, not a write-path side effect — "register whatever key this signer implies, on first write" is the same coupling one layer up — and the write path verifies and fails loudly, naming the remedy, because a node with no registered key would write sealed bodies it could never crypto-shred. The export carries event_dek rows wrapped, verbatim and excludes a shredded event's DEK: a key that never crosses the restore boundary cannot be resurrected by one, which is stronger than replay and order-independent. Declared, and load-bearing: this closes the KEY, not the guarantee — a restored node recovers who it peered with and zero patients (#500, still open; the key without the bytes opens nothing. Index note, 2026-09-06: the wording accepted with this ADR said "the medium still carries the federation plane only". DR slice 2c changed the first half — the medium now carries the clinical plane with its custody — and not the second: nothing restores it, so the "zero patients" outcome stands and #500 stays open. ADR-0066's own text is immutable and carries a dated erratum saying the same) — "node-default data-at-rest keys survive" has no subject at all (no such tier exists; the export slot has no producer) and must not be read as satisfied — and cairn-sync still derives its own secret, made loud at startup rather than fixed. The export also becomes authorization-relevant in slice 2 when it gains the actor registry. Accepted (supersedes ADR-0052 decision 4's derivation clause only; upholds and strengthens 0026 decision 4; applies principles 1/4/9/12 and 0005/0011/0012/0013/0017/0026/0052/0056) 2026-08-24
0067 A restore reads the clinical plane, and what that costs: ADR-0026 decision 1's promise that "the clinical event log survives" a solo node's total hardware loss was false in two ways, each closed by a different slice — the KEY by ADR-0066 (#495), the BYTES' write half by DR slice 2c (#500, the medium became a CAIRNB3 image carrying every event_log row with its wrapped DEK), and the READ half by this one. Five decisions, all architectural rather than local. (1) The actor registry re-enters on the export container's AEAD alone — no per-row signature, the ONE part of a restore that is not verify-on-apply, accepted because whoever holds the export AND its passphrase already controls the node completely, and therefore fenced twice (refuses an enrolled node; refuses a registry holding any row not in the restored set), granted to the node role and explicitly NOT the advisory-actor role, and PRINTED to the operator — a limitation living only in a design doc is one nobody finds. The door replays rather than re-adjudicates: re-running the fresh-enrollment collision guards would refuse this node's own legitimate revoke/supersede rows, which trip them by construction. (2) Erasure does not propagate backwards into media already written, and a body shredded AFTER a capture keeping its DEK on that medium is CORRECT, not a leak — a backup is only a backup if it can restore the state at the time it was taken, and a medium that dropped the key later could only do so by rewriting a segment it has already signed, forfeiting integrity. Never filter old segments. The mirror half: a body shredded BEFORE its first capture travels as ciphertext with no key, so a restore keys custody decisions on whether a record carries a DEK, never on whether the event is sealed. Completing an erasure across backups is rotation — capture fresh, destroy old — and that interval IS the maximum time an erasure takes to complete across all copies: the clinic's policy call, not Cairn's (principle 9; ADR-0005's deletion is best-effort and declared). (3) The ceremony's order is load-bearing — node plane → custody + registry → CLINICAL plane → finalize_identity LAST — because the door wraps each DEK to the REGISTERED public half, every door resolves its author through actor_current, and identity last keeps the whole restore inside the un-enrolled fence so a catastrophic clinical failure leaves a database with no genesis, still restorable from the same medium. That is true only because the registry door is RESUMABLE, so resumability is a requirement of the ordering, not a convenience of the door. Two preconditions pinned by SOURCE guards. (4) Supersedes ADR-0026 decision 2's IMPLEMENTATION WORDING only — the built system is a medium format plus a capture/restore command, not a cold-peer daemon configuration, and its registry re-entry is not verify-on-apply; the mechanism changed, the guarantee did not. ADR-0026 decision 1's clinical promise may be cited as met from here forward; its promise 2 (node-default data-at-rest keys survive) still has no subject at all and must not be read as satisfied by this. (5) A restore-originated quarantine entry is not subject to the per-peer quota — the quota bounds a hostile peer and a restore has none, the bytes it would refuse are bytes the node is about to lose permanently, and its own "the watermark freezes instead" promise needs a cursor and a re-serving peer that a restore does not have; unbounded must not mean unreported, so the pen's row and byte totals are reported. A restore pins no re-offer floor. Accepted (supersedes ADR-0026 decision 2's implementation wording; applies principles 1/2/4/9/12 and 0001/0005/0026/0052/0066) 2026-09-10
0068 Provenance warns, never gates, on the restore path: DR slice 2a deferred "whether an unsigned segment should ever be restorable without operator confirmation" to 2d, whose design answered it in a sentence supporting two readings — "clinical segments inherit the same Provenance treatment the node plane already gets" — where the node plane's actual treatment is a print, never a block (each Provenance arm in the restore path is a println! confirmation or an eprintln! warning), so one reading was already built and the other was a blocking gate existing nowhere in the tree. ADR-0067 recorded neither, so an unsigned CAIRNB3 medium restores every patient record on a stderr warning with the reasoning written down nowhere (#571). Decision: provenance warns; it never gates, on either plane. Four reasons in binding order: (1) refusing converts a partial loss into a total one — the ruling the slice already made for a torn tail (2c reversed itself on exactly this), an Unknown(tag) plane, and a legacy CAIRNB1/B2 medium, and restore is where the trade is sharpest because an operator reaches it when everything else has failed; (2) a gate does not buy what it appears to — per-event signatures stop forgery, not omission, and chain_report treats unsigned segments as chain-verified on purpose (an unavailable signing key must not make a medium unreadable), so a silently-removed revoke or erasure.shred.asserted is invisible to a prompt that ratifies an identity, not a record set; (3) principle 3 forbids the mechanism by name (confirmation dialogs are explicitly NOT an acceptable safety mechanism), and the physical affordance is already the strongest in the system — the operator attached this medium from their own off-site store; (4) input is not ceremony — the recovery-code prompt is not a counter-example, because it asks for a secret only the operator holds and nothing can substitute for, while a provenance confirmation asks them to ratify a judgment the machine has already made and printed. The four warning arms are deliberately unequal and each names the check the operator can actually perform, including the CAIRNB3-attested arm printed to stdout rather than as a warning, because reporting it as unsigned would be a false statement to a human at the exact moment they decide whether to trust a restore. Consequences: the third human act DR slice 1's paper-parity section named does not exist, but this ADR does NOT declare M = 2 — #512's count is re-derived by the measurement it owes, against the shipped command; 2d's design test 18 is restated, not dropped. Still not true: an unsigned medium's omissions stay undetectable and the remedy is a signed capture, not a prompt — the honest surface is verify-backup, still federation-only (#567), which is where this gap belongs. Accepted (refines 0067; applies principle 3 and 0026/0067) 2026-09-10
0069 The restore takes its recovery code from a file, so a drill can be rehearsed: restore needs two secrets and treated them unalike — the passphrase for the NEW key has --passphrase and CAIRN_KEY_PASSPHRASE, while the OLD node's recovery code, the one retained off-node artifact, had no flag and no environment variable at all and was read through rpassword, which opens /dev/tty and fails on any non-tty. A piped code did not merely get ignored: the read errored, the export never opened, and the restore finished having recovered zero patients while exiting non-zero — #500's own signature, inside the mechanism built to prevent it. So a clinic could not rehearse its disaster recovery, the restore CLI could not be tested (#570), and the #512 rig had to allocate a pseudo-terminal. Decision: an optional --old-recovery-code-file <PATH> — a path, never a flag value or an env var, because the recovery code plus the medium beside it is the clinic's whole record in the clear, and a path keeps it off the process table, out of shell history and out of the environment (and composes with /dev/stdin and named pipes for free). Named "old" because this command also mints and prints a NEW code — the #512 rig matched "recovery code" and the new node's banner satisfied it two steps early. Read in the step-0 pre-flight, before a byte is minted, so a wrong path costs nothing rather than an identity; a blank file is refused, because normalize_recovery_code would turn it into an empty secret whose failure is indistinguishable from a wrong code. Rejected: refusing the flag alongside a sealed key to preserve #527/#562's "no cron-run command reaches print_recovery_code" — that note was already false (a medium with no export sibling never reaches the prompt, so a sealed restore of one has always run unattended), so the refusal would have guarded one path, keyed on the wrong signal, and cost the rig the ceremony it measures. Replaced by an IsTerminal-keyed warning that reports without claiming to prevent. Still not true: the minted code still goes to stderr on both paths (filed separately); M > N stands and #512 stays open; the time budget is not re-measured. Accepted 2026-09-11
0070 A late key reaches the chart: custody that lands after its event was admitted used to open the body and leave the chart empty (the AFTER INSERT dispatcher never fires on a re-apply) — reached by pull --full, requeue, a keyless-first restore and a strict re-submit. Both doors now run the event's heal-safe registered appliers when they newly write event_clear for an event already in the log — after the substitution guard, in the door's own posture, only if replay-eligible — through the same heal-safe dispatch re-adjudication uses; a catalog guard requires every custody-reading applier to be heal-safe, so requeue's reproject_owed retires. Refines ADR-0057 and ADR-0052. Accepted (refines 0057/0052) 2026-09-15
0071 A restore that left records behind exits INCOMPLETE: cairn-node restore could not tell a cron-driven drill — the thing ADR-0069 built the non-interactive path FOR — that three nights of charts were still on the medium. Five outcomes leave records unrestored; three exited 0 behind loud notices no wrapper reads (a torn tail, records past a mid-file chain break, records in a plane this build cannot route) and two exited 1 for a state that was not failure — one apologising in as many words ("this exit code says the restore is INCOMPLETE, not that it failed"), the other offering a remedy under a status that said the run had failed — for a vocabulary the command did not have. All five now exit 3 (INCOMPLETE), the status requeue has used since #578, printed after the whole summary; 1 narrows to FAILED — the ceremony was BLOCKED — and is checked first, since such a run also pens everything sealed. A WRONG recovery code is NOT that path (Ok(None), an honest degradation) and lands on 3 — the same status as the end state it shares — or on 0 where the medium carried no clinical records to leave behind (#613). One pure predicate over five scalars, so the status can never disagree with the notices above it; one exit vocabulary across both binaries, held by a test. Reverses 2c round 2's exit-0-on-a-torn-medium pin and nothing else of it: ADR-0068 decision 1 is about REFUSING, and a status taken after everything applicable has been applied refuses nothing. Rejected: keep 0 and record why; non-zero for a chain break only; a code per cause; and the issue's literal three-cause scope, which would have reported the most recoverable outcome as FAILED and the least recoverable as INCOMPLETE. Accepted 2026-09-16
0072 A restore loses no record silently: ADR-0071 published exit 0 as a contract, which turns every remaining way to reach it dishonestly into a defect — three were found within hours, two of them restores that exit 0 having left a record behind. (a) restore_node_event (db/009) was the ONE write door with no substitution guard: its ON CONFLICT DO NOTHING discarded a second, DIFFERENT event under an event_id already held, in silence. That door is self-trusting and its own comment already calls the medium "attacker-appendable", so an appended event reusing the clinic's peer.revoked id drops the revocation and the node comes back trusting a peer the clinic revoked, at exit 0 — and the count could never catch it, since apply_medium returns what was OFFERED. (b) A clinical event whose TYPE this build cannot classify is admitted uninterpreted (ADR-0056), counted applied, and reported nowhere — per ADR-0012 the case that ACTUALLY happens, unlike the unroutable PLANE that ADR-0071 gives exit 3. The obvious fix for (a) — port db/005's guard — would have written #608's <> fail-open into the floor a third time, so instead ONE pure cairn_refuse_substitution (db/053, SCHEMA_GENERATION 52→53, both loader lists) compares IS DISTINCT FROM for all three doors; db/009 calls it once after its branch and deliberately WITHOUT GET DIAGNOSTICS, which a later edit inside either branch would disarm silently. (b) is reported, not re-verdicted: the record IS in the log, which is exactly what exit 0 claims, and an upgrade heals it with nothing left on the medium — the explicit answer to Unrestored's "a sixth cause gets a deliberate decision, not a silent widening", and it is no. restore --help's stated limits move with the code. Rejected: the verbatim port; closing #608 outright (it inverts a live test on a path ADR-0070 just settled); db/001 placement (no generation bump, so #605 stands); refuse-but-continue on the node plane (no completeness accounting exists); exit 3 for a deferred record; widening what exit 0 claims. Accepted 2026-09-17
0073 The node plane refuses a substitution at both live doors, and pens it: ADR-0072's review found db/007's two node_event writers — submit_node_event and apply_remote_node_event, the live federation admission gate — discarding a second, DIFFERENT event under a held event_id in silence at five ON CONFLICT DO NOTHING sites (#619). Stated precisely, since #619's own scenario overstates it: its "A keeps trusting C" cannot happen, because trust_peer reads only events THIS node authored. The real costs are a silent, permanent divergence of the replicated node plane — sharpest for a dropped rival GENESIS, after which that peer's key never resolves and its every event is refused, logged as "recoverable, non-fatal" — and, at the local door, a node that keeps trusting a peer it revoked. Each door is restructured to db/009's shape: every ON CONFLICT arm falls through to ONE tail that reads the held address unconditionally and calls the shared cairn_refuse_substitution AFTER the branch — two call sites, not five; the admission gate's three clock merges fold into one, after the guard. A RAISE does not wedge the node pull — its P0001 arm skipped-and-advanced all along (a non-P0001 error does freeze it, permanently when deterministic: #621) — so the real question was what the puller does with a substitution, and the answer is pen it: the puller asks node_event whether it already holds the id under a DIFFERENT address — by STATE, never by SQLSTATE (P0001 is the contract both pull loops route on — db/001's comment above cairn_decode_hex_or_raise (#228) for the node loop, cairn-sync's refusal_is_deliberate (#267) for the clinical one — so a distinct code in the shared helper would turn cairn-sync's clinical pen into a freeze) and never by message text — and pens it whichever check refused it, since it can never apply. The scoping deny-all keeps skip-and-advance; this is the first member of §6.3's "genuinely refused history" and decides nothing else of #268. The hand-written inventory of guarded doors becomes a pg_proc catalogue rule pinned by name (five writers); the quarantine help, PullStats doc and INTEGRITY line move with the code. SCHEMA_GENERATION stays 53 (db/007 edited in place — #605 named). The lookup reads the event_id in the door's grammar (a mirror of Postgres's string_to_uuid), since the uuid crate's narrower one let an oddly spelled rival be skipped (#624 follows up). Sixteen mutations are each killed at the assertion naming their claim — among them the lookup-failure freeze turned into a skip, pinned by running the pull under a role that cannot read node_event, and addressing the whole frame, pinned by a held-and-equal refusal that must be skipped, never penned. Rejected: a distinct SQLSTATE; matching message text; five inline call sites; refuse-and-skip; all of #268 now; widening the rule to actor_event (#569); a migration only to force a generation bump. Accepted 2026-09-19
0074 A deterministic door failure is a refusal, not a fault: the node puller read the non-P0001 space as one thing — "transient, retry next cycle" — and froze the cursor under it. But db/007 failed DETERMINISTICALLY without reaching a verdict in four places on caller-supplied bytes: the event_id and target_event_id casts (22P02) and the node_event_hlc_nonneg and node_event_role_check CHECKs (23514, the one #621 did not list). Each recurs identically on every retry, so the freeze was PERMANENT — every later event on that link held behind the poison one, including that peer's own peer.revoked, nothing penned and so no ack remedy. That is #228's failure, closed then for hex only. Stated precisely, since #621's own scenario overstates it: serve streams only rows already in the serving peer's log, which passed the same casts and CHECKs, so an honest peer on the same schema cannot serve one. What remains is a misbehaving peer (which wedges only its own link, and could stall it by going silent anyway — the marginal harm is that the stall is reported as transient and never heals) and, the one that matters under principle 11, cross-version CHECK-vocabulary skew: db/009 already widened the op CHECK in place, and a future role widening would freeze every older node's link to a newer peer for ever. So: the doors become total — cairn_uuid_or_raise (on pg_input_is_valid, the cast's OWN grammar, so no second parser can drift narrower as the uuid crate did in PR #623), cairn_hlc_nonneg_or_raise (both in db/001, for #198's reason) and a role vocabulary that is ONE function the table's CHECK itself calls — at all three signed-bytes doors; and the puller splits the non-P0001 space: this node's own trouble (classes 08 40 42 53 55 57 58, plus XX001/XX002 claimed by full code, or no SQLSTATE at all) still freezes, everything else is PENNED in the database's vocabulary, ack-able and auto-released if a later build admits it. An unknown code counts as deterministic on purpose — a wrongly penned event is delayed and self-healing, a wrongly frozen link is permanent. A malformed field joins the SKIP class (a verdict that heals on upgrade), so ADR-0073's substitution carve-out stands alone and #268 is untouched. The CHECKs stay: the door is the legible refusal, the constraint is the floor for raw SQL (principle 12). SCHEMA_GENERATION stays 53 (db/001, db/007 and db/009 edited in place). Fifteen mutations, fifteen killed. Rejected: a distinct SQLSTATE (it would turn cairn-sync's clinical pen into a freeze); re-validating in Rust (two parsers are two protocols); an EXCEPTION WHEN check_violation handler (db/001's stated objection — it relabels unrelated internal errors); either half alone. Filed: #626, the clinical plane's identical shape. Accepted 2026-09-20
0075 The step-3 prompt is a nudge, not a completeness claim: slice 2c measured the registration funnel's five-row step-3 prompt over 50,000 real names — db/046 is a disjunction, so a search returns ~100 candidates and 92% of registrations signed incomplete: true, a flag carrying no signal (#671). Ranking put an exactly-typed duplicate first every time, but a wrong-DOB duplicate was shown 1 in 5 (after this ADR's ranking, 500/500; a surname typo with a simply wrong DOB 203/500 — the repair path's residue). Maintainer's clinical judgement: duplicates are common (typos in hard names), the person at the desk cannot be made to browse, so accept duplicates and make repair (link, principle 2) easy — the safety measure is how fast a duplicate is FOUND, and the commit-time check / worklist / link gesture are filed as the next thread. So: the prompt is best-effort and truncation is normal (the funnel design's revisit condition is retired); search.incomplete means only that the SEARCH was partial — ADR-0061's own meaning, restored (no wire change; pre-2026-09-26 fixture bodies may carry true for truncation); truncation is shown on screen, never signed; ranking gains an identifier-match key, a callsign-typed-whole key, name tokens matched (exact or a typed prefix, as db/046 matches), a DOB near-miss and an exact-token tie-break, reorder only (figures drawn from the name pool's namesake-heavy first rows; representative re-run #685). Rejected: signing a 'withheld by rule' weak tier (it withholds the wrong-DOB duplicate); a signed count; a new contradicting field; raising PROMPT_CAP; making the prompt a gate. Accepted 2026-09-26
0076 Duplicate repair: a linked chart reads as one, and a human's judgement outranks a machine's: ADR-0075 made repair by link the safety answer to duplicates; the survey before designing it found a link repaired nothing a clinician could see (no read follows a link), "different people" had no home, a peer's un-attested matcher link could override a reviewer's unlink (latest-HLC-wins), and #334's cross-chart reconciliation group became the normal post-link case. So: a linked chart reads as one combined record (rows name their source chart; demographics not combined); writes stay per chart; chart commands name the displayed SET and refuse on change; "different people" is a human-attested unlink (never a displayed-not-chosen candidate); patient_link ranks attested before un-attested, then HLC (convergent; a machine never overrides a human either way); the step-3 prompt shows one row per person and signs every member chart (displayed_count counts charts); a per-node matcher worker proposes on every identity-evidence change and never auto-links. No wire change, no new event type. Rejected: opened-chart-only view; safety-content-only combining; per-chart front-door rows; node-local rejection; a new distinct algebra member; running the matcher from the window; a Rust/SQL scorer; commit-time auto-link. Accepted 2026-09-27
0077 An unlink may be filed under the record it was judged from: R2a refused an unlink unless a subject chart was held here, so #699's A–B–C case (A held, the far link B–C wrong, B and C unregistered here) could not be split from the record that showed it. Decision (maintainer, 2026-09-28): an unlink where neither subject is held is filed under the opened chart when it is held and its record reads both subjects (re-checked under db/018's link lock before signing); a link never is; the pair is always the payload's; an opened chart unrelated to the pair is refused; "still joined?" is asked of the subjects (high ∈ person_charts(low)). An audit found no reader takes the pair from the envelope (only db/005 step 8b's admission and db/048's chart-scoped grade key on it): no wire change, no new event type. Rejected: filing a link this way; ignoring a stray --from; a per-member unlink. Accepted 2026-09-30
0078 A machine's unlink does not settle a possible duplicate: the matcher skips a pair only when it reads as one record or has an ATTESTED patient_link row (db/057's rule; #741), so an un-attested unlink — an agent's, or any unreviewed writer's — no longer keeps a pair off the banner: the pair is proposed, auto-apply sends it to review rather than linking over the unlink, and the banner and worklist show it as a dispute. A pair joined by an un-attested link reads as one record and is R1b's doubt to show, not this rule's. Accepted (supersedes ADR-0076's skip-rule wording only) 2026-10-08
0079 A date of birth one keying error away is not a contradiction: the DOB comparator graded any differing digit DISAGREE (−4), so "same name, typo'd DOB" scored below the review floor and a real duplicate could be withdrawn unseen. Decision (maintainer, 2026-10-11; decisions 3 and 4 revised 2026-10-12): a new NEAR level for two full dates one keying error apart (one digit, adjacent digits swapped, day and month swapped, years one apart), weighted +2 (weak evidence for), recorded as dob_near_miss evidence; a DOB graded NEAR or DISAGREE never auto-links (db/016 vetoes only a clash of two verified DOBs); a name+monthday blocking pass for year typos on common names; the default comparator profile's behaviour (ADR-0014), not core policy. Measured: missed true duplicates 351 → 266 of 2 000, false auto-links unchanged. Rejected: shared first-or-surname + DOB within a year and a day (~2 false proposals per registration at 100 000 charts); reusing PARTIAL; a banding rescue over −4; pricing NEAR at 0; capping only NEAR. Accepted 2026-10-12
0080 A connect replays the schema only when it changed: the loader replayed every db/*.sql on every connect, and an idempotent replay still took ACCESS EXCLUSIVE on event_log, the identity tables and 34 views (97 strong locks measured), so a connect during any long read stalled clinical writes (#726). Decision (maintainer, 2026-10-12): stamp node_schema with the loader's identity and a sha2-256 of exactly the files it replayed (db/058); skip the whole replay when generation, loader and hash all match; every doubt replays; a content hash, not the generation, so an in-place function fix still reaches every database; the heal stays generation-gated (ADR-0057). Tests keep their isolation via test_serial_guard. Rejected: per-statement catalogue guards (cannot cover views); a generation-number gate (strands in-place fixes); a hash without the loader identity (cairn-sync's subset stamp); changing the matcher (its ACCESS SHARE was never the problem). Accepted 2026-10-12

Template

# ADR-NNNN — <title>

- **Status:** Proposed | Accepted | Superseded by ADR-NNNN
- **Date:** YYYY-MM-DD
- **Supersedes:** ADR-NNNN (if any)

## Context
<the forces at play: the problem, constraints, and what made this a real decision.>

## Decision
<the choice, stated plainly.>

## Consequences
<what becomes easier, what becomes harder, what we are now betting on, and how we'd know if the
bet fails.>