From 713df7a6128f0a8d9e63fd2c46e62c269aaaee9b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 17 Aug 2026 12:57:47 +0000 Subject: [PATCH 1/2] fix(rag): route the medication_dose_risk class to the strong route before the deadline (A1 R1) Packet S1b, mitigation-ladder rung 3 for issue #231's residual R1: after S1, "Lithium dosing?" stayed 4/4 source-only, 3/4 as provider_timeout, because the fast-routed attempt failed the quality gates and fast_unsupported_retry_strong launched a strong generation into the fast route's leftover ~10-13s. chooseAnswerRoute now returns the strong route (new distinct reason medication_dose_risk_strong_route) at the routed-generation fallthrough for the medication_dose_risk class, so the deadline is created with the strong 35s budget and the class gets one fully budgeted strong attempt instead of a doomed leftover-budget retry. All unsupported refusals and provider-free extractive paths fire before the new branch and are unchanged; table_threshold and every other class keep byte-identical routing (pinned by new tests). No budget, threshold, quality-gate, or shouldRetryWithStrongAfterFast change. Knock-ons in the same change: the LAI-management and agitation-typo-dosing validated-extractive short-circuits now key on the new strong signature so their measured provider-free timeout skips keep firing (with negative tests for the retired fast signature), and the six supported dosing-class golden cases additionally allow the strong route ("unsupported" stays excluded). The budget-cap contract test burns 10s of retrieval fake-time so its reserve assertion stays discriminating below every plausible timeout cap. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01NWGr4LHgBRDVM1sGVdfCSa --- src/lib/rag/rag-eval-cases.ts | 12 +++--- src/lib/rag/rag-extractive-first.ts | 9 ++-- src/lib/rag/rag-routing.ts | 17 ++++++++ tests/rag-answer-fallback.test.ts | 65 +++++++++++++++++------------ tests/rag-extractive-first.test.ts | 18 +++++++- tests/rag-routing.test.ts | 36 ++++++++++++++-- 6 files changed, 116 insertions(+), 41 deletions(-) diff --git a/src/lib/rag/rag-eval-cases.ts b/src/lib/rag/rag-eval-cases.ts index 604b541dbf..34732e9be1 100644 --- a/src/lib/rag/rag-eval-cases.ts +++ b/src/lib/rag/rag-eval-cases.ts @@ -762,7 +762,7 @@ export const ragEvalCases: RagEvalCase[] = [ expectedQueryClass: "medication_dose_risk", supported: true, expectedFiles: ["CG.MHSP.ClozapinePresAdminMonitor.pdf"], - allowedRoutes: ["extractive", "fast"], + allowedRoutes: ["extractive", "fast", "strong"], minCitations: 1, latencyTargetMs: 2000, }, @@ -805,7 +805,7 @@ export const ragEvalCases: RagEvalCase[] = [ expectedQueryClass: "medication_dose_risk", supported: true, expectedFiles: ["MHSP.AgitationArousalPharmaMgt.pdf"], - allowedRoutes: ["extractive", "fast"], + allowedRoutes: ["extractive", "fast", "strong"], minCitations: 2, latencyTargetMs: 2000, }, @@ -845,7 +845,7 @@ export const ragEvalCases: RagEvalCase[] = [ category: "routine", supported: true, expectedFiles: ["MHSP.LongActingInjectable.pdf"], - allowedRoutes: ["extractive", "fast"], + allowedRoutes: ["extractive", "fast", "strong"], minCitations: 2, latencyTargetMs: 2000, }, @@ -1075,7 +1075,7 @@ export const ragEvalCases: RagEvalCase[] = [ expectedQueryClass: "medication_dose_risk", supported: true, expectedFiles: ["CG.MHSP.ClozapinePresAdminMonitor.pdf"], - allowedRoutes: ["extractive", "fast"], + allowedRoutes: ["extractive", "fast", "strong"], minCitations: 1, latencyTargetMs: 2000, }, @@ -1098,7 +1098,7 @@ export const ragEvalCases: RagEvalCase[] = [ category: "routine", supported: true, expectedFiles: ["CG.MHSP.ClozapinePresAdminMonitor.pdf"], - allowedRoutes: ["extractive", "fast"], + allowedRoutes: ["extractive", "fast", "strong"], minCitations: 1, latencyTargetMs: 2000, requireVisualEvidence: true, @@ -1112,7 +1112,7 @@ export const ragEvalCases: RagEvalCase[] = [ expectedQueryClass: "medication_dose_risk", supported: true, expectedFiles: ["MHSP.AgitationArousalPharmaMgt.pdf"], - allowedRoutes: ["extractive", "fast"], + allowedRoutes: ["extractive", "fast", "strong"], minCitations: 1, latencyTargetMs: 2000, }, diff --git a/src/lib/rag/rag-extractive-first.ts b/src/lib/rag/rag-extractive-first.ts index 5cb8e30407..521ca7fc2c 100644 --- a/src/lib/rag/rag-extractive-first.ts +++ b/src/lib/rag/rag-extractive-first.ts @@ -15,6 +15,7 @@ import { isSourceBoundCommunityHomeVisitRequirementsQuery, retainCitedExtractiveFallbackEvidence, } from "@/lib/rag/rag-extractive-answer"; +import { MEDICATION_DOSE_RISK_STRONG_ROUTE_REASON } from "@/lib/rag/rag-routing"; import type { RagQueryClass, RetrievalConfidenceGateStatus, SearchResult } from "@/lib/types"; /** @@ -300,8 +301,8 @@ export function hasValidatedGenericLaiManagementExtractiveAnswer(args: { if ( !genericLaiManagementQuery || args.queryClass !== "medication_dose_risk" || - args.route.mode !== "fast" || - args.route.reason !== "clinical_fast_grounded_synthesis" || + args.route.mode !== "strong" || + args.route.reason !== MEDICATION_DOSE_RISK_STRONG_ROUTE_REASON || !args.sourceBacked ) { return false; @@ -337,8 +338,8 @@ export function hasValidatedAgitationArousalTypoDosingExtractiveAnswer(args: { if ( normalizedQuery !== validatedAgitationArousalTypoDosingQuery || args.queryClass !== "medication_dose_risk" || - args.route.mode !== "fast" || - args.route.reason !== "clinical_fast_grounded_synthesis" || + args.route.mode !== "strong" || + args.route.reason !== MEDICATION_DOSE_RISK_STRONG_ROUTE_REASON || args.gateStatus !== "passed" || !args.sourceBacked ) { diff --git a/src/lib/rag/rag-routing.ts b/src/lib/rag/rag-routing.ts index e7e5453b12..378f39622a 100644 --- a/src/lib/rag/rag-routing.ts +++ b/src/lib/rag/rag-routing.ts @@ -23,6 +23,13 @@ export type AnswerRoute = { export const SOURCE_BACKED_REVIEW_FALLBACK_REASON = "source_backed_review_fallback"; +// Dosing-class queries that reach the routed-generation fallthrough go to the strong +// route up front, so the 35s strong budget exists before the deadline is created. +// On the fast route their failed attempts retried strong inside the fast route's +// leftover budget (fast_unsupported_retry_strong) and died as provider_timeout — +// the dominant "Lithium dosing?" fallback mode after S1 (issue #231 residual R1). +export const MEDICATION_DOSE_RISK_STRONG_ROUTE_REASON = "medication_dose_risk_strong_route"; + const unsupportedSimilarityThreshold = 0.32; const strongRetrievalThreshold = 0.64; const extractiveRetrievalThreshold = 0.76; @@ -1164,6 +1171,16 @@ export function chooseAnswerRoute(args: { }; } + if (queryClass === "medication_dose_risk") { + return { + mode: "strong", + model: args.strongModel, + reason: MEDICATION_DOSE_RISK_STRONG_ROUTE_REASON, + strongestScore, + documentCount: documents, + }; + } + return { mode: "fast", model: args.fastModel, diff --git a/tests/rag-answer-fallback.test.ts b/tests/rag-answer-fallback.test.ts index a8c458f52f..6ecf651565 100644 --- a/tests/rag-answer-fallback.test.ts +++ b/tests/rag-answer-fallback.test.ts @@ -2027,15 +2027,15 @@ describe("RAG structured-output fallback", () => { expect(answerCalls[0]?.[2].instructions).toContain("Within one named scale and source"); expect(answerCalls[0]?.[2].instructions).toContain("cite the smallest sufficient directly supporting chunk set"); expect(answerInput).toContain("answer_plan.intent: clinical_synthesis"); - expect(answerInput).toContain("answer_plan.route_mode: fast"); - expect(answerInput).toContain("answer_plan.model_strategy: fast_model_then_quality_gate"); + expect(answerInput).toContain("answer_plan.route_mode: strong"); + expect(answerInput).toContain("answer_plan.model_strategy: strong_model_then_quality_gate"); expect(answerInput).toContain("answer_plan.source_policy: required_citations"); - expect(answer.routingMode).toBe("fast"); - expect(answer.routingReason).toContain("clinical_fast_grounded_synthesis"); + expect(answer.routingMode).toBe("strong"); + expect(answer.routingReason).toContain("medication_dose_risk_strong_route"); expect(answer.smartApiPlan?.answerPlan).toMatchObject({ intent: "clinical_synthesis", - routeMode: "fast", - modelStrategy: "fast_model_then_quality_gate", + routeMode: "strong", + modelStrategy: "strong_model_then_quality_gate", sourcePolicy: "required_citations", }); expect(answer.answer.replace(/\*\*/g, "")).toMatch(/clozapine Monitoring Form/i); @@ -2276,7 +2276,7 @@ describe("RAG structured-output fallback", () => { expect(answer.answer).toMatch(/source support|indexed document|supports this query|ECT Procedure/i); }); - it("retries template-like fast answers with the strong model before returning", async () => { + it("retries template-like dosing-class strong answers with a quality retry before returning", async () => { vi.stubEnv("OPENAI_API_KEY", "test-key"); vi.stubEnv("RAG_SEARCH_CACHE_TTL_MS", "0"); vi.stubEnv("RAG_ANSWER_CACHE_TTL_MS", "0"); @@ -2386,7 +2386,7 @@ describe("RAG structured-output fallback", () => { expect(generateStructuredTextResult).toHaveBeenCalledTimes(2); expect(answer.routingMode).toBe("strong"); - expect(answer.routingReason).toContain("fast_template_retry_strong"); + expect(answer.routingReason).toContain("strong_quality_retry"); expect(answer.openAIRequestIds ?? []).toEqual(["req_fast_template", "req_strong_natural"]); expect(answer.grounded).toBe(true); expect(answer.confidence).toBe("medium"); @@ -3561,7 +3561,7 @@ describe("RAG structured-output fallback", () => { expect(new Set(answer.sources.map((result) => result.document_id))).toEqual(new Set(["fsh-lithium"])); expect(answer.latencyTimings?.answer_retry_count).toBe(2); expect(answer.latencyTimings?.answer_retry_reasons).toEqual([ - "fast_max_output_tokens_retry_strong", + "strong_max_output_tokens_retry_strong", "strong_max_output_tokens", ]); expect(answer.openAIRequestIds).toEqual(["req_truncated_1", "req_truncated_2"]); @@ -4481,7 +4481,7 @@ describe("RAG structured-output fallback", () => { const plainAnswer = answer.answer.replace(/\*\*/g, ""); expect(generateStructuredTextResult).toHaveBeenCalledTimes(1); - expect(answer.routingMode).toBe("fast"); + expect(answer.routingMode).toBe("strong"); expect(answer.grounded).toBe(true); expect(plainAnswer).not.toMatch(/^Dosage\b/i); expect(plainAnswer).not.toContain("alternative agent where possible"); @@ -4524,7 +4524,7 @@ describe("RAG structured-output fallback", () => { ); const plainAnswer = answer.answer.replace(/\*\*/g, ""); - expect(answer.routingMode).toBe("fast"); + expect(answer.routingMode).toBe("strong"); expect(answer.grounded).toBe(true); expect(plainAnswer).not.toMatch(/^Dosage\b/i); expect(plainAnswer).not.toContain("chart reference only"); @@ -4992,14 +4992,17 @@ describe("budget-aware generation deadlines", () => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-07-14T00:00:00.000Z")); vi.stubEnv("OPENAI_API_KEY", "test-key"); - // Stubbed far above the 25_000ms fast-route budget so the granted timeout can only - // come from the deadline: budget - 2_000ms recovery reserve = 23_000ms. Reverting the - // call site to the reserve-free requestTimeoutMs would grant the full 25_000ms. + // Stubbed far above the granted window below so the value can only come from the + // deadline. The retrieval mock additionally burns 10_000ms of the 35_000ms strong + // budget, keeping the deadline term (35_000 - 10_000 - 2_000 = 23_000ms) below every + // plausible timeout cap — including the 30_000ms default that full-file runs pin when + // an earlier import froze env — so this assertion discriminates in both run modes. + // Reverting the call site to the reserve-free requestTimeoutMs would grant 25_000ms. vi.stubEnv("OPENAI_ANSWER_TIMEOUT_MS", "60000"); vi.stubEnv("RAG_SEARCH_CACHE_TTL_MS", "0"); vi.stubEnv("RAG_ANSWER_CACHE_TTL_MS", "0"); - // Same retrieval fixture as the model-synthesis test above, which routes fast. + // Same retrieval fixture as the model-synthesis test above, which routes strong. const clozapineSource = source({ id: "clozapine-monitoring-1", document_id: "clozapine-doc", @@ -5013,8 +5016,17 @@ describe("budget-aware generation deadlines", () => { hybrid_score: 0.94, text_rank: 0, }); + let retrievalTimeBurned = false; const rpc = vi.fn(async (name: string) => { - if (retrievalRpcBaseName(name) === "match_document_chunks_text") return { data: [clozapineSource], error: null }; + if (retrievalRpcBaseName(name) === "match_document_chunks_text") { + // Burn retrieval wall-clock exactly once so the deadline term is the binding + // one at the generation call regardless of the effective timeout cap. + if (!retrievalTimeBurned) { + retrievalTimeBurned = true; + vi.setSystemTime(new Date(Date.now() + 10_000)); + } + return { data: [clozapineSource], error: null }; + } if (retrievalRpcBaseName(name) === "get_related_document_metadata") return { data: [], error: null }; return { data: [], error: null }; }); @@ -5048,16 +5060,17 @@ describe("budget-aware generation deadlines", () => { skipCache: true, }); - // Fake timers pin elapsed-at-call to exactly 0ms, so the received timeout must equal - // budget - reserve. This is the assertion that fails if generationRequestTimeoutMs is - // reverted to requestTimeoutMs at the generation call site. + // Fake timers pin elapsed-at-call to exactly the burned 10_000ms, so the received + // timeout must equal budget - burned - reserve. This is the assertion that fails if + // generationRequestTimeoutMs is reverted to requestTimeoutMs at the generation call + // site (that revert would grant 25_000ms). expect(generateStructuredTextResult).toHaveBeenCalledTimes(1); - expect(grantedTimeoutsMs).toEqual([answerRouteBudgetMs.fast - generationRecoveryReserveMs]); - expect(answer.latencyTimings?.route_budget_ms).toBe(answerRouteBudgetMs.fast); + expect(grantedTimeoutsMs).toEqual([answerRouteBudgetMs.strong - 10_000 - generationRecoveryReserveMs]); + expect(answer.latencyTimings?.route_budget_ms).toBe(answerRouteBudgetMs.strong); // The attempt used its whole window, yet the reserve kept the source-backed recovery // inside the route budget. expect(answer.latencyTimings?.route_deadline_exceeded).toBe(false); - expect(answer.latencyTimings?.total_latency_ms).toBeLessThan(answerRouteBudgetMs.fast); + expect(answer.latencyTimings?.total_latency_ms).toBeLessThan(answerRouteBudgetMs.strong); expect(answer.routingReason).toContain("generation_fallback:provider_timeout"); expect(answer.sources.length).toBeGreaterThan(0); }); @@ -5065,8 +5078,8 @@ describe("budget-aware generation deadlines", () => { it("skips the truncation self-heal when the budget reserve would be breached", async () => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-07-14T00:00:00.000Z")); - // Burn 20_000ms of the 25_000ms fast budget inside the first attempt before it - // resolves truncated: the 5_000ms left is below generationRecoveryReserveMs + + // Burn 20_000ms of the 35_000ms strong budget inside the first attempt before it + // resolves truncated: the 15_000ms left is below generationRecoveryReserveMs + // minimumGenerationRetryMs (22_000ms), so the strong self-heal must be skipped // instead of spending the recovery reserve on a guaranteed-discard retry. const { answer, generateStructuredTextResult } = await lithiumTruncatedGenerationAnswer(20_000); @@ -5075,7 +5088,7 @@ describe("budget-aware generation deadlines", () => { // The skip is recorded without counting as a retry; the terminal truncation throw // then lands on the existing source-backed recovery. expect(answer.latencyTimings?.answer_retry_reasons).toEqual([ - "truncation_retry_skipped_budget_reserve:fast_max_output_tokens", + "truncation_retry_skipped_budget_reserve:strong_max_output_tokens", "generation_max_output_tokens", ]); expect(answer.latencyTimings?.answer_retry_count).toBe(1); @@ -5097,7 +5110,7 @@ describe("budget-aware generation deadlines", () => { expect(generateStructuredTextResult).toHaveBeenCalledTimes(2); expect(answer.latencyTimings?.answer_retry_reasons).toEqual([ - "fast_max_output_tokens_retry_strong", + "strong_max_output_tokens_retry_strong", "strong_max_output_tokens", ]); expect(answer.latencyTimings?.answer_retry_count).toBe(2); diff --git a/tests/rag-extractive-first.test.ts b/tests/rag-extractive-first.test.ts index 2eca7a3934..1823622e2f 100644 --- a/tests/rag-extractive-first.test.ts +++ b/tests/rag-extractive-first.test.ts @@ -519,7 +519,7 @@ The details (i.e.: medication name, formulation, route, dose and directions for query: "How are long acting injectables managed?", queryClass: "medication_dose_risk", results: laiResults, - route: { mode: "fast", reason: "clinical_fast_grounded_synthesis" }, + route: { mode: "strong", reason: "medication_dose_risk_strong_route" }, }); expect(chooseValidatedExtractiveShortCircuit(args)).toEqual({ @@ -527,6 +527,14 @@ The details (i.e.: medication name, formulation, route, dose and directions for }); // The LAI skip is only offered while the retrieval gate passed. expect(chooseValidatedExtractiveShortCircuit({ ...args, gateStatus: "blocked" })).toBeNull(); + // The dosing class no longer reaches the fast fallthrough; the retired fast + // signature must not qualify. + expect( + chooseValidatedExtractiveShortCircuit({ + ...args, + route: { mode: "fast", reason: "clinical_fast_grounded_synthesis" }, + }), + ).toBeNull(); }); it("short-circuits only the measured typo dosing query when its single-source candidate validates", () => { @@ -534,7 +542,7 @@ The details (i.e.: medication name, formulation, route, dose and directions for const args = proceduralArgs({ query, queryClass: "medication_dose_risk", - route: { mode: "fast", reason: "clinical_fast_grounded_synthesis" }, + route: { mode: "strong", reason: "medication_dose_risk_strong_route" }, results: [ source({ id: "agitation-repeat-dose-guidance", @@ -561,6 +569,12 @@ The details (i.e.: medication name, formulation, route, dose and directions for ).toBe(false); expect(hasValidatedAgitationArousalTypoDosingExtractiveAnswer({ ...args, gateStatus: "blocked" })).toBe(false); expect(hasValidatedAgitationArousalTypoDosingExtractiveAnswer({ ...args, sourceBacked: false })).toBe(false); + expect( + hasValidatedAgitationArousalTypoDosingExtractiveAnswer({ + ...args, + route: { mode: "fast", reason: "clinical_fast_grounded_synthesis" }, + }), + ).toBe(false); }); it("returns the blocked-recovery marker for the score-blocked routine document lookup", () => { diff --git a/tests/rag-routing.test.ts b/tests/rag-routing.test.ts index f28eadec6d..00b7dd871b 100644 --- a/tests/rag-routing.test.ts +++ b/tests/rag-routing.test.ts @@ -68,9 +68,25 @@ describe("RAG answer routing", () => { expect(selected.reason).toBe("strong_routine_retrieval"); }); - it("uses fast model synthesis for routine medication questions with strong single-source support", () => { + it("routes routine dosing-class questions to the strong model before the deadline is created", () => { const selected = route("What clozapine monitoring is required?", [source()]); + expect(selected.mode).toBe("strong"); + expect(selected.model).toBe("strong-model"); + expect(selected.reason).toBe("medication_dose_risk_strong_route"); + }); + + it("routes bare dosing questions to the strong model instead of the fast fallthrough", () => { + const selected = route("Lithium dosing?", [source()]); + + expect(selected.mode).toBe("strong"); + expect(selected.model).toBe("strong-model"); + expect(selected.reason).toBe("medication_dose_risk_strong_route"); + }); + + it("keeps the table-threshold fallthrough on the fast synthesis route", () => { + const selected = route("What sedation score threshold applies?", [source({ text_rank: 0.05 })]); + expect(selected.mode).toBe("fast"); expect(selected.model).toBe("fast-model"); expect(selected.reason).toBe("clinical_fast_grounded_synthesis"); @@ -1145,8 +1161,9 @@ describe("RAG answer routing", () => { }); it("retries a single-source clinical fast failure with the strong model when retrieval is strong", () => { - const selected = route("What clozapine monitoring is required?", [source()]); + const selected = route("What sedation score threshold applies?", [source({ text_rank: 0.05 })]); + expect(selected.reason).toBe("clinical_fast_grounded_synthesis"); expect( shouldRetryWithStrongAfterFast({ route: selected, @@ -1156,10 +1173,23 @@ describe("RAG answer routing", () => { citations: [], routingReason: "structured_parse_fallback", }, - results: [source()], + results: [source({ text_rank: 0.05 })], }), ).toBe(true); }); + + it("never enters the after-fast strong retry for the pre-deadline dosing strong route", () => { + const selected = route("Lithium dosing?", [source()]); + + expect(selected.mode).toBe("strong"); + expect( + shouldRetryWithStrongAfterFast({ + route: selected, + answer: { grounded: false, confidence: "unsupported", citations: [] }, + results: [source()], + }), + ).toBe(false); + }); }); describe("adversarial-manipulation query guard", () => { From 5908d8667b6bfd554c4f569b17f6e4919773d647 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 17 Aug 2026 13:01:57 +0000 Subject: [PATCH 2/2] docs(rag): record S1b review and update the HANDOVER status row (PR #2035) Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01NWGr4LHgBRDVM1sGVdfCSa --- ...1351c6ebd83d0ae0a980e65d12eda5b6.record.md | 1 + docs/rag-improvement/HANDOVER.md | 36 +++++++++---------- 2 files changed, 19 insertions(+), 18 deletions(-) create mode 100644 docs/branch-review-records/bcb5e0bf76d1501946c3d093a30dfd8c1351c6ebd83d0ae0a980e65d12eda5b6.record.md diff --git a/docs/branch-review-records/bcb5e0bf76d1501946c3d093a30dfd8c1351c6ebd83d0ae0a980e65d12eda5b6.record.md b/docs/branch-review-records/bcb5e0bf76d1501946c3d093a30dfd8c1351c6ebd83d0ae0a980e65d12eda5b6.record.md new file mode 100644 index 0000000000..d1c383201b --- /dev/null +++ b/docs/branch-review-records/bcb5e0bf76d1501946c3d093a30dfd8c1351c6ebd83d0ae0a980e65d12eda5b6.record.md @@ -0,0 +1 @@ +| 2026-08-17 | claude/s1b-rag-dosing-routing-6u1mik | 713df7a6128f0a8d9e63fd2c46e62c269aaaee9b | RAG answer routing: medication_dose_risk pre-deadline strong route (S1b/R1, #231) + extractive-first short-circuit signatures + golden allowedRoutes | PR #2035 open; behaviour change awaiting owner merge + post-merge canary pair | verify:pr-local heavy scope (lint, typecheck, full test, build, eval:rag:offline 586/586, medication checks) exit 0; focused vitest 103/103; rag-answer-fallback 90/90; check:rag:fixtures 36 golden cases; check:production-readiness offline-expected | diff --git a/docs/rag-improvement/HANDOVER.md b/docs/rag-improvement/HANDOVER.md index 3564aeecf8..a9ce816bc6 100644 --- a/docs/rag-improvement/HANDOVER.md +++ b/docs/rag-improvement/HANDOVER.md @@ -66,24 +66,24 @@ generation-quality verdict on fallback`), merged 2026-08-13 — structured ## 2. Status table — update in every programme PR -| Packet | Scope | Branch | PR | State | Canary / evidence refs | -| ---------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | --------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Guide | Programme guide | `claude/rag-plan-review-guide-vhrls9` | #1895 | Merged 2026-08-13 | docs-only | -| Handover | Multi-session handover + coordination | `claude/rag-plan-review-guide-vhrls9` | #1908 / #2024 | Merged 2026-08-13; coordination layer PR #2024 | docs-only | -| S0 | A1 phase 1: structured fallback diagnostics | `claude/lithium-generation-quality-debug-ji1vce` | #1899 | Merged 2026-08-13 | offline 93/93 focused | -| S1 | A1 phase 2: rung-1 verification-faithfulness fixes | `claude/s1-rag-mitigation-231-86c182` | #2022 | Merged 2026-08-17 (squash `2bd146eed`, landed by content) | 8 pre-fix + 5 post-fix live probes 2026-08-17; offline 583/583; canary pair run 31964560921 (baseline `8f8d111ab`) -> run 32025082010 (`2bd146eed`): recall 1.0/1.0, zero per-case rr regressions, answer gate 45/45; rung-2 measurement in `docs/audit/live-drift-forensics-2026-08.md` §5 | -| S1b | A1 rung 3 (R1): pre-deadline strong routing for dosing class | `claude/rag-a1-r1-routing-` | — | Ready — dispatch now (owner decided R1 before S2, 2026-08-17) | needs canary pair | -| S1c | A1 residuals R2 + R3: claim-support strictness | `claude/rag-a1-r2-r3-claim-support-` | — | Blocked on S1b + its canary | needs canary pair | -| G1 | Governance: provenance tag for document-summary rows (Option B) | `claude/rag-g1-document-context-origin-` | — | Ready — disjoint; owner decided Option B 2026-08-17 | no canary (no behaviour change) | -| S2 | A2 (+A3): composition menu + moderate length | `claude/rag-a2-composition-` | — | Blocked on S1b + S1c | canary pair + `eval:answer-quality` + Gate E | -| S2b | A3: moderate length (if separate review needed) | `claude/rag-a3-length-` | — | Blocked on S2 | — | -| S3 | A4: follow-up suggestion refinement | `claude/rag-a4-follow-ups-` | — | Blocked on S2 + S2b | — | -| S4 | B0: adversarial fixtures + baseline + register | `claude/rag-b0-adversarial-fixtures-` | — | Ready — dispatch now (parallel-safe) | — | -| S5 | B1+B2: telemetry assessment + offline harness | `claude/rag-b1-b2-harness-` | — | Blocked on S4 | — | -| S6 | B3: Docling lab benchmark | `claude/rag-b3-docling-lab-` | — | Blocked on S4 | — | -| S7+ | B4 shadow / B5 Ragas / B6 reranker / B7 DSPy | — | — | Gated — owner decision | — | -| #212 T1–T3 | Runtime row contracts (rag.ts, rag-candidate-sources.ts, src/app/api) — sibling stream sharing `src/lib/rag/**` | — | #1946 / #1981 / #2023 | Merged (T3 squash `440a34f71` 2026-08-17) | see the #212 ledger row; RAG surface complete for the cast class | -| #212 T4 | Runtime row contracts: `worker/main.ts` (11 casts) — sibling stream | `claude/ledger-212-tranche-4-worker-` | — | Ready — dispatch now | Clinical Governance Preflight; closes #212 if the audit supports it | +| Packet | Scope | Branch | PR | State | Canary / evidence refs | +| ---------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | --------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Guide | Programme guide | `claude/rag-plan-review-guide-vhrls9` | #1895 | Merged 2026-08-13 | docs-only | +| Handover | Multi-session handover + coordination | `claude/rag-plan-review-guide-vhrls9` | #1908 / #2024 | Merged 2026-08-13; coordination layer PR #2024 | docs-only | +| S0 | A1 phase 1: structured fallback diagnostics | `claude/lithium-generation-quality-debug-ji1vce` | #1899 | Merged 2026-08-13 | offline 93/93 focused | +| S1 | A1 phase 2: rung-1 verification-faithfulness fixes | `claude/s1-rag-mitigation-231-86c182` | #2022 | Merged 2026-08-17 (squash `2bd146eed`, landed by content) | 8 pre-fix + 5 post-fix live probes 2026-08-17; offline 583/583; canary pair run 31964560921 (baseline `8f8d111ab`) -> run 32025082010 (`2bd146eed`): recall 1.0/1.0, zero per-case rr regressions, answer gate 45/45; rung-2 measurement in `docs/audit/live-drift-forensics-2026-08.md` §5 | +| S1b | A1 rung 3 (R1): pre-deadline strong routing for dosing class | `claude/s1b-rag-dosing-routing-6u1mik` | #2035 | PR open 2026-08-17 — offline gates green; owner merges | canary pair pending: baseline run 32025082010 (`2bd146eed`) -> post-merge dispatch (owner-approved); offline 586/586 + verify:pr-local heavy scope green | +| S1c | A1 residuals R2 + R3: claim-support strictness | `claude/rag-a1-r2-r3-claim-support-` | — | Blocked on S1b + its canary | needs canary pair | +| G1 | Governance: provenance tag for document-summary rows (Option B) | `claude/rag-g1-document-context-origin-` | — | Ready — disjoint; owner decided Option B 2026-08-17 | no canary (no behaviour change) | +| S2 | A2 (+A3): composition menu + moderate length | `claude/rag-a2-composition-` | — | Blocked on S1b + S1c | canary pair + `eval:answer-quality` + Gate E | +| S2b | A3: moderate length (if separate review needed) | `claude/rag-a3-length-` | — | Blocked on S2 | — | +| S3 | A4: follow-up suggestion refinement | `claude/rag-a4-follow-ups-` | — | Blocked on S2 + S2b | — | +| S4 | B0: adversarial fixtures + baseline + register | `claude/rag-b0-adversarial-fixtures-` | — | Ready — dispatch now (parallel-safe) | — | +| S5 | B1+B2: telemetry assessment + offline harness | `claude/rag-b1-b2-harness-` | — | Blocked on S4 | — | +| S6 | B3: Docling lab benchmark | `claude/rag-b3-docling-lab-` | — | Blocked on S4 | — | +| S7+ | B4 shadow / B5 Ragas / B6 reranker / B7 DSPy | — | — | Gated — owner decision | — | +| #212 T1–T3 | Runtime row contracts (rag.ts, rag-candidate-sources.ts, src/app/api) — sibling stream sharing `src/lib/rag/**` | — | #1946 / #1981 / #2023 | Merged (T3 squash `440a34f71` 2026-08-17) | see the #212 ledger row; RAG surface complete for the cast class | +| #212 T4 | Runtime row contracts: `worker/main.ts` (11 casts) — sibling stream | `claude/ledger-212-tranche-4-worker-` | — | Ready — dispatch now | Clinical Governance Preflight; closes #212 if the audit supports it | Update rule: the session that opens a packet's PR edits its row (branch, PR number, state) in the same PR. A later session updating another packet may also correct stale rows