From 18a535cde83de776d1e3873385e220c3e307b02e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 17 Aug 2026 17:49:04 +0000 Subject: [PATCH 1/2] fix(rag): bind claim-leading for/in conditions in the high-risk trigger check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 'For elderly patients, start lithium at 500 mg nocte.' extracted no trigger tokens (only when/if/unless/during/after/before and verb-then-for shapes did), so a digit-free population qualifier never had to appear in the supporting segment — an atom-free neighbouring bullet that merely mentions the population could lend its topics and the adult dose could be mis-bound to the elderly claim. A claim-leading 'For ,' / 'In ,' now binds its condition exactly like when/if phrasing. Tightening only: compatibleHighRiskTrigger can only flip accept to reject; faithful condition-first restatements whose segment covers the condition are pinned green, and the full offline suite (616) is unchanged. Documented as the residual risk in PR #2052; the digit-bearing variant ('over 65 years') was already caught by the atom-free-neighbour rule. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TNq5LiNDoZVaggA3mHBT5Q --- src/lib/rag/rag-claim-support.ts | 5 ++++ tests/rag-claim-support.test.ts | 48 ++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/src/lib/rag/rag-claim-support.ts b/src/lib/rag/rag-claim-support.ts index b4df666079..ad521cf044 100644 --- a/src/lib/rag/rag-claim-support.ts +++ b/src/lib/rag/rag-claim-support.ts @@ -166,6 +166,11 @@ function highRiskTriggerTokens(value: string) { const triggerPatterns = [ /\b(?:when|whenever|if|unless|during|after|before)\b\s*([^,;.!?]+?)(?=\s*,|\s+\b(?:administer|avoid|cease|continue|discontinue|escalate|give|prescribe|start|stop|use|withhold)\b|[;.!?]|$)/gi, /\b(?:administer|avoid|cease|continue|discontinue|escalate|give|prescribe|start|stop|use|withhold)\b[^,;.!?]{0,80}?\bfor\b\s*([^,;.!?]+?)(?=\s+\bfor\b|\s*,|[;.!?]|$)/gi, + // Condition-first phrasing: a claim-leading "For , …" + // or "In , …" binds its condition exactly like when/if phrasing — + // previously this shape extracted no tokens, so the condition never had to + // appear in the supporting segment (S1c follow-up). + /^\s*(?:for|in)\s+([^,;.!?]{1,60}?)\s*,/gi, ]; for (const pattern of triggerPatterns) { for (const match of value.matchAll(pattern)) { diff --git a/tests/rag-claim-support.test.ts b/tests/rag-claim-support.test.ts index 426cb2e76a..886b98b825 100644 --- a/tests/rag-claim-support.test.ts +++ b/tests/rag-claim-support.test.ts @@ -285,6 +285,54 @@ describe("deterministic claim support", () => { ).toBe(false); }); + it("binds a claim-leading population condition to the supporting segment (condition-first trigger)", () => { + // "For elderly patients, …" previously extracted no trigger tokens, so an + // atom-free neighbouring bullet that merely mentions the population could + // lend its topics and let the adult dose be mis-bound to the elderly claim. + // (A digit-bearing population phrase like "over 65 years" is already caught + // by the atom-free-neighbour rule; this pins the digit-free phrasing.) + const adultDoseWithPopulationProse = source( + "adult-dose-population-prose", + [ + "• The usual oral starting dose for adults is 500 mg nocte.", + "• Elderly patients require cautious dose titration.", + ].join("\n"), + { title: "Lithium Clinical Guideline(EMHS)", file_name: "Lithium Clinical Guideline(EMHS).pdf" }, + ); + + expect( + sourceDirectlySupportsAnswerText( + "For elderly patients, start lithium at 500 mg nocte.", + adultDoseWithPopulationProse, + ), + ).toBe(false); + }); + + it("keeps supporting a faithful condition-first restatement whose segment covers the condition", () => { + const emhs = source( + "emhs-lithium-condition-first", + [ + "• The usual oral starting dose for adults is 500 mg nocte and for patients over 65 years it", + "is 250 mg nocte.", + ].join("\n"), + { title: "Lithium Clinical Guideline(EMHS)", file_name: "Lithium Clinical Guideline(EMHS).pdf" }, + ); + + expect(sourceDirectlySupportsAnswerText("For patients over 65 years, start lithium at 250 mg nocte.", emhs)).toBe( + true, + ); + }); + + it("binds a claim-leading in-condition the same way", () => { + const renal = source("renal-monitoring", "Monitor lithium levels closely in renal impairment."); + const hepatic = source("hepatic-monitoring", "Monitor lithium levels closely in hepatic impairment."); + + expect(sourceDirectlySupportsAnswerText("In renal impairment, monitor lithium levels closely.", renal)).toBe(true); + expect(sourceDirectlySupportsAnswerText("In renal impairment, monitor lithium levels closely.", hepatic)).toBe( + false, + ); + }); + it("keeps a wrapped escalation recipient in the directly supporting source segment", () => { const wrappedRule = source( "wrapped-escalation-rule", From da6eb129be225d98adcb2d8475f5854865ef36a6 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 17 Aug 2026 17:55:10 +0000 Subject: [PATCH 2/2] docs: record the PR #2065 review ledger entry Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TNq5LiNDoZVaggA3mHBT5Q --- ...33b3ef164093f54fa8df20452c2f8e144ffa1546bb7603c142e.record.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/branch-review-records/ed2853a1fe7ad33b3ef164093f54fa8df20452c2f8e144ffa1546bb7603c142e.record.md diff --git a/docs/branch-review-records/ed2853a1fe7ad33b3ef164093f54fa8df20452c2f8e144ffa1546bb7603c142e.record.md b/docs/branch-review-records/ed2853a1fe7ad33b3ef164093f54fa8df20452c2f8e144ffa1546bb7603c142e.record.md new file mode 100644 index 0000000000..da67ac8483 --- /dev/null +++ b/docs/branch-review-records/ed2853a1fe7ad33b3ef164093f54fa8df20452c2f8e144ffa1546bb7603c142e.record.md @@ -0,0 +1 @@ +| 2026-08-17 | claude/s1c-trigger-condition-first-4pb1at | 18a535cde83de776d1e3873385e220c3e307b02e | S1c follow-up: claim-leading for/in condition binding in highRiskTriggerTokens (protected RAG surface, tightening only) | PR #2065 open; digit-free population mis-bind closed fail-first; merge after S1c canary; own post-merge canary pair owner-approved | vitest rag-claim-support 160/160; eval:rag:offline 616/616 zero flips; verify:pr-local heavy failed:(none); check:production-readiness offline provider gaps only |