Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 |
5 changes: 5 additions & 0 deletions src/lib/rag/rag-claim-support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <population>, <directive> …"
// or "In <state>, …" 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)) {
Expand Down
48 changes: 48 additions & 0 deletions tests/rag-claim-support.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading