feat(rag): adversarial fixture contract, validator, baseline record and data-flow register (packet S4 / B0) - #2036
Conversation
…record and data-flow register Implements programme packet S4 (B0) from docs/rag-improvement/HANDOVER.md, per docs/rag-improvement/README.md section B0. All of it is offline: no retrieval, ranking, selection, generation, or answer-contract code is touched. - scripts/fixtures/rag-adversarial-cases.v1.json: 24 synthetic cases, three in each of the eight named categories, using invented agents so no real clinical text can be mistaken for a source. Its published shape is rag-adversarial-cases.schema.json. - Six PHI-shaped canary strings planted across four categories, unmistakably fake so detection is an exact literal scan implicating no real person. - npm run check:rag:adversarial-fixtures, backed by a dependency-free, network-free CLI and a pure-function module. The existing check:rag:fixtures is untouched. - The printed report is scanned for every registered canary before it reaches stdout, is built from ids and counts only, and a canary may never be written into an assertion list a runner would echo. - Synthetic provenance is enforced by title prefix plus a real-clinical-source denylist, both hard failures. - rag-adversarial-baseline.v1.json carries the six-field report key, pinned by test for field set and order, with the prompt version cross-checked against rag-versioning.ts. A gate is either recorded with its run or explicitly pending, so a number cannot be entered without provenance; the three provider-backed gates stay pending an owner-approved dispatch. - docs/rag-improvement/data-flow-register.md is the Gate A register, including four explicitly recorded gaps. - The now-built paths and command are removed from the planned-path allowlists in check-docs-links.mjs and check-docs-script-refs.mjs. - tests/rag-adversarial-fixtures.test.ts joins the offline contract suite list, so the contract holds on every PR without new CI routing; routing the adversarial runner is packet B2's work. RAG impact: no retrieval behaviour change - offline fixtures and validation only Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FWiDFShT7JuZAjTJHhJ7Wq
…rsarial-fixtures-5ho5tp
origin/main advanced to 181c847 while this packet was in progress. The branch merged it cleanly before its first push, so the baseline record now names that commit as the tree its offline gates were measured on, rather than the branch's original base. npm run eval:rag:offline re-ran post-merge with the same result (24 suites, 597 tests). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FWiDFShT7JuZAjTJHhJ7Wq
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FWiDFShT7JuZAjTJHhJ7Wq
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Caution Review failedAn error occurred during the review process. Please try again later. Comment |
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FWiDFShT7JuZAjTJHhJ7Wq
|
| GitGuardian id | GitGuardian status | Secret | Commit | Filename | |
|---|---|---|---|---|---|
| - | - | Generic High Entropy Secret | 670afaf | tests/rag-adversarial-fixtures.test.ts | View secret |
| 36220292 | Triggered | Generic High Entropy Secret | 57fd5ae | scripts/fixtures/rag-adversarial-cases.v1.json | View secret |
| 36220293 | Triggered | Generic High Entropy Secret | 57fd5ae | scripts/fixtures/rag-adversarial-cases.v1.json | View secret |
| 36220293 | Triggered | Generic High Entropy Secret | f08e160 | tests/rag-adversarial-fixtures.test.ts | View secret |
🛠 Guidelines to remediate hardcoded secrets
- Understand the implications of revoking this secret by investigating where it is used in your code.
- Replace and store your secrets safely. Learn here the best practices.
- Revoke and rotate these secrets.
- If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.
To avoid such incidents in the future consider
- following these best practices for managing and storing secrets including API keys and other credentials
- install secret detection on pre-commit to catch secret before it leaves your machine and ease remediation.
🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.
…rsarial-fixtures-5ho5tp # Conflicts: # docs/scripts-index.md # scripts/fixtures/rag-offline-contract-tests.json
…rsarial-fixtures-5ho5tp # Conflicts: # docs/rag-improvement/HANDOVER.md
…e conflict resolution The branch merged origin/main twice while open. The baseline record now names the current base and carries the re-measured offline numbers: the contract list grew to 25 suites / 602 tests because two suites were added concurrently - tests/rag-adversarial-fixtures.test.ts here and tests/search-route-round-trip-budget.test.ts on main - with the remaining test movement coming from packet S1b (PR #2035). ragEvalCases is still 44 and answerQualityEvalCases still 30, so both provider-backed gate denominators are unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FWiDFShT7JuZAjTJHhJ7Wq
…flag them Gitleaks (generic-api-key, entropy 3.83) and GitGuardian both flagged the first version of these tokens on push. The MRN, DOB, phone and address canaries carried identifier-shaped digit runs, which read as high-entropy secrets. The file has never contained credential material, but a canary that trips a secret scanner is the wrong shape for the job: it must be conspicuously non-secret. Tokens now carry the PHI category in words only, and the rule is enforced rather than merely observed - the validator regex is letters-only, the published schema says so, and a unit test asserts both the committed tokens and the rejection of a digit-bearing one, so a future canary cannot bring this failure back. The tip is clean, but the PR range still contains the original commit, so its one Gitleaks fingerprint is recorded in .gitleaksignore alongside the repository's existing synthetic-identifier false positives. GitGuardian's two incidents are SaaS-side against that same historical commit and cannot be resolved from the repository. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FWiDFShT7JuZAjTJHhJ7Wq
… identifier GitGuardian flagged the assertion line itself: proving the letters-only rule with a full MRN-shaped literal recreates exactly the string the rule exists to keep out of the repository. A single digit proves the same thing and carries no entropy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FWiDFShT7JuZAjTJHhJ7Wq
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FWiDFShT7JuZAjTJHhJ7Wq
Summary
Implements programme packet S4 (B0) from
docs/rag-improvement/HANDOVER.md, perdocs/rag-improvement/README.md§B0. Everything here is offline: no retrieval, ranking, selection, generation, or answer-contract code is touched.scripts/fixtures/rag-adversarial-cases.v1.jsonholds 24 synthetic cases, three in each of the eight categories the guide names: prompt injection, citation fabrication, unsupported claim, empty/conflicting evidence, scope/tenant, provider failure, adversarial metadata, and cost/timeout abuse. Every excerpt, document title, and drug name is invented — the fixtures use made-up agents (zolinapine, meraflox, cortavine) so no real clinical text can be mistaken for a source.scripts/fixtures/rag-adversarial-cases.schema.jsonpublishes the shape.CANARY-PHI-…) name a patient name, MRN, date of birth, phone number, address, and email address, planted in case text across four categories. They carry their PHI category in words and no digits at all — see the secret-scanner note below — so each is unique, exactly greppable, and conspicuously not a real identifier.npm run check:rag:adversarial-fixtures, backed byscripts/check-rag-adversarial-fixtures.mjs(CLI) andscripts/rag-adversarial-contract.mjs(pure functions). It is deterministic and network-free: it parses three repository files and prints counts. It has no dependencies and adds none. The existingcheck:rag:fixturesis untouched.forbiddenOutputSubstrings, because a runner echoing that list would itself print the canary.SYNTHETIC, and a denylist of real clinical source names (EMHS, RANZCP, NICE, Maudsley, Therapeutic Guidelines, and similar) is a hard validation failure rather than a warning.scripts/fixtures/rag-adversarial-baseline.v1.jsoncarries the six-field report key —commit_sha,dataset_version,eval_config_version,model_version,embedding_version,index_version— whose field set and order are pinned by test, plus the prompt version (cross-checked againstsrc/lib/rag/rag-versioning.tsat validation time) andsemanticRerankEnabled: false.docs/rag-improvement/baseline-record.mdexplains it.recordedwith a result and the run or file it came from, orpending_owner_runwith a stated reason and no result. The three provider-backed gates (36-case retrieval, 44-case answer gate, 30-case answer quality) are recorded as pending, because dispatching them needs owner approval and they were not run for this commit. The two offline gates carry their real measured results.docs/rag-improvement/data-flow-register.mdis the Gate A artefact: inputs, processes, sinks with retention and de-identification status, provider egress, and four explicitly recorded gaps (worker temp-path cleanup is asserted rather than proven; provider retention terms are unexecuted under ledger#053; upload does not screen for identifiers; telemetry canary-absence tests are packet B1's deliverable).scripts/check-rag-adversarial-fixtures.mjs,scripts/fixtures/rag-adversarial-cases.v1.json, anddocs/rag-improvement/data-flow-register.mdare gone from theALLOWLISTinscripts/check-docs-links.mjs, andcheck:rag:adversarial-fixturesis gone from the allowlist inscripts/check-docs-script-refs.mjs. Those paths and that command now exist, so both gates validate them for real.tests/rag-adversarial-fixtures.test.tsruns the same validation as the CLI and is registered inscripts/fixtures/rag-offline-contract-tests.json, soeval:rag:offlineand the full unit suite hold the contract on every PR. CI routing for the adversarial runner is packet B2's work and is deliberately not done here — no workflow file and noscripts/ci-change-scope.mjsentry is changed.docs/rag-improvement/HANDOVER.md§2's S4 row is updated in this PR as the handover rule requires;docs/README.mdanddocs/scripts-index.mdregister the new documents and scripts.RAG impact: no retrieval behaviour change — offline fixtures and validation only
Secret scanners, and why the canaries changed shape
The first push of this branch made both secret scanners red, and the fix is worth stating plainly because it is a real finding about how to build a canary.
The original tokens embedded identifier-shaped digit runs — an all-zero MRN body, an 1899 date of birth, an all-zero mobile number — to look like the PHI they simulate. Gitleaks flagged one as
generic-api-keyat entropy 3.83 and GitGuardian raised two incidents. Neither is a credential; both are true statements about the shape of the strings.Rather than silence the scanners, the tokens changed: every canary now carries its PHI category in words and contains no digit at all (
CANARY-PHI-MRN-NEVER-A-REAL-RECORD). A canary needs to be unique and exactly greppable, not entropy-dense, so nothing of value was lost — and the property is now enforced rather than merely observed. The validator regex is letters-only,rag-adversarial-cases.schema.jsondocuments why, and a unit test asserts both that every committed token complies and that a digit-bearing one is rejected. A future canary cannot reintroduce the failure.One
.gitleaksignorefingerprint is added, because Gitleaks scans the whole PR commit range and the original commit57fd5aestays in it even though the tip is clean. It sits alongside the repository's existing synthetic-identifier false positives and follows their documented pattern. GitGuardian's two incidents are SaaS-side against that same historical commit and cannot be resolved from the repository; they need dashboard action or nothing, since the tip carries no match.Verification
npm run verify:pr-localScript changes fail the selector closed to the heavy offline scope, so this ran the full set — lint, typecheck, the full unit suite, build, and
eval:rag:offline. Summary line from the final run, on the tree this branch now carries:Decisive lines from the individual gates:
npm run formatwas run over the whole tree and the result is committed.The two unit suites flagged as pre-existing Windows-host failures —
tests/session-start-hook.test.tsandtests/check-playwright-browser-revision.test.ts— are both inside those 641 passing files in this Linux container, so the failure does not reproduce here, no merge-base isolation was needed, and no failure is being carried into this PR. (TheFAILstrings visible in the unit-suite log are expected stdout from thecheck:function-grantsnegative fixtures, which assert that the guard rejects bad SQL.)origin/mainadvanced three times while this branch was open. It was merged in each time,git merge-tree --write-treeis clean against the current base, and two genuine conflicts were resolved by keeping both sides:scripts/fixtures/rag-offline-contract-tests.json(this packet and main each appended a suite) anddocs/rag-improvement/HANDOVER.md§2 (this packet's S4 row and packet S1b's row).docs/scripts-index.mdconflicted only on its generated count line and was regenerated withnpm run docs:update. The S1b row was also corrected from "PR open" to merged, verified from git — PR #2035 landed on main as merge commit92f7618.The offline contract list moved from 23 suites / 583 tests to 25 / 603 across those merges:
tests/rag-adversarial-fixtures.test.tsfrom this packet,tests/search-route-round-trip-budget.test.tsfrom main, and S1b's test changes. The baseline record states all of it so the movement is not mistaken for drift.ragEvalCasesis still 44 andanswerQualityEvalCasesstill 30, so neither provider-backed denominator changed.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.
npm run verify:releasebefore release or handoff confidence claimsVerification not run: this is a packet handoff, not a release, 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 is changed. The command is provider-backed and fires only via the owner-approved
eval-canarydispatch.verify:pr-localdid runeval:rag:offline, which validates the golden fixture and the offline contract suites, and it is green above.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.
npm run check:production-readinesswhen clinical workflow, privacy, environment, Supabase, source governance, or deployment behavior changedVerification not run: no clinical workflow, privacy, environment, Supabase, source-governance, or deployment behaviour changes. The data-flow register documents existing behaviour; it does not alter any.
npm run check:deployment-readinesswhen deployment startup, hosting, or rollout behavior changedVerification not run: no deployment startup, hosting, or rollout behaviour changes.
Risk and rollout
docs:check-linksanddocs:check-scriptsnow validate four previously exempted references instead of skipping them. Both gates are green above, and both would fail loudly rather than silently if a referenced path or command were later removed. The.gitleaksignoreaddition pins one historical-commit fingerprint and does not weaken the rule at tip, which the letters-only canary contract now keeps clean.Clinical Governance Preflight
Clinical KB Database(sjrfecxgysukkwxsowpy) — unchanged, and named as such in the register's provider-egress table. No Supabase configuration, migration, or connection is touched.SYNTHETIC, the drugs are invented, and naming a real clinical source in a fixture is a hard validation failure.similarity: 1must not by itself raise the confidence label, and an excerpt that supersedes another must have the relationship surfaced rather than silently resolved.Notes
HANDOVER.md§1 records "answer gate 45/45" for canary run32025082010at2bd146eed, butsrc/lib/rag/rag-eval-cases.tsdefines 44ragEvalCasesboth at that commit and at this one, andscripts/eval-rag.tsreports its rates overresults.length. The baseline records the gate as 44 cases and states the discrepancy rather than guessing which figure is right. It should be reconciled against run32025082010's own report — packet S5 is the natural place, since its harness consumes this record.scripts/ci-change-scope.mjsrouting entry, and noscripts/verify-pr-local.mjsselector entry is changed; packet B2 owns routing the adversarial runner. No dependency is added, in particular no JSON Schema runtime — the schema file is the published contract and the.mjsmodule is the enforcer, with a test asserting that every key the schema marks required is actually rejected when missing.eval-canarypair at the merge commit, flip the three pending gates torecordedwith their run ids, and updatecommit_shain the baseline record.