Skip to content

fix(rag): exempt deterministic document-list/table answers from prose sanitizer#207

Merged
BigSimmo merged 1 commit into
mainfrom
claude/structured-answer-sanitizer
Jul 2, 2026
Merged

fix(rag): exempt deterministic document-list/table answers from prose sanitizer#207
BigSimmo merged 1 commit into
mainfrom
claude/structured-answer-sanitizer

Conversation

@BigSimmo

@BigSimmo BigSimmo commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Completes item #1 from the answer-quality triage — the direct-document-lookup-nocc failure.

Root cause (traced end-to-end)

buildDocumentSupportListAnswer / buildTableOrVisualSourceLookupAnswer build deterministic, template answers from source metadata — not model prose. But finalizeRagAnswerQualityCore ran them through sanitizeAnswerText, a clinical-prose extractor. It stripped their document names (facility-code suffixes like (NOCC)(AKG) and semicolon-separated entries read as non-prose), turning a valid answer:

"I found 5 indexed documents…: National Outcomes and Casemix Collection (NOCC)(AKG); …; and Mental Health Medically Cleared For Discharge Procedure (EMHS Policy)."

into garble the quality gate then correctly refused — so the case had 5 citations but grounded=false. (Verified with a QGATE_DEBUG RAW-vs-CLEAN dump: polishClinicalAnswerProse alone is clean; sanitizeStructuredText does the mangling.)

Fix

A preformatted flag on RagAnswer, set by the two deterministic builders and propagated by buildExtractiveAnswer. finalizeRagAnswerQualityCore returns preformatted grounded answers untouched — they're well-formed by construction and carry no free-text clinical claims, so the prose sanitizer/gate is neither needed nor safe for them. The shared sanitizer and quality gate are unchanged for model prose (the "don't touch the load-bearing sanitizer" constraint).

Scope (deliberately narrow)

Fixes the document-list case only. agitation-arousal-table-lookup is queryClass: table_threshold, which routes to fact synthesis (not a structured builder) and produces genuinely incoherent table-cell text the gate correctly refuses — a separate synthesis-quality issue, intentionally left out.

Verification (full golden RAG eval, live Supabase + OpenAI)

  • NOCC now grounded (extractive, 5 citations, no failures).
  • grounded_supported_rate 0.933 → 0.967.
  • No regression from this change. The one unsupported flip in the eval run (unsupported-close-title-noise) is pre-existing LLM variance, not this fix: it is preformatted: None (this fix never touches its path) and refuses 3/3 on re-run.
  • New unit test asserts facility-code suffixes survive intact; tests/rag-answer-fallback.test.ts 26/26; typecheck / lint / prettier clean.

🤖 Generated with Claude Code

… sanitizer

Document-support-list and table/visual source-reference answers are built
deterministically from source metadata by buildDocumentSupportListAnswer /
buildTableOrVisualSourceLookupAnswer — not model prose. But
finalizeRagAnswerQualityCore ran them through sanitizeAnswerText, a clinical-
*prose* extractor, which stripped their document names: facility-code suffixes
like "(NOCC)(AKG)" and semicolon-separated entries read as non-prose and were
dropped, turning a valid answer ("I found 5 indexed documents…: National
Outcomes and Casemix Collection (NOCC)(AKG); …") into garble the quality gate
then correctly refused. So `direct-document-lookup-nocc` failed with 5 citations
but grounded=false.

Add a `preformatted` flag on RagAnswer, set by those two builders and propagated
by buildExtractiveAnswer; finalizeRagAnswerQualityCore returns preformatted
grounded answers untouched (they are well-formed by construction and carry no
free-text clinical claims, so the prose sanitizer/gate is neither needed nor
safe for them). The shared sanitizer and gate are unchanged for model prose.

Scope: this fixes the document-list case only. `agitation-arousal-table-lookup`
is class table_threshold, which routes to fact synthesis (not a structured
builder) and produces genuinely incoherent table-cell output the gate correctly
refuses — a separate synthesis-quality issue, intentionally out of scope.

Validated (full golden RAG eval, live): NOCC now grounded (5 citations),
grounded_supported_rate 0.933 → 0.967. No regression from this change — the one
unsupported flip in the run (unsupported-close-title-noise) is pre-existing LLM
variance: it is preformatted:None (untouched by this fix) and refuses 3/3 on
re-run. New unit test asserts facility-code suffixes survive; rag-answer-fallback
26/26; typecheck/lint/prettier clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@BigSimmo
BigSimmo enabled auto-merge (squash) July 2, 2026 18:22
@BigSimmo
BigSimmo merged commit a5551e2 into main Jul 2, 2026
4 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: bb4da7d9fe

ℹ️ 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 thread src/lib/rag.ts
// The clinical-prose sanitizer/quality gate below is designed for model prose and would
// strip their document names (facility codes like "(NOCC)(AKG)" read as non-prose),
// turning a valid answer into garble that then fails the gate. Return them untouched.
if (answer.preformatted && answer.grounded) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve preformatted answers through the client renderer

When this path returns a document-list answer intact, the browser still re-sanitizes answer.answer: I checked ClinicalDashboard.tsx, where safeAnswerText is always built with sanitizeAnswerDisplayText(answer?.answer ?? "") and passed as safeAnswerText || answer.answer before NaturalLanguageAnswer calls primaryAnswerDisplayText, which also uses the display/prose sanitizers. For the fixed NOCC-style query the API answer now keeps (NOCC) (AKG), but the rendered answer can still strip or mangle that suffix before display, so the user-facing regression remains unless the renderer honors preformatted or skips display sanitization for it.

Useful? React with 👍 / 👎.

@BigSimmo
BigSimmo deleted the claude/structured-answer-sanitizer branch July 5, 2026 11:43
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