Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
| 2026-08-18 | claude/s2-rag-composition-7330b0 | aab67a1849472ab2db47bba9b2b63d24cc06cec3 | src/lib/rag/answer-composition.ts (new), src/lib/rag/rag.ts buildAnswerInput + answerSections maxItems 6, src/lib/rag/rag-answer-instructions.ts, src/lib/rag/rag-versioning.ts (prompt v19, schema v4), src/lib/openai.ts prompt-cache key, adversarial baseline re-capture, HANDOVER/README/behaviour-map docs, tests | packet S2 (A2 + A3) built and self-reviewed: intent-conditioned related_information_menu line + moderate length targets; RAG behaviour change, canary pair 32100681177 -> post-merge dispatch owed; PR opened for owner merge | focused vitest 122/122 (answer-composition 9, prompt pins 5, rag-answer-fallback, openai-cache); check:maintainability-budgets rag.ts 4362/4362; check:rag:fixtures 36 cases / 26 suites; eval:rag:offline 26 suites / 623 tests; eval:rag:adversarial:offline 25/25 (3 KNOWN_DIVERGENCES pinned); check:production-readiness (provider env absent in worktree); verify:pr-local heavy scope: lint/typecheck green, unit 7023 passed / 1 pre-existing Windows path flake in tests/session-start-hook.test.ts reproduced at merge base 4ea310e48; build green; medication checks green |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
{
"version": 2,
"id": "a727ac1a-1d72-41bd-88f9-76945528bc97",
"createdOn": "2026-08-18",
"action": "add",
"payload": {
"pri": "P2",
"type": "rec",
"summary": "ci-change-scope rag_eval_changed regex misses src/lib/rag/** (post-#994 layout), so a src/lib/rag-only PR skips eval:rag:adversarial:offline and the RAG eval CI job",
"detail": "scripts/ci-change-scope.mjs:290 matches only src/lib/rag.ts and src/lib/rag-*.ts (the pre-#994 layout); src/lib/rag/rag.ts, src/lib/rag/rag-answer-instructions.ts, src/lib/rag/answer-composition.ts do not set rag_eval_changed=true. verify-pr-local.mjs:123-124 then selects only check:rag:fixtures and ci.yml:413-425 skips the safety/RAG eval job. Packet S2 (2026-08-18) was covered only because it also touched tests/answer-*.test.ts and scripts/fixtures/*. Fix: add a src/lib/rag/ prefix (or /^src\\/lib\\/rag\\//) to ragEvalPatterns with a scope test proving src/lib/rag/rag.ts alone trips rag_eval_changed; workflow/policy scope, own PR (operational-risk classifier), never bundled with a RAG behaviour change.",
"source": "packet S2 review, docs/rag-improvement/HANDOVER.md",
"issueUlid": "01M09QHKD3SDQSFDZJB25ZJCCS"
}
}
21 changes: 21 additions & 0 deletions docs/rag-behaviour/behaviour-map.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -113,3 +113,24 @@ answerShape}` where `answerShape` is provider-safe counts/lengths only — never
`generation_fallback:generation_quality_failed` degraded token, cache exclusion, and the
source-only fallback are byte-for-byte unchanged. Do not use these fields to relax a gate;
they exist so a live degraded answer can name the gate that rejected it.

## 8. Answer composition menu (packet S2, 2026-08-18)

- `src/lib/rag/answer-composition.ts` is a pure map from (`RagQueryClass`,
`ClinicalQueryIntent`) to a **related-information menu**: the `answerSections` kinds the
model should attempt when — and only when — the retrieved excerpts support them.
`buildAnswerInput` (`rag.ts`) serialises it as one `related_information_menu:` line in the
"Interpreted clinical task" block; `answerInstructions` §"Answer sections" tells the model the
menu is advisory, evidence-gated, cited like any other section, and subordinate to the
verbatim narrow-question rule. Prompt version `clinical-rag-answer-v19`, schema `maxItems` 6.
- Rule: the query class is authoritative; the heuristic intent refines only
`medication_dose_risk` / `table_threshold`, and only on `escalation_risk` (dosing/threshold
menus otherwise). `comparison` and `broad_summary` carry fixed menus; `document_lookup` and
`unsupported_or_general` deliberately carry **none** — the latter is the only class where
`isOverExpandedSimpleGeneratedAnswer` (> 95 words / > 1 section) can fire. `definition`
intent does not silence a menu because `intentFromSignals` matches "long-term" / "determine".
- Prompt-only: no pipeline stage, `RagAnswer` field, render block, routing, retrieval,
ranking, selection, claim-support, or finalizer change. Verification and the render trust
ladder apply to menu sections unchanged. Contract pins: `tests/answer-composition.test.ts`
(all 48 class×intent cells), `tests/rag-answer-composition-prompt.test.ts`,
`tests/rag-answer-fallback.test.ts` (menu line in the real prompt input).
45 changes: 26 additions & 19 deletions docs/rag-improvement/HANDOVER.md

Large diffs are not rendered by default.

15 changes: 10 additions & 5 deletions docs/rag-improvement/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -90,11 +90,16 @@ this plan:
(`src/lib/rag/rag-second-stage.ts`) → `chooseAnswerRoute` (`src/lib/rag/rag-routing.ts`) →
fast/strong generation (reasoning-effort routing, not different models) → numeric
verification, claim support, citation sanitisation → render policy trust ladder.
- **Answer shape today:** the `answer` field is prompted to 1–3 sentences (~35–75 words);
`answerSections` carries 0–1 sections for simple facts, 2–5 for complex questions. The
prompt (`answerInstructions`, `rag.ts:3150`) and the "Interpreted clinical task" block
built by `buildAnswerInput` already carry `intent`, `query_class`, `answer_focus`,
`answer_scope`, and the full `answer_plan.*` fields.
- **Answer shape today (post-S2, prompt `clinical-rag-answer-v19`, 2026-08-18):** the
`answer` field is prompted to 2–4 sentences (~60–110 words) for complex questions, with the
narrow-question rule verbatim (a definition, one threshold, a single dose, or a yes/no stays
1–3 sentences, ~35–75 words); `answerSections` carries 0–1 sections for simple facts, 3–6
for complex questions when the excerpts support them (schema `maxItems` 6). The prompt
(`answerInstructions`, `src/lib/rag/rag-answer-instructions.ts`) and the "Interpreted
clinical task" block built by `buildAnswerInput` carry `intent`, `query_class`,
`answer_focus`, `answer_scope`, the A2 `related_information_menu` line
(`src/lib/rag/answer-composition.ts`), and the full `answer_plan.*` fields. Before S2 the
targets were 1–3 sentences / 35–75 words and 2–5 sections.
- **Budgets:** `unsupported 0 / extractive 12s / fast 25s / strong 35s`; a
truncation self-heal retries with `strongRetryMaxOutputTokens`. Source-only fallback
(`source_backed_review_fallback`) fires on quality-gate failure, ungrounded extractive
Expand Down
43 changes: 31 additions & 12 deletions docs/rag-improvement/baseline-record.md
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
# RAG evaluation baseline record

**Status:** maintained record, created 2026-08-17 by programme packet S4 (B0). This is the
**Status:** maintained record, created 2026-08-17 by programme packet S4 (B0); re-recorded
2026-08-18 by packet S2 when the answer prompt moved to `clinical-rag-answer-v19` (the validator
cross-checks the prompt version, so a prompt bump always re-captures this record). This is the
baseline named by [README.md](README.md) §B0. The machine-readable source of truth is
`scripts/fixtures/rag-adversarial-baseline.v1.json`, validated by
`npm run check:rag:adversarial-fixtures`; this page explains what the fields mean and why the
Expand All@@ -14,31 +16,31 @@ so two reports can be compared without guessing what changed between them.

| Field | This baseline | Where it comes from |
| --------------------- | -------------------------------------------------------------- | --------------------------------------------------------------------------- |
| `commit_sha` | `92f7618c0ccac336ef6c245b89f37b046f4eac32` | `git rev-parse HEAD` of the evaluated tree. Full 40 characters, enforced. |
| `commit_sha` | `b7aa925f0ae19e89a9f0acf842b4a80d84083fb5` | `git rev-parse HEAD` of the evaluated tree. Full 40 characters, enforced. |
| `dataset_version` | `rag-adversarial-cases.v1` | The fixture dataset's own `datasetVersion`; cross-checked against the file. |
| `eval_config_version` | `rag-eval-config-v1` | Bumped by hand whenever a case list, threshold, or gate semantic changes. |
| `model_version` | `answer=gpt-5.6-terra; fast=gpt-5.6-terra; strong=gpt-5.6-sol` | The resolved answer-model defaults in `src/lib/env.ts`. |
| `embedding_version` | `text-embedding-3-small@1536` | `OPENAI_EMBEDDING_MODEL` and `EMBEDDING_DIMENSIONS` in `src/lib/env.ts`. |
| `index_version` | `20260814151000_validate_therapy_favourites_content_type` | The latest applied migration — the index shape the retrieval RPCs run on. |
| `index_version` | `20260818090000_schema_drift_snapshot_history_probe` | The latest applied migration — the index shape the retrieval RPCs run on. |

The field set and its order are pinned by `tests/rag-adversarial-fixtures.test.ts`. Adding,
removing, or reordering a field is a deliberate contract change, not an edit.

Two further values sit outside the key because they qualify the whole record rather than
identify a run: `promptVersion` is `clinical-rag-answer-v18`, cross-checked at validation time
identify a run: `promptVersion` is `clinical-rag-answer-v19`, cross-checked at validation time
against `src/lib/rag/rag-versioning.ts` so the record cannot describe a superseded prompt; and
`semanticRerankEnabled` is `false`, which the validator requires, because issue `#001` keeps the
ambiguity-band semantic reranker off until an approved comparison exists.

## 2. Gate results

| Gate | Cases | Status | Result |
| ---------------------- | ----- | ----------------- | ----------------------------------------------------------------- |
| `retrieval_golden` | 36 | pending owner run | Provider-backed; last recorded green at `2bd146eed`. |
| `answer_gate` | 44 | pending owner run | Provider-backed; the recorded denominator is unreconciled (§3). |
| `answer_quality` | 30 | pending owner run | Provider-backed; no comparison recorded at this commit. |
| `offline_contract` | 25 | recorded | 25 suites, 603 tests passed. |
| `adversarial_fixtures` | 24 | recorded | 24 synthetic cases, 8 categories, 6 canaries, report canary-free. |
| Gate | Cases | Status | Result |
| ---------------------- | ----- | ----------------- | ---------------------------------------------------------------------- |
| `retrieval_golden` | 36 | pending owner run | Provider-backed; last recorded green at `4ea310e48` (run 32100681177). |
| `answer_gate` | 44 | pending owner run | Provider-backed; 44/44 at `4ea310e48` under prompt v18 (§3). |
| `answer_quality` | 30 | pending owner run | Provider-backed; S2 before/after requested, not run (§4). |
| `offline_contract` | 26 | recorded | 26 suites, 623 tests passed. |
| `adversarial_fixtures` | 24 | recorded | 24 synthetic cases, 8 categories, 6 canaries, report canary-free. |

Three of the five gates are marked `pending_owner_run` rather than carrying a number. That is
the point of the record's shape, not a gap in it: `eval:retrieval:quality`, the `ragEvalCases`
Expand All@@ -62,7 +64,24 @@ Cases — None`, and its per-case diagnostics table lists exactly 44 rows — ma
commit. `HANDOVER.md`'s original "45/45" was a transcription error and has been corrected
to 44/44. The gate denominator recorded here (44) stands.

## 4. Related
## 4. Re-capture for prompt v19 (packet S2, 2026-08-18)

Packet S2 (README §A2 + §A3) changed the answer prompt — the `related_information_menu`
line and the 60–110-word / three-to-six-section targets — and bumped `ragAnswerPromptVersion`
to `clinical-rag-answer-v19`, so this record was re-captured against the evaluated code
commit `b7aa925f0ae19e89a9f0acf842b4a80d84083fb5`. Offline gates were re-run at that commit
(`eval:rag:offline`: 26 suites / 623 tests; `check:rag:adversarial-fixtures`: 24 cases, canary-free);
the three provider-backed gates stay `pending_owner_run`, carrying run `32100681177` at
`4ea310e48` (prompt v18) as `priorRun` — that run is the baseline half of the S2 canary pair.

One caveat travels with the `answer_quality` gate: `scoreAnswerQualityEvalCase`
(`src/lib/rag/rag-eval-cases.ts`) scores readability over the answer **plus every section
body** with a 220-word ceiling. The S2 targets can exceed that by design, so a readability=0
flag caused only by total length is a metric artefact to adjudicate (raise the ceiling with an
`eval_config_version` bump, or accept), not evidence of a regression. The scorer was left
untouched in S2 so the before/after comparison runs under one definition.

## 5. Related

- `scripts/fixtures/rag-adversarial-baseline.v1.json` — the record itself.
- `scripts/fixtures/rag-adversarial-cases.v1.json` — the 24 synthetic adversarial cases.
Expand Down
3 changes: 0 additions & 3 deletions scripts/check-docs-links.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,9 +47,6 @@ const ROOT_PREFIXES = [
// designed-but-unbuilt drivers and hypothetical future splits.
const ALLOWLIST = new Set([
"scripts/reindex-shadow.ts", // designed-only harness driver (docs/reindex-shadow-harness-design.md)
// Planned-but-unbuilt files named by the RAG improvement guide (docs/rag-improvement/README.md):
"src/lib/rag/answer-composition.ts",
"scripts/probe-generation-quality.ts", // lands with PR #1899; remove this entry after it merges
"docs/site-map.generated.md", // hypothetical future split named in docs/process-hardening.md
// Legacy pre-(search-app) paths still cited in docs/ledger/redesign records:
"src/app/page.tsx",
Expand Down
28 changes: 14 additions & 14 deletions scripts/fixtures/rag-adversarial-baseline.v1.json
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,51 @@
{
"baselineVersion": "rag-adversarial-baseline.v1",
"capturedAt": "2026-08-17",
"promptVersion": "clinical-rag-answer-v18",
"capturedAt": "2026-08-18",
"promptVersion": "clinical-rag-answer-v19",
"semanticRerankEnabled": false,
"reportKey": {
"commit_sha": "92f7618c0ccac336ef6c245b89f37b046f4eac32",
"commit_sha": "b7aa925f0ae19e89a9f0acf842b4a80d84083fb5",
"dataset_version": "rag-adversarial-cases.v1",
"eval_config_version": "rag-eval-config-v1",
"model_version": "answer=gpt-5.6-terra; fast=gpt-5.6-terra; strong=gpt-5.6-sol",
"embedding_version": "text-embedding-3-small@1536",
"index_version": "20260814151000_validate_therapy_favourites_content_type"
"index_version": "20260818090000_schema_drift_snapshot_history_probe"
},
"gates": [
{
"id": "retrieval_golden",
"caseCount": 36,
"status": "pending_owner_run",
"blockedReason": "eval:retrieval:quality is provider-backed (Supabase + OpenAI) and fires only via the owner-approved eval-canary dispatch. Not run at this commit.",
"priorRun": "Recorded green at 2bd146eed: eval-canary post run 32025082010, document and content recall 1.0/1.0, zero per-case reciprocal-rank regressions (docs/rag-improvement/HANDOVER.md §1)."
"blockedReason": "eval:retrieval:quality is provider-backed (Supabase + OpenAI) and fires only via the owner-approved eval-canary dispatch. Not run at this commit; the packet S2 post-merge dispatch is the post half of the pair.",
"priorRun": "Recorded green at 4ea310e48 (the S2 merge base): eval-canary run 32100681177, document and content recall 1.0/1.0, zero per-case reciprocal-rank regressions — the S1d confirmation run and the baseline half of the S2 canary pair (docs/rag-improvement/HANDOVER.md §2)."
},
{
"id": "answer_gate",
"caseCount": 44,
"status": "pending_owner_run",
"blockedReason": "The ragEvalCases answer gate runs inside the provider-backed eval-canary dispatch. Not run at this commit.",
"priorRun": "HANDOVER §1 records 'answer gate 45/45' for run 32025082010 at 2bd146eed. src/lib/rag/rag-eval-cases.ts defines 44 ragEvalCases both at 2bd146eed and at this commit, so the recorded denominator is unreconciled — reconcile it against run 32025082010's report before treating either number as the baseline."
"blockedReason": "The ragEvalCases answer gate runs inside the provider-backed eval-canary dispatch. Not run at this commit; prompt v19 changes answer composition and length, so the pre-S2 result is history, not this tree's result.",
"priorRun": "44/44 at 4ea310e48 in eval-canary run 32100681177 (denominator reconciled by packet S5 — docs/rag-improvement/baseline-record.md §3), under prompt v18."
},
{
"id": "answer_quality",
"caseCount": 30,
"status": "pending_owner_run",
"blockedReason": "npm run eval:answer-quality over answerQualityEvalCases is a provider-backed OpenAI/Supabase evaluation requiring explicit owner approval per run.",
"priorRun": "No before/after comparison has been recordedfor this commit; the set is the Gate E fixed question base named by docs/rag-improvement/README.md §A2."
"blockedReason": "npm run eval:answer-quality over answerQualityEvalCases is a provider-backed OpenAI/Supabase evaluation requiring explicit owner approval per run. Requested (not executed) by packet S2 as the Gate E before/after comparison.",
"priorRun": "No before/after comparison has been recorded. Note for the S2 comparison: scoreAnswerQualityEvalCase's readability metric caps answer + section text at 220 words (src/lib/rag/rag-eval-cases.ts), which the S2 length targets can exceed by design; readability=0 flags driven only by total length are a metric artefact to adjudicate, not a regression."
},
{
"id": "offline_contract",
"caseCount": 25,
"caseCount": 26,
"status": "recorded",
"result": "25 offline contract suites, 603 tests passed; golden fixture validation 36 cases / 25 suites.",
"evidence": "npm run eval:rag:offline on this branch over 92f7618c0ccac336ef6c245b89f37b046f4eac32, 2026-08-17: 'Test Files 25 passed (25) / Tests 603 passed (603)'. The list held 23 suites / 583 tests before this branch started. Two suites were added concurrently and both are in the count: tests/rag-adversarial-fixtures.test.ts by this packet, and tests/search-route-round-trip-budget.test.ts on main. The remaining test-count movement is packet S1b (PR #2035), merged into the base while this branch was open."
"result": "26 offline contract suites, 623 tests passed; golden fixture validation 36 cases / 26 suites.",
"evidence": "npm run eval:rag:offline on this branch over b7aa925f0ae19e89a9f0acf842b4a80d84083fb5, 2026-08-18: 'Test Files 26 passed (26) / Tests 623 passed (623)'. Packet S2 added tests/answer-composition.test.ts (9 tests) to the list; the remaining movement from the S4 record (25 suites / 603 tests at 92f7618c) landed on main between the two captures (S1c, S1d, G1, #212 T4)."
},
{
"id": "adversarial_fixtures",
"caseCount": 24,
"status": "recorded",
"result": "24 synthetic cases across 8 categories, 6 registered canaries, report canary-free.",
"evidence": "npm run check:rag:adversarial-fixtures at 92f7618c0ccac336ef6c245b89f37b046f4eac32, 2026-08-17."
"evidence": "npm run check:rag:adversarial-fixtures at b7aa925f0ae19e89a9f0acf842b4a80d84083fb5, 2026-08-18 (prompt v19)."
}
]
}
3 changes: 2 additions & 1 deletion scripts/fixtures/rag-offline-contract-tests.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,5 +23,6 @@
"tests/rag-round-trip-budget.test.ts",
"tests/search-round-trip-budget.test.ts",
"tests/search-route-round-trip-budget.test.ts",
"tests/rag-adversarial-fixtures.test.ts"
"tests/rag-adversarial-fixtures.test.ts",
"tests/answer-composition.test.ts"
]
2 changes: 1 addition & 1 deletion src/lib/openai.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -158,7 +158,7 @@ function requestOptions(options?: Pick<TextGenerationOptions, "operation" | "tim
function promptCacheKeyFor(operation: OpenAIOperation) {
switch (operation) {
case "answer":
return "clinical-rag-answer-v18";
return "clinical-rag-answer-v19";
case "summary":
return "clinical-document-summary-v1";
case "vision_caption":
Expand Down
Loading
Loading