Skip to content

fix(redesign): ground best-source superlatives on their own citation#571

Merged
HomenShum merged 1 commit into
mainfrom
fix/evidence-claim-citation-gate
Jul 17, 2026
Merged

fix(redesign): ground best-source superlatives on their own citation#571
HomenShum merged 1 commit into
mainfrom
fix/evidence-claim-citation-gate

Conversation

@HomenShum

Copy link
Copy Markdown
Owner

What

applyDeterministicResponsePolicy (convex/domains/redesign/chatRuns.ts) sanitized "best/strongest source|claim" superlatives only when the run had zero URL-backed evidence rows. In a mixed run — one real URL plus several cached section labels (Setup, Rising Action) — the superlative passed through, so an answer could assert "strongest supported claim with its best source" while the rendered best source was a label with no URL. On a paid runtime path that is false confidence, not honest degradation.

Closes #566. This is the fifth and last P1 from the 2026-07-16 production audit; #565 fixed the other four (or verified them already fixed by #550/#561/#564).

The fix — sentence-level citation gate

A superlative now stands only if its own sentence carries a [N] marker resolving to a URL-backed evidence row. Otherwise that sentence is rewritten to "source or claim requiring verification" and the source-needed limitation is appended.

  • Superlative cites [1] (the URL-backed row) → kept.
  • Superlative cites [2] (a cached label) → rewritten, even though [1] exists elsewhere in the run.
  • Superlative with no citation → rewritten.

Mechanics:

  • Widened the evidence param to carry idx (production rows already have it — set at the geminiEvidence/fallbackEvidence/memoryEvidence construction; only the local param type omitted it). idx is optional so existing callers compile.
  • supportedIdx = the idx set of rows that are URL-backed and not blocked/unsupported/fetch_blocked — computed from the same supportedRows filter that already drives supportedUrls (no duplicated predicate).
  • The "best/strongest" pattern is extracted to one UNSUPPORTED_SUPERLATIVE_SOURCE constant shared by the sanitizer (replace) and the gate (test) so they can't drift — the same class of bug as the determiner fix in fix(redesign): honor title-only requests behind any determiner #565.
  • When no superlative is present, the parsed memo is returned byte-for-byte as before; the run-level sanitize-all path for zero-URL runs is unchanged.

Verification

  • npx tsc -p convex --noEmit --pretty false → 0 errors
  • npx vitest run convex/domains/redesign/chatRuns.responseShape.test.ts → 16 passed (3 new)
  • Guard proven: reverting only the gate to the old run-level check makes the cached-label and uncited cases fail with expected 'acme is the strongest supported claim…' not to contain 'strongest supported claim' (2 failed / 14 passed); restored → 16 passed
  • This test file is already in the CI runtime-smoke allowlist (added in fix(redesign): honor title-only requests behind any determiner #565), so the new guards run in CI

No visual proof: the answer body is runtime-produced; no rendered surface markup changed.

Deliberately not in scope

Full claim→source binding at claim granularity (the claim.sourceRefIds model in shared/productAnswerControl.ts) is a larger design change touching both renderers and is not attempted here. This PR closes the specific audited honesty gap; issue #566 can track the broader model if desired.

🤖 Generated with Claude Code

applyDeterministicResponsePolicy only rewrote unsupported "best/strongest
source|claim" superlatives when the run had zero URL-backed evidence rows.
In a mixed run - one real URL plus several cached section labels like Setup
or Rising Action - the superlative passed through, so an answer could claim
"strongest supported claim with its best source" while the rendered best
source was a label with no URL. On a paid runtime path that is false
confidence, not honest degradation.

The gate is now sentence-level: a superlative stands only if its own sentence
carries a [N] citation resolving to a URL-backed evidence row; otherwise that
sentence is rewritten and the source-needed limitation is appended. A
superlative citing a cached label, or citing nothing, is rewritten even when a
URL-backed row exists elsewhere in the run. The best/strongest pattern now
lives in one shared constant used by both the sanitizer and the gate so they
cannot drift.

Closes the residual left open by the 2026-07-16 audit follow-up (#565 fixed
four of five P1s; this is the fifth).

Closes #566

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nodebench-ai Ready Ready Preview, Comment Jul 17, 2026 7:10pm

Request Review

@HomenShum
HomenShum enabled auto-merge (squash) July 17, 2026 19:09
@HomenShum
HomenShum merged commit 6198dc3 into main Jul 17, 2026
16 checks passed
@HomenShum
HomenShum deleted the fix/evidence-claim-citation-gate branch July 17, 2026 19:15
@github-actions

Copy link
Copy Markdown

Demo: walkthrough of the surfaces this PR changed is available as a workflow artifact (pr-demo-571) at https://github.com/HomenShum/NodeBenchAI/actions/runs/29606894022

HomenShum added a commit that referenced this pull request Jul 17, 2026
* fix(scratchnode): repair red handoff tests and close CI allowlist gap

Three ScratchnodeEventsSurface tests failed on clean main with a TypeError
because the api mock predated ImportRecapButton's
domains.product.scratchnodeImport query, and CI never saw the red because
the runtime-smoke job runs an explicit allowlist that omitted the file.

The api mock is now a Proxy that degrades unknown function references to
unresolved queries (the surface's honest gates render nothing) instead of
crashing at property access, the convex/react mock gains the useMutation
the component already imports, two scenarios cover the import affordance's
published/unresolved gates, and the file joins the runtime-smoke allowlist
so future drift turns CI red.

Closes #567

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(composer): make run cancellation spatially stable, not timer-only

The submit->Stop same-slot swap meant a double-click slower than the
400ms arming window (Windows accessibility settings allow ~900ms) could
still cancel the run it just created. Stop and submit are now both always
rendered: Stop holds a reserved slot (visibility:hidden while idle) and
submit stays at identical coordinates while streaming, merely disabled —
so the second click of a double-click lands on an inert control at every
interval. The arming delay remains as defense in depth.

Closes #568

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(chat): honor sentence, paragraph, and word-limit response shapes

detectRequestedResponseShape recognized only title-only and bullets, so
"in one sentence", "a single paragraph", and "under 50 words" silently
rendered the five-section memo despite the user's explicit constraint.

Three new shapes are detected, instructed (via an exhaustive
responseShapeSystemInstructions switch replacing the ternary chain), and
deterministically enforced in applyDeterministicResponsePolicy: first
sentence extraction, prose collapse, and sentence-accumulating word
budgets. Honesty survives compaction — unsupported runs carry a
"Source needed" prefix or inline limitation instead of busting the
user's limit. JSON/table shapes stay model-side deliberately: the memo
render path is markdown prose, and a deterministic policy cannot
reconstruct valid structured output from parsed memo fields.

Closes #569

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): pin 390px chat geometry by computed grid, seal audit lane

The audit's mobile collapse (grid 70px 320px, chat clipped to a 70px
strip) was invisible to both the CSS-source string guard and the
document-overflow boolean. one-flow-regression (Tier B, per-PR) now
asserts the computed grid-template-columns of .rd-shell__main resolves
to one track and main#main-content stays >300px at 390x844. Reversion-
proved by reinjecting a higher-specificity two-column rule: computed
geometry returns exactly the audited 70px/320px shape, which the new
assertions reject.

Also seals the audit-residual work in the redesign-chat changelog lane
and backfills the #565/#571 entries' pending merge markers.

Closes #570

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: hshum <hshum@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <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.

Evidence 'best source' gate is run-level, not claim-level

2 participants