feat(rag): intent-conditioned related-information menu and moderate answer length (packet S2 / A2 + A3) - #2097
Conversation
…nswer length (packet S2 / A2 + A3) Adds src/lib/rag/answer-composition.ts, a pure map from (RagQueryClass, ClinicalQueryIntent) to the answerSections kinds the model should attempt when the retrieved excerpts support them, serialised as one related_information_menu line in buildAnswerInput's "Interpreted clinical task" block. The generation prompt gains one bullet under "Answer sections" describing the menu as advisory, evidence-gated, cited, and subordinate to the unchanged narrow-question rule. Length targets move from 1-3 sentences / 35-75 words to 2-4 sentences / 60-110 words for complex questions, and sections from two-to-five to three-to-six when supported; the narrow-question rule stays verbatim so definitions and single thresholds do not bloat. The structured-output schema's answerSections.maxItems rises 5 -> 6 so the decoder allows what the prompt asks for. ragAnswerPromptVersion v18 -> v19 (and ragAnswerSchemaVersion v3 -> v4) so the response cache and prompt cache roll; the provider wrapper's fallback prompt_cache_key moves in lockstep with its test pin. Grounding contract, verification, claim support, routing, retrieval, ranking, selection, and the render trust ladder are untouched. rag.ts stays inside its 4362-line no-growth budget (+2 lines: one import, one interpretedTask entry). Also removes the two stale planned-path entries from scripts/check-docs-links.mjs now that the module and the #1899 probe exist, and adds the composition suite to the offline RAG contract list (26 suites). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ord packet S2 in the programme ledger Re-records scripts/fixtures/rag-adversarial-baseline.v1.json against the evaluated S2 code commit b7aa925 (promptVersion clinical-rag-answer-v19, index_version 20260818090000, offline_contract 26 suites / 623 tests, adversarial fixtures 24 cases canary-free); provider-backed gates stay pending_owner_run with the S1d confirmation run 32100681177 (4ea310e) carried as priorRun. baseline-record.md gains a section explaining the re-capture and the 220-word readability-ceiling confound for the S2 eval:answer-quality comparison. HANDOVER: S1d row corrected to canary pair 32052479537 -> 32100681177 green; S2 row opened; S2b marked not needed (A3 shipped inside S2); S3 precondition updated. README section 2 anchor now describes the post-S2 answer shape; behaviour-map gains section 8 (answer composition menu). Queues an issues inbox request for the ci-change-scope rag_eval_changed regex gap on src/lib/rag/** (own PR). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-7330b0) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in:12 minutes Limit details: You’ve used all 1 included review currently available under your plan. You completed 101 included PR reviews in the past 7 days; at that activity level, included reviews refill at 1 review per hour. Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (18)
Comment |
This pull request has been ignored for the connected project Preview Branches by Supabase. |
…s row Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Uh oh!
There was an error while loading. Please reload this page.
Summary
Packet S2 of the RAG improvement programme (
docs/rag-improvement/HANDOVER.md§3, README §A2 + §A3): intent-conditioned related-information composition plus the moderate answer-length increase. Both are prompt-surface changes and share one canary pair, so they ship together (the combined diff stayed reviewable — S2b is not needed).RAG impact: behaviour change — canary pair 32100681177 -> post-merge eval-canary dispatch (owner-approved; the baseline run is the S1d confirmation run on
4ea310e48, this PR's merge base)src/lib/rag/answer-composition.tsmaps (RagQueryClass,ClinicalQueryIntent) to a related-information menu — theanswerSectionskinds a psychiatrist colleague would append unprompted for that question type — andbuildAnswerInput(src/lib/rag/rag.ts) serialises it as onerelated_information_menu:line in the "Interpreted clinical task" block, keyed on the finalqueryClassandqueryAnalysis.intent.answerInstructionsgains one bullet under "Answer sections" describing the menu as advisory, evidence-gated, cited like any other section, and subordinate to the verbatim narrow-question rule. No new pipeline stage,RagAnswerfield, or render block; grounding, verification, claim support, routing, retrieval, ranking, selection, and the render trust ladder are untouched.- If the question is narrow (a definition, one threshold, a single dose, a yes/no), answer only that. …) is byte-identical, and the length line restates that a narrow question keeps 1-3 sentences / 35-75 words. The structured-output schema'sanswerSections.maxItemsrises 5 → 6 so the decoder allows what the prompt now asks for.ragAnswerPromptVersionv18 → v19 andragAnswerSchemaVersionv3 → v4 (src/lib/rag/rag-versioning.ts) sorag_response_cacheand the OpenAI prompt cache roll; the provider wrapper's fallbackprompt_cache_key(src/lib/openai.ts) and its pin intests/openai-cache.test.tsmove in lockstep, as the v17→v18 commit did.check:rag:adversarial-fixturescross-checks the baseline record'spromptVersionagainst the live prompt, soscripts/fixtures/rag-adversarial-baseline.v1.jsonwas re-recorded against the evaluated code commitb7aa925f0ae19e89a9f0acf842b4a80d84083fb5(v19,index_version20260818090000_schema_drift_snapshot_history_probe, offline gates re-run; provider gates staypending_owner_runwith run 32100681177 aspriorRun).docs/rag-improvement/baseline-record.md§4 explains it.canary pair 32052479537 -> 32100681177 green, S2b marked not needed, S3 precondition updated; README §2 answer-shape anchor updated;docs/rag-behaviour/behaviour-map.md§8 added; the two stale planned-path entries inscripts/check-docs-links.mjsremoved (S4 precedent);tests/answer-composition.test.tsadded to the offline RAG contract list (26 suites); an/issues addinbox request queued for theci-change-scope.mjsrag_eval_changedregex gap onsrc/lib/rag/**(own PR, not bundled).Menu table (class is authoritative; intent refines only the two clinical-fact classes)
queryClassintentmedication_dose_riskescalation_riskmonitoring_timing— monitoring schedule and levels;contraindications_cautions— contraindications and cautions;escalation_risk— escalation and stop triggers;medication_dose— dose adjustment for renal or hepatic impairment and older adultsmedication_dose_risk/table_thresholdescalation_riskrequired_actions— immediate actions;thresholds— the thresholds that trigger them;escalation_risk— who to contact or refer to;documentation— what to documenttable_thresholdescalation_riskthresholds— adjacent thresholds or bands in the same scale;required_actions— required actions per band;escalation_risk— escalation pathwaycomparisoncomparison— decision factors;comparison— per-source differences and conflicts;required_actions— switching or washout considerationsbroad_summaryrequired_actions— weighted management map: risk, first-line, adjuncts, monitoring, special populations;documentation— documentation and forms;source_gap— source gapsdocument_lookup,unsupported_or_generalrelated_information_menu: none — no related-information menu for this question type; apply the Answer sections rules as writtenDesign notes, each verified against source: (1)
intentFromSignals(src/lib/clinical-search.ts) matches "long-term" / "determine" asdefinition, sodefinitiondoes not silence a menu — narrowness is the prompt rule's job; (2)unsupported_or_generalis the only class whereisOverExpandedSimpleGeneratedAnswer(> 95 words / > 1 section) can fire, so it carries no menu; (3) README's "related documents" item is not offered —relatedDocumentsis deterministic (buildRelatedDocumentsSafe) and already renders at high trust; (4)queryClasscan be overridden by mode/classifier whilequeryAnalysis.intentstays heuristic, so the map is total over all 48 class×intent cells (pinned by test).Prompt diff (
src/lib/rag/rag-answer-instructions.ts)rag.ts:+import { relatedInformationMenuLine } from "@/lib/rag/answer-composition";,+relatedInformationMenuLine(queryClass, queryAnalysis.intent),ininterpretedTask(afteranswer_scope), andmaxItems: 5→6—rag.tssits at exactly 4362/4362 lines of its no-growth budget.trustCaps check (README §A2 asks for it)
trustCapsinsrc/lib/answer-render-policy.tshas no section-count key ({sources, rows, quotes, visual, related});rows(6 medium / 8 high) caps evidence-map rows keyed by primary sources andrelated(4, high only) capsrelatedDocuments.answerSectionsrender uncapped inClinicalDashboard.tsx(safeAnswerSections), and neither Zod,sanitizeAnswerSections, noranswer-stream-contract.tscaps the count — so no cap change is needed. Two accepted residuals:src/lib/ward-output.tskeeps its 5-row structured-support table (a 6th section still renders in the sections list), and the summary route sharesanswerJsonOutputSchemaForResults, so its ceiling also becomes 6 while its own prompt (ragSummaryPromptVersion) is untouched.Verification
npm run verify:pr-local— heavy scope selected (runtime, lock parity, format:changed, sitemap/docs/ledger checks, lint, typecheck, full unit suite, build,eval:rag:offline,eval:rag:adversarial:offline, medication checks): every stage green through lint and typecheck;npm run test→Test Files 1 failed | 651 passed | 2 skipped (654) / Tests 1 failed | 7023 passed | 27 skipped (7051)— the one failure istests/session-start-hook.test.ts > still writes the env file when Claude Code provides one(Linux/tmpvs Windows temp path shape), reproduced identically at the merge base4ea310e48in a detached worktree (Tests 1 failed | 4 passed (5)), so it is a pre-existing Windows-host flake, not this diff. Becauseverify:pr-localstops at the first failing stage, the remaining selected stages were run directly and are green:npm run build→✓ Compiled successfully in 66s/Client bundle secret surface check passed.(exit 0);check:medication-interactions→data/medication-interaction-index.json is up to date (525 rows).;check:medication-lexicon-report→docs/medication-interaction-lexicon-review.md is up to date (37 catalogue terms).;eval:rag:offlineandeval:rag:adversarial:offlineas below.npx vitest run tests/answer-composition.test.ts tests/rag-answer-composition-prompt.test.ts tests/rag-answer-fallback.test.ts tests/openai-cache.test.ts→Test Files 4 passed (4) / Tests 122 passed (122)npm run check:maintainability-budgets→[maintainability] src/lib/rag/rag.ts: 4362/4362 linesnpm run check:rag:fixtures→Offline RAG fixture and manifest validation passed (36 golden cases, 26 suites).npm run eval:rag:offlineatb7aa925f0→Test Files 26 passed (26) / Tests 623 passed (623)—Offline RAG fixture and production-contract checks passed.npm run eval:rag:adversarial:offline→Adversarial fixture contract passed (24 synthetic cases, 8 categories, 6 canaries)./Tests 25 passed (25)— the three S5KNOWN_DIVERGENCESpins are unchanged (the harness fails with "known divergence resolved" if one flips; none did).npm run check:production-readiness→ PASS 2 (Node 24.19.0; query-hash boot guard); the FAIL rows are the missing provider env in this worktree (no.env.local:NEXT_PUBLIC_SUPABASE_URL,SUPABASE_SERVICE_ROLE_KEY,OPENAI_API_KEY) — a capability gap of the offline session, not a regression.node scripts/check-docs-links.mjs→docs link check passed: 1867 repo path references resolve.(after removing the two stale allowlist entries)npm run eval:retrieval:qualitynot run: retrieval/ranking/selection untouched; 36/36 is covered by the post-merge canary dispatch below.Requested, not executed (provider-backed; owner approves each):
eval-canarydispatch — the post half ofcanary pair 32100681177 -> <post>; gates recall 1.0/1.0, zero per-case rr regressions, answer gate 44/44.npm run eval:answer-quality— the 30answerQualityEvalCasesbefore/after comparison (Gate E fixed base). Known metric confound:scoreAnswerQualityEvalCasescores readability over the answer plus every section body with a 220-word ceiling (src/lib/rag/rag-eval-cases.ts); the S2 targets can exceed it by design, so a readability=0 flag driven only by total length is an artefact to adjudicate (raise the ceiling with aneval_config_versionbump, or accept), not a regression. The scorer was left untouched here so the comparison runs under one definition — your call after seeing the distribution.medication_dose_risk×drug_dosing), two risk/withhold (× escalation_risk), two threshold-band, one comparison, one management summary, one document lookup, one bare definition (should stay narrow). Watch for: unsupported menu sections (should be absent, not padded), trust flips tolowfrom a single unverified numeric token in a new section (hides quotes/related), and anyoverexpanded_simple_answerfallbacks (should not occur — the class carrying that gate gets no menu).Risk and rollout
sanitizeAnswerSections/ claim support, and the finalizer's S1d extractive recovery still applies. Longer output spends more of the route budget; S1b (dosing → strong before the deadline) and S1d (final-gate gap recovery) landed first for exactly this reason. Live behaviour is only trusted after the canary pair and the Gate E comparison.ragAnswerPromptVersionv19 isolates the caches, so a revert to v18 serves no S2-shaped cached answers. No migration, index, env, or dependency change.eval:answer-qualityare requested above and require explicit approval; no provider call was made in this session.Clinical Governance Preflight
Clinical KB Database(sjrfecxgysukkwxsowpy)Notes
rag-routing.ts,rag-claim-support.ts,rag-extractive-answer.ts(finalizer),answer-render-policy.ts,smart-rag-api.ts,types.ts,clinical-search.ts,rag-eval-cases.ts,ward-output.ts,ci-change-scope.mjs, every retrieval/ranking/selection file, and the S5KNOWN_DIVERGENCES. Condition-first for/in binding (fix(rag): bind claim-leading for/in conditions in the high-risk trigger check (S1c follow-up) #2065, reverted by revert(rag): back out #2065 claim-leading condition binding after live canary regression #2088) is not reintroduced.npm run ledger:append(immutable record, no table edit). HANDOVER §2 S2 row carries the PR number.buildRelatedInformationMenufromsrc/lib/rag/answer-composition.ts; blocked on this merge + its green canary pair.