feat(rag): tag document-summary rows with a document_context provenance origin - #2053
Conversation
…ce origin Packet G1 (docs/rag-improvement/HANDOVER.md §3), owner decision 2026-08-17, Option B. Closes the legibility half of the H5a live residual. buildDocumentSummaryResults stamps similarity: 1 on every document-summary row. That is a constant, not a measured cosine — summarizeDocument loads every committed chunk of one document, so there is no query to score against. Until now it carried no similarity_origin at all, which left a fabricated 1.0 indistinguishable from a perfect vector match at every surface that reads a row. Adds "document_context" to the similarity_origin union (types.ts), accepts it in the streamed-preview client-source validator (answer-stream-contract.ts, now an allow-set rather than a !== chain so the two cannot drift), and stamps it on the summary rows. The tag is deliberately NOT "synthetic_text". That value marks scores imputed from lexical/structural match strength on the general answer path, where a title hit can masquerade as semantic evidence, and deriveConfidence excludes it from the "high" bar. The summary route has no match strength to inflate and its citations are verified by the same grounding pipeline, so per the owner decision deriveConfidence is unchanged and document summaries still reach "high". Tagging them "synthetic_text" would have silently capped every summary at "medium" — recorded as rejected Option A. rag.ts synthetic_similarity_count likewise keeps its strict equality so the RC9 imputed-score signal stays a single population. Four discriminating pins, each mutation-checked against the change it exists to catch (folding document_context into the deriveConfidence exclusion, widening the telemetry filter, and removing the stamp each turn the matching pin red): - summary rows carry the tag (rag-retrieval-row-contract.test.ts) - two document_context citations at >= 0.82 still yield "high", while the identical scores tagged synthetic_text still yield "medium" (rag-score.test.ts) - synthetic_similarity_count ignores the new value (rag-score.test.ts) - the stream validator accepts every declared union member, with a compile-time exhaustiveness guard so a future union member cannot be added without updating the test (answer-incremental-delivery.test.ts) recordSearchScoreTelemetry is exported solely so the telemetry pin can exercise the real counter rather than pinning its source text; it is not a route surface. RAG impact: no retrieval behaviour change — provenance tag only; confidence derivation unchanged, pinned by test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VHxUr5eyRwrBiXKMoqoARq
…y number `issues:done` and `issues:update` were unusable for any row minted after the ULID migration. `issueRowFingerprint` matched only `/^#(\d+)$/` and looked the row up by `entry.number`, which is null on ULID-backed rows, so it returned null for every Crockford display locator — and `ledger-inbox.mjs` reads a null fingerprint as "no such row" and refuses the request. The symptom was npm run issues:done '#J912J9' ledger inbox: ledger request rejected: #J912J9 is not in Open items about a row plainly present in Open items. The optimistic-concurrency check was therefore unreachable for exactly the rows that can support it. Resolves by exact display id first, falling back to the legacy numeric interpretation. The order matters and shape-based branching does not work: Crockford's alphabet includes 0-9, so a ULID-derived locator can be entirely digits (the writer test's own id is `#041061`) and is indistinguishable from a legacy id by pattern alone. Branching on shape reads such a row as a legacy id and hunts for a sequential number no ULID row has — which is how the first attempt at this fix still returned null for a row it could see. Null-safety is preserved: an absent, archived, or malformed id still returns null, so the inbox's refusal survives for rows that genuinely are not open. Found while queueing the G1 closures in this PR, which is what made the two requested rows unclosable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VHxUr5eyRwrBiXKMoqoARq
Two immutable inbox requests, applied by a later `npm run issues:reconcile` from a serialized ledger branch: - `#J912J9` (P1 issue) — the governance question "may a fabricated similarity of 1 on document-summary rows earn the high confidence label a clinician reads?" Answered by the owner's 2026-08-17 Option B decision and implemented here. - `#0MSNT8` (P3 task) — the G1 implementation row that decision created. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VHxUr5eyRwrBiXKMoqoARq
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VHxUr5eyRwrBiXKMoqoARq
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
This pull request has been ignored for the connected project Preview Branches by Supabase. |
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in:5 minutes Limit details: You’ve used all 1 included review currently available under your plan. You completed 96 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 (15)
Comment |
CI triageCI failed on this PR. Automated classification of the 2 failed job(s):
Compared with main CI run #11693 (cancelled). Classification is evidence routing, not permission to ignore a failure. Exact quarantined Playwright identities remain governed by the flake ledger. |
…conflict (PR #2053) Also condense the recordSearchScoreTelemetry provenance comment in src/lib/rag/rag.ts (6 lines -> 1) to clear the check:maintainability-budgets no-growth budget (4364 -> 4360 of 4362). No behavior change.
Uh oh!
There was an error while loading. Please reload this page.
Summary
docs/rag-improvement/HANDOVER.md§3) — provenance tag for document-summary rows, owner decision 2026-08-17, Option B.buildDocumentSummaryResultsstampssimilarity: 1on every document-summary row. That is a constant, not a measured cosine:summarizeDocumentloads every committed chunk of one document, so there is no query to score against. Until now it carried nosimilarity_originat all, which left a fabricated1.0indistinguishable from a perfect vector match at every surface that reads a row. This adds"document_context"to thesimilarity_originunion (src/lib/types.ts), accepts it in the streamed-preview client-source validator (src/lib/answer-stream-contract.ts), and stamps it inbuildDocumentSummaryResults(src/lib/rag/rag-row-contracts.ts). Per the decision,deriveConfidenceis unchanged — it still excludes only"synthetic_text", so document summaries still reach"high"— andrag.tssynthetic_similarity_countstill counts only"synthetic_text". Option A (tag as"synthetic_text", capping every summary at"medium") is recorded as rejected.docs/clinical-hazard-analysis.mdH5a is updated to mark the decision implemented and to name the remaining residual.issueRowFingerprintresolves rows by display id, not only by number. Independent of the RAG change and separately revertible.npm run issues:doneandissues:updatewere unusable for any row minted after the ULID migration: the lookup matched only/^#(\d+)$/and keyed onentry.number, which isnullon ULID-backed rows, so it returnednullfor every Crockford display locator — andledger-inbox.mjsreads anullfingerprint as "no such row" and refuses the request. Discovered because it made the two closures below impossible to queue. Shape-based branching does not work here: Crockford's alphabet includes0-9, so a ULID-derived locator can be entirely digits (#041061) and is indistinguishable from a legacy id by pattern alone; the fix resolves by exact display id first and falls back to the legacy numeric interpretation. Null-safety for absent, archived, and malformed ids is preserved and pinned./issuesclosures as immutable inbox requests, applied by a laternpm run issues:reconcilefrom a serialized ledger branch:#J912J9(P1 issue — the governance question this packet answers) and#0MSNT8(P3 task — the G1 implementation row that decision created).recordSearchScoreTelemetryis exported solely so the telemetry pin exercises the real counter rather than pinning its source text; it is not a route surface.RAG impact: no retrieval behaviour change — provenance tag only; confidence derivation unchanged, pinned by test.
Why this is safe to land without a canary
Retrieval, ranking, selection, and ordering are untouched: no comparator, no score, no threshold, and no candidate-source path changes. The only runtime difference is one additional string field on rows built by
buildDocumentSummaryResults, whose sole caller issummarizeDocument— never the general answer path.deriveConfidenceandsynthetic_similarity_countboth branch on=== "synthetic_text", so neither observes the new value. The owner's 2026-08-17 decision recorded no canary for exactly this reason.Every pin was mutation-checked rather than assumed — each was made to fail against the change it exists to catch:
"document_context"citations ≥ 0.82 still yield"high""document_context"into thederiveConfidenceexclusion (rejected Option A)synthetic_similarity_countignores the new valuebuildDocumentSummaryResultsissueRowFingerprintresolves an all-digit display idVerification
npm run verify:pr-localFull offline unit suite (inside
verify:pr-local, and captured directly):eval:rag:offline(run directly and again insideverify:pr-local):check:rag:fixtures:Focused suites for the touched files (
npx vitest run tests/rag-score.test.ts tests/rag-retrieval-row-contract.test.ts tests/answer-incremental-delivery.test.ts tests/answer-stream-contract.test.ts tests/rag-imputation-contract.test.ts):npm run formatwas run and the result committed; whole-treeprettier --check .reportsAll matched files use Prettier code style!.npm run verify:uiwhen UI, routing, styling, browser behavior, reduced-motion, or forced-colors behavior changedUI verification not run: no component, route, style, or browser-behaviour file is touched. The diff is three
src/libmodules, one script, tests, and docs.npm run verify:releasebefore release or handoff confidence claimsVerification not run: this is an ordinary PR handoff, not a release-confidence claim, and
verify:releaseis provider-backed.npm run eval:retrieval:quality(must stay 36/36) when retrieval, ranking, selection, chunking, or scoring behavior changedVerification not run: no retrieval, ranking, selection, chunking, or scoring behaviour changed — this adds a provenance string to rows on a route that performs no matching. The gate is also provider-backed and needs explicit owner approval, which was not given for this packet; the owner's decision records no canary for it.
npm run eval:rag -- --limit 15+npm run eval:quality -- --rag-onlywhen answer generation, the synthesis prompt, or answer post-processing changedVerification not run: answer generation, the synthesis prompt, and answer post-processing are untouched. Both commands are provider-backed and were not authorized.
npm run check:production-readinesswhen clinical workflow, privacy, environment, Supabase, source governance, or deployment behavior changedRun because the diff classifies as clinical-risk. It reports the documented offline-profile provider capability gap, not a regression from this change — both failures are absent credentials in this container, and nothing in the diff touches env, Supabase, or provider configuration:
The Supabase target line confirms the pinned project ref is unchanged.
npm run check:deployment-readinesswhen deployment startup, hosting, or rollout behavior changedVerification not run: no deployment, startup, hosting, or rollout behaviour changed.
Risk and rollout
deriveConfidence) is deliberately untouched and pinned by a discriminating test pair, so the confidence label a clinician reads is identical before and after. The realistic failure mode was a consumer ofsimilarity_originthat assumed a closed two-value union and would now see a third value; every such consumer was enumerated (deriveConfidence,rag.tstelemetry,answer-stream-contract.ts,answer-client-payload.ts) and each is either strict-equality on"synthetic_text"or now explicitly allows the new value. The ledger fix carries its own small risk of over-matching an id; it is bounded by resolving against the Open-items table itself and pinned with absent/archived/malformed cases.git revertany of the four commits independently — the RAG tag, the ledger tooling fix, the queued closures, and the ledger record are separate commits with no ordering dependency between them before merge. Reverting the RAG commit restores untaggedsimilarity: 1rows with no other behavioural difference.Clinical Governance Preflight
Clinical KB Database(sjrfecxgysukkwxsowpy)On the last item specifically: no clinical decision-support behaviour changed, so the classification is unaffected. The change makes an existing fabricated score legible rather than altering any answer, confidence label, or trust verdict a clinician sees.
Notes
1.0should contribute to a confidence label at all remains open by design — Option B was chosen because tagging alone has no measured safety cost, whereas Option A downgrades every document summary without measured gain. The tag is what makes a future gate cheap: any later control can now discriminate the document-summary route without re-deriving provenance.docs/clinical-hazard-analysis.mdH5a records this residual explicitly.rag-routing,rag-claim-support, and therag-extractive-answerfinalizer (packets S1b/S1c/S1d).docs/rag-improvement/HANDOVER.md. This PR edits only the single G1 status-table row, so a conflict should be a trivial one-row resolution, but it is worth landing these in a deliberate order.#292duplicate check performed before starting: no open PR touchessimilarity_origin, and none touches the ledger id tooling./issuesclosures are queued as inbox requests only. They take effect whennpm run issues:reconcileruns from a serialized ledger branch after this lands.🤖 Generated with Claude Code
https://claude.ai/code/session_01VHxUr5eyRwrBiXKMoqoARq
Generated by Claude Code