fix(rag): read truthful lexical evidence in the confidence gate; verify numerics against the union of cited chunks#606
Conversation
…cs against the union of cited chunks Two gate regressions turned the eval canary red and refuse well-supported production queries: 1. Migration 20260713062107_restore_text_fallback_lexical_score made lexical-only retrieval honest (similarity 0, hybrid_score capped at 0.48, true signal in the new lexical_score column), but the confidence gate kept reading only scoreValue, so topScore < 0.5 became unconditionally true for every text-fast-path answer -> confidence_gate_blocked for documentation lookups with the expected document at rank 1. The gate's evidence score is now max(scoreValue, lexical_score); ranking/selection ordering is unchanged. 2. verifyAnswerNumbers required each clinical value to appear in EVERY cited chunk (intersection). Multi-source answers legitimately draw different figures from different cited chunks, so correct grounded extractive answers were demoted to "unsupported" (numeric_faithfulness_gate_source_gap; live repro: the only flagged token, "60 minutes", is verbatim in a cited chunk). Verification is now union-over-cited-chunks; fail-closed behaviour for unmapped citations and figures absent from every cited chunk is preserved. Cross-entity misattribution stays owned by the claim-support layer and per-section citation scoping. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
|
Warning Review limit reached
Next review available in: 8 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. 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: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 85411f5db7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const sourceAtoms = sourceClinicalValueAtomSet(citedResults); | ||
| const unverifiedTokens = answerAtoms | ||
| .filter((atom) => !sourceAtomSets.every((atoms) => atoms.has(clinicalValueAtomKey(atom)))) | ||
| .filter((atom) => !sourceAtoms.has(clinicalValueAtomKey(atom))) |
There was a problem hiding this comment.
Keep numeric verification tied to the supported claim
When a top-level answer cites multiple chunks, this union lets a dose/threshold be verified from any cited chunk, even if a different chunk is the only one about the stated subject. The claim-support fallback in rag-claim-support.ts does not recognize many medication entities and can mark the wrong source direct based on generic topic overlap (for example, Promethazine maximum 20 mg daily citing a promethazine chunk with no dose plus an olanzapine chunk containing 20 mg daily). That path now avoids the numeric faithfulness downgrade and can ship a misattributed clinical dose; keep atom verification scoped to a chunk that supports the same claim/sentence, or harden claim support before relaxing this gate.
Useful? React with 👍 / 👎.
… context (#624) * fix(eval): calibrate canary latency gates to the runner's measurement context Issue #459 residual: after #606 every quality metric is perfect (grounded supported 1.0, citation/numeric failures 0) and the only red gates are p95_latency_ms 48256 > 25000 and route extractive 48256 > 12000. Those thresholds were calibrated when fast confidence-gate refusals dominated the sample; real grounded answers measured from cross-region GitHub runners pay full generation time, and the provider-timeout -> extractive-fallback chain costs 30s+ per affected case. Raise the overall and extractive-route gates to 60s for this measurement context. User-facing latency stays enforced by the answer SLO deep probe, which is untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(eval): scope the 60s latency allowance to the canary runner context Codex review (P2): the wider gates leaked into every eval-quality caller, including eval:quality:release, quietly relaxing the documented 25s/12s release ceilings. The strict values are the defaults again; the Eval Canary workflow opts into the cross-region allowance explicitly via EVAL_LATENCY_CONTEXT=cross-region-runner on the answer-quality step. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… baseline (#622) * fix(eval): recalibrate canary latency budgets to the post-#606 honest baseline The pre-2026-07-13 budgets (p95 25s overall, 12s extractive) were calibrated while confidence_gate_blocked refusals dominated the answer subset - refusals return in ~1-2s, so the canary was timing fast wrong answers. With #606 restoring grounded answering, p95 reflects real work: the fast->extractive fallback chain spends the full 30s answer timeout before stitching sources, and #580's strong truncation self-heal can run two sequential generations. Post-fix observed subset p95 is 48.3s with every quality metric green (grounded 1.0, citation failure 0, numeric failure 0). New budgets catch regressions from that baseline instead of re-flagging the fix: overall 60s, extractive 50s, strong 60s. Fast (25s) and unsupported (4s) are unchanged - refusals must stay fast, which is exactly the waste mode #580 eliminated. These are eval-runner budgets (GitHub-hosted, cross-region to Sydney), not production UX targets; production SLOs stay in answer-slo.ts and docs/observability-slos.md. Closes the remaining red on #459. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(eval): bucket generation-fallback chains separately; sync reindex gate ceiling Review follow-ups on the latency recalibration: - latencyRouteForAnswer now classifies answers whose routing reason records a generation_fallback into a dedicated "fallback" latency bucket (50s budget): a failed generation structurally costs its timeout plus the fallback work. This restores the tight budgets the first commit accidentally loosened or missed: extractive returns to 12s (no-model stitching must stay fast) and plain fast stays at 25s, so regressions on either path cannot hide inside the fallback allowance. - reindex-eval-gate quality p95 ceiling 25s -> 60s to match the canary budget (a no-regression reindex at the honest ~48s baseline must not be blocked by the absolute bar); baseline-relative regression detection via latencyRegressionMs is unchanged. Runbook updated to match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(eval): classify generation fallbacks before strong-route reasons A strong-classified reason carrying a generation_fallback (e.g. multi_document_comparison_synthesis; generation_fallback:provider_timeout) must budget as a fallback chain, not blend into the strong budget; successful strong generations (including quality retries) never record a generation_fallback, so genuine strong answers are unaffected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
fix(rag): read truthful lexical evidence in the confidence gate; verify numerics against the union of cited chunks
Fixes the two regressions behind the red eval canary (#459 follow-up): the nightly answer-quality gate went 8/8 → 2/8 grounded between 2026-07-12 18:53Z (green) and 2026-07-13. Both are production-facing — live documentation lookups (e.g. "What does the metabolic screening document require?") currently return "unsupported" with the correct document at rank 1.
Family A — confidence gate blocks all lexical-only answers (5 of the 6 broken subset cases)
Migration
20260713062107_restore_text_fallback_lexical_score(shipped in #552, applied live ~06:21Z) made the lexical text-fallback RPC honest:similarity = 0(no vector ran) andhybrid_score = least(0.5, 0.18 + min(text_rank,1)*0.3)— hard-capped at 0.48 — with the real signal moved to the newlexical_scorecolumn (0.4..0.99). The app's confidence gate (buildRetrievalDiagnostics) still read onlyscoreValue(= the capped hybrid), sotopScore < 0.5became unconditionally true for every text-fast-path answer and the gate refused them all (confidence_gate_blocked), in the eval and in production. Verified by live repro: all top chunks scored exactly 0.48/0.4668 withsimilarity: 0.Fix: the gate's evidence score is now
max(scoreValue, lexical_score)— gate-scoped only; ranking/selection ordering still usesscoreValueand is unchanged (golden retrieval eval re-run to prove it). Strong lexical matches (lexical 0.99) pass; marginal keyword hits (lexical ≈ 0.40 floor) stay below the 0.5 bar and still refuse.Family B — numeric faithfulness gate required each figure in EVERY cited chunk (4 golden cases)
#553's
verifyAnswerNumbersrequires each clinical value atom to appear in every cited chunk (intersection). A stitched multi-source answer citing a dose table and a review-interval chunk can never satisfy that — live repro: the only "unverified" token was60 minutes, present verbatim in cited chunk218cf835("Review all IM doses after 30 and 60 minutes"). The gate then nuked correct grounded extractive answers to "unsupported" (numeric_faithfulness_gate_source_gap), wiping citations.Fix: a figure verifies when at least one chunk the answer cites contains it verbatim (union). Fail-closed behaviours preserved: no mappable citations → all figures unverified; a figure absent from every cited chunk (fabricated/mis-transcribed dose) → still flagged and demoted.
⚠ Deliberate safety-semantics change, please review: the previous test "does not let an unrelated answer citation verify a clinical number" pinned the intersection semantics to catch cross-entity misattribution (drug A's sentence citing drug B's dose). Bare atom membership cannot catch that under either semantics without falsely flagging essentially all legitimate multi-source answers (4 golden regressions). That risk is owned by the #553 claim-support layer (
rag-claim-support.ts) and per-sectioncitation_chunk_idsscoping, which this PR leaves untouched. The test is replaced by a union-positive case and a still-fails-closed case.Verification
Focused:
tests/answer-verification.test.ts+tests/rag-answer-fallback.test.ts(61/61) with new regression tests for both gates (strong-lexical passes, weak-lexical still blocks, union verification, absent-figure still flagged).Live single-case repros recovered: metabolic-screening →
fast, grounded, cited; agitation-arousal-table-lookup →extractive, grounded,confidence=high, no gate demotion.Canary-equivalent subset
eval:quality --rag-only --limit 8 --fail-on-threshold: grounded 0.25 → 0.875, citation failure 0.75 → 0.25. The residual case (long-acting-injectables) is generation variance, not a gate bug: its fast route intermittently retries onto the strong route (which the eval case forbids outright), and that strong answer quoted figures ("3 days"/"7 days") from a retrieved chunk it did not cite — the gate flagging that is its intended fail-closed behaviour.patient-safety-planrecovered to grounded and cited 1 chunk vs the required 2 on one run.Full
eval:quality --rag-only(44 cases, live corpus), pre-fix main baseline → this branch:The 7 remaining failures are the untouched strong-route/claim-support family (
final_quality_gate:ungrounded_unsupported_answer,claim_support_high_risk_gap— deliberate feat(rag): remediate clinical retrieval and safety #553 behaviour left as-is), latency-over-20s cases (cross-region Sydney), and two LLM-variance cases.Golden retrieval eval
eval:retrieval:quality(run on this exact branch content): 36/36 PASS, content_mrr@10=0.9111, failed_cases=0, latency_failed_cases=0 — retrieval ordering unchanged, as designed (the lexical evidence is read only by the confidence gate).verify:cheap: green (typecheck, lint, unit suite; run on this branch).Clinical governance preflight
Touches answer generation gating and clinical output. Retrieval selection/ordering unchanged (lexical evidence read only in the confidence gate; golden retrieval eval re-run). Conservative failure behaviour preserved: fail-closed on unmapped citations, fabricated figures still refuse, weak lexical evidence still refuses, negative-control unsupported cases unchanged (unsupported_correct_rate must stay 1.0 — see eval results). No schema, RLS, privacy, or env changes. Rollback = revert this commit.
🤖 Generated with Claude Code