Skip to content

fix(rag): read truthful lexical evidence in the confidence gate; verify numerics against the union of cited chunks#606

Merged
BigSimmo merged 1 commit into
mainfrom
claude/canary-gate-fixes
Jul 13, 2026
Merged

fix(rag): read truthful lexical evidence in the confidence gate; verify numerics against the union of cited chunks#606
BigSimmo merged 1 commit into
mainfrom
claude/canary-gate-fixes

Conversation

@BigSimmo

Copy link
Copy Markdown
Owner

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) and hybrid_score = least(0.5, 0.18 + min(text_rank,1)*0.3)hard-capped at 0.48 — with the real signal moved to the new lexical_score column (0.4..0.99). The app's confidence gate (buildRetrievalDiagnostics) still read only scoreValue (= the capped hybrid), so topScore < 0.5 became 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 with similarity: 0.

Fix: the gate's evidence score is now max(scoreValue, lexical_score) — gate-scoped only; ranking/selection ordering still uses scoreValue and 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 verifyAnswerNumbers requires 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 was 60 minutes, present verbatim in cited chunk 218cf835 ("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-section citation_chunk_ids scoping, 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-plan recovered 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:

    Metric main (pre-fix) fixed
    Grounded supported rate 0.2333 0.8333
    Citation failure rate 0.5 0.1136
    Numeric grounding failure rate 0.1591 0
    Unsupported correct rate 1.0 1.0 (negative controls unchanged)
    Source governance danger failure rate 0 0

    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

…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>
@supabase

supabase Bot commented Jul 13, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project sjrfecxgysukkwxsowpy because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@BigSimmo, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 8 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bef1cbdd-d3f0-4230-8538-c85ec20f77bc

📥 Commits

Reviewing files that changed from the base of the PR and between 5098fe3 and 85411f5.

📒 Files selected for processing (4)
  • src/lib/answer-verification.ts
  • src/lib/rag.ts
  • tests/answer-verification.test.ts
  • tests/rag-answer-fallback.test.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/canary-gate-fixes
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch claude/canary-gate-fixes

Comment @coderabbitai help to get the list of available commands.

@BigSimmo
BigSimmo enabled auto-merge (squash) July 13, 2026 15:20
@BigSimmo
BigSimmo merged commit ce99cbc into main Jul 13, 2026
15 of 16 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +437 to +439
const sourceAtoms = sourceClinicalValueAtomSet(citedResults);
const unverifiedTokens = answerAtoms
.filter((atom) => !sourceAtomSets.every((atoms) => atoms.has(clinicalValueAtomKey(atom))))
.filter((atom) => !sourceAtoms.has(clinicalValueAtomKey(atom)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

BigSimmo added a commit that referenced this pull request Jul 13, 2026
… 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>
BigSimmo added a commit that referenced this pull request Jul 13, 2026
… 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>
@BigSimmo
BigSimmo deleted the claude/canary-gate-fixes branch July 14, 2026 09:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant