Skip to content

Benchmark validity: close the 3 audit-flagged holes (sense-leak + 2 factsCore gaming surfaces)#121

Draft
anantham wants to merge 18 commits into
mainfrom
fix/opus-benchmark-validity
Draft

Benchmark validity: close the 3 audit-flagged holes (sense-leak + 2 factsCore gaming surfaces)#121
anantham wants to merge 18 commits into
mainfrom
fix/opus-benchmark-validity

Conversation

@anantham

Copy link
Copy Markdown
Owner

Closes the three validity items surfaced by the adversarial audit — the ones you said were yours to call. All three are free to do now and expensive later: they change factsCore/senseF1, so once a paid v2.2 board exists they'd force a re-run to re-score; done before the first paid run, they cost nothing.

⚠️ Read first — this PR is stacked on the five validity PRs

The fixes genuinely depend on the merged state: A1/A2 need #117's facts-scorer.ts; the C1 leak guard reads #116's 27-phase set and #118's goldens. So this branch contains the merge of #114/#116/#117/#118/#119 plus my two fix commits. Merge those five first — then this PR's diff collapses to just the two commits below (or ping me and I'll re-cut it cleanly against the new main; if you squash-merge the five, I'll rebase). Review my two commits, not the whole diff.

C1 — de-leak the lexicographer worked example (commit 1)

The lexicographer prompt teaches viharati → "dwells", and 8 ranked phases (z/af/ai/az/ba/bc/bd/bg) grade exactly that sense → free senseF1 credit (30% of rank). Same leak class you removed phase-ad/ag/aj for, on the sense axis; the anatomist-only phase-contract guard misses it. This is what dinged #116's "honest, uncontaminated ranking set."

  • Fix: reglossed to gacchati → goes/going/went, verified absent from every ranked phase's graded surfaces and senses; the tense-ripple lesson is preserved.
  • Guard: new lexico-example-leak.test.ts — no gloss taught in either lexico worked example may be graded in any ranked phase. RED against the old example (prints viharati → "dwells" in 8 phases), green after. Durable against future recurrence.

A1 — POS scored over ALL golden words (commit 2)

Word-class was scored over content words only, so labelling every word content earned a free 1.0 (function-word errors never checked). Now scored over every golden word; a phase with no content word stays null (no facts to grade), so the "returns null" edge is preserved.

A2 — root spray-cap (commit 2)

Root was recall-only: any matching √root counted as correct with no penalty for the wrong roots also sprayed — rewarding hedging. (#117 hardened MORPH's denominator against exactly this but left ROOT open.) A hit buried in > authority + ROOT_SPRAY_SLACK(2) roots is now sprayed, not correct; a focused answer (right root + a little slack) still counts.

Verification

  • 2 new facts-scorer tests + updated assertions, all verified RED against the pre-fix scorer (6 failures); the C1 guard RED against the old example. All green after.
  • tsc at the 17-error baseline; full sutta-studio suite green (60).

With this + #120 (the wasted-spend fix), all audit findings that affect the paid run are closed. The only remaining gate is the twelve model IDs.

🤖 Generated with Claude Code

anantham and others added 17 commits July 16, 2026 10:40
Agent git worktrees live under .claude/worktrees/ INSIDE the repo. Vitest run from
the main checkout therefore discovered every worktree's duplicate test copies —
inflating test counts and coverage (a reviewer's "five-file verification" ran ten
files / 91 tests because it also picked up a stale copy under
.claude/worktrees/opus-p1-tech-debt).

Added '**/.claude/**' to both test.exclude and coverage.exclude. Vitest matches
exclude globs relative to the config root, so this is safe when run from inside a
worktree (its own tests are at tests/… with no .claude segment) while excluding the
worktrees when run from the main checkout. Verified: a normal test file still runs
from inside the worktree with the exclude active.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sd5oCcYiQBnGAiNT4SAo8L
A model occasionally returns two prompts for the SAME [ILLUSTRATION-N] marker. Only
one image can render at a marker (the image cache key is chapterId:placementMarker),
so the duplicate is dead weight — and worse, it costs a second PAID generation whose
result overwrites the first.

The validator did not catch it: when the shared marker is already present in the text,
the `jsonMarkers > textMarkers` branch filters "unmatched" prompts by presence-in-text,
so both duplicates pass through. My P1.1/P1.2 consolidation routed all three providers
through this validator, widening the exposure of that latent gap.

Two fixes (approved: deduplicate keep-first, not fail-and-regenerate):

1. Validator (responseValidators.ts) — dedupe suggestedIllustrations by placementMarker,
   keep-first, BEFORE the count-based reconciliation. A surviving duplicate would also
   inflate jsonMarkers and defeat the text-vs-json balance checks, so this both stops the
   double-bill and keeps the reconciliation honest. This is the primary fix: duplicates
   never leave validation.

2. Image generator (imageGenerationService.ts) — defense-in-depth at the point money is
   actually spent. The generation loop keys every call by chapterId:marker; two entries
   sharing a marker issued two paid generateImage calls and the post-generation
   findIndex(...) wrote both results into the FIRST entry. The needs-generation list is
   now deduped by marker (with a logged drop) so it can't double-fire regardless of what
   reaches it.

Tests: both verified RED against the pre-fix code — the validator returns 2 entries where
it should return 1, and the generator issues 2 paid calls where it should issue 1. A
distinct-marker control confirms non-duplicates still each generate.

Part of the P1/P2 review-fix batch (PR #114). #5 (worktree exclude) already landed;
#2 (budget ledger), #3 (Claude/Gemini abort), #4 (pending leak) still to come.

tsc: 17 pre-existing errors, none in touched files.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sd5oCcYiQBnGAiNT4SAo8L
handleTranslate claims the chapter in pendingTranslations before the budget checks so
two rapid triggers can't both slip past the dedup guard during the awaits (P1.6). But
releasePending() was only wired to the two graceful early-returns. If a budget await
rejected — the dynamic import, hasKnownPricing (which may hit the network for '/'
models), or getNovelTranslationCost's IndexedDB read — the throw left the claim set, and
the guard at the top of handleTranslate then blocked EVERY future attempt on that chapter
until a full reload.

Fix: wrap the budget block so any throw releases the claim, then rethrows (fail-closed —
no paid translation runs on a budget we couldn't verify). The claim still has to be set
BEFORE the budget awaits, so moving it later (which would avoid the leak) is not an option
— that reopens the P1.6 double-fire race.

Test: verified RED against the pre-fix slice — the chapter stays in pendingTranslations
after the pricing check throws, and a second attempt is silently blocked by the leaked
claim (hasKnownPricing called once, not twice).

Part of the P1/P2 review-fix batch (PR #114). #5, #1 already landed; #2 (budget ledger)
and #3 (Claude/Gemini abort) next.

tsc: 17 pre-existing errors, none in touched files.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sd5oCcYiQBnGAiNT4SAo8L
P1.4 recorded failed/truncated translation calls in api_metrics, but the budget GATE
never read them — getNovelTranslationCost summed only successfully-persisted translation
versions. So failed, truncated, and failed-retry calls were billed by the provider yet
invisible to the cap. The docstring even admitted it ("the api_metrics ledger is the
eventual complete source"); this wires that source in.

Design — a hybrid of two DISJOINT sources, so nothing is double-counted:
  1. Persisted translation versions (estimatedCost) — every successful paid version.
  2. FAILED translation calls from api_metrics — billed but never became a version.
A call is either a persisted success or a recorded failure, never both, so adding the two
is exact. The join is by chapterId: verified that the value adapters record as api_metrics
chapterId is the same stableId the budget gate keys its chapters on (translationService
passes it through "for API metrics tracking"; all three adapters record it). New
apiMetricsService.getFailedTranslationCostForChapters(stableIds) reads the persisted store
and sums the failed-translation subset for those chapters; it degrades to 0 on a read error
so the version sum (the primary signal) always stands.

Still imperfect and documented as such: a SUCCESSFUL version the user deleted escapes both
sums (its metric is a success, its version is gone) — a much narrower gap than before, when
every failed call was invisible.

Also (the second half of #2): an EMPTY OpenAI response threw before the metric block, so
that billed call recorded nothing. The empty check now runs after the cost/metric helpers
and records a failed metric before throwing — same treatment as truncation and parse
failure.

Tests: both verified RED against pre-fix — the budget sum is 0.08 (versions only) where it
should be 0.20 with failed spend, and the adapter records no metric on an empty response.

Part of the P1/P2 review-fix batch (PR #114). #5, #1, #4 already landed; #3 (Claude/Gemini
abort) next.

tsc: 17 pre-existing errors, none in touched files.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sd5oCcYiQBnGAiNT4SAo8L
P1.3 made the Translator abort a per-attempt signal, but only OpenAIAdapter forwarded it
to its SDK. Claude took NO signal at all, and Gemini only checked the signal AFTER
generateContent returned — so a timeout started a retry while the original Claude/Gemini
request was still running and billing. The P1.3 test only asserted the signal became
aborted, never that a provider received it.

Both SDKs support native cancellation; we just weren't using it:

- Claude: translateWithClaude now takes an abortSignal and passes it as the Anthropic
  SDK's request options — claude.messages.create(payload, { signal }). ClaudeAdapter
  forwards request.abortSignal (translate path) and input.abortSignal (chatJSON path).
- Gemini: model.generateContent(request, { signal }) — @google/generative-ai ≥0.24
  supports it via SingleRequestOptions.signal. The "Gemini doesn't support native abort"
  comment was outdated; the after-the-fact check is kept as a belt-and-braces fallback.

Tests: a new test captures each SDK call and asserts the signal is threaded in — verified
RED against the pre-fix providers (they call the SDK with a single arg, no signal). Three
pre-existing ClaudeAdapter assertions were updated for the new second argument (a
deliberate behavior change).

Completes the P1/P2 review-fix batch (PR #114): #5, #1, #4, #2, #3 all landed.

tsc: 17 pre-existing errors, none in touched files.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sd5oCcYiQBnGAiNT4SAo8L
…ance

The published leaderboard hardcoded `closedBook: true` with the note "Models receive
ONLY the raw Pāli phrase — no dictionary or retrieval context." That contradicted reality
AND its own source list: the benchmark feeds DPD attestations (roots/POS/attested senses)
to both the Anatomist and Lexicographer passes (the DPD source is even listed right below
as "factual authority"). The P2.1 anatomist grounding sharpened the contradiction — the
benchmark was never closed-book (the lexicographer was always DPD-grounded).

Set `closedBook: false` and rewrote the note to state accurately what models receive: raw
Pāli PLUS DPD attestations at the Anatomist/Lexicographer passes, while still withholding
the SuttaCentral dictionary, retrieval context, and prior-phase window that production adds
(the SUTTA-014 parity gap) — so benchmark output is LESS grounded than production, not
un-grounded.

This is the code that stamps the field on the next generated board; the current published
JSON is a stale July-3 preview that the v2.2 rerun replaces.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sd5oCcYiQBnGAiNT4SAo8L
…budget cap

#2 — held-out leakage. The 30-phase ranked set claimed to be "honest, uncontaminated,"
but phase-ad, phase-ag and phase-aj each grade the exact `ātāpī sampajāno satimā` sequence
that phase-aa teaches the model as a worked example (config/suttaStudioExamples) — the
satipaṭṭhāna refrain recurs verbatim, so those three phases were the answer key in the
prompt. Removed them from phasesToTest (now 27); re-goldening can't help, the sequence IS
the example, and the refrain is still exercised once as phase-aa. Added
tests/scripts/sutta-studio/phase-contract.test.ts, which derives the example sequences from
the actual prompt example objects and fails if any ranked phase grades one — verified RED
against the pre-fix config (it lists ad/ag/aj) and green after removal. Nothing hardcodes a
30-phase count; the survivorship logic reads phasesToTest.length.

#6 — no spend cap. The runner estimated cost after each response, let an unknown price
become null (counting as $0), and had no cumulative stop, so a mispriced model or runaway
loop could spend unbounded. New SpendGuard (scripts/sutta-studio/spend-guard.ts) tracks
cumulative spend; every LLM call accrues, and the run ABORTS at the next loop boundary once
BENCHMARK_CONFIG.maxSpendUsd (default $50) is reached. Enforcement is at the model- and
phase-loop tops, NOT inside a pass runner — a pass runner's try/catch would swallow the
throw into a per-phase failure and let the run keep spending; a loop-top throw propagates to
runBenchmark().catch and aborts. A null-cost call is charged a conservative $0.25 estimate
so unpriced spend drives toward the cap instead of slipping through as $0. Unit-tested.

NOT addressed here (still gates a paid run, per the review): the roster is still 6 models
not the intended 12, the 40/30/30 formula decision, and the full 30-phase golden-contract
repair (#1) — those are operator-directed and larger.

tsc: 17 pre-existing errors, none in touched files. Full sutta-studio suite green (81).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sd5oCcYiQBnGAiNT4SAo8L
…ph + vocab

The proposed facts score (advisory today, ranked under the v2.2 40/30/30 proposal) had two
confirmed defects the review flagged:

1. A bogus assertion could RAISE the macro. Morph was graded per-word, gated on the model
   having asserted *something*, and fitsReading passed VACUOUSLY when the DPD reading was
   silent on the asserted key. So `{note: "nonsense"}` made the word "assert morph", the
   junk key vacuously "fit", and morph appeared as a free 1.0 category — the reviewer
   measured a weak macro rise 0.500 → 0.667.

2. A correct assertion could score WRONG on vocabulary. `case: "ins"` vs DPD's `instr` are
   the same case, but the exact-string compare called it a miss (macro → 0.333).

Fix:
- Grade morph per AUTHORITY-KNOWN key, not per model assertion. For each eligible word,
  the gradeable keys are the gender/case/number a reading actually specifies; the model's
  value on such a key is scored (correct iff it matches some reading), and keys no reading
  covers — like an invented `note` — are IGNORED. The denominator is authority-set, so extra
  assertions can't game it; omission of a known key stays uncharged (silence ≠ wrong), but an
  unconfirmable assertion is no longer free.
- Canonicalise morph vocabulary before comparison (`ins`≡`instr`, `m`≡`masc`, `sg`≡`sing`, …),
  so equivalent abbreviations match.
- `pos` is excluded from morph (open-ended vocabulary; the word-class check covers it) and its
  docstring now says honestly that the `pos` breakdown is a word-class agreement check, not
  morphological part-of-speech — matching the reviewer's accepted "root/word-class macro".

Tests: two new guards reproduce the reviewer's exact repros, verified RED against the pre-fix
scorer (junk inflates morph to 1/1; `ins` scores 0/1). One existing morph test updated for
the deliberate per-word → per-key change (kāye 2/2, kurūnaṁ 0/2). Facts + quality scorer
suites green (28).

NOT addressed (still gates a paid run, operator-directed): the 12-model roster, the 40/30/30
formula decision, and the full 30-phase golden-contract repair (#1).

tsc: 17 pre-existing errors, none in touched files.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sd5oCcYiQBnGAiNT4SAo8L
…hanical fixes + gate

The board scores each phase by aligning the model's analysis to a golden. For that to be fair
the golden must cover the SAME Pāli word sequence the prompt shows the model. It didn't: a
mechanical audit confirmed only 6/30 ranked phases matched. The dominant cause — 16 phases —
was a golden that is a correct contiguous SLICE of a shared segment, but with no `wordRange`
to slice the PROMPT to match, so the model was prompted with the whole segment yet graded on a
fraction (100% "coverage" while ignoring most prompted words). This is the shared-segment
problem ADR SUTTA-003 opened and left unfinished beyond 7 phases.

Part 1 (this commit, mechanical — no golden CONTENT changed):
- Added `_phases[].wordRange` to the 16 SLICE phases, slicing each prompt to exactly its
  existing golden span. MATCH goes 6/30 → 22/30. The golden word sequences were already
  correct; only the prompt derivation was missing the slice.
- New gate tests/scripts/sutta-studio/golden-prompt-contract.test.ts derives the prompt
  sequence exactly as the runner does and compares it (normalised) to the golden. Verified RED
  without the wordRange additions (all 16 SLICE phases listed as violations), green after. A
  second test keeps the pending list honest — it fails if a listed phase is repaired but not
  removed from the list.

Part 2 (drafted, NEEDS OPERATOR DECISION — docs/roadmaps/GOLDEN-CONTRACT-REPAIR.md):
- 8 phases where the golden SPLITS a joined token the prompt presents as one whitespace token
  (sandhi `etadavoca`→`etad`+`avoca`; the `'ti` quotative in the breathing section) or OMITS a
  word. `wordRange` can't split a token, so these need golden CONTENT edits. The doc gives each
  phase's exact mismatch and the recommended resolution: make the golden follow the Anatomist's
  one-word-per-whitespace-token rule (sandhi moves to morpheme SEGMENTS, not word boundaries),
  and add the omitted words. Tracked in the test's KNOWN_SANDHI_PENDING allowlist. I did NOT
  edit golden content unilaterally — the sandhi-tokenization policy is a scholarly call.

So: the mechanical majority is repaired and the contract is now enforceable; the remaining 8
are documented with a proposed direction, pending your sign-off on the sandhi policy.

tsc: 17 pre-existing errors, none in touched files. Sutta-studio suite green (43).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sd5oCcYiQBnGAiNT4SAo8L
…word-per-token)

Operator approved the one-word-per-whitespace-token policy. This reconciles the 8 remaining
phases so all 30 ranked phases satisfy the golden/prompt contract (30/30).

Anatomist golden:
- Merged each split pair into ONE word matching the prompt's whitespace token; the sandhi
  split is preserved in that word's morpheme SEGMENTS (etad·avoca; assasāmī·ti), which still
  reconstruct the surface (verified). The CONTENT word's id survives the merge (so its senses
  stay valid), e.g. `etad`(fn)+`avoca`(content) → the merged `etadavoca` keeps avoca's id.
  Relations retargeted to the survivor; intra-word relations dropped.
- Added the two omitted function words: `vā` in phase-an (with wordRange [0,8)); for phase-aq,
  merged `sato`+`va`→`satova` and added the second `satova` the prompt has.

Downstream goldens (the merge changed anatomist word ids, so these had to follow or they'd
dangle — verified none dangle after):
- Lexicographer: dropped the removed function words' sense entries (the merged word is scored
  on the surviving content word's senses); added a sense entry for the inserted 2nd satova.
- Weaver: retargeted linkedPaliId to the surviving word.
- Typesetter: remapped layoutBlocks to the surviving ids and de-duplicated.

Gate: tests/scripts/sutta-studio/golden-prompt-contract.test.ts now normalises on the Pāli
LETTER sequence (quotes/punctuation dropped — the golden writes `'ti`, the source prints a
curly ‘…’) and its KNOWN_SANDHI_PENDING list is EMPTY. Verified RED against the pre-Part-2
golden (all 8 listed as violations), green after.

Validated: all 4 goldens internally consistent (segment reconstruction, zero dangling
word/segment refs). tsc 17 (baseline). Sutta-studio suite green (43). Diffs localised to the
8 phases; the golden-repair doc updated to DONE.

With this, review finding #1 is closed — the last mechanical benchmark-validity blocker. What
remains before the paid retake is operator-directed: the 40/30/30 formula and the 12 model IDs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sd5oCcYiQBnGAiNT4SAo8L
…re weights)

Operator resolved the review's finding #3: the 40/30/30 weights are the FINAL score, not the
fidelity-internal weights nested inside the old 60/25/15. So the ranked score becomes:

    overall = gateFactor × (0.40·segmentationF1 + 0.30·factsCore + 0.30·senseF1)

quality-scorer.ts (RUBRIC_VERSION 2.1 → 2.2; leaderboard RANKED_RUBRIC_VERSION → 2.2):
- segmentationF1 — morpheme-boundary micro-F1 vs golden (unchanged).
- factsCore — root + word-class macro from scoreFactsDetail. Morph is EXCLUDED (advisory this
  cycle per the reviewer); its gaming + vocabulary were fixed separately (review #4) so it can be
  promoted later. Scored against the golden's DPD-verified √tooltips (no external DPD needed).
- senseF1 — strict sense-english micro-F1 (scoreSenseFidelityDetail, SUTTA-012 drop-penalised).
- Usability and richness are RETIRED from rank (still computed for display); alignment, the LLM
  judge and the reader probe stay advisory.
- Components a phase's golden doesn't provide (e.g. a function-only phase with no senses)
  renormalise over the present weights rather than being charged 0.
- fidelityScore stays the v2.1 value, still used as the leaderboard's "has a golden → rankable" gate.

The board's methodology string now discloses the v2.2 formula; ADR SUTTA-013 amended with the
decision + implementation.

Note the facts-scorer import creates a call-time cycle with quality-scorer (which exports
alignWords/tokenize back) — safe because both sides use the other's exports only inside functions.

Tests: 4 new guards (the 40/30/30 weights, the retirement of usability/richness, renormalisation),
verified RED against the v2.1 scorer (it returns 0 where v2.2 returns 0.55 — it never read
factsCore/senseF1). Full sutta-studio + scorer suite green. tsc 17 (baseline).

This is a rubric bump — v2.1/v2.2 scores are not comparable (version gate enforces it), so it needs
a FLEET RE-RUN before the board is republished. Remaining follow-ups: board columns for
factsCore/senseF1, and the twelve model IDs for the run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sd5oCcYiQBnGAiNT4SAo8L
…nse axis

The lexicographer PROMPT teaches `viharati → "dwells"` in its ripple worked example, and 8
ranked phases (z/af/ai/az/ba/bc/bd/bg) grade exactly that sense — so a model copying the
example earns free senseF1 credit (30% of the ranked score). Same leak class the benchmark
removed phase-ad/ag/aj for, but on the sense axis instead of segmentation; the phase-contract
guard (anatomist-only) misses it. This contradicts benchmark-config's "honest, uncontaminated
ranking set".

Reglossed the example to `gacchati → goes/going/went` (verified absent from every ranked
phase's graded surfaces AND senses), preserving the tense-ripple demonstration. Added
lexico-example-leak.test.ts: no gloss taught in either lexico worked example may be a graded
sense in any ranked phase — RED against the old example (lists viharati:dwells across 8
phases), green after. Durable: any future example teaching a ranked gloss fails CI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…root spray-cap)

factsCore is 30% of the ranked score (root + word-class macro). Two goodhart holes found by
the adversarial audit:

A1 — POS (word-class) was scored over golden CONTENT words only, so a model labelling every
word `content` collected a free 1.0 (its function-word errors were never checked). Now scored
over EVERY golden word. A phase with no content word stays null (no facts to grade), so the
"returns null" edge is preserved.

A2 — root was recall-only / precision-blind: any asserted √root matching the authority counted
as correct, no penalty for the wrong roots also sprayed, so hedging many roots to guarantee a
hit was rewarded (#117 hardened MORPH's denominator against exactly this but left ROOT open). A
hit buried in > authority + ROOT_SPRAY_SLACK(2) roots is now `sprayed`, not correct; a focused
answer (right root + a little slack) still counts.

2 new tests (A1 penalty, A2 spray-vs-focused) + updated existing assertions, all verified RED
against the pre-fix scorer. tsc at baseline, full sutta suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 18, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
lexicon-forge Ready Ready Preview, Comment Jul 18, 2026 7:19am

report-facts-layer.ts rendered only fab/sil/drop, so the new 'sprayed' root failure category
(audit A2) was invisible and the displayed breakdown no longer summed to root failures. Aggregate
and display root.sprayed in the root column.

Co-Authored-By: Claude Opus 4.8 (1M context) <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.

1 participant