Skip to content

test(notes): add LLM-notes eval harness and prompt snapshot tests - #609

Merged
goosewobbler merged 3 commits into
mainfrom
feat/542-eval-harness
Aug 1, 2026
Merged

goosewobbler merged 3 commits into
mainfrom
feat/542-eval-harness

Conversation

@goosewobbler

@goosewobbler goosewobbler commented Jul 21, 2026 •

Copy link
Copy Markdown
Owner

Implements the LLM-notes eval harness from #542 — the deterministic quality net for releasekit's clearest differentiator.

The gap this closes

Nothing measured whether LLM notes were good, only structurally valid. Prompt drift was invisible in review diffs (no prompt was snapshot-tested), and a wording tweak silently busted the entire response cache with no signal.

What's here

test/eval/ harness

  • Golden fixtures — a real-shaped commit set (release-notes-basic.json) with a fixed date, so cache keys are stable.
  • Record/replay on the existing cache format — responses are replayed through withContentHashCache's on-disk format, so CI is deterministic with no API keys. A cache miss (i.e. a changed prompt/input → changed key) fails loudly with a re-record hint — the missing fixture is the drift signal.
  • Deterministic assertions (pure, reusable) — no <!-- releasekit- --> marker leaks, no duplicated "Updated dependencies" churn, length bounds, past-tense leaning. These are the regression net and hold for both replayed and live output.
  • Opt-in live mode — RELEASEKIT_EVAL=1 runs a real Ollama model (recording as it goes); RELEASEKIT_EVAL_RECORD=1 re-seeds fixtures from the human-readable *.recorded.md.

Prompt snapshot tests

Pins the five default task system prompts (enhance, categorize, enhanceAndCategorize, releaseNotes, summarize). A prompt edit now shows up as a reviewed snapshot diff in the same PR instead of a silent change that reshapes every generation. The task prompt builders are exported for this (module-level, not public package API).

Scope note

The first eval case covers the free-text release-notes path. The structured enhance-and-categorize path (which unlocks the category-distribution assertion) is the next case the harness is deliberately built to take — the record/replay + assertion infrastructure is path-agnostic.

Testing

  • Full notes suite green (441 passed, 2 opt-in e2e skipped), incl. the new eval (replay) + 5 prompt snapshots.
  • Verified the record → strict-replay round trip: RELEASEKIT_EVAL_RECORD=1 seeds the committed cache fixture, and the default run replays it deterministically.
  • typecheck + biome check clean; coverage only instruments src/**, so the eval helpers don't affect thresholds.

Part of #542. Independent of the #544 CLI-envelope work (#608) — touches only packages/notes.

Greptile Summary

This PR implements the LLM-notes eval harness from #542, adding a golden-fixture record/replay infrastructure and prompt snapshot tests to catch quality regressions and prompt drift in the release-notes pipeline.

  • Eval harness (test/eval/): golden commit sets replay through the real pipeline against committed cache fixtures using withContentHashCache's on-disk format, so CI is deterministic with no API keys. A changed prompt busts the cache key and fails loudly. Opt-in live (Ollama) and record-from-markdown modes are available. The refresh flag added to withContentHashCache is the enabler for write-through recording.
  • Deterministic quality assertions (assertions.ts): four pure checks — marker leak detection, duplicate dependency churn, length bounds, and past-tense leaning — serve as the regression net for both replayed and live output.
  • Prompt snapshot tests (prompts-snapshot.spec.ts): all five default task system prompts are pinned as Vitest snapshots, making any prompt edit an explicit, reviewable CI event rather than a silent cache-busting change.

Confidence Score: 5/5

Safe to merge — changes are purely additive test infrastructure and narrow source exports; no production behaviour is altered.

The refresh flag in withContentHashCache is a simple, well-tested write-through option. All source changes are visibility-only exports for testing. The eval harness, assertions, fixtures, and snapshot tests introduce no runtime risk. The two observations are about edge-case gaps in the tense-detection heuristic that don't affect correctness of the current fixture or the replay pipeline.

Files Needing Attention: No files require special attention.

Important Files Changed

Filename Overview
packages/notes/src/llm/cache.ts Adds a refresh option to withContentHashCache — skips the read and always calls the provider, allowing record/replay modes to overwrite existing entries. Well-tested with a new unit case.
packages/notes/test/eval/harness.ts Eval harness wiring three modes (replay / record-from-markdown / live-Ollama) through withContentHashCache. Fixed provider name and capabilities ensure record→replay keys match.
packages/notes/test/eval/assertions.ts Pure, content-based quality checks for generated notes: marker leak detection, duplicate dependency churn, length bounds, and past-tense leaning. The IRREGULAR_PAST set is small but the 0.6 ratio threshold provides tolerance for missed irregular verbs.
packages/notes/test/eval/notes-eval.spec.ts Two tests: golden-fixture replay through the real pipeline with four quality assertions, and a capabilities-fixation unit test that guards the record→replay key-stability invariant.
packages/notes/test/unit/prompts-snapshot.spec.ts Snapshots all five default task system prompts to make prompt drift an explicit, reviewable event in CI.
packages/notes/test/unit/llm/cache.spec.ts New refresh test verifies that the write-through path replaces a stale entry and that a subsequent non-refresh read finds the fresh value.
biome.jsonc Adds the eval cache fixtures directory to Biome's ignore list so compact provider-response blobs are not reformatted on re-record.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Test run] --> B{Env flags?}
    B -->|default| C[strictOfflineProvider]
    B -->|RELEASEKIT_EVAL_RECORD=1| D[loadRecorded\n*.recorded.md]
    B -->|RELEASEKIT_EVAL=1| E[OllamaProvider\nlive model]

    C --> F[withContentHashCache\nrefresh=false]
    D --> G[cannedProvider\nfixed content]
    G --> H[withContentHashCache\nrefresh=true]
    E --> I[asEvalProvider\nre-brand + tee to *.recorded.md]
    I --> J[withContentHashCache\nrefresh=true]

    F -->|cache hit| K[return fixture]
    F -->|cache miss| L[throw with re-record hint]
    H -->|always writes| M[cache fixture]
    J -->|always writes| M

    K --> N[generateReleaseNotes]
    M --> N

    N --> O{Quality assertions}
    O --> P[findMarkerLeaks]
    O --> Q[findDuplicateDependencyChurn]
    O --> R[checkLengthBounds]
    O --> S[checkPastTenseLeaning]
Loading

Reviews (3): Last reviewed commit: "test(notes): pin eval capabilities and w..." | Re-trigger Greptile

Adds the eval harness for the LLM-notes pipeline:

- test/eval/: golden commit-set fixtures run through the real pipeline with
  provider responses replayed deterministically via the existing on-disk cache
  format (withContentHashCache) — no API keys in CI. Deterministic assertions
  (no marker leaks, no duplicated "Updated dependencies" churn, length bounds,
  past-tense leaning) are the regression net and hold for replayed and live
  output alike. Opt-in live mode (RELEASEKIT_EVAL=1) runs a real Ollama model;
  RELEASEKIT_EVAL_RECORD=1 re-seeds fixtures from the human-readable *.recorded.md.
- Prompt snapshot tests pin the five default task system prompts (enhance,
  categorize, enhanceAndCategorize, releaseNotes, summarize) so a wording tweak —
  which reshapes every generation and busts the response cache — becomes a
  visible, reviewed diff instead of a silent change. The task prompt builders are
  exported for this (module-level, not public API).

First eval case covers the free-text release-notes path; the structured
enhance-and-categorize path (category-distribution assertions) is the next case
the harness is built to take.

Part of #542.

Claude-Session: https://claude.ai/code/session_01K6CmYRXTZQYs4MNo6AETxT

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@goosewobbler goosewobbler added type:enhancement New feature or request area:tests Updates to tests labels Jul 21, 2026
@goosewobbler goosewobbler added this to the 0.42.0 milestone Jul 21, 2026
@github-actions

github-actions Bot commented Jul 21, 2026 •

Copy link
Copy Markdown
Contributor

Standing release PR: #614 · v0.41.3 queued (4 packages) · open 15m · ✅ ready to merge

Release Preview — v0.41.2 → v0.41.3

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.41.3

Changed

  • pin eval capabilities and write through the cache instead of clearing it (notes)
  • bump the production-dependencies group across 1 directory with 6 updates (deps) (PR #613)
  • bump anthropics/claude-code-action from 1.0.179 to 1.0.183 (deps) (PR #610)
  • bump the development-dependencies group with 3 updates (deps-dev) (PR #612)
  • address eval-harness review — refresh fixtures, tense-check numbered lists (notes)
  • add LLM-notes eval harness and prompt snapshot tests (notes)

All packages → 0.41.3 (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.41.3 0.41.3 0.41.3

Updated automatically by ReleaseKit

@goosewobbler goosewobbler changed the title test(notes): LLM-notes eval harness and prompt snapshot tests test(notes): add LLM-notes eval harness and prompt snapshot tests Jul 21, 2026
Comment thread packages/notes/test/eval/harness.ts
Comment thread packages/notes/test/eval/assertions.ts Outdated
github-actions Bot and others added 2 commits July 21, 2026 04:04
…eck numbered lists

- Record/live modes clear the cache before running, so an edited recording or a
  live re-run regenerates the fixture instead of reusing a read-first cache hit
  (withContentHashCache returns the entry before ever calling the provider).
- checkPastTenseLeaning inspects numbered list items too, so a model that switches
  to a numbered list can't bypass the tense check vacuously.
- biome ignores the recorded cache fixtures (opaque compact blobs written by
  withContentHashCache); formatting them churned on every re-record.

Claude-Session: https://claude.ai/code/session_01K6CmYRXTZQYs4MNo6AETxT

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d of clearing it

Two follow-ups from review of #609.

The live-mode wrapper passed the real provider's capabilities through. Capabilities
are not free-floating metadata: a task reads them to decide whether to send a
structured-output schema/toolName, and both are part of the cache key. Ollama
advertises structuredOutputs: true while the harness declares false, so a live
recording of a structured case would key differently from the replay that has to
read it back — a permanent cache miss. The free-text release-notes case only works
today because it never sends a schema, so this was latent until enhance-and-
categorize lands, which is the case the harness was built to take next. Every mode
now reports the same fixed CAPABILITIES, guarded by a test that feeds the wrapper
an Ollama-shaped provider and asserts what it reports back.

The module-scope rmSync fixed the stale-fixture bug by deleting the whole committed
fixture directory at import time. Vitest evaluates the harness once per test file,
so a second eval spec under record or live mode would have each fork wiping the
shared directory and racing the other's writes; a live run against a stopped Ollama
also left the fixture deleted in the working tree. `withContentHashCache` takes a
`refresh` option instead — recording modes bypass the read and write over the entry,
which is what re-recording means. Pruning entries orphaned by a prompt change is now
an explicit `rm -rf` documented in the harness header, rather than an import side
effect.

Live mode also tees its response back into `*.recorded.md` with the model that
produced it, so the human-readable fixture and the cache entry stay the same
generation. The cache key pins the model to EVAL_IDENTITY so replay works, which
left nothing recording what actually generated a fixture the length and tense
bounds are implicitly calibrated against.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FADRmu9uhtCk3StMoqwV27
@goosewobbler
goosewobbler merged commit 8fa5327 into main Aug 1, 2026
31 checks passed
@goosewobbler
goosewobbler deleted the feat/542-eval-harness branch August 1, 2026 18:28
@github-actions github-actions Bot mentioned this pull request Aug 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:tests Updates to tests type:enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant