Skip to content

ADR-0062 — The sensitivity stream and the inverted unknown

  • Status: Accepted
  • Date: 2026-08-10
  • Derives from: ADR-0006 decision 3 (sensitivity is a graded, multi-source, append-only stream; the effective grade is a projection; declassification is an authorized overlay, never an erasure) — this ADR decides the six things ADR-0006 left open, plus four more the implementation forced
  • Applies: principle 2 (never merge, always link; never erase, always overlay) · principle 4 (acknowledged uncertainty, ADR-0003) · principle 9 (policy-neutral infrastructure, ADR-0005) · principle 11 (additive-only evolution, ADR-0012) · ADR-0043 (agent advisories are dismissable) · ADR-0045 (collation-free tiebreak) · ADR-0048 (twin/floor-check registry) · ADR-0052 (born-sealed bodies; §2's plaintext list; §9's custody-relative commitment) · ADR-0053 (per-write human authorship) · ADR-0056 (the floor gates effect, not presence) · ADR-0057 (registered apply dispatch) · ADR-0060 (a local-authoring rule is never a wire rule) · ADR-0061 decision 4 (the authorship gate that was refused, and why this one is not that one)
  • Canonical spec home: identity §5.9
  • Errata: E1 (under decision 1) and E2/E3 (under decision 7), appended 2026-08-10 — E1 and E2 after the implementation review found two passages describing code that does not exist in the shape stated, E3 after the final whole-branch review added a third local-door ceremony judgement (a chart-wide grade must name its own chart). Each is a marked blockquote immediately below the passage it corrects, the original wording is preserved above it, and no decision content changes — see the errata rule in README.

Context

§5.9 has been settled architecture since v0.8 and unbuilt ever since. ADR-0006 decision 3 fixes its shape — graded, multi-source, append-only assertions; the effective grade is a projection; the highest standing assertion wins; declassification is an authorized overlay, never an erasure — and deliberately stops there, because what is confidential is cultural, regional and personal, and Cairn ships the mechanism rather than the policy.

Building it (issue #232) made two things clear.

#232 is four subsystems, not one, and only the first is decided here:

Piece State
A the sensitivity stream: graded append-only assertions + the effective-grade projection this ADR
B safety-projection emission (de-identified class + severity, coarsened by the grade) filed; carries #294
C sequester: custody narrowing (re-wrap / withdraw DEKs) filed; blocked on #231
D break-glass: audited key-use, partition-honest filed; blocked on C

Part A enforces nothing, and saying so is part of the decision. It computes and reports a grade; nothing withholds content on the strength of one. That is not an unfinished edge — it is the only honest place to stop. A projection-layer filter with no custody narrowing beneath it is theatre one layer up (principle 12: the floor is in the database, and a client talking raw SQL walks straight past anything above it). And C genuinely cannot be built yet: cairn-sync serve verifies an unwrap-key certificate against its own signature and self-consistency only, never against the admitted-peer trust set (#231), so transport is today the sole gate on read-custody. Narrowing a body's custody to two named clinicians while that hole stands is defeated by asking the serve port for the DEK.

The second thing: ADR-0006's shape is under-determined in ways that each have a safe answer and an unsafe one that looks identical in review. Those are what follow.

Decision

1. Three subject granularities; the effective grade is the max over all three

An assertion names exactly one subject: an event, a thread (today a medication_id), or a patient (the whole chart). The effective grade of an event is

effective(E) = max-by-rank over standing assertions on { E, E's thread, E's patient }
standing     = asserted AND NOT withdrawn

with ties broken on content_address — a BYTEA multihash, collation-free per ADR-0045/#115. The tiebreak decides only which assertion is named as the reason; the grade itself is order-free.

Erratum E1 (2026-08-10) — factual; the decision is unchanged. The rule as written above is incomplete, and this ADR is its only why home, so a reader implementing from it alone would rebuild a fail-open the code deliberately closed. cairn_effective_sensitivity (db/048 section 11) carries a fourth arm beyond {E, E's thread, E's patient}: an assertion that cannot be matched to a subject on this chart still coarsens chart-wide, bounded by the querying event's own envelope patient_id. Three shapes take that arm — an unrecognised subject_kind (a future peer's episode, which ADR-0056 requires the floor to admit); a patient-kind assertion whose subject_id names a different patient; and an event-kind assertion naming an event not present on this chart.

It exists because the earlier shape failed open. A mis-targeted assertion — a UI bug, a typo, a hostile peer — matched none of the three arms and therefore contributed nothing, silently withholding protection, while an entirely unrecognised kind correctly coarsened. That reserved the safe behaviour (coarsen on confusion) for kinds a future peer invents and withheld it from a kind we already know, mis-used. An assertion naming something we cannot match here was still an attempt to protect something, so it coarsens rather than evaporating — the same direction every other decision here takes.

The third shape includes an event that has simply not replicated yet, and that is deliberate. Set-union sync has no ordering, so an event-scoped assertion can arrive before the event it targets — the same arrival-order independence decision 3 states for withdrawals. On a partially-replicated node that transiently coarsens the whole chart, and it self-resolves the moment the target event lands, at which point the row moves to the precisely-targeted event arm. The code carries an explicit do not narrow this arm comment: there is no local signal that distinguishes "not yet arrived" from "never will", and guessing wrong in that narrowing is the disclosure direction the arm exists to prevent.

Max is the whole reason this converges. It is commutative, associative and idempotent — a join-semilattice, i.e. a grow-only CRDT — so set-union sync converges on the grade with HLC ordering mattering not at all. Two nodes that receive the same assertions in opposite orders compute the same answer, with no last-writer-wins rule to get wrong. (The convergence test names given equal custody in its title, and decision 9 is why.)

Inheritance is computed at READ, never backfilled at write. Grading a thread therefore covers events authored before the grading and events authored after it, with no migration, no re-signing, and nobody having to remember. That is what makes the mechanism usable by a clinician who realises three weeks late that a thread should have been confidential from the start.

And uncertainty can only ever protect. Every unknown ranks MAX (decision 2) and combines by max, so doubt anywhere in the chain raises the grade. There is no path through this computation where confusion lowers protection.

2. An unrecognised grade ranks MAX — deliberately inverting the clock_grade precedent

'routine' → 0 · 'sensitive' → 10 · 'restricted' → 20 · 'sequestered' → 30 · ELSE → 2147483647

Open TEXT, no CHECK domain: a future grade from an upgraded peer is admitted verbatim (principle 11, additive-only). Gaps of 10 leave room to interpose deployment terms later without renumbering.

The ELSE is the decision. It inverts cairn_clock_grade_rank's ELSE 0 (db/040_clock_confidence_grade.sql), and the inversion is load-bearing rather than an inconsistency:

  • In db/040, an unrecognised value ranking 0 withholds reject power. The worst case is that a peer's newer clock grade fails to reject something. Safe.
  • Here, an unrecognised value ranking 0 would withhold protection. An older node reading a peer's newer protected-witness grade as "not sensitive" emits an uncoarsened safety projection and renders the body in the clear — a leak on exactly the events that most needed protecting.

The dangerous property is that the wrong answer looks right: it matches the established pattern one migration over, so a future reviewer "fixing" the inconsistency would reopen the leak while believing they were tidying up. The failure mode is chosen to be over-coarsening (honest degradation, repaired by upgrading the node) and never disclosure (unrecoverable). This is ADR-0006's own when unsure, err toward essential, in the confidentiality dimension. The inversion is stated in a shouting comment at the function for that reason.

Absence is not unknown, and the distinction survives into the code. No assertion at all contributes nothing and reads as routine; only an unparseable or unrecognised grade value ranks MAX. Collapsing the two would make every event in every record maximally sensitive. This is principle 4's not-yet-asked versus unknown — the same distinction the fourth founding principle exists to keep, applied to a rank function.

3. Declassification is withdraw-by-reference, evaluated as a set difference at read

A sensitivity.grade-withdrawal.asserted names the specific assertion's content_address. That assertion stays in the log, readable and re-assertable. Nothing is erased (ADR-0006 decision 3), and the chart history shows that the grade was lowered, by whom, and why.

Standing is a set difference evaluated at READ, never a row deletion at apply, and that is not a stylistic choice. Set-union sync has no ordering, so a withdrawal can arrive before the assertion it withdraws — and it must still take effect when the assertion lands. A delete-at-apply implementation would silently drop such a withdrawal on the floor and leave a grade standing that a human had accountably removed. So: no foreign key from withdrawal to assertion, no delete, and one function (cairn_sensitivity_standing) as the single definition of what "still applies" means. Same arrival-order independence as ADR-0059's a strike NULLs the anchor rather than deleting the row.

The corollary of no-FK is that a withdrawal is only as well-targeted as its author. Standing therefore pins the withdrawal's own patient_id as well as the target address: a content address being globally unique makes a withdrawal unambiguous about which assertion it names, but does not make a cross-chart withdrawal impossible, and the remote door is deliberately lenient (decision 7). Without that pin, a withdrawal authored on chart B naming chart A's assertion strips chart A's protection — the unrecoverable direction.

4. Sensitivity assertions are plaintext by necessity — extending ADR-0052 §2

ADR-0052 §2 enumerates what stays unsealed because the machinery binds on it. Sensitivity assertions join that list, and the reason has the same shape as the shred tombstone's:

A node must READ the grade in order to coarsen, and coarsening is exactly what a node holding no custody of the graded body must still do.

Sealing the grade under the key it governs is circular — the node that most needs to know "treat this carefully" is precisely the one that cannot open it. So both event types are born unsealed, replicate unconditionally (dial 1, §5.9), and are readable everywhere the chart is.

This is not a weakening of ADR-0052; it is the same rule ADR-0052 already states. Sealing is for content; what the machinery must act on travels in the clear, and is therefore held to a strict discipline about what it may contain — decision 5.

5. The matched blacklist category never travels on the wire

ADR-0006 decision 4 warns that a plaintext scope key (department = sexual-health) can be the whole disclosure. A plaintext, unconditionally-replicated body carrying category: "termination-of-pregnancy" is the disclosure this mechanism exists to prevent — worse than the scope key, because it is attached to the exact event it describes.

So an assertion carries subject, grade and provenance (human | advisory) — never the matched category. Where a tag came from is node-local audit at most. The blacklist lookup (cairn_sensitivity_candidate, decision 8) returns (grade, category) to its caller, and the caller puts only the grade on the wire.

The table it reads, sensitivity_category_map, ships empty and stays empty in the migration, with the SQL mirror asserting exactly that. Cairn ships the lookup mechanism, never the list: what is sensitive is cultural, regional and personal, and a seeded row would be an un-reviewable policy choice smuggled in as infrastructure (principle 9).

Note

Erratum E5 (2026-08-11) — the decision is unchanged; its enforcement was missing. As shipped in the first review round, "never travels" was guaranteed only by cairn-event's builder having no category field. A builder is not a floor: ADR-0021 explicitly blesses bespoke UIs, and principle 12 puts the compatibility/safety boundary in the database, precisely because a client talking raw SQL reaches submit_event directly. So the invariant this decision calls the disclosure the whole mechanism exists to prevent was, in the layer that matters, unenforced.

cairn_sensitivity_ceremony_ok now refuses an assertion whose payload carries a category key, at the LOCAL authoring door. The door choice is not the usual ADR-0060 caution but a sharper point: a peer that sent a category has already leaked it — the bytes are on the wire and in that peer's log — so refusing at apply would un-disclose nothing and would only fork the event set. Stopping nodes from authoring the disclosure is the only thing a door can actually accomplish here. Both halves are pinned (sensitivity_floor.rs, sensitivity_ceremony.rs).

6. ADR-0043's "agent advisories are dismissable by anyone" does not reach a protective auto-tag

ADR-0043 makes agent advisories dismissable by anyone — correctly, because an advisory that only its author can clear becomes permanent noise. An auto-applied protective tag is authored by an advisory actor, so read literally, ADR-0043 would let any user silently strip it.

Dismissing a protective tag is a lowering, and every lowering routes through decision 7's ceremony.

Stated as its own decision because the alternative is not a gap but a quiet contradiction between two accepted ADRs, resolving in the unsafe direction: a reader implementing dismissal from ADR-0043 alone would build a one-click strip of a confidentiality grade and have an ADR to cite for it. Both event types are classified ('additive', targets_other_author = FALSE) for a related reason — a withdrawal is cross-author by design (ADR-0006 requires declassification by authority), so it must not be routed through the ADR-0043 self-only suppression owner-gate, and the ceremony is its substitute control.

7. Raising is frictionless; lowering is a ceremony — at the LOCAL authoring door only

Act Local door (submit_event, db/005 step 8a) Remote door (apply_remote_event)
Raise, event / thread no ceremony — any accountable contributor admit
Raise, patient (chart-wide) rationale required admit
Withdrawal (lowering) bound human author (ADR-0053) + rationale admit

Erratum E2 (2026-08-10) — factual; the decision is unchanged. The table's withdrawal row, and the sentence at the end of this section claiming the remote door admits "exactly the three shapes the local door refuses", both misdescribe the shipped split. Two corrections, and the second is the one that matters:

  1. The ceremony is two shapes, not three. cairn_sensitivity_ceremony_ok (db/048 section 12) makes exactly two judgements — a rationale on a chart-wide raise, and a bound human author on a withdrawal. crates/cairn-node/tests/sensitivity_ceremony.rs accordingly pins two local-refuses/remote-admits pairs, not three.
  2. A withdrawal's non-empty rationale is NOT part of the local ceremony. It is a structural floor in cairn_check_sensitivity_withdrawal (db/048 section 4), registered in the ADR-0048 twin-check registry and dispatched through cairn_event_twin — which both doors call (db/005_submit.sql step 8, db/020_apply_remote_event.sql step 8). So a rationale-less peer withdrawal is refused remotely, contrary to the admit in the table's withdrawal row. Only the bound-human-author half is local-door-only.

This is consistent with the file's own structural-vs-ceremony split rather than an exception to it. A structural check judges the shape of the claim being made — is this event well-formed as the thing it says it is — which every honest peer's event satisfies regardless of local policy, so it is safe at both doors and wedges nothing. A ceremony check judges who authored it and under what local accountability, which peers legitimately answer differently, so it must stay local. db/045's registration floor works exactly the same way, for exactly this reason. The decision's substance is untouched: raising stays frictionless, lowering stays accountable, and the reasoning in both numbered points below — including the refusal of a raise is itself a disclosure — stands as written.

Erratum E3 (2026-08-10) — factual; the decision is unchanged. The final whole-branch review added a third local-door judgement before merge, so the table's Raise, patient (chart-wide) row and E2's count above are both now understated. cairn_sensitivity_ceremony_ok refuses a chart-wide raise whose payload subject_id is not the envelope's patient_id, and crates/cairn-node/tests/sensitivity_ceremony.rs pins three local-refuses/remote-admits pairs. The reason is the one asymmetry this whole ADR turns on, applied to a mis-typed pair of hand-entered UUIDs: decision 9's catch-all makes the authored-on chart coarsen (honest, visible), but nothing can make the chart the author meant to seal stop reading routine — no read model can detect it, because nothing on that chart ever mentions the assertion. A clinician who believes they sealed a chart and did not is the unrecoverable direction, so it is refused where the author is still present. It is a ceremony, not a structural floor, by this section's own test: a peer's identically mis-typed event is still admitted at the apply door, where refusing a protective act would fork the event set and be a disclosure in itself. Raising stays frictionless for event- and thread-scoped grades, which is where decision 8 says the friction must not fall.

The asymmetry is the matcher's false merge ≫ false split one axis over: never block a protective act; always make a protection-removing act accountable.

The ceremony is a local-authoring rule and never a wire rule (ADR-0060; the #342 trap), for two independent reasons:

  1. Peers run different local policies. A door check at apply would let one peer's honestly rationale-less act be refused by another peer's stricter node, forking the event set and wedging replication on entirely honest traffic — the failure this project has now hit four times (ADR-0056, ADR-0058, ADR-0061 decision 3, #268).
  2. For a raise it is strictly worse than a wedge. Refusing a peer's protective assertion leaves this node computing a lower grade than the peer already holds — so the refusal is itself a disclosure. A "stricter" door would be the less safe one.

Both halves are tested, not merely commented: the remote door is pinned to admit exactly the three shapes the local door refuses.

Why a bound human author here, when ADR-0061 decision 4 refused one for registration. ADR-0061 rejected an authorship gate because it blocks care documentation: registration sits upstream of everything, so a refused registration means nothing at all can be recorded about that patient, and staff route around it by registering cooperative patients as John Does. None of that applies here. A withdrawal is an administrative act with a consent basis, not care documentation, and refusing it blocks nothing clinical — the content stays fully readable to everyone who already has custody; only the grade stays high, which is the safe direction to be stuck in. The asymmetry between the two ADRs is deliberate and reasoned, not an oversight, and is recorded here so that a future reader harmonising them does not "fix" one into the other.

8. Chart-wide grading is expressible, deliberately effortful, and never automatic

Whole-chart grading is necessary, and it cannot be served by grading threads: the staff member treated at their own hospital, the public figure, the domestic-violence case where the fact of any care is the risk, child protection. The catastrophic failure in every one of those is a new thread opened by a clinician who does not know, and patient-scope is the only subject that covers threads nobody has imagined yet.

It is also the one act here whose blast radius is the entire record. Once part B lands, a chart-wide grade coarsens every safety signal on that chart — the metformin interaction and the penicillin allergy blur along with the reason the grade exists. Two consequences follow, and the second is the serious one:

  • The signal stops carrying information. If everything on a chart is blurred, blurring distinguishes nothing — §5.12's alert-fatigue disease in the confidentiality dimension.
  • Break-glass fatigue. The clinician learns that on this patient they always have to break glass, so they break it reflexively on arrival. That is principle 3's named enemy — the confirmation-dialog click-through — reappearing as an audited access event, and it is worse than the dialog: every reflexive break-glass writes a record that looks like a deliberate, justified access, so the one that mattered becomes indistinguishable from the three hundred that did not. The audit log degrades from evidence to noise, and it does so silently.

Three controls, and deliberately no cap:

  1. A chart-wide raise requires a rationale — the single exception to frictionless raising. It is what the person who later has to unwind it gets to read.
  2. The automatic path cannot express a chart-wide candidate at all. cairn_sensitivity_candidate returns (grade, category) and has no subject column to fill in even by accident; the caller pairs the grade with the event or thread that carried the coded field. A coded hit on one drug blanket-grading an entire chart is precisely "chart-wide as the default for highly sensitive records", which is the thing the friction exists to prevent.
  3. The read surface always names which subject wonsequestered (chart-wide) versus sequestered (this thread). Without it, nobody can tell why a chart is uniformly blurred, and therefore nobody can fix it: a chart-wide assertion is one thing to go and look at, while twenty individually-graded threads are twenty.

Capping chart-wide below sequestered was considered and rejected — see the rejected alternatives.

Warning

Erratum E6 (2026-08-11) — factual; control 1 as shipped did not cover what it claimed. Control 1 was implemented as subject_kind = 'patient', but decision 1's read model grants chart-wide effect to every subject kind it does not recognise (the conservative reading of a future peer's vocabulary). The gate and the effect were therefore keyed on different things, and only the gate was narrow: an assertion with subject_kind: "chart" — or any other unrecognised string — bought the full chart-wide blast radius with no rationale and no ceremony, through the LOCAL door. Every control in this decision was bypassable by mis-spelling one field, and the branch's own test suite pinned that shape as admitted.

The rule is now stated in terms of blast radius rather than spelling: a rationale is owed unless subject_kind is one of the two we positively know is narrowly scoped (event, thread). A future kind inherits the requirement for free by not appearing in that list — the same safe-default-by-omission discipline decision 10's type gate uses.

The same round widened the mis-target rule (decision 7's "a chart-wide grade must name THIS chart") to all three subject kinds, for the argument this ADR already made: --patient and --subject-id are two hand-typed UUIDs in every case, and the read model can only ever repair the over-protecting half. The predicate is "known here and demonstrably on another chart", never "not known to be here", so arrival-order independence is preserved — a target that has not replicated yet is never treated as a mis-target.

Control 3 gained a related correction: the catch-all arm now reports the winning subject as coarsened rather than echoing the row's raw subject_kind. Echoing printed "this event" for an assertion that was in fact blurring the whole chart by mis-target — the precise confusion control 3 exists to prevent — and it also collided with the none sentinel, since subject_kind is an open vocabulary and {"subject_kind":"none"} is a structurally valid assertion. The documented "did anything win" test is now content_address IS NOT NULL.

9. The effective grade is node-relative, not a global fact

medication_id lives inside the sealed payload, and event_log carries no thread column in the clear. The medication projections are populated through cairn_clear_payload, so on a node holding no custody the rows are absent, E → thread resolution fails, and a thread's grade would not apply — producing a lower grade on precisely the node least entitled to see anything.

The resolution follows decision 2's direction rather than adding machinery:

thread contribution =
    resolved T                                   → max over standing assertions on T
    unresolved, chart HAS thread-assertions      → max over ALL the chart's thread-assertions
    unresolved, chart has none                   → nothing

The middle rule is a precise conservative bound, not a sentinel: an unresolvable event belongs to some thread on this chart, so the tightest safe answer is the max over that chart's thread grades. It needs no artificial MAX value, and the third rule keeps the uncertainty from biting where it cannot matter — without it, every medication event on every custody-less node would coarsen maximally, recreating decision 8's everything-is-blurred problem by accident.

The bound is required by §5.9 today, not only by part C. When an event is crypto-shredded (rung 3), db/037 scrubs its derived projection rows, so its thread is unresolvable on every node, permanently — including the authoring one. Without the bound, a shredded event's thread grade evaporates at the moment of shred and its safety projection renders uncoarsened, contradicting §5.9's "the safety projection outlives the body it protects — coarsens but survives." The bound is what makes coarsen-but-survive true after a shred, independently of sequester.

Hence: the effective grade is a function of local custody, not a global fact — and it is non-monotone in custody. Gaining custody can lower a displayed grade as the bound collapses to the true value. This is a known pattern here rather than a surprise: ADR-0052 §9 found exactly the same thing about ADR-0049's thread commitment, which born-sealing turned from a pure function of the content-event set into a function of local custody. Two consequences carry forward, and both must be designed for rather than discovered:

  • A UI showing a grade must tolerate it dropping as DEKs arrive. A grade that falls is not a bug report.
  • Any cross-node equality test is valid only given equal custody. Stated loosely, such a test either fails spuriously or — far worse — gets "fixed" by deleting the bound, reopening the leak it exists to close. The custody qualifier is in the test's own name for that reason.

It opens no new inference channel: the bound reveals the chart's highest thread grade to a node that can resolve nothing, but assertions are plaintext and replicate unconditionally (decision 4), so that grade was already readable there.

10. The conservative bound is scoped to thread-bearing event types

Applied bluntly, "thread unresolvable ⇒ take the bound" is also true of every note, demographic edit, identity assertion, registration and sensitivity event — none of which can ever belong to a medication thread, resolved or not. The effect was that a single thread-scoped sequestered assertion coarsened the entire chart: every note, every demographic field, everything. Thread-scoping silently behaved like chart-wide scoping, defeating the reason a narrower subject kind exists at all.

So the bound applies to clinical.% and to unrecognised/future event types, while types this version positively knows are thread-free contribute nothing. Note the direction of the default: cairn_event_type_has_no_thread returns TRUE only for the namespaces we have confirmed (demographic. / identity. / note. / patient. / sensitivity. / erasure.); anything unrecognised keeps the bound, mirroring decision 2's ELSE MAX. A future clinical stream inherits the bound for free simply by not appearing in that list — the safe default requires nobody to remember to add it.

This is principle 4 one level up. A note having no medication thread is a fact, not uncertainty, and coarsening on a fact we hold is not caution — it is fabricated doubt, and it costs exactly the precision that made thread-scoping worth building. Acknowledged uncertainty means acknowledging uncertainty where it exists, and declining to invent it where it does not.

Rejected alternatives

Unknown ranks 0, matching cairn_clock_grade_rank (db/040). The consistent-looking answer, and the leaking one. Rank 0 in db/040 withholds reject power; rank 0 here withholds protection, so an older node reads a peer's newer grade as "not sensitive" and renders a confidential body in the clear. Consistency between two rank functions is worth nothing when the two ranks mean opposite things. Rejected in favour of MAX, with the reasoning shouted in a comment at the function, because this is the one "cleanup" most likely to be attempted in good faith.

Capping chart-wide below sequestered. Tempting, given decision 8's blast radius: let a chart-wide assertion reach restricted at most, so no single act can seal an entire record. Rejected, because whole-chart sequestration is exactly right for a legitimate protected-witness deployment, and foreclosing it is Cairn taking a policy stance about which patients deserve which protection — precisely what principle 9 forbids. The controls are friction, non-automaticity and visibility (decision 8's three); the ceiling stays open.

Self-only withdrawal — the ADR-0043 shape. Let only the assertion's author withdraw it. It has the appeal of a clean ownership rule, and it deadlocks every real case: the asserting clinician has retired, the patient who requested the grade has left the practice, the advisory actor that auto-tagged it has been superseded. A protection nobody alive can lower is not a strong protection — it is a record that accumulates permanent, un-removable grades until the grading mechanism is worked around entirely. ADR-0006 decision 3 already requires declassification by authority, not by ownership; the ceremony (decision 7) is what makes authority accountable, and it is why both event types are classified targets_other_author = FALSE rather than being routed through the self-only suppression gate.

A plaintext thread reference on event_log. This would make thread resolution custody-free and delete decisions 9 and 10 entirely — a real simplification, which is why it deserves a recorded refusal rather than silence. It fails on its own terms: "these eight events form one thread" is itself linkage information, and thread size and timing re-identify. It would trade a coarsening cost for a disclosure, on a plane that replicates unconditionally — the exact trade decision 5 refuses one paragraph over. It is also an envelope wire change, which ADR-0052 §2 scopes deliberately.

Known limitations

Thread resolution resolves only a thread's current head (#374). cairn_event_thread maps an event to its thread by looking its content_address up across the medication projections — and four of those five tables (medication_statement, medication_cessation, medication_coding, medication_dose_correction) are one-row-per-key upserts carrying only the winning event's address. Only medication_dose_event is keyed per event. So every superseded medication event resolves to NULL even on a node with full custody, and falls into decision 9's bound.

The direction is safe — unresolvable coarsens, never exposes — but the population taking the bound is far broader than intended: the bound exists to cover custody gaps and shredded bodies, and it is silently absorbing ordinary superseded events on fully-custodial nodes. The precision cost is real (a chart with one sequestered thread coarsens every superseded event to sequestered rather than to its own thread's grade). Fixing it means resolving from the event's own body rather than from a winner-keyed projection, which puts a body read on the safety-critical grade path and must behave identically on a custody-less node — a decision, not a patch. Recorded here rather than left in the code so that a reader of decision 9 does not assume resolution is general.

Warning

Erratum E4 (2026-08-11) — factual; the limitation is real but narrower than stated. The bolded sentence above — "every superseded medication event resolves to NULL even on a node with full custody" — is wrong, and the same overstatement was carried in db/048's section 10 comment. Both have been corrected in the code; the sentence stands here because ADRs are immutable.

db/032 registers medication_dose_seed_initial for clinical.medication.asserted, seeding a medication_dose_event row whose dose_event_id is that assert's own event_id and whose content_address is that assert's own, under ON CONFLICT (dose_event_id) DO NOTHING. That row is never overwritten. So a superseded clinical.medication.asserted — and likewise a superseded clinical.medication-dose-change.assertedstill resolves precisely, permanently, on a node with custody. The phrase "only unsuperseded dose points" was wrong on its own terms too: dose points are not superseded in that table at all.

The real limitation is narrower and differently shaped. Resolution is lost for:

  • a superseded ceased or coding event (medication_cessation / medication_coding are keyed on medication_id, so a later event overlays the address), and

  • a re-corrected dose-correction (medication_dose_correction is keyed on the dose point it corrects, so correcting the same point twice drops the earlier correction's address).

Direction of the error: the ADR overstated how much falls into the bound, so the precision cost is smaller than described and nothing about the safety argument changes — the bound over-protects either way. crates/cairn-node/tests/sensitivity_ladder.rs uses a coding event to manufacture the unresolved case for exactly this reason, and says so; that test was right while this section was not. Issue #374 tracks the underlying design question unchanged.

Consequences

Easier. - A grade is stated once and applies forever afterwards, in both temporal directions, with no backfill: thread inheritance is computed at read. - Convergence is free and needs no ordering rule — max over a set is a grow-only CRDT. - The mechanism is genuinely policy-neutral: an empty blacklist, an open grade vocabulary, three ADR-0006 workflows that are the same call site with different callers, and no shipped opinion about what is confidential. - Every failure direction in the computation is over-coarsening. There is no path where confusion, version skew, a mis-targeted assertion or a missing DEK lowers a grade.

Harder. - A grade is now node-relative (decision 9). Every future consumer — UI, safety projection, sequester — has to be written knowing that the number it reads can fall when custody arrives, and that two honest nodes may legitimately disagree. - A withdrawal's rationale is clear text, forever, and it replicates. A rationale naming the condition ("patient consented after her termination follow-up") leaks precisely what the grade protects. The UI must warn at the point of entry; a sealed-rationale variant is filed as a follow-on, and until it exists this is a real, live hazard rather than a theoretical one. - Chart-wide grading is available to anyone who can write a rationale, and its second-order cost (break-glass fatigue) does not show up until part B lands and part D is in routine use — which is late. The three controls are what we have; whether they are enough is the bet below.

The bet. That an advisory, honest, always-computable grade beats an enforcing one built too early. Part A withholds nothing, and deliberately: the enforcement that matters is custody narrowing in the DB floor, and shipping a projection-layer filter first would have produced a system that looks protective, that a clinician would trust, and that a raw-SQL client walks straight past. We would rather ship a grade that is honest about enforcing nothing than a filter that is dishonest about enforcing something.

How we would know the bet fails. Charts drifting toward uniform chart-wide grades — a rising share of patient-subject assertions, or a rising share of events whose winning subject is chart-wide, is the leading indicator of decision 8's failure mode, and it is a one-line query over sensitivity_assertion. Once part D lands, the direct measure is break-glass rate per chart: a chart on which every access is a break-glass is a chart whose audit log has stopped being evidence. If either climbs, the answer is a paper-parity investigation of how grades are being applied — not a stricter floor, which is what pushed people to blanket-grade in the first place.

First instance. db/048_sensitivity_stream.sql (the ladder, both structural floors, the two retained sets, the standing/thread/effective read model, the ceremony, and the empty category map), the ceremony call at db/005_submit.sql step 8a, crates/cairn-event/src/sensitivity.rs (the pure wire builders and twins), crates/cairn-node/src/sensitivity.rs with the sensitivity-assert / sensitivity-withdraw / patient-sensitivity verbs, and db/tests/048_sensitivity_stream_test.sql mirroring the Rust suites. SCHEMA_GENERATION 47 → 48; db/048 is loaded by both the cairn-node and cairn-sync schema lists, because a node that stores the assertion in event_log without the projection computes routine and renders the body in the clear.