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 | 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",