fix: identity matcher rebuilt against a frozen adversarial corpus (3rd wrong-patient reopening) + armed-coverage metric#16
Merged
Merged
Conversation
Deterministic, seeded generator (seed 20260710) of 4360 labeled (recorded_band, observed_band) pairs: 2200 different_entity (the false-accept side: prefix-extension names, single-letter sibling edits, transpositions, Jr/Sr suffixes, shared clinical text, DOB off-by-one-field, MRN digit swaps, adjacent-row mixtures) and 2160 same_entity (the false-abort side: OCR confusions, splits/joins, dropped short tokens, case/whitespace jitter, segment reordering, occlusion, spurious tokens, compound noise). Frozen BEFORE evaluating or touching the matcher: the sha256 manifest is committed (docs/validation/adversary_corpus_manifest.json) and pinned by tests, so any post-hoc tuning of the corpus toward the matcher is detectable in git history. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CKrVJJy5jWVCkXAqgUqtqZ
…rd P0 reopening) Confirmed vulnerability: band_match returned (coverage=1.0, residue=0) — VERIFIED — for sibling rows: 'Belford, Phil' vs 'Belford, Philip' (containment tier), the reverse, 'Smith, John' vs 'Smith, Joan' (similarity tier, 0.75 >= 0.7), and 'Belford, Phil' vs 'Belford, Phillipa'. On the frozen adversarial corpus the legacy matcher's false-accept rate was 53.9% overall (DOB off-by-one 99.1%, Jr/Sr 99.1%, single-letter edits 98.2%, transpositions 95.5%, prefix extensions 72.3%, MRN digit swaps 50.0%). The rebuild: - token matching accepts ONLY OCR-equivalence — identical after canonicalizing real OCR confusion classes (l/1/i, O/0, 5/s, 2/z, 8/b, 9/g, rn/m, cl/d, vv/w) — plus full-consumption token splits/joins. The partial-containment and 0.7-similarity tiers are gone: both accepted semantic extensions of name tokens. - unmatched tokens split into ABSENCE (uncovered runs — OCR dropout, budgeted as before) and CONTRADICTION (near-miss similarity >= 0.62, semantic containment with alphabetic residue, replacement by an unexplained observed token, generational-suffix presence on one side) with its own zero budget. Operating point picked from the ROC on the frozen corpus (sweep of contradiction_sim x coverage x run_cap x contradiction_cap, before/ after chart + tables committed under docs/validation/): coverage 0.8, run cap 4, contradiction_sim 0.62, contradiction cap 0 -> false accept 0.000% (was 53.9%), false abort 10.69% (was 12.1%), NOT the Pareto-min false-abort corner: at cov 0.7/run 8 the zero rests entirely on the contradiction rule (FA 60.8% if it is evaded) whereas at 0.8/4 the older budgets independently catch 79.5% — defense in depth over 2.7pp of availability concentrated in unreadable-name occlusion shapes. All four sibling probes pinned as permanent mismatches; operating point pinned by boundary tests; corpus-wide zero-false-accept regression test added. Full unit suite green (364), true-row live shapes (OpenEMR modal permutation, OCR jitter, split/join) still verified. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CKrVJJy5jWVCkXAqgUqtqZ
Identity verification covers ONLY armed steps, and real bundles arm a minority (live OpenEMR checks armed 4/12; a fresh MockMed demo bundle arms 1/8). That fact was previously a buried sentence in a live-check note; an unarmed click proceeds with NO identity check at all. Now: - workflow.json: per-step identity_armed / identity_unarmed_reason written by the compiler (with the concrete reason: no readable band text / only the target's own label / too generic after volatile filtering) so an operator can audit protection BEFORE running. - REPORT.md: every run report states 'N of M click steps identity-armed' and lists the unarmed steps by id, intent and reason (computed over the whole bundle at run start, not just executed steps; pre-metric bundles get an honest fallback reason). - Benchmark generators (MockMed + OpenEMR): compiled-arm rows and arm aggregates carry the coverage; BENCHMARK.md methodology sections render it. The committed BENCHMARK.md files' results.json predate the metric, so they carry an explicit 'not captured in this results.json' note instead of fabricated numbers. - docs/LIMITS.md: the dangerous list now LEADS with the coverage gap, and the wrong-entity section is updated for the 2026-07-10 matcher rebuild (near-name siblings, corpus rates, occlusion-abort rationale). - docs/validation/VALIDATION.md: 2026-07-10 fix update — the third wrong-patient reopening said plainly with the four probe strings, frozen-corpus methodology, before/after rates per category, ROC operating point with the stated cost weighting, and the coverage metric surfaces. Verified end-to-end: CLI demo-record -> compile -> replay produces a REPORT.md with '1 of 8 click steps identity-armed' and per-step reasons; e2e CLI smoke test now asserts the section and the bundle fields. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CKrVJJy5jWVCkXAqgUqtqZ
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CKrVJJy5jWVCkXAqgUqtqZ
The hybrid generator (PR #14, merged after this branch forked) reuses _compiled_run and _arm_aggregate, so its compiled-arm rows and aggregates already carry the identity-coverage fields; this renders them in the BENCHMARK.md methodology section. The committed benchmark/hybrid/results.json predates the metric, so the regenerated BENCHMARK.md carries the explicit not-captured note (verbatim regeneration verified — one-line diff). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CKrVJJy5jWVCkXAqgUqtqZ
All 13 probes VERIFY against the shipped matcher at the shipped operating point (reproduced locally, wrong-patient direction); they are committed FIRST, asserting mismatch, so the acceptance criteria for the matcher redesign are on record before the redesign or corpus v2: - Blocker 1: confusion-collided distinct names (Neil/Nell, Clay/Day, Marnie/Mamie, Gail/Gall) — the v1 corpus excluded this class by construction, so its 0.000% headline was partially tautological. - Blocker 2: sub-MIN_BLOCK tokens invisible to contradiction (middle initial, SEX column, 2-char names). - Blocker 3: observed-side superset always verifies (appended tokens, two-row merge, wrong row mentioning the recorded patient). - Major 4: fully absent 4-char name at the run cap verifies with the identity token never read. Safe-direction pins (hyphenated split, Bob/Robert, Alison/Allison, MRN/DOB edits, digit-class homoglyphs, param-mode raw-run rejection of Neil->Nell) pass today and must keep passing. The Ann Marie/Annmarie join edge is pinned as a disclosed residual. The 13 probe tests FAIL at this commit by design; they pass after the matcher redesign lands. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CKrVJJy5jWVCkXAqgUqtqZ
…ruction Versioned extension of the frozen corpus (v1 generator and manifest are untouched, history intact): own seed (20260711), own SHA manifest, committed BEFORE the redesigned matcher is evaluated on it — the same freeze discipline as v1, so the corpus-v2 commit precedes the matcher-fix commit in git history. 2240 pairs across the reviewer-identified excluded classes: - different_entity (1590): confusion-collided names generated systematically from the letter-letter members of the frozen confusion table over the v1 name lists (name-only / realistic distinct-IDs / identical-IDs probe shape), middle initial, sex column, 2-char names, observed-superset shapes (appended name, merged second row, title/cc row mentioning the recorded patient), and absent 4-char name tokens. - indistinguishable (200) — NEW third label: the true row misread by a letter-letter confusion, textually identical to its different-entity twin. ABORT is the correct outcome for both readings; scoring counts abort as justified (not a false abort) and verify as a false accept. - same_entity (450): the availability side the new budgets must not kill — digit-class-only OCR noise (names contain no digits, so no collision is possible), lowercase adjacent-row bleed, hyphenated surname splits. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CKrVJJy5jWVCkXAqgUqtqZ
…blockers
All 13 reviewer probes (committed first in
tests/test_identity_out_of_corpus.py, failing) now pass; corpus v2 was
frozen in the preceding commit, before this change was evaluated on it.
Four new decision budgets, all zero-tolerance at the operating point:
- SUSPECT chars (Blocker 1): a name-plausible token matched ONLY by a
letter-letter confusion equivalence (Neil/Nell i-l, Clay/Day cl-d,
Marnie/Mamie rn-m) is indistinguishable from a real sibling — the
honest outcome is an abort for BOTH readings, corroborating identical
MRN/DOB notwithstanding (the probes pin exactly that). Digit/symbol
confusions ('Phi1', '5ample') stay clean: names contain no digits, so
no collision with a different name is possible. This ports the spirit
of param mode's raw longest_run check (which already rejected
Neil->Nell) into context mode.
- Short-token replacement (Blocker 2), COUNT-based: a replaced 1-2 char
alphabetic token (middle initial J->K, SEX column M->F, 2-char names
Al->Bo) is contradiction. Multiset accounting, because a replaced
initial can duplicate the sex letter and look 'explained' per-pair.
- Unexplained observed name-shaped tokens (Blocker 3): context mode
gains the observed-side budget param mode always had — appended
middle names, two-row OCR merges, and message/cc rows that merely
MENTION the recorded patient all refuse; lowercase adjacent-row bleed
stays exempt (the legitimate spurious class, 0% false aborts on v2).
- Absent name-like token (Major 4): a fully absent 4+ char alphabetic
token refuses even inside the generic run cap — identity must not
verify with its identity token never read. Trailing-numerics dropout
keeps the old tolerance (class-weighted, not blanket). The old pin
test_pure_absence_boundary_at_run_cap is FLIPPED accordingly.
The replayer now extracts the LIVE band exactly as the compiler
extracted the recorded band (target's own crop excluded at the resolved
point, volatile lines dropped against the replay date): the previous
asymmetry meant the label and live clock cells appeared as observed-side
extras, which is what made an observed-superset budget impossible.
Measured (frozen corpora, regression-netted in
tests/test_identity_corpus_rates.py): 0 false accepts across v1 (2200
wrong-entity pairs), v2 (1590 wrong-entity + 200 indistinguishable),
and the 13 probes; v1 false aborts 21.2% (up from 10.7% — the
availability bill of closing the blockers, per-class breakdown in the
regenerated IDENTITY_ROC.md), v2 legitimate-noise classes 0.0%;
indistinguishable class 200/200 abort. Letter-letter jitter on the true
row now aborts (pinned; disclosed) — the flipped twin of the Neil/Nell
fix. Full suite green including e2e (43/43 live record-compile-replay).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CKrVJJy5jWVCkXAqgUqtqZ
…st limits
Re-run the full ROC on corpora v1+v2 with three-label scoring
(different_entity / same_entity / indistinguishable) and the six-budget
decision sweep; re-picked operating point keeps 0.8/4/0 and adds
suspect=0, unexplained-name=0, absent-name-cap=3:
- FA 0.000% across v1+v2 (3990 wrong-entity/indistinguishable pairs) —
every number explicitly scoped to 'corpus v1+v2 plus the 13
out-of-corpus probes', with the operating-point-fit limitation stated
plainly (freezing prevents tuning the corpus toward the matcher, not
the thresholds toward the corpus; v1's zero was shown partially
tautological one review ago and the same criticism applies to v2's).
- FAbort v1 21.2% / v2 0.0%; indistinguishable class 200/200 abort.
- The cheaper zero-FA Pareto corner (cov 0.85 / run 8 / absent-name
off, FAbort 15.86%) is rejected with an empirical counter-example:
its Major-4 protection is a band-length artifact (the same absent
4-char name at coverage 0.915 verifies there; the absent-name cap
refuses structurally).
- Occlusion recount CORRECTS the earlier framing: 102/216 occlusion
aborts at the shipped decision (107/224 at production) still had
BOTH name tokens readable — trailing DOB/MRN loss, an availability
cost, not the 'correct epistemic refusal' previously claimed.
VALIDATION.md's original sentences carry strikethrough corrections.
- Realistic-exposure analysis: the Blocker-1 probes used identical
MRNs (unrealistic); with differing readable IDs the absence/
contradiction budgets catch 180/180 without the suspect rule; the
true residual exposure is name-as-only-discriminative-token bands,
where the suspect rule is the only defense and covers only the
frozen confusion table.
- LIMITS.md restores and EXPANDS the honest disclosure this PR had
deleted ('names within OCR-jitter distance verify'): the residual
verify classes are now listed plainly (Ann Marie/Annmarie join,
case/whitespace-only differences, 1-2 char letter-letter confusions,
added short tokens), plus the permanent indistinguishable-class
aborts and the ~21% compiled-only availability price.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CKrVJJy5jWVCkXAqgUqtqZ
Second adversarial review of PR #16 found a 5th wrong-patient P0 reopening: the round-3 suspect budget guards NAME tokens only (_name_plausible is False for any token containing a digit), so the rule was OFF for MRNs/account numbers while confusion canonicalization (l/1, O/0, S/5, Z/2, B/8, g/9) still applied to them. A different patient's alphanumeric identifier differing only by one letter/digit-confusable char silently VERIFIED, defeating MRN-based disambiguation of same-name patients. Committed FIRST, FAILING, as acceptance criteria (reproduced locally): - probes 14-16: MRN/Acct l/1, O/0, S/5 confusions verify (must abort) - probe 17: two same-name patients, MRN the sole discriminator, one confusable char apart -> verify (the canonical clinical case; must abort regardless of name raw-match) - probe 18: same hole fires in param mode (MRN as parameter) - availability-cost boundary: true-row MRN OCR noise (A01234->AO1234) must abort under the chosen safety-first design (documented cost) Controls that must keep passing: all-digit MRN diff (748291 vs 748292) mismatches via coverage not suspect; raw-equal MRN with name-side digit noise ('Belford'->'Be1ford') still verifies (the fix is scoped to RECORDED identifier tokens, not any observed digit). The 6 new failing tests pass after the identifier-suspect fix; corpus v3 is frozen before that fix. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CKrVJJy5jWVCkXAqgUqtqZ
Versioned extension (v1/v2 untouched, history intact): own seed (20260712), own SHA manifest, committed BEFORE the identifier-suspect matcher change is evaluated on it — same freeze discipline, so the corpus-v3 commit precedes the matcher-fix commit. 300 different_entity pairs, one class id_letter_digit_collision: two entities identical in every token EXCEPT an alphanumeric identifier (MRN/account/chart ref) differing by exactly one letter/digit-confusable position (l/1, i/1, o/0, s/5, z/2, b/8, g/9), generated systematically from the confusion pairs. This is the class v1's mrn_digit_swap could not surface: v1 only swapped/changed DIGITS (748291 vs 748292), which are never in one confusion class. A VERIFY here is a wrong-patient action — the identifier is the sole discriminator and is exactly what MRN-based disambiguation relies on. The generator renders one row template per pair and formats it with each identifier, so the identifier is provably the only differing token (pinned by test_v3_pairs_are_confusion_equivalent_and_id_only_differ). No same-entity identifier-noise class is added: under the chosen safety-first design all confusion-differing recorded identifiers abort, so such a label would be unwinnable by construction; the availability cost is measured directly on v2's digit_confusion_true_row class. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CKrVJJy5jWVCkXAqgUqtqZ
…atient reopening
The round-3 suspect budget guarded NAME tokens only: _name_plausible is
False for any token containing a digit, so the rule was OFF for
MRNs/account numbers while the confusion canonicalization (l/1, O/0,
S/5, Z/2, B/8, g/9) still applied to them. A DIFFERENT patient's
alphanumeric identifier one letter/digit-confusable char apart
('A01234' vs 'AO1234') silently VERIFIED, defeating MRN-based
disambiguation of same-name patients (verified in param mode too).
Fix: _suspicious_pair now also returns True when the RECORDED token
contains a digit (an identifier matched only across a confusion). A
confusion-only match on such a token is charged to the zero suspect
budget -> abort. Chosen design is option A of the review (no
corroboration escape): a confusion-differing identifier aborts even when
name and DOB raw-match, so two same-name patients distinguished ONLY by
an OCR-confusable identifier char never verify. Option B (allow if
name+DOB corroborate) was rejected because two real patients can share a
name and DOB, so the MRN is the sole unique key and B would re-admit
exactly the Doe John wrong-patient case.
Scoping on the RECORDED token is what keeps name-with-digit-noise
verifying while identifier-with-digit aborting: the recording carries
the ground truth of the token's type. 'Belford' -> 'Be1ford' is clean
(recorded all-alpha = name); 'A01234' -> 'AO1234' aborts (recorded has a
digit = identifier). All-DIGIT differences (748291 vs 748292) are not
confusion-equivalent and mismatch via coverage/contradiction as before.
Measured (frozen v1+v2+v3, regression-netted): 0 false accepts across
all three including v3's 300 id_letter_digit_collision pairs and the 18
out-of-corpus probes. Availability cost, honest: true-row identifier OCR
noise now aborts (indistinguishable at band level) — v2
digit_confusion_true_row 0% -> 48.7%, v1 overall 21.2% -> 28.2% (budgets
updated). Residual verify: short 1-2 char all-alpha codes confused with
a digit (recorded token has no digit; under the 3-char name floor).
Full suite green including e2e (43/43).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CKrVJJy5jWVCkXAqgUqtqZ
…isclosures Re-run the full ROC on corpora v1+v2+v3 (6900 pairs) after the identifier-suspect fix; operating point confirmed (same six/seven caps, FA 0.000% / FAbort 26.17% / indistinguishable-abort 100% across all three). New content: - IDENTITY_ROC.md: a '5th reopening' section (the identifier letter/digit collision, chosen design A with the option-B rejection rationale, the RECORDED-token scoping, and the honest true-row availability cost); a v3 per-category table (id_letter_digit_collision legacy 100% -> 0.0%); the realistic-exposure table gains the v3 row (300/300 verify without the suspect rule, 0 with it) and CORRECTS the first review's 'ids differ -> 180/180 without the suspect rule' claim as name-collision-only (it did not cover the letter/DIGIT identifier case). Scope re-stated as v1+v2+v3 plus the 18-probe set. - LIMITS.md: the contradicted-list 'swapped MRN digits' is qualified to 'all-DIGIT' (the letter/digit case is now a suspect, not a contradiction); the suspect budget is described as name AND identifier; the residual-verify list gains the short all-alpha-code case and the true-row-identifier-noise availability cost; the halt price is updated 21% -> 28%; every zero-claim re-scoped to v1+v2+v3 + 18 probes. - VALIDATION.md: the realistic-exposure bullet gets a second-review caveat, and a new 'SECOND review / 5th reopening' subsection records the hole, the fix, corpus v3, and the availability cost. No claim left standing that the final matcher falsifies. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CKrVJJy5jWVCkXAqgUqtqZ
abrichr
added a commit
that referenced
this pull request
Jul 11, 2026
…ct stale-code finding The branch was originally cut from a stale local main that predated the identity-matcher fixes (#16/#17/#19), so the compiled arm ran against the pre-#16 matcher and recorded 3 sibling wrong-actions — a stale-code artifact. Rebased onto current main and re-ran the identity-sensitive cells: the compiled arm now safe-halts both the near-lexical sibling (Sorenson/Sorensen) and the decoy, 3/3 each — 0 identity wrong-actions. The browser identity fixes transfer to desktop-rendered OCR text. Narrative corrected in BENCHMARK.md; non-identity findings (UIA-tree gap, DPI/theme defeat vision but halt-not-miswrite, full prlctl automation) unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CKrVJJy5jWVCkXAqgUqtqZ
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fix: identity matcher rebuilt against a frozen adversarial corpus — near-name siblings (3rd wrong-patient reopening) + armed-coverage metric
The vulnerability (confirmed, reproduced)
band_matchreturned(coverage=1.0, residue=0)— VERIFIED — for all of:Belford, Phil 1985-03-12 MvsBelford, Philip 1985-03-12 M(containment tier: 'Phil' ⊂ 'Philip')Smith, John ...vsSmith, Joan ...(similarity tier: 0.75 ≥ 0.7)Belford, Phil ...vsBelford, Phillipa ...(containment tier)This is the wrong-patient P0 reopening a third time (pixel-lookalikes → bands; residue-blind coverage + param disarm → token matcher + residue cap; now near-name siblings). Real EMR rows are full of near-name siblings, and downstream note verification does NOT catch a wrong-patient write — the note really is saved, in the wrong chart.
1. Held-out corpus FIRST, frozen before any evaluation or fix
To break the fix-against-the-probe cycle, commit 1 of this branch adds
openadapt_flow/validation/adversary_corpus.py: a deterministic seeded generator (seed 20260710) of 4360 labeled pairs — 2200different_entity(prefix-extension names, single-letter edits, transpositions, Jr/Sr/II suffixes, same-surname/-first siblings, shared clinical text, DOB off-by-one-field, MRN digit swaps, adjacent-row mixtures) and 2160same_entity(OCR confusions, token splits/joins, dropped short tokens, case/whitespace jitter, segment reordering, occlusion, spurious tokens, compound noise). The sha256 manifest is committed and pinned by tests, so post-hoc tuning of the corpus toward the matcher is detectable in git history. The generator was not modified after first evaluation (no generator bugs found; byte-identical to the pre-evaluation commit).2. Measured, then fixed, operating point from the ROC
Current (pre-fix) matcher on the corpus: 53.9% false-accept overall. By category: DOB off-by-one 99.1%, Jr/Sr 99.1%, single-letter edits 98.2%, transpositions 95.5%, prefix extensions 72.3%, MRN swaps 50.0%, same-surname 15.5%. False-abort 12.1%.
The rebuild (
runtime/identity.py): tokens match ONLY on OCR-equivalence (canonical identity under real engines' confusion classes: l/1/i, O/0, 5/s, 2/z, 8/b, 9/g, rn/m, cl/d, vv/w) plus full-consumption splits/joins — the containment and 0.7-similarity tiers are gone. Unmatched tokens split into absence (uncovered runs, budgeted as before) and contradiction (near-miss ≥ 0.62 on canonical forms, semantic containment with alphabetic residue, replacement by an unexplained observed token, generational suffix on one side) with a zero budget.Post-fix: 0.000% false accepts (0/2200), 10.69% false aborts (better than before on both axes). ROC swept over
contradiction_sim × coverage × run_cap × contradiction_cap; chart + tables + raw sweep committed underdocs/validation/(identity_roc.png,IDENTITY_ROC.md,identity_roc.json).Operating point, weighting said out loud (false accept = wrong-patient write, catastrophic; false abort = ~$0.10 hybrid fallback; priced at 4+ orders of magnitude):
coverage 0.8 / run_cap 4 / sim 0.62 / contradiction_cap 0. Deliberately NOT the Pareto-min-false-abort corner (0.7/8, FAbort 7.96%): that corner's zero rests entirely on the contradiction rule (evade it → FA 60.8%), while at 0.8/4 the older budgets independently stop 79.5% — defense in depth, bought with 2.7pp of false aborts concentrated in occlusion shapes whose identity tokens were never read (refusing those is correct).Pins: all four sibling probes as permanent mismatch tests; operating-point boundary tests; a corpus-wide zero-false-accept regression test (a rate, not a probe list) + 12% false-abort budget. True-row live shapes (OpenEMR modal permutation, OCR jitter, splits/joins, MockMed fixtures) still verified.
3. Protection coverage as a first-class metric
Identity verification covers ONLY armed steps — live OpenEMR checks armed 4/12; a fresh MockMed demo bundle arms 1/8. Now auditable everywhere:
identity_armed/identity_unarmed_reason(concrete reasons: no readable band text / only the target's own label / too generic after volatile filtering) — auditable before running.Verification
ANTHROPIC_API_KEYunset throughout).demo-record → compile → replayproduces REPORT.md with the coverage section and per-step reasons; bundle JSON carries the audit fields.🤖 Generated with Claude Code
https://claude.ai/code/session_01CKrVJJy5jWVCkXAqgUqtqZ