Skip to content

test(notes): add the structured enhance-and-categorize eval case - #617

Merged
goosewobbler merged 11 commits into
mainfrom
test/542-structured-eval-case
Oct 23, 2026
Merged

goosewobbler merged 11 commits into
mainfrom
test/542-structured-eval-case

Conversation

@goosewobbler

@goosewobbler goosewobbler commented Aug 1, 2026 •

Copy link
Copy Markdown
Owner

Completes the assertion scope of #542. The harness shipped covering the free-text release-notes path; the structured enhance-and-categorize path is the case it was deliberately built to take next, and the one that unlocks the category-distribution assertion the issue lists.

Two assertions the free-text case can't reach

checkCategoryDistribution — grouping has to discriminate, not merely be legal. The task validator already rejects category names outside the configured set, so a model that drops every entry into one bucket passes validation and still produces a useless grouping. This catches the three ways that degrades: too few populated categories, an emitted-but-empty category, and one category swallowing most of the entries. The share bound is skipped below four entries, where a lopsided split isn't evidence of laziness.

checkNoEntryLoss — every enhanced entry survives categorization exactly once. It compares by identity, not by count (a dropped entry and a duplicated one cancel out in a total): the task's categories[].entries are the same objects as its enhancedEntries, so each must sit in exactly one bucket, once, and no bucket may hold anything else.

The golden case

The input is production-shaped — the bare subject plus scope and originalType, as both commit parsers emit — so the pinned prompt is the one production builds. Its scopes are restricted to api/config, and the eval asserts that no disallowed scope reaches a bucket, so grouping unvalidated entries fails it. The recorded response is hand-authored and re-keyed with RELEASEKIT_EVAL_RECORD=1.

The eval capabilities advertise no structured outputs, so the case exercises the text → JSON parse path, not the schema/tool-call branch. The prefix-leak guard is built from the case's own type/scope labels, so a legitimate lead-in in a live run isn't flagged.

loadGoldenCase is now generic in its context type, since the tasks take different ones — release notes a ReleaseNotesContext, this an EnhanceContext & CategorizeContext.

Harness

Both offline modes refuse a corrective retry (the task rejected the recorded response): replay can't answer it, and a re-seed that answered with the same canned markdown would cache orphan entries that replay then serves. The error now says the recording was rejected instead of advising a re-record that would fail the same way.

Assertion unit tests

Added assertions.spec.ts. The assertions are the regression net, so each needs a case that actually trips it — a check that only ever returns [] passes every eval run while catching nothing. Covers the pre-existing assertions too.

Verification

  • Mutation checks. Each of these fails the eval: grouping that drops one entry and duplicates another (entries[i === 1 ? 0 : i] in groupByCategory); grouping the pre-scope-validation entries; grouping the raw, un-enhanced input.
  • The drift signal fires on the new case. Editing the enhance-and-categorize system prompt changes the cache key, and the run fails with the re-record hint rather than silently replaying stale output.
  • Recording is per-case. Re-seeding into an empty cache reproduces exactly the two committed entries; the release-notes fixture is untouched.
  • Notes suite: 464 passed, 2 skipped. Typecheck and lint clean.

Closes #542.

🤖 Generated with Claude Code

Completes the assertion scope of #542. The harness shipped with the free-text
release-notes path; the structured path is the case it was built to take next, and
the one that unlocks the category-distribution assertion the issue lists.

Two assertions the free-text case can't reach:

`checkCategoryDistribution` — grouping has to discriminate, not merely be legal. The
task validator already rejects category names outside the configured set, so a model
that drops every entry into one bucket passes validation and still produces a useless
grouping. Catches too few populated categories, an emitted-but-empty category, and one
category swallowing most of the entries. The share bound is skipped below four entries,
where a lopsided split isn't evidence of anything.

`checkNoEntryLoss` — every entry survives categorization exactly once, catching both
drops and duplication across buckets.

`loadGoldenCase` is now generic in its context type, since the tasks take different
ones — release notes a ReleaseNotesContext, this an EnhanceContext & CategorizeContext.

Also adds unit tests for the assertion module. These are the regression net, so each
needs a case that actually trips it; without one the eval would pass every run while
catching nothing.

Verified the drift signal fires on the new case: editing the enhance-and-categorize
system prompt changes the cache key and the run fails with the re-record hint rather
than silently replaying stale output. Recording the new case left the existing
release-notes fixture untouched, confirming the per-case refresh.

Part of #542.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FADRmu9uhtCk3StMoqwV27
@github-actions

github-actions Bot commented Aug 1, 2026 •

Copy link
Copy Markdown
Contributor

Standing release PR: #614 · v0.42.0 queued (4 packages) · open 82d 20h 47m · ✅ ready to merge

Release Preview — v0.41.2 → v0.42.0

Note: Labels on this PR are advisory in standing-pr mode. Bumps come from conventional commits in the standing PR; override by editing labels on the standing PR itself. Add release:immediate to bypass the standing PR and release this PR directly.

These changes will be added to the release PR (#614) when merged:

Changelog

monorepo 0.41.2 → 0.42.0

Added

  • BREAKING envelope the version and publish commands, unwrap on the pipe (cli) (PR #616)

Changed

  • trim eval comments and escape the prefix-leak labels (notes)
  • add a Live Tests job, starting with notes against Ollama Cloud (PR #697 · closes #589)
  • run tests when packages/forge or packages/git change (PR #705 · closes #704)
  • cut isOllamaUnreachable's tests to the three that pin its rule (notes) (PR #702)
  • generate release notes with the OLLAMA_MODEL repo variable's model (PR #703)
  • make the LLM e2e infra-tolerant and close a false green (notes) (PR #618)
  • fix the skipped-bump entries (troubleshooting) (PR #695)
  • run the integration suite in CI and fix its stale sync-tags fixture (version) (PR #685 · closes #654)
  • tighten eval comments (notes)
  • flag console in package source (biome) (PR #668)
  • skip CodeQL, release preview and title checks on the standing release PR (PR #672)
  • add .git-blame-ignore-revs for the block-statement sweep (PR #680)
  • add commands and gotchas to AGENTS.md, and drop CLAUDE.md (PR #673)
  • make noExplicitAny an error outside specs and mocks (biome) (PR #682)
  • update package versions and lockfile (deps) (PR #681)
  • add braces to the new single-statement bodies
  • drop low-value eval additions (notes)
  • close the last copy-detection and scope-check gaps (notes)
  • only call bucketed entries copies when their content matches (notes)
  • tighten the eval harness and fixture after second review (notes)
  • check entry identity in the eval and use a production-shaped golden input (notes)
  • give the eval categories their required descriptions (notes)
  • add the structured enhance-and-categorize eval case (notes)

Fixed

  • BREAKING apply schema publish defaults when the config has no publish block (publish) (PR #690 · closes #667)
  • BREAKING report each error's exit-code family as its envelope category (core) (PR #708)
  • BREAKING default to conventionalcommits and reject unsupported presets (version) (PR #694 · closes #652)
  • commit a hybrid package's Cargo.toml in a direct release (release) (PR #691 · closes #678)
  • BREAKING sync every committed Cargo.lock that records a bumped crate (version) (PR #674 · closes #650)
  • push the current branch when git.branch is unset (publish) (PR #696 · closes #688)
  • keep per-package and baseline tags in --input (publish) (PR #684 · closes #675)
  • skip crates marked publish = false (publish) (PR #693 · closes #692)
  • demote Dependabot deps-dev bumps by default (notes) (PR #645 · closes #644)

All packages → 0.42.0 (sync versioning)

  • @releasekit/version
  • @releasekit/notes
  • @releasekit/publish
  • @releasekit/release

After merge — predicted release

No version escalation — this PR's changes will be included in the queued release without affecting the projected versions.

Package Standing PR This PR After merge
monorepo 0.42.0 0.42.0 0.42.0

Updated automatically by ReleaseKit

github-actions Bot and others added 6 commits August 1, 2026 22:17
From Greptile review of #617, and a real defect in the fixture.

`LLMCategory.description` is required, and `buildCategorySection` renders each
category as `- "<name>": <description>`. The fixture omitted them, so the prompt the
eval measured against contained `- "New": undefined` — a shape production never
produces. The case was pinning malformed categorization guidance.

Re-recorded from a clean cache, since the corrected prompt changes the key. That also
drops the entry orphaned under the old key — the clean-slate procedure the harness
header documents, now exercised for the first time. The release-notes fixture is
untouched, confirming per-case recording.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FADRmu9uhtCk3StMoqwV27
…aped golden input

Review follow-ups for the structured eval case:

- `checkNoEntryLoss` compared totals only, so a dropped entry and a
  duplicated one cancelled out (a groupByCategory mutation that did
  exactly that passed the eval). It now checks identity: every enhanced
  entry sits in exactly one bucket once, no bucket holds anything else,
  and enhancement kept the golden entry count. The same mutation now
  fails with "entry 0 was categorized 2 times / entry 1 was dropped".
- The golden input kept raw `feat(api): ...` subjects with no `scope`,
  a shape neither commit parser produces, so the pinned prompt wasn't
  the one production builds. Use the parsed shape (bare subject,
  `scope`, `originalType`, no empty `issueIds`) and re-key the cache from
  the hand-authored recorded response.
- A cache miss on a corrective retry now says the recorded response was
  rejected, instead of advising a re-record that would fail identically.
- Note that the case exercises the text -> JSON path, not the
  structured-output branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQN3v9Z4f1mu3xK4Z7K44e
- Record mode answered corrective retries with the canned markdown,
  caching orphan entries that replay then served, so a rejected
  recording failed later on a misleading distribution assertion. Both
  offline modes now refuse a corrective retry at the source.
- checkNoEntryLoss reports grouping that copies entries as a broken
  identity assumption instead of N phantom "dropped" entries.
- Restrict the golden case's scopes to api/config, so grouping has to
  use the scope-validated entries: grouping the pre-validation entries
  now fails the eval (it passed before). Prompt and cache key unchanged.
- Retarget the prefix-leak guard to a leading `word(scope):` / `word:`
  prefix; the old pattern only matched tokens the prompt never contains.
- checkCategoryDistribution: drop the possibly-undefined reduce seed
  (TS18048 under tsconfig.test.json).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQN3v9Z4f1mu3xK4Z7K44e
…ches

Third-review follow-ups:

- checkNoEntryLoss's "copies" diagnosis matched on counts alone, so
  bucketing the raw, un-enhanced input (a real regression) was reported
  as a harmless copy and the developer told to relax the check. It now
  requires each stray to be bucketed once and to match a distinct
  missing entry by content; anything else reads as loss. Partial copies
  are named as copies; unrelated objects and duplicated copies are not.
- Assert directly that no disallowed scope reaches a bucket, so grouping
  the unvalidated response fails on its own, not only via identity.
- Build the prefix-leak pattern from the golden case's own type,
  originalType and scope labels, so a legitimate lead-in ("CLI: ...")
  in a live run isn't flagged.
- checkCategoryDistribution's doc listed two failure modes; it checks
  three.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQN3v9Z4f1mu3xK4Z7K44e
Fourth-review nits:

- Cover the length guard in isOneToOneCopy: one copied entry plus one
  genuinely dropped entry must read as loss, not as a clean copy that
  hides the drop.
- Compare copies by key-sorted JSON, so a copy rebuilt with its keys in
  another order is still recognised.
- Match scopes the way production validates them (case-insensitive
  unless the config says otherwise), so a live model's "API" for an
  allowed "api" is not a false failure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQN3v9Z4f1mu3xK4Z7K44e
Remove the copy diagnosis in checkNoEntryLoss (isOneToOneCopy,
canonicalJson and their spec cases) — the check fails either way, it
only changed the message for a refactor that doesn't exist — the regex
escaping of the fixture's fixed labels, and the case-insensitive scope
match, which only mattered for live-model runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQN3v9Z4f1mu3xK4Z7K44e
main now enforces style/useBlockStatements (#679).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQN3v9Z4f1mu3xK4Z7K44e
github-actions Bot and others added 2 commits October 22, 2026 22:18
Keep the non-obvious why; drop restated mechanics and examples the code already shows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@goosewobbler
goosewobbler merged commit 3950a44 into main Oct 23, 2026
31 checks passed
@goosewobbler
goosewobbler deleted the test/542-structured-eval-case branch October 23, 2026 15:11
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.

LLM notes: eval harness — golden fixtures, cached-response replay, prompt snapshots

2 participants