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-NNNNand the new oneSupersedes: 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 |
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.>