fix(rag-eval): split the readability metric into fragmentation and a v19-derived length bound - #2129
Conversation
…v19-derived length bound
scoreAnswerQualityEvalCase scored readability as one boolean --
wordCount >= 5 && wordCount <= 220 && !fragmentPattern.test(text) -- over
answerTextForQuality, which sums the answer field plus every section heading
and body, and reported one conflated reason ("fragmented or too long").
Packet S2 (#2097, dda4956, prompt clinical-rag-answer-v19) raised the answer
field to 60-110 words and sections to three-to-six, so a correctly shaped v19
answer can exceed 220 words. The metric could then no longer separate intended
length from the fragmentation regression it exists to catch.
Fragmentation and length are now evaluated independently and each failure
reports its own reason. Fragmentation is unchanged. The 900-word ceiling is
derived from the v19 contract rather than raised by judgement: the 110-word
answer upper target, 6 sections (answerSections.maxItems), and each section's
48-char heading plus 600-char body schema maxima, converted at a deliberately
low 5 chars/word so the bound cannot fail a well-formed answer --
110 + 6 * (648 / 5) = 887.6, rounded up to 900.
The two checks share the single readability metric key because
AnswerQualityMetric is a closed union consumed by scripts/eval-answer-quality.ts
as a total Record<AnswerQualityMetric, number>, and two existing tests pin the
five-key set; a sixth key would break both while adding no evaluative power.
Bumps eval_config_version to rag-eval-config-v2 in the adversarial baseline and
baseline-record (gate-semantic change: readability rates are not comparable
across the boundary), and sets the packet S3 row in HANDOVER.md to merged.
RAG impact: no retrieval behaviour change -- evaluation scorer only; gate semantics change is carried by the eval_config_version bump
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in:30 minutes Limit details: You’ve used all 1 included review currently available under your plan. You completed 101 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 (6)
Comment |
This pull request has been ignored for the connected project Preview Branches by Supabase. |
Immutable review record for the RAG eval scorer readability split, travelling with its owning product PR rather than a ledger-only branch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Uh oh!
There was an error while loading. Please reload this page.
Summary
readabilityanswer-quality metric into two independently scored checks.scoreAnswerQualityEvalCaseinsrc/lib/rag/rag-eval-cases.tsscored readability as a singleboolean —
wordCount >= 5 && wordCount <= 220 && !fragmentPattern.test(text)— overanswerTextForQuality, which sums the answer field plus every section heading and body, andreported one conflated reason,
"fragmented or too long". Packet S2 (feat(rag): intent-conditioned related-information menu and moderate answer length (packet S2 / A2 + A3) #2097,dda4956ff, promptclinical-rag-answer-v19) raised the answer field to 60–110 words and sections to three-to-six,so a correctly shaped v19 answer can exceed 220 words. At that point the metric could no longer
separate intended length from the fragmentation regression it exists to catch. Fragmentation and
length are now evaluated independently and each failure reports its own reason.
fragmentPatternand its effect on the score are identical; onlyits reporting is now separate.
eval_config_versionfromrag-eval-config-v1torag-eval-config-v2inscripts/fixtures/rag-adversarial-baseline.v1.jsonanddocs/rag-improvement/baseline-record.md,because this is a gate-semantic change: a
readabilityrate recorded under v1 is not comparable toone recorded under v2. The
answer_qualitygate'spriorRunnote andbaseline-record.md§4,which both recorded the 220-word confound as an artefact to adjudicate, now record it as resolved
(a new §4a carries the derivation).
docs/rag-improvement/HANDOVER.md§2 toMerged 2026-08-18 (squash 511d22f4d), landed by content; Track A complete. Verified against gitand the GitHub PR record: PR feat(answer): compose follow-up chips from the S2 menu, gate them on evidence (packet S3 / A4) #2108 is
MERGEDwith merge commit511d22f4d59ea6838bae2983d5c3d27137d042bb, already an ancestor of this branch's base. This is theonly HANDOVER edit in this PR.
RAG impact: no retrieval behaviour change — evaluation scorer only; gate semantics change is carried by the eval_config_version bump
Derivation of the 900-word length ceiling
The ceiling is derived from S2's own stated targets and the enforced response schema, not raised by
judgement:
src/lib/rag/rag-answer-instructions.ts("about 60-110 words")answerSections.maxItemsinsrc/lib/rag/rag.ts(matches the prompt's "three to six")headingmaxLength 48 +bodymaxLength 600, both insrc/lib/rag/rag.ts110 + 6 × (648 / 5) = 887.6, rounded up to 900 words. The>= 5word floor for empty or stubanswers is unchanged.
This is a contract ceiling, not a style ceiling. Conciseness is enforced by the prompt itself and
measured by
scoreAnswerTargeting; this bound only asks whether the text could have come from aschema-conformant v19 generation at all. Above 900 words it could not, so the check still catches
runaway duplication and the deterministic composition paths (
rag-extractive-answer.ts,rag-comparison.ts) that build aRagAnswerin code without passing through the JSON schema.Why split rather than simply raise the ceiling
Raising 220 to 900 alone would have removed the false positive but left the actual defect in place:
one boolean and one reason string, so a length failure and a fragmentation failure would still be
indistinguishable in the report. The metric exists to catch fragmentation, and a length-driven zero
that cannot be told apart from a fragmentation-driven zero silently degrades that. The split fixes
the diagnostic and the new ceiling fixes the false positive — both were needed, and the split is the
part with lasting value.
One constraint worth recording
The two checks share the single
readabilitymetric key rather than becoming a sixth metric.AnswerQualityMetricis a closed union consumed byscripts/eval-answer-quality.tsas a totalRecord<AnswerQualityMetric, number>, and two existing tests pin the exact five-key set. A sixth keywould therefore break that aggregation and those pins while adding no evaluative power — the two
checks are already scored and reported independently.
scripts/eval-answer-quality.tsemits onlymetricandscoreper case, neverreason, so the report shape is unchanged; only the scoresemantics move, which is exactly what the
eval_config_versionbump records.Verification
npm run verify:pr-localHeavy scope selected (executable source changed): runtime, installed-lock parity, format, docs and
workflow contracts, ledger checks, lint, typecheck, the full unit suite, and the RAG offline gates.
npx vitest run tests/rag-eval-cases.test.ts—Test Files 1 passed (1)/Tests 32 passed (32)(27 pre-existing plus 5 new).
npm run eval:rag:offline—Test Files 26 passed (26)/Tests 623 passed (623);Offline RAG fixture and manifest validation passed (36 golden cases, 26 suites).npm run check:rag:fixtures—Offline RAG fixture and manifest validation passed (36 golden cases, 26 suites).npm run eval:rag:adversarial:offline—Adversarial fixture contract passed (24 synthetic cases, 8 categories, 6 canaries).,Test Files 1 passed (1)/Tests 25 passed (25),Offline adversarial fixture validation and regression harness passed.The threeKNOWN_DIVERGENCESpins (cite-mismatched-attribution,scope-other-owner-document,scope-guessed-chunk-id) remain pinned and untouched.The new tests were mutation-checked rather than trusted for being green. Restoring the old 220-word
ceiling fails
scores a long but clean v19-shaped answer as readablewithAssertionError: expected +0 to be 1, and forcingfragmentedText = falsefails both fragmentationtests. Each new test therefore detects the specific regression it claims to.
Note on the 623 count:
tests/rag-eval-cases.test.tsis not one of the 26 offline contract suiteslisted in
scripts/fixtures/rag-offline-contract-tests.json, which is whyeval:rag:offlinereportsthe same 623 as the S2 baseline record rather than 628. The five new tests are covered by the full
unit suite inside
verify:pr-localand by the focused run above — this is suite membership, not asilent skip.
UI verification not run: no UI, routing, styling, browser, reduced-motion, or forced-colors behaviour
changed.
npm run verify:uiwhen UI, routing, styling, browser behavior, reduced-motion, or forced-colors behavior changednpm run verify:releasebefore release or handoff confidence claimsVerification not run — provider-backed and not authorised for this task:
npm run eval:retrieval:quality,npm run eval:rag,npm run eval:quality,npm run eval:answer-quality,npm run verify:release,and
npm run check:supabase-project. No retrieval, ranking, selection, or generation behaviourchanged, so this diff requires no live eval-canary pair.
npm run eval:retrieval:quality(must stay 36/36) when retrieval, ranking, selection, chunking, or scoring behavior changed — CI cannot run it (needs live keys), so run it locally and paste the summary. A metadata/governance-weighting change once buried correct docs (recall 1.0→0.76) and only this eval caught it.npm run eval:rag -- --limit 15+npm run eval:quality -- --rag-onlywhen answer generation, the synthesis prompt, or answer post-processing changed (grounded-supported must not drop; citation-failure 0)npm run check:production-readinesswhen clinical workflow, privacy, environment, Supabase, source governance, or deployment behavior changednpm run check:deployment-readinesswhen deployment startup, hosting, or rollout behavior changedRisk and rollout
scoreAnswerQualityEvalCase's readability branch, which is consumed exclusively byscripts/eval-answer-quality.ts— a provider-backed evaluation script, not the runtime answerpath. No retrieval, ranking, selection, generation prompt,
ragAnswerPromptVersion, or blockingeval-canary threshold was touched. The residual risk is evaluative: the 900-word ceiling is
deliberately permissive, so a bloated but schema-conformant answer will now score readable where
the old 220 ceiling would have flagged it. That ceiling was already unreliable after S2 because it
flagged correct answers too, and conciseness remains measured by
scoreAnswerTargeting.git revertthis PR. It restores the previous single-boolean scorer and therag-eval-config-v1key together; no data migration, cache invalidation, or canary is involved.access occurred, and no canary was dispatched.
Clinical Governance Preflight
Clinical KB Database(sjrfecxgysukkwxsowpy)Every item above holds unchanged. This diff alters how an offline evaluation scorer reports one
metric; it does not touch citation requirements, document access, privacy scoping, Supabase
configuration, credential handling, demo-corpus separation, source governance, or any clinical
decision-support behaviour that would change the SaMD/TGA assessment.
Notes
docs/rag-improvement/baseline-record.mdgains a §4a recording the split, the derivation, and theexplicit consequence that
readabilityrates are not comparable across the v1/v2 boundary.src/lib/rag/rag-eval-cases.ts,tests/rag-eval-cases.test.ts,scripts/fixtures/rag-adversarial-baseline.v1.json,docs/rag-improvement/baseline-record.md, and the single S3 row indocs/rag-improvement/HANDOVER.md.