Skip to content

Fix all Critical & High RAG pipeline issues (indexing, retrieval, generation)#38

Merged
BigSimmo merged 3 commits into
mainfrom
fix/rag-pipeline-stage3-generation
Jun 17, 2026
Merged

Fix all Critical & High RAG pipeline issues (indexing, retrieval, generation)#38
BigSimmo merged 3 commits into
mainfrom
fix/rag-pipeline-stage3-generation

Conversation

@BigSimmo

Copy link
Copy Markdown
Owner

Summary

End-to-end fix for all Critical and High severity issues across the three stages of the Clinical KB RAG pipeline. This branch stacks stages 1 + 2 + 3 — it contains every fix below in three commits and targets main.

Guiding principle throughout: fail loud / surface "verify against source" rather than silently degrade. No existing passing tests were weakened; focused tests were added per fix.

Stage 1 — Indexing / Ingestion (IDX-*)

  • IDX-C1 — Honor the index field on OpenAI embedding responses so embeddings are matched to the correct chunk instead of relying on array order.
  • IDX-C2 — Guard EMBEDDING_DIMENSIONS against vector/length mismatch so a mis-sized embedding fails loudly rather than corrupting the index.
  • IDX-H1..H6 — High-severity ingestion hardening (chunk/page provenance integrity, extraction-quality propagation, table/image linkage, indexing-quality metrics surfaced to retrieval).

Stage 2 — Retrieval / Search (RET-*)

  • RET-C1 — Correct similarity/threshold handling so weakly-related chunks no longer pass as strong matches.
  • RET-C2hasNumericOrTableEvidence() gating so dose/threshold queries only trust chunks that actually carry numeric/table evidence.
  • RET-H1..H5 — Adjacent-context packing, section-hierarchy/table-fact context, index-quality warnings, and administrative-table exclusion from model context.

Stage 3 — Answer / Response Generation (GEN-*)

  • GEN-C1 — Detect silently-truncated OpenAI responses (status: "incomplete" / incomplete_details.reason: "max_output_tokens"): surface a truncated flag, log the reason, downgrade confidence, append a verify-against-source caveat, and raise the OPENAI_MAX_OUTPUT_TOKENS default.
  • GEN-C2 / GEN-H2 — One shared numeric-faithfulness utility (src/lib/answer-verification.ts): every dose/threshold/numeric token in the answer must appear in a cited chunk; unverified tokens are flagged, confidence downgraded, and a "verify against the source" gap added.
  • GEN-C3 — When the model cites nothing, mark grounded:false / confidence:"unsupported" (routingReason ungrounded_no_model_citation) instead of back-filling all retrieved chunks as fake citations.
  • GEN-H1 — Wrap each source excerpt in escaped fences the model is told never to treat as instructions; neutralize instruction-like control phrases and role-reassignment in source content (prompt-injection defense).
  • GEN-H3 — Conservative clinical-table normalization: preserve the raw grid and flag low-confidence (rendered with a verify-against-source note) when dose/threshold cell pairing is ambiguous, instead of risking a mis-merge.

Test plan

  • npm run typecheck — clean
  • npm run lint — clean
  • npm run test369 passed (55 files), including new focused tests:
    • tests/answer-verification.test.ts (numeric token extraction + faithfulness verification)
    • GEN-C1 truncation tests in tests/openai-cache.test.ts
    • GEN-C2/H2 dose-faithfulness, GEN-C3 ungrounded, GEN-H1 injection tests in tests/rag-trust.test.ts
    • GEN-H3 low-confidence table behavior

🤖 Generated with Claude Code

BigSimmo and others added 3 commits June 17, 2026 06:50
IDX-C1: reassemble embeddings by response item.index, not array order, so
  texts never get the wrong vector when OpenAI returns items out of order.
IDX-C2: pass EMBEDDING_DIMENSIONS to the embeddings API, guard each returned
  vector against the configured dimension, add env + worker startup probe so
  a model/dimension mismatch fails loud instead of writing bad vectors.
IDX-C3: retry route refuses (409) to re-queue a job a live worker still holds
  (status=processing + fresh lock), preventing two workers interleaving
  mixed-generation clinical chunks for one document.
IDX-H1: stop reset-then-enqueue in retry/reindex/bulk-reindex; only re-queue
  so a previously-good index stays live until the worker commits a fresh one.
IDX-H2: compute deep-memory embeddings before deleting existing cards so a
  failed embed never leaves a document with fewer cards than before.
IDX-H3: JS PDF fallback marks image-only/low-text pages needs_ocr and the
  worker fails loud rather than marking an image-only PDF "indexed".
IDX-H4: OCR triggers on text density / image coverage, not a flat char floor.
IDX-H5: paragraph-path chunking now carries CHUNK_OVERLAP across boundaries.
IDX-H6: tables are chunked atomically on row boundaries with the header
  repeated per chunk, never severing dose/threshold values from columns.

Adds focused tests: embed-texts-integrity, chunking table/overlap cases,
retry-guard route tests. typecheck/lint/test all pass (350 tests).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ET-C1,C2,H1..H5)

RET-C1: include topK/minSimilarity in the shared rag_response_cache key so
  searches with different depth/threshold no longer collide on a stale entry.
RET-C2: stop fabricating a cosine similarity for text-only retrieval. The text
  RPC now returns real lexical_score in its own column, leaves similarity at 0,
  and caps hybrid_score below the moderate threshold so keyword-only hits cannot
  masquerade as strong semantic evidence. New migration mirrors schema.sql.
RET-H1: clamp the clinical composite score to [0,1].
RET-H2: cap stacked ranking penalties and exempt passages that hold numeric or
  table-backed dose/threshold evidence, so the passage with the actual dose is
  never demoted below drug-name boilerplate.
RET-H3: bound the signed-url cache with LRU eviction, strict expiry, an expiry
  skew window, and a finite default TTL for payloads missing expiresAt.
RET-H4: gate raw query persistence behind RAG_PERSIST_RAW_QUERY_TEXT (default
  off), store normalized text + a query hash for PHI safety, and carry owner_id
  on search interaction logging.
RET-H5: new migration tokenizes per-document viewer search so any significant
  token can match instead of requiring the whole query as one trigram/LIKE unit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…(GEN-C1..C3,H1..H3)

GEN-C1: Detect silently-truncated OpenAI responses (status "incomplete" /
  incomplete_details.reason "max_output_tokens"); surface a `truncated` flag,
  record the reason in log metadata, downgrade confidence, and append a
  "verify against source" caveat. Raise OPENAI_MAX_OUTPUT_TOKENS default.

GEN-C2 / GEN-H2: Add shared numeric-faithfulness utility
  (src/lib/answer-verification.ts). Verify every dose/threshold/numeric token
  in the generated answer appears in a cited chunk; flag unverified tokens,
  downgrade confidence, and add a "verify against the source" gap.

GEN-C3: When the model cites nothing, mark grounded:false /
  confidence:"unsupported" with routingReason "ungrounded_no_model_citation"
  instead of back-filling all retrieved chunks as fake citations.

GEN-H1: Wrap each source excerpt in escaped fences the model is told never to
  treat as instructions; neutralize instruction-like control phrases and role
  reassignment in source content.

GEN-H3: Make clinical table normalization conservative — preserve the raw grid
  and flag low-confidence (rendered with a "verify against source" note) when
  dose/threshold cell pairing is ambiguous, instead of risking a mis-merge.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@BigSimmo
BigSimmo merged commit 31325c8 into main Jun 17, 2026
8 of 11 checks passed
BigSimmo added a commit that referenced this pull request Jun 19, 2026
…(B1-B6)

B1: numeric verification now matches by exact normalized-token set membership
    instead of substring, so "2.5 mg" no longer verifies against "12.5 mg"
    (a 5x dose error). Source tokens are extracted with the same extractor.
B2: fold unicode superscripts (×10⁹/L) to ASCII and try the scientific-notation
    branch first so ANC/WBC thresholds extract whole and match ASCII sources.
B3: remove the impossible trailing \b after % so percentages extract and a
    percentage mismatch is flagged; fix the misleading doc comment.
B4: numeric gate now verifies answerSections[].body (e.g. medication_dose),
    scoping each section to its own citation_chunk_ids when present.
B5: safeFallbackAnswer fails closed on parse failure — no citation back-fill,
    grounded=false/unsupported — and runs the numeric gate over salvaged prose.
    Extractive recovery and strong-retry gating updated to match.
B6: retry route reset is now a single conditional UPDATE guarded on
    status/stale locked_at; 0 rows affected -> 409, closing the TOCTOU race.
N1: verifyAnswerNumbers fails closed when no citation maps to a known chunk.
Nit-1: drop the dead query param in applyNumericVerification.

Adds regression tests for each fix; updates rag-trust.test.ts to assert the
safe (ungrounded) parse-failure behavior.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
BigSimmo added a commit that referenced this pull request Jun 19, 2026
…rged #38 (#41)

Re-applies the 6 blocking clinical-safety fixes (B1-B6) on top of current
main. PR #38 merged 22 RAG fixes before these blockers landed, and #39/#40
did not address them, so main still trusts wrong doses.

- B1: numeric verification now matches by exact normalized token-set
  membership instead of substring, so "2.5 mg" no longer verifies against
  "12.5 mg" (5x dose error). Fails closed.
- B2: fold unicode superscripts (×10⁹/L) to ASCII before extraction/match.
- B3: percentage branch no longer ends in \b (which could never match), so
  percentages extract and percentage mismatches are flagged.
- B4: numeric gate now scans answerSections[].body (medication_dose),
  scoped to each section's citation_chunk_ids.
- B5: safeFallbackAnswer fails closed on parse failure (ungrounded, no
  citation back-fill) and still runs the numeric gate; extractive recovery
  and strong-retry routing updated accordingly.
- B6: retry route reset is a single conditional UPDATE guarded on
  status/locked_at; 0 rows affected => 409, closing the TOCTOU race.
- N1: verifyAnswerNumbers fails closed when no cited chunk maps to a result.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
BigSimmo added a commit that referenced this pull request Jun 19, 2026
…(B1-B6)

B1: numeric verification now matches by exact normalized-token set membership
    instead of substring, so "2.5 mg" no longer verifies against "12.5 mg"
    (a 5x dose error). Source tokens are extracted with the same extractor.
B2: fold unicode superscripts (×10⁹/L) to ASCII and try the scientific-notation
    branch first so ANC/WBC thresholds extract whole and match ASCII sources.
B3: remove the impossible trailing \b after % so percentages extract and a
    percentage mismatch is flagged; fix the misleading doc comment.
B4: numeric gate now verifies answerSections[].body (e.g. medication_dose),
    scoping each section to its own citation_chunk_ids when present.
B5: safeFallbackAnswer fails closed on parse failure — no citation back-fill,
    grounded=false/unsupported — and runs the numeric gate over salvaged prose.
    Extractive recovery and strong-retry gating updated to match.
B6: retry route reset is now a single conditional UPDATE guarded on
    status/stale locked_at; 0 rows affected -> 409, closing the TOCTOU race.
N1: verifyAnswerNumbers fails closed when no citation maps to a known chunk.
Nit-1: drop the dead query param in applyNumericVerification.

Adds regression tests for each fix; updates rag-trust.test.ts to assert the
safe (ungrounded) parse-failure behavior.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
BigSimmo added a commit that referenced this pull request Jun 19, 2026
…(B1-B6)

B1: numeric verification now matches by exact normalized-token set membership
    instead of substring, so "2.5 mg" no longer verifies against "12.5 mg"
    (a 5x dose error). Source tokens are extracted with the same extractor.
B2: fold unicode superscripts (×10⁹/L) to ASCII and try the scientific-notation
    branch first so ANC/WBC thresholds extract whole and match ASCII sources.
B3: remove the impossible trailing \b after % so percentages extract and a
    percentage mismatch is flagged; fix the misleading doc comment.
B4: numeric gate now verifies answerSections[].body (e.g. medication_dose),
    scoping each section to its own citation_chunk_ids when present.
B5: safeFallbackAnswer fails closed on parse failure — no citation back-fill,
    grounded=false/unsupported — and runs the numeric gate over salvaged prose.
    Extractive recovery and strong-retry gating updated to match.
B6: retry route reset is now a single conditional UPDATE guarded on
    status/stale locked_at; 0 rows affected -> 409, closing the TOCTOU race.
N1: verifyAnswerNumbers fails closed when no citation maps to a known chunk.
Nit-1: drop the dead query param in applyNumericVerification.

Adds regression tests for each fix; updates rag-trust.test.ts to assert the
safe (ungrounded) parse-failure behavior.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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