Skip to content

fix(insights): the narrative names the budget that failed the run - #51

Merged
babaliauskas merged 17 commits into
mainfrom
fix/insight-budget-status
Oct 12, 2026
Merged

babaliauskas merged 17 commits into
mainfrom
fix/insight-budget-status

Conversation

@babaliauskas

Copy link
Copy Markdown
Collaborator

What this changes

The run-insights narrative (the AI-written summary at the top of report.html) is now told which budget failed the run. On a FAIL, a summary that blames a budget that held is rejected.

  • Each budget shows its result in the prompt. Every gating budget, both the overall rows and the slice rows, enters the FACTS block as budget "<label>": limit <x>, <status>. The status is one of breached, within, measured nothing, or over its limit but not confirmed at this sample size.
  • failed_budgets and verdict_reason. These are new FACTS lines: the breached rows, and the decision's own reason. The reason is reworded so it never quotes config keys (migration_policy.max_invariant_violations → "the trace-rule violations budget", scoped to the slice when the reason names one). Its figures join the number allow-list. Both keys are banned from prose, like the other FACTS keys.
  • Instruction rule. Only a breached budget may be named as the cause of a FAIL or as something to fix. The verdict summary must agree with verdict_reason.
  • validate_blame. This runs on a FAIL only, over verdict_summary and recommendation.
    • It splits the text into clauses and masks every budget label, longest first. Masking folds case, hyphens and quote marks.
    • It rejects a clause in which a within budget shares space with a blame word that isn't negated: breach/exceed/fail/violate/address/miss, or over/above/beyond plus a limit word.
    • A held budget is excused when a limit-relative holding phrase follows it ("within its limit", "under the +200% budget", "within policy", or a clause-final "held"/"passed"). It is also excused when it is a negated stand-alone mention ("not Cost increase or Trace-rule violations").
    • A rejection retries with the misblamed budgets named. A second rejection ships the templated prose.
  • Docs. DOCS.md, llms-full.txt, docs/faq.md and CHANGELOG are updated, including the cache caveat: an existing run keeps its old narrative until its insights.json is deleted.

No change to the config, suite, state.json, scores.jsonl, insights.json or bundle schemas.

Why

Run r_20261012_converter_d418e5 failed on the latency ceiling alone (+175.3% against +30%), and every other budget held. Its narrative said the target model "failed to meet the budget for Trace-rule violations" and recommended "address the Trace-rule violations". That budget had passed at 0 against a limit of 1. The prompt had listed each budget's limit without its result, so the model guessed the cause. The existing number and identifier checks could not catch an accusation written in plain words.

Verified locally (CLI from this branch)

Run Verdict Narrative
r_20261012_planner_d51f9c FAIL, trace rules 4/11 against 1 AI prose names the trace-rule budget, copying the plain-worded reason. It says the other 7 budgets held.
r_20261012_converter_c80f30 FAIL, latency +47.4% against +30% Templated fallback (model: none): "the latency increase budget was breached". Nothing blames Trace-rule violations. Both AI attempts were rejected, and why is not yet known. The reason is printed in compare's warnings and was not captured. It is either a blame-check false match or the older invented-number check. Follow-up: rerun report and capture the warning.
r_20261012_triage_697d54 PASS Correct. It also confirms #50 (semantic none with the tolerance note; agent_contract not attempted).

Known limits of the blame check

The blame check is a heuristic safety net; the prompt rule is the primary fix. The reviewer measured it on probe sets:

  • Correct sentences wrongly flagged: 7 of 34 realistic correct FAIL-on-latency sentences. Each costs a retry; two in a row ship the templated prose without findings. Examples: "neither … nor … were breached", "held steady", "within bounds/tolerance".
  • Accusations that still pass: 16 of 36 contrived ones. Examples: "stayed within budget at first, then exceeded it", "below the ceiling on 3 prompts, failed on the rest", "under the new pricing policy exceeded its limit".

The accepted gaps are pinned in tests and listed in DOCS.md.

Follow-ups (not in this PR)

  • Tool budgets on a tool-free suite still read "2 measured nothing", and the AI suggests verifying tool calls on a suite that has none.
  • When the source model broke the same trace rules, the narrative doesn't mention it (the decision's recommendations do).
  • The templated budget-breach advice says "Address the blocking evaluators" even when there are none.
  • Optional: add cap/bound/tolerance to the limit words (measured: 13/18 → 8/18 of "within bounds"-style false alarms).

Test plan

  • pytest passes: 3093 passed, 95% coverage (make ci)
  • ruff check, ruff format --check clean
  • mypy --strict src/evalshift_cli clean
  • CHANGELOG.md updated under ## [Unreleased]
  • (If user-visible) docs updated
  • Capture why the converter run's AI narrative was rejected twice

Release: 1.4.2, together with #50, via a separate chore(release) PR after merge.

🤖 Generated with Claude Code

babaliauskas and others added 17 commits October 12, 2026 20:21
…eason

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The prompt listed each budget's limit with no result, so a FAIL on the
latency ceiling alone was explained as 'failed to meet the budget for
Trace-rule violations' — a budget that held. Each budget line now ends
with its status, the breached ones ride on failed_budgets, the decision's
reason travels with them, and the instruction allows only a breached
budget to be named as the cause.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ve copies

The decision's reason travels into a FACTS block the model copies
verbatim, but the policy words it with evalshift.yaml keys: a trace-rule
FAIL quoted migration_policy.max_invariant_violations, a banned token
that cost a faithful generation its retry, and a dropped-params FAIL
quoted fail_on_dropped_params and drop_params, which would ship to
readers. The reason now reaches the facts in plain words, figures intact.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
On a FAIL the verdict summary and recommendation are scanned for a
'within' budget sharing a clause with a non-negated blame word. A hit is
rejected like an invented number, the retry names the labels, and two
rejections ship the templated prose, which already names the breached
budget.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
validate_blame took held labels only and matched them by substring, so a
breached slice label such as "'checkout' slice cost increase" accused
its held overall row, and a breached label carrying a blame word
("Trace-rule violations") accused a held neighbour. Labels from both
sides are now located longest first, non-overlapping, and masked before
the scan; _build_insight passes the non-held labels as others.

', with', ', however,' and a dash now end a clause too, so a retry phrased
the way the hint invites ('..., with Cost increase within policy') is
accepted.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A slice label reaches the model as "'checkout' slice cost increase" and
may come back bare or curly-quoted; the held overall "cost increase"
inside it then claimed the span. _normalise now drops straight and curly
quotes from labels and prose alike.

The neighbour test's sentence split at ', with' and passed without the
masking; it now keeps both labels in one clause and asserts the
accusation that appears when others is omitted.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…erdict reason

The policy words a slice-only trace-rule fail as "in the 'security' slice,
over the 0 allowed by migration_policy.max_invariant_violations". The plain
rewrite turned the key into "the trace-rule violations budget", pinning the
slice's limit on the overall budget the FACTS block marks within; a summary
that copied the reason, as instructed, was rejected as a misblame every time.

A key quoted beside a slice's limit is now named with the same scoped label
the FACTS block lists the slice row under, so the blame scan's masking claims
it. The overall case is unchanged. The Facts.verdict_reason comment now says
what the field actually is.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…failure

The blame scan rejected faithful FAIL narratives, and two rejections ship the
templated prose with no findings:

- "over the" / "above the" matched "+102% over the source". Over, above and
  beyond now accuse only with a limit noun (limit, budget, ceiling,
  threshold, maximum, allowance), one word or figure allowed between.
- A run-level "failed" accused every held budget in its clause, as in
  "the run failed on Latency increase, as Cost increase and Trace-rule
  violations all stayed within budget". Each mention of a held label is now
  judged on its own. It is excused by a negation right before it ("not Cost
  increase or Trace-rule violations"), or by an un-negated holding word after
  it (within, held, stayed, remains, passed, inside, under the/its, ...) with
  no blame word between and no other label between unless joined by
  and/or/nor/a comma. A label is blamed only when a mention is not excused
  and the clause holds an un-negated blame word.

The shipped sentence, "address the Trace-rule violations", and "exceeded its
limit and stayed within budget" are still caught. So is a holding word that
belongs to another label ("failed on Cost increase as Latency increase stayed
within its ceiling").

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A FAIL from comparison severity or dropped parameters breaches no budget, and
its FACTS block has no failed_budgets line, yet the retry hint sent the model
to "the budgets under failed_budgets". With failed_budgets empty the hint now
reads "No budget was breached; ..."; its first sentence is unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…tch costs

DOCS.md and llms-full.txt said a false match "costs one retry". It costs the
retry, which is shared with invented-number rejections, and a repeated one
ships the templated prose, which has no findings. Both now say so, list the
limit noun "over/above/beyond" needs, describe the negated-mention and
holding-word excuses and the "No budget was breached" retry hint, and name a
slice's trace-rule limit for its slice in the plain-worded reason. The DOCS.md
Run-insights paragraph is split at "On a `fail`", and the touched llms-full.txt
bullet is rewrapped to its width. The changelog entry notes that naming a held
budget to clear it is not blame.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The two excuses added to the blame scan were loose enough to wave through
clear accusations that were caught before:

- Any holding verb excused the label before it, so "Cost increase remained
  above the +200% limit", "held at +250%, exceeding its limit" and "under the
  target model breached its ceiling" shipped. A holding phrase now counts only
  when it is measured against the limit: "within", "inside/under/below" the
  same limit phrase the blame words use (one shared constant), or a bare
  "held"/"holds"/"passed" that ends its clause.
- A negation before a label excused it even when the label modified a noun,
  so "no Cost increase headroom left" shipped. The negation now excuses only a
  name that stands alone: the label, and any labels chained to it, followed by
  a comma, the clause end or or/and/nor/but.

Two misses are accepted and pinned: "stayed within budget at first, then
exceeded it" and "Address Cost increase so it stays within budget".

Also read a slice's scope up to "' slice, over" rather than the first quote,
so a slice named "o'brien" keeps its scoped budget in the verdict reason.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ases

DOCS.md and llms-full.txt now say a holding phrase must be measured against
the limit, that a negated label must stand alone, and which two misses are
accepted.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A bare "within" still excused any held label before it, so "Cost increase
within the checkout flow exceeded its limit", "Cost increase, within hours,
broke its ceiling" and "Address Cost increase within the next sprint" shipped.
All four holding prepositions (within, inside, under, below) now need a limit
phrase after them.

The shared limit phrase widens so correct phrasings stop being flagged: the
determiner is optional and may be "their", up to two comma-free words or
figures may precede the noun, and "policy" joins the limit nouns, so
"within budget", "within policy", "under their limits" and "under the +200%
cost limit" hold. The blame side widens with it: "went over budget" and "went
over their limits" now accuse.

"below the target's ceiling" and "below the ceiling on 3 prompts" are limit
phrases, so they still excuse the label before them; both are pinned as
accepted gaps. DOCS.md, llms-full.txt and the docstrings list the accepted
words and the four accepted misses.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@babaliauskas
babaliauskas merged commit 527e698 into main Oct 12, 2026
4 checks passed
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