Comprehensive code + ADR review — 2026-07-02¶
Scope: the full repository as of dddd494 — the SQL enforcement floor (db/*.sql),
the Rust crates (cairn-node, cairn-event, cairn-sync, cairn_pgx), the Python
matcher, and ADRs 0001–0039 — reviewed adversarially for logic flaws and design
footguns (poor early decisions that get expensive later).
Method: seven parallel review passes (SQL floor; cairn-node; event-format core; ADRs 0001–0018; ADRs 0019–0039; spec-vs-implementation audit; matcher), findings cross-checked and the headline claims re-verified directly against the code before inclusion. Conflicting findings between passes were adjudicated by reading the code (noted inline where it happened).
Overall verdict: the foundations are unusually strong — the crypto engineering, the grant-floor discipline, the sign-the-bytes-verbatim canonicalization, and the ADR log's honesty about its own limits all hold up under adversarial reading. The trouble concentrates in three places: (1) the clinical sync path is a second write door that bypasses the floor and can silently lose events, (2) a cluster of settled-ADR promises the code doesn't yet keep, with no deferral note anywhere, and (3) a set of wire/design decisions that are one-line fixes today and unretrofittable in a year.
Disposition of every finding (fixed in this branch vs. filed as a GitHub issue) is in the Disposition table at the end of this document.
A. Verified code bugs¶
A1 — CRITICAL: cairn-sync can silently and permanently lose clinical events¶
crates/cairn-sync/src/main.rs:742-766. Every apply_signed error — including
transient DB errors and deterministic insert failures on validly signed events — is
counted as a "verify failure" and skipped, while the watermark advances to the max HLC
of the events that did apply. Batch [E1@10 fails transiently, E2@20 succeeds] →
watermark 20 → E1 is never offered on this link again (server ships
WHERE hlc >= watermark). The only trace is a counter in a log line. This breaks
set-union, the invariant the whole architecture rests on. The node-event plane in
cairn-node/src/sync.rs got this exactly right (advance only on clean EOF), so the
correct pattern already exists in the repo.
Fix direction: advance only past the contiguous applied prefix; treat only
BadSignature-class errors as skippable, and quarantine those durably.
A2 — HIGH: the sync apply path is an unguarded second write door¶
crates/cairn-sync/src/main.rs:167-226 verifies the signature in Rust, then
raw-INSERTs with owner privileges. Skipped for every replicated event: actor
enrollment, fail-closed event_type_class classification, the attestation gate on
suppressing events, the demographic hard-twin rule, and — because the
ON CONFLICT DO NOTHING at line 185 has no conflict target — the event-id
substitution check that submit_event explicitly raises on (db/005:160). A
hostile peer can sync in an un-attested visibility.suppress signed by an unenrolled
key; two nodes holding different bytes under one event_id diverge forever with no
alarm. ADR-0021 puts the floor below the inter-node path; today it only guards
local authors. The node-event plane already has its in-DB gate
(apply_remote_node_event) — the clinical plane needs its sibling.
Related sub-findings folded into this item:
- Twin asymmetry (M8): three divergent twin-fallback renderers (Rust
plaintext_twin(), resolve_twin in cairn-sync, SQL cairn_twin_skeleton); a
twin-less demographic event is rejected by submit_event but accepted (with a
derived twin) by apply_signed.
- A5b: projection triggers can RAISE at apply time (see A5) — apply-path policy
must clamp+flag, never veto a validly-signed event.
A3 — HIGH: t_effective ≤ t_recorded is enforced nowhere¶
ADR-0003 calls this "an envelope invariant… rejected/flagged at write"; ADR-0022 step
5.3 lists it in the door. There is no CHECK, no submit validation, nothing in Rust,
and no deferral note. A signed event claiming t_effective = 2031-01-01 is accepted
silently. This is the project's own falsification guard, and the one settled-ADR
promise flatly contradicted by the code.
Related: t_effective is a free-form string cast per-node under local TimeZone
GUCs (cairn-sync main.rs:183, db/005:155) — an offset-less timestamp is a
different UTC instant on different nodes, and a malformed one is a poison event
that (combined with A1) vanishes silently.
A4 — HIGH: patient_identifier is the one non-convergent projection¶
db/010_demographics.sql:97 uses ON CONFLICT DO NOTHING — first-applied wins,
and "first applied" is node-local, not a function of the event set. Two assertions
normalizing to the same match_key but differing in value/provenance leave
different surviving rows on different nodes, and the match-veto (db/016) reads
exactly those columns — so two honest nodes can compute different hard-veto
verdicts. Every other projection (011–014, 018) uses the order-independent
DO UPDATE … WHERE tuple > pattern; this one should too.
A5 — HIGH: identity-linkage projection — race + config-dependent RAISE¶
db/018_identity_linkage.sql:154-232. (a) cairn_recompute_component is a
non-serializable read-modify-write under READ COMMITTED — concurrent link(A,B) and
link(B,C) can leave person_member missing the union. (b) The oversize guard
RAISEs based on a node-local GUC (cairn.max_component_size), so a node
configured with a smaller cap can permanently refuse a validly-signed event its peers
accepted. Projection maintenance must never veto event application.
Fix direction: transaction-scoped advisory lock keyed on the component before recompute; clamp-and-flag instead of RAISE on the apply path (the RAISE redesign is folded into the A2 issue).
A6 — MAJOR: actor enrollment rests on implicit default privileges¶
db/004_actors.sql:50-59. No explicit REVOKE exists on actor_event /
enroll_actor — safe today only because enroll_actor is invoker-rights and no
grant exists. One future GRANT, or copy-pasting the SECURITY DEFINER pattern from
the other doors, silently collapses the admission gate (self-enroll any pubkey →
"legitimately signed" events). No negative test exists, unlike the raw-INSERT floor
tests.
A7 — MAJOR (cairn-node): write-side limits missing (three findings, one theme)¶
- (a) No event-size ceiling at any admit door vs hard 8 MiB caps on every read
path (
sync.rs:84read-only;medium.rs:197debug_assert!only; nooctet_lengthcheck in db/005/007/009). One oversized event wedges sync at that seq forever, andbackup_tooverwrites the previous good medium before the read-back check fails. - (b)
pull_intohas no I/O timeout (sync.rs:322-334) — a stalled pinned peer freezes the run loop including trust refresh, so revocations stop taking effect on the running daemon. Same forserve(no handshake timeout / connection cap). - (c)
LocalStatesilently drops unknown CBOR fields on old readers (localstate.rs:57-96) — a future restore quietly discards content (e.g. episode DEKs) the module's own comments promise to fail loud on, in the format declared can't-retrofit.from_cbornever checksversion.
A8 — MAJOR: the cairn_pgx #[pg_test] fixtures don't compile¶
extensions/cairn_pgx/src/lib.rs:60,105 — both EventBody literals lack the
plaintext_twin field added to the struct (E0063 under the pg_test feature). The
in-DB verification safety net hasn't been runnable since that field landed.
A9 — Matcher findings (advisory tier, but they shape the human worklist)¶
- (a) HIGH: name token-count mismatch grades DISAGREE
(
comparators.py:184-185) — "Mary Smith" vs "Mary Jane Smith" takes a −2.0 clash penalty when every token matches; systematically punishes multi-token naming cultures, the population ADR-0014 exists to protect. Subset-match should grade PARTIAL. - (b) HIGH: a hard-vetoed pair whose veto-subject fields drag the total below the
review threshold is persisted nowhere (
banding.py:64-68) — an effective auto-reject, which ADR-0014 §6 forbids. Force REVIEW when a veto coexists with strong positive evidence. - (c) MED: no Unicode NFC normalization anywhere (adapter
lower().split(), SQLlower(value)) — NFD vs NFC of the same name grades DISAGREE and breaks blocking. NFC +casefold()is culture-neutral canonicalization. - (d) MED:
matcher_versionpins only the weights (banding.py:71-84) — thresholds and comparator config are invisible to the ADR-0011/0029 recall handle. - (e) MED:
parse_dobaccepts any integer year (adapter.py:33-47) — a two-digit-year import fabricates the largest DISAGREE penalty in the table. - (f) MED-LOW:
"unknown"sex sentinel fabricates evidence in both directions (EXACT on unknown-vs-unknown, DISAGREE on unknown-vs-male). - (g) LOW (batched issue): stale proposals never withdrawn on re-score below
review; name provenance rank is max-across-history (inflates evidence toward
false-merge); blocking eval KeyError on non-empty DBs; the synthetic eval's
_repairmakespair_completenessstructurally blind to the blocking blind spot.
A10 — Other verified floor/daemon items (smaller)¶
- Recall epoch bug:
db/006_recall.sql:26joinsactor_current, so a contamination cascade querying a superseded epoch under-selects (documented in code; issue-worthy per house rule 5). - Suppression owner-gate open:
db/005:114-130— any enrolled human can suppress any author's event (DEFERREDcomment; ADR-0022 step 5.5). - TRUNCATE not covered by append-only triggers (defense-in-depth gap).
blob_storecomment overclaims (db/003:26-30): the CHECK verifies length only, not BLAKE3-vs-address (pgcrypto has no BLAKE3); verification is L2-side.patient_link-BFS missing index on thehighside (db/018).- Attestation (M7):
verify_attestationnever bindsattester_key_idto the verifying key (the event-sideSignerKeyMismatchgate has no attestation counterpart); the token is verified then discarded — the responsibility proof cannot be re-verified later. - Winner-tiebreak residual: adjudicated —
asserted_originis the HLC node id, so honest nodes can't tie; but nothing enforces per-node HLC-tuple uniqueness against a buggy client. Appending a total-order key (thevaluePK member) to the winnerORDER BYs makes convergence unconditional. - cairn-node minors: signing-key seed written with default file perms (not
0600); SPKI algorithm OID unchecked in
peer_pubkey_hex;restorecan exit non-zero after fully succeeding (tty-less passphrase prompt); single-entry passphrase atinit/seal-key;db.rshardcodesNoTls; pairing offers never expire; HLC merge has no drift clamp;hlc_counteris 32-bit; sync-cursor ratchet has no sanity clamp; transient-vs-gate errors conflated inpull_intoskip logic. - cairn-sync (M9/M10): unbounded
EventsAfterbatch + 30 s timeout → permanent no-progress loop on large backlogs (paginate); first-writer-winsbyte_lenonblob_note_referencelets one bad reference permanently wedge a blob fetch (the server ships the true total in every slice frame; client ignores it).
B. ADR/design footguns — cheap now, expensive later¶
B1 — The erasure ladder doesn't compose with three neighboring ADRs¶
ADR-0005's crypto-shred meets three plaintext leaks nobody owns: the safety projection (ADR-0006) replicates in the clear and no ADR says what happens to it on shred (shred it → the Rh-warning use case dies; keep it → rungs 2–4 are incomplete); the legibility twin (ADR-0012) — no ADR states whether it sits inside the DEK boundary, and outside it the shred is theater (twin + FTS + RAG embeddings survive); attachments (ADR-0013) are content-addressed by plaintext hash, so a blob written unencrypted can never be retro-sealed — the signed event immortalizes the plaintext digest. Rung 2's "no discoverable institutional record" is unachievable as written: the envelope is plaintext by construction and its rows can't be removed (the rescue path — pseudonymous registration from episode start — is a day-one precondition the ADR doesn't wire in). Since seal-at-write is the only window forever, the encryption-default decision is the classic cheap-now/impossible-later call. Needs one composing ADR that owns the intersection.
B2 — The revocation cascade trusts the attacker's own clock¶
ADR-0018's compromise boundary keys on t_recorded — stamped by the authoring node.
A revoked actor backdates fabricated events to before T and launders them through any
peer that hasn't heard the revocation; they arrive everywhere marked "authored while
credentialed", permanently. Fix is a day-one shape: record
first-seen-by-honest-node at ingestion and flag
max(t_recorded, first_receipt) > T as a clash (the ADR-0003 pattern).
Unretrofittable onto years of already-ingested events.
B3 — Human key custody is unspecified, and the default is node custody¶
ADR-0011 node-binds agent keys explicitly; for humans nothing is said, and ADR-0008's badge-tap flow implies the node signs on the clinician's behalf — meaning the node can mint "Dr X attested this" for anyone enrolled, and the likeliest authorship dispute is clinician-vs-institution. "Signature proves origin" quietly degrades to "the node asserts origin." Decide now (it constrains the ADR-0008 hardware story).
B4 — Two wire-freeze items in ADR-0015's orbit¶
- (a) COSE_Sign1 is single-signer, yet "native multi-signer support" is cited as a reason for choosing COSE — either ratify COSE_Sign or amend to one-signature-per-event with co-signing via overlay; the rationale as written is wrong either way.
- (b) No domain separation between the three signing contexts (event /
attestation / pairing bundle) — same key type, empty
external_aad, no context marker; cross-context replay fails today only by structural luck, and additive evolution erodes that. Both invalidate existing signatures if fixed after freeze.
B5 — ADR-0028's closed role enum has no unknown-member story¶
A 2031 node adds a bearing role; a 2026 node can't classify it into the
bearing/contributory partition everything safety-relevant branches on — a
properly-vouched note projects as un-vouched AI content (a precise untruth).
Encode the partition in the role value (bearing:…/contrib:… or a signed
per-contributor bit) and define an explicit "vouching-unknown" degrade. An
enum-spelling decision, free today.
B6 — Demographic recency is keyed to the wrong time axis¶
The winner views order by last_hlc_wall — when the assertion was typed
(t_recorded), not when it was true (t_effective). A clerk transcribing a 2015
passport today displaces last month's patient-stated post-divorce name; a
legacy-import 2019 address outranks the current one — ADR-0038's own headline use
case (post-discharge letters, ambulance dispatch) goes to the wrong place. The
project built the bitemporal model for exactly this distinction and didn't consult it
here; neither ADR discusses which axis recency means.
B7 — ADR-0016's essential tier contradicts ADR-0001 and skips the threat model¶
If the tier is a synced projection, the multi-master merge problem ADR-0001 designed away is back; if it's an event subset (as the boundary rule implies), "current state, not history" is false as a storage claim, sizing is off by up to an order of magnitude for elderly polypharmacy patients, and there's no bootstrap/compaction story. Meanwhile the population's problem lists replicate to the weakest node in the federation with only legal (not mechanical) custody enforcement.
B8 — Smaller design items (one sentence each in their ADR/spec homes)¶
- Nothing forbids CDS from reading administrative sex (trans-pregnancy/teratogen failure) — one normative sentence, enforceable via the ADR-0023 conformance suite.
- Addresses have no temporary/validity semantics ("staying at my daughter's" wins recency forever — ADR-0038).
- Mid-episode name change desynchronizes from printed wristbands/labels — the wrong-chart failure ADR-0008 exists to prevent, unnamed in ADR-0036.
- The medico-legal export (ADR-0019) doesn't bundle the ADR-0027 time-anchor proofs, so it can't prove the record wasn't authored last week — the accusation it exists to defeat.
- ADR-0003's hard reject-at-write meets the RTC-less Pi (cold boot with stale clock flags every honest morning note as falsification); ADR-0027's graded interval is the fix but ADR-0003's normative wording stands unamended.
- ADR-0021/
language-substrate.mdname RLS as part of the present-tense floor; zero RLS exists (ROADMAP correctly lists it as Phase-2). - ADR-0005 rung-2 as written promises what the plaintext envelope cannot deliver (see B1).
- Notification ladder (ADR-0009) "bottoms out in a reachable human" overstates — routing guarantees an inbox, not a human; the out-of-band seam (SMS/phone) is the actual paper-parity counterpart of the telephone callback.
- Matcher normalizer version skew across nodes fires spurious vetoes (ADR-0033/0035) — safe but silent workload; require output-stable normalizer evolution.
- Projection rebuild cost over decades is unmeasured (add to benchmark matrix).
- Attachment descriptor metadata replicates in clear (apply the ADR-0006 coarsening ladder to it).
- Attachment-reference field names disagree between
db/001docs (digest) anddb/005code (digest_hex) — the one shape declared can't-retrofit.
C. What held up under adversarial review¶
The seal/keystore work (Argon2id + XChaCha20, compile-pinned zeroization discipline,
tamper tests on both recipients), fsio::atomic_write, the backup
verify-before-write ordering, the mutual-pinning transport with resumption disabled,
the sign-the-bytes-verbatim canonicalization (which structurally kills the class of
cross-node digest-divergence bugs), the signer_key_id↔verifying-key binding, the
grant floor with pinned search_path on every SECURITY DEFINER door, the append-only
triggers, the node-event admission gate and advance-only cursor, ADR-0031's surrogate
confinement (verified: nothing leaks), the ADR-0036/0037/0038 winner SQL matching the
ADRs exactly, the blob tier's per-slice + whole-blob verification, and the matcher's
core no-data-is-never-disagreement scoring. ADRs 0002, 0004, 0007, 0010, 0014, 0017,
0020–0023, 0026, 0029, 0030, 0032, 0034, 0035 read as internally sound.
Disposition¶
Fixed in this branch (with tests where a harness exists), or filed as a GitHub issue.
| # | Finding | Disposition |
|---|---|---|
| A1 | cairn-sync silently loses events (watermark past failures) | Fixed — contiguous-prefix watermark + BadSignature-vs-transient classification (cairn-sync/src/main.rs) |
| A3 | t_effective ≤ t_recorded ceiling unenforced |
Fixed — ceiling check in submit_event + test (db/005, demographics.rs) |
| A4 | patient_identifier non-convergent (first-apply-wins) |
Fixed — HLC-deterministic DO UPDATE + convergence test (db/010, demographics.rs) |
| A5a | Linkage recompute race | Fixed — transaction advisory lock (db/018) |
| A6 | Actor-enrollment gate rests on implicit privileges | Fixed — explicit REVOKEs + negative test (db/004, floor_enforced.rs) |
| A7a | No write-side event-size ceiling | Fixed — cairn_max_event_bytes() at all admission doors + serve-side skip (db/001,005,007,009, sync.rs) |
| A7b | pull_into/serve no timeout; no session cap |
Fixed — with_io_timeout on every net step + serve semaphore (sync.rs) |
| A7c | LocalState silently drops unknown fields |
Fixed — deny_unknown_fields + version gate + tests (localstate.rs) |
| A8 | cairn_pgx test fixtures don't compile | Fixed — added plaintext_twin; cargo check --features pg_test now clean (cairn_pgx) |
| A9a | Name subset graded DISAGREE | Fixed — subset→PARTIAL + tests (comparators.py) |
| A9b | Veto buries its own strong-evidence pair | Fixed — veto+identifier-EXACT forces REVIEW + tests (banding.py) |
| A9c | No Unicode NFC normalization | Fixed — NFC+casefold in adapter + SQL tokenizer + generator mirror + tests |
| A9e | parse_dob accepts 2-digit year |
Fixed — 4-digit-year gate + tests (adapter.py) |
| A9f | unknown sex sentinel fabricates evidence |
Fixed — sentinel→absence + test (adapter.py) |
| A10 | Winner tiebreak not total | Fixed — appended value/display/use_key; collation caveat → #69 |
| A10 | patient_link high-side index |
Fixed — partial index (db/018) |
| A10 | blob_store comment overclaim |
Fixed — comment + constraint renamed (db/003) |
| M7 | Attestation attester_key_id unbound |
Fixed — key binding + test (cairn-event/src/lib.rs) |
| H3 | Sync silently absorbs event_id substitution | Fixed — substitution guard in apply_signed (cairn-sync) |
| L12 | Signing-key file world-readable | Fixed — 0600 perms (cairn-sync) |
| — | SPKI algorithm OID unchecked | Fixed — Ed25519 OID pin (transport.rs) |
| A2 / A5b / M8 / H4 | Clinical sync bypasses the floor; apply-path RAISE; twin triple-impl; t_effective TZ | Issue #91 |
| B1 | Erasure ladder composition | Issue #92 |
| B2 | Revocation cascade trusts author clock | Issue #93 |
| B3 | Human key custody unspecified | Issue #94 |
| B4 | COSE single-signer + domain separation | Issue #95 |
| B5 | Closed role enum unknown-member encoding | Issue #96 |
| B6 / M4 / M5 | Demographic recency time-axis, temp address, name display | Issue #97 |
| B7 | Essential tier vs ADR-0001 + threat model | Issue #98 |
| A10 | Suppression owner-gate + recall epoch | Issue #99 |
| A9d | matcher_version recall-key completeness |
Issue #100 |
| M9 / M10 / blob | Sync pagination, blob byte_len wedge, in-DB BLAKE3 | Issue #101 |
| A10 misc | cairn-node/cairn-sync operational minors | Issue #102 |
| B8 | Clinical-safety prose (CDS sex, export anchors, RLS prose, …) | Issue #103 |
| A9g | Matcher stale proposals, provenance-rank, eval blind spot | Pre-existing #79, #84 |
| — | TRUNCATE guard on append-only tables | Intentionally not fixed — owner-gated by design (would break the owner reset path); noted in #102 |