You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit e49bada
Browse filesBrowse the repository at this point in the historyBrowse files
Copy file name to clipboardExpand all lines: DOCS.md
+4-2Lines changed: 4 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -533,6 +533,8 @@ Pairwise A/B comparison per criterion: the judge sees the two outputs anonymised
533
533
534
534
Regressions carry machine-readable labels that the report and hosted diff group by: `FORMAT_FAILURE`, `SEMANTIC_REGRESSION`, `TOOL_SELECTION_DRIFT`, `TOOL_GROUND_TRUTH_MISS` (both models missed the same recorded tool ground truth — a broken-harness signal, not a migration finding; see [Migration policy and CI gating](#migration-policy-and-ci-gating)), `ARGUMENT_VALUE_DRIFT`, `TOOL_TRACE_STRUCTURE_DRIFT`, `TOOL_ORDER_DRIFT`, `DANGEROUS_ACTION_DRIFT`, `MISSING_VERIFICATION_STEP`, `UNNECESSARY_TOOL_CALL`, `REFUSAL_REGRESSION`, and `INVARIANT_VIOLATION` (the target broke a hand-written [trace rule](#trace-invariants), stamped whatever the source did). That is the complete set — every label the evaluators emit is declared in `evaluators/failures.py`. The machine labels live in `scores.jsonl`, `report.json` and the bundle; every rendered surface (the HTML report, decision prose, the run narrative) shows the plain-language display name instead — `TOOL_SELECTION_DRIFT` renders as "Different tools chosen" — with the mapping declared beside the labels in `evaluators/failures.py`.
535
535
536
+
The decision's `failure_categories` (the report's **Top regression causes** panel, and the narrative's "Most common failure … (N examples)") count each label once per **distinct example**, not per scored row: `tool_selection`stamps `TOOL_SELECTION_DRIFT` on both its conformance and its divergence row, so one example that picked another tool is one, not two. `INVARIANT_VIOLATION` is further narrowed to blocking entries — see [Trace invariants](#trace-invariants).
537
+
536
538
`ARGUMENT_VALUE_DRIFT` counts **regressions**: it is stamped only when the target scored below the source. Under `against: expected` both models can miss the same recorded expectation by the same margin — a zero delta, and a fact about your ground truth rather than a migration defect, already reported as such. Policy budgets are unaffected by the label: `max_tool_argument_drift` counts calls whose *target* score fell below `tool_argument_drift_floor`.
537
539
538
540
### Cost
@@ -783,14 +785,14 @@ CI wiring (on `analyze` and `compare`):
783
785
784
786
**A FAIL's cause is read off the facts, not inferred.** Every gating budget, overall and slice rows alike, enters the prompt with its status beside its limit — `budget "<label>": limit <x>, <status>`, where the status is `breached`, `within`, `measured nothing`, or `over its limit but not confirmed at this sample size`. Beside them go the `failed_budgets` list (the breached rows, overall and slice, named as the decision prose names them: "'security' slice overall regression rate") and, when the decision carries one, its `reason` as `verdict_reason`, in plain words: the `evalshift.yaml` keys it quotes are reworded (`migration_policy.max_invariant_violations` becomes "the trace-rule violations budget", or "the 'security' slice trace-rule violations budget" where the reason quotes it beside that slice's limit, and `fail_on_dropped_params` / `drop_params` are put into words), and its figures join the permitted numbers. `breached` is the only status that may be blamed: the instruction allows only a breached budget to be named as why the run failed or as something to fix, forbids describing a `within` budget as breached, exceeded, failed, missed, violated or something to address, and requires the verdict summary to agree with `verdict_reason`. Both new keys are FACTS keys like any other, so echoing either in prose rejects the generation. A narrative once explained a FAIL as "failed to meet the budget for Trace-rule violations" on a run where that budget held and only the latency ceiling was breached: the prompt had listed limits without results, so the model guessed.
785
787
786
-
On a `fail` the verdict summary and recommendation are also scanned for a held budget that shares a clause with a blame word (breach…, exceed…, fail…, violat…, address…, miss/missed/misses/missing, unmet, not met, broke, broken, and over/above/beyond followed by a limit phrase — an optional "the", "its" or "their", at most two words or figures, then a limit noun: limit, budget, ceiling, threshold, maximum, allowance or policy, singular or plural, as in "over budget" or "over the +200% cost limit" — so "+102% over the source" is not blame) that is not negated by a "not", "no", "never" or "without" in the 24 characters before it. Each mention of a held budget in that clause is judged on its own, and a mention is excused — it names the budget without accusing it — in two cases. A negation directly before it, with only other budget labels, "or", "and", "nor" or commas between, clears it when the name stands alone — the label, and any labels chained to it, is followed by a comma, the end of the clause, or "or", "and", "nor" or "but": "failed only because of Latency increase, not Cost increase or Trace-rule violations" passes, while "failed with no Cost increase headroom left" is still rejected, because there the negation belongs to the noun the label modifies. A holding phrase after it describes it when the phrase is measured against the limit — "within", "inside", "under" or "below" followed by the same limit phrase the blame words use ("within policy", "within budget", "under their limits", "under the +200% cost limit"), or a bare "held", "holds" or "passed" with nothing but punctuation after it to the end of the clause ("Trace-rule violations held.") — provided that phrase is not itself negated, no blame word stands between the two, and no other budget label does unless joined by "and", "or", "nor" or a comma. A verb or a preposition alone is not a holding phrase: "the migration failed on Latency increase alone, as Cost increase and Trace-rule violations all stayed within budget" passes, while "Cost increase remained above the +200% limit", "Cost increase held at +250%, exceeding its limit", "Cost increase under the target model breached its ceiling", "Cost increase, within hours, broke its ceiling", "Cost increase exceeded its limit and stayed within budget" and "failed on Cost increase as Latency increase stayed within its ceiling" are still rejected. Four misses are accepted: a holding phrase excuses the mention before it whatever follows, so "Cost increase stayed within budget at first, then exceeded it", "Address Cost increase so it stays within budget", "Cost increase below the target's ceiling failed the run" and "Cost increase, below the ceiling on 3 prompts, failed on the rest" pass. A held budget is blamed when one of its mentions is not excused and the clause has an un-negated blame word. Clauses split at sentence punctuation (`. ; : ! ?`), at while, but, whereas, although, though and however, at ", and", ", but", ", with" and ", however,", and at an em or en dash — not at a bare "and" or a bare comma, because "exceeded latency and failed to meet Trace-rule violations" accuses both. Every budget label, held or not, is located longest-first and masked before the scan, matching case-insensitively with hyphens, whitespace and quote characters folded: "checkout slice cost increase" is still the slice's label rather than a mention of the held "Cost increase" inside it, and "Trace-rule violations" does not read as a violation of whichever budget shares its clause. A match rejects the generation like an invented number, the retry names the misblamed budgets ("Your previous answer blamed budgets that held: …", followed by "No budget was breached" on a fail no budget caused), and two rejections ship the templated prose, which names the breached budget, or the overall blocking regressions when no budget was breached, and otherwise points the reader at the decision's recommendations and the budget table. The scan is a safety net, not a parser: it catches the shipped phrasing and its close variants, and the prompt change is the primary fix. A false match is not free: it costs the retry, which is shared with invented-number rejections, and a repeated one ships the templated prose, which has no findings. A narrative is cached per run (see below), and the cache key does not change with the CLI, so an existing run keeps its old prose until its `insights.json` is deleted.
788
+
On a `fail` the verdict summary and recommendation are also scanned for a held budget that shares a clause with a blame word (breach…, exceed…, fail…, violat…, address…, miss/missed/misses/missing, unmet, not met, broke, broken, and over/above/beyond followed by a limit phrase — an optional "the", "its" or "their", at most two words or figures, then a limit noun: limit, budget, ceiling, threshold, maximum, allowance or policy, singular or plural, as in "over budget" or "over the +200% cost limit" — so "+102% over the source" is not blame) that is not negated by a "not", "no", "never" or "without" in the 24 characters before it. Each mention of a held budget in that clause is judged on its own, and a mention is excused — it names the budget without accusing it — in two cases. A negation directly before it, with only other budget labels, "or", "and", "nor" or commas between, clears it when the name stands alone — the label, and any labels chained to it, is followed by a comma, the end of the clause, or "or", "and", "nor" or "but": "failed only because of Latency increase, not Cost increase or Trace-rule violations" passes, while "failed with no Cost increase headroom left" is still rejected, because there the negation belongs to the noun the label modifies. A holding phrase after it describes it when the phrase is measured against the limit — "within", "inside", "under" or "below" followed by the same limit phrase the blame words use ("within policy", "within budget", "under their limits", "under the +200% cost limit"), or a bare "held", "holds" or "passed" with nothing but punctuation after it to the end of the clause ("Trace-rule violations held.") — provided that phrase is not itself negated, no blame word stands between the two, and no other budget label does unless joined by "and", "or", "nor" or a comma. A verb or a preposition alone is not a holding phrase: "the migration failed on Latency increase alone, as Cost increase and Trace-rule violations all stayed within budget" passes, while "Cost increase remained above the +200% limit", "Cost increase held at +250%, exceeding its limit", "Cost increase under the target model breached its ceiling", "Cost increase, within hours, broke its ceiling", "Cost increase exceeded its limit and stayed within budget" and "failed on Cost increase as Latency increase stayed within its ceiling" are still rejected. Four misses are accepted: a holding phrase excuses the mention before it whatever follows, so "Cost increase stayed within budget at first, then exceeded it", "Address Cost increase so it stays within budget", "Cost increase below the target's ceiling failed the run" and "Cost increase, below the ceiling on 3 prompts, failed on the rest" pass. A held budget is blamed when one of its mentions is not excused and the clause has an un-negated blame word. Clauses split at sentence punctuation (`. ; : ! ?`), at while, but, whereas, although, though and however, at ", and", ", but", ", with" and ", however,", and at an em or en dash — not at a bare "and" or a bare comma, because "exceeded latency and failed to meet Trace-rule violations" accuses both. Every budget label, held or not, is located longest-first and masked before the scan, matching case-insensitively with hyphens, whitespace and quote characters folded: "checkout slice cost increase" is still the slice's label rather than a mention of the held "Cost increase" inside it, and "Trace-rule violations" does not read as a violation of whichever budget shares its clause. A match rejects the generation like an invented number, the retry names the misblamed budgets ("Your previous answer blamed budgets that held: …", followed by "No budget was breached" on a fail no budget caused), and two rejections ship the templated prose, which names the breached budget, or the overall blocking regressions when no budget was breached, and otherwise points the reader at the decision's recommendations and the budget table. The scan is a safety net, not a parser: it catches the shipped phrasing and its close variants, and the prompt change is the primary fix. A false match is not free: it costs the retry, which is shared with invented-number rejections, and a repeated one ships the templated prose, which has no findings. A narrative is cached per run (see below), and the cache key does not include the CLI version: a newer CLI regenerates an existing run's prose only when it changes a figure the narrative describes, so a prompt or scan change alone keeps the old prose until its `insights.json` is deleted.
787
789
788
790
```yaml
789
791
defaults:
790
792
insights_model: gemini-3.1-flash-lite-preview # optional; falls back to judge_model
791
793
```
792
794
793
-
- **Cost**: one model call per run (a second only when the first generation is rejected). The narrative is cached in `insights.json` and keyed on the run's `config_hash` plus the model id, so re-running `report` or `push` costs nothing; changing either invalidates the cache and regenerates.
795
+
- **Cost**: one model call per run (a second only when the first generation is rejected). The narrative is cached in `insights.json` and keyed on the run's `config_hash`, the model id and the figures it describes (the FACTS block, read off the run's scores, analysis and persisted decision), so re-running `report` or `push` costs nothing. Changing any of them invalidates the cache and regenerates — including a re-run of `evaluate` or `analyze` on the same run after an `evalshift.yaml` edit, which `config_hash` alone cannot see because it is fixed when the run is created. `push` ships the narrative the last `report` wrote, so run `report` after a re-`analyze` before pushing.
794
796
- **Skipped** silently when `--no-insights` is passed, and with a warning when no API key is set for the chosen model or the run directory has no usable `evalshift.yaml`. A skip is never an error.
795
797
- **Never fatal.** Any failure inside generation is logged and leaves the narrative empty; a run that already has its statistics is not worth failing over a missing paragraph.
796
798
- **What gets sent to the model**: the pre-rendered figures, plus the **worst 8 regressions'** inputs and both models' outputs (each truncated to 2000 characters). That is the same exposure `llm_judge` already has, but it is real — if your suite carries data you would not send to an LLM judge, run with `--no-insights`.
0 commit comments