feat(ci): golden-set regression eval for LLM verifier behaviour (#129) - #640
Conversation
…5ai#129) Frozen corpus run through a real VerifierPipeline (real ClaimExtractor + EvidenceJudge on the pinned VERIFIER_MODEL, fixture-backed sources) that fails when a known-good input regresses to a worse verdict class. Asserts the verdict class (approved / approved_with_disclaimer / blocked), the stable signal despite generation stochasticity. Flake policy: re-run a first-sample miss up to 3x, decide by majority. New golden-eval.yml runs on main push + dispatch + weekly cron (not per-PR, cost), gated on ANTHROPIC_API_KEY and skips cleanly when absent. - Split harness into a pure layer (goldenRunner.ts, type-only verifier import, key- and build-free) and a model layer (goldenModel.ts, real pipeline wiring) - goldenModel.test.ts drives the real pipeline to blocked/approved key-free via a stub LlmProvider, so the wiring is load-bearing in CI, not decorative - typecheck:golden (chained into typecheck) puts test/golden under tsc, which test/ otherwise escapes; caught pre-existing noUncheckedIndexedAccess bugs - Corpus: every judge-dependent answer carries a hard trigger signal, else shouldTriggerVerifier skips extraction and the entry silently approves - README documents the trigger trap and the add-a-corpus-entry-for-a-new-agent procedure; v2 (deterministic source + full-turn eval) tracked in byte5ai#639 Refs byte5ai#130, byte5ai#131, byte5ai#132. Follow-up byte5ai#639.
Merge prep + blast-radius reviewMerged current Local verification (Node 22.22.3, merged branch)
All six golden Real blast radiusThe headline
The module split is the strongest part of the design: Two findings1. On merge, this ships as a permanent no-op — and reports green while doing it.
The skip-clean design is right for forks. The problem is that on the primary repo it is indistinguishable from a passing eval, so #129 closes while the regression signal it exists to provide stays at zero — with nothing that will ever go red to remind anyone. Worth either configuring the secret before merge, or making the unconfigured case visibly distinguishable from a real pass (e.g. a step-summary line, or a scheduled-run-only warning annotation). 2. The drift canary can itself drift.
Neither is a merge blocker — the harness is well built and strictly better than no eval — but #1 determines whether merging actually delivers the issue's value. Minor
Issue #129's two acceptance hints both check out: the job fails on a regressed verdict class, and the README has an "Adding a corpus entry" procedure. |
Addendum — two more, after a second adversarial pass3.
Two things keep this from being urgent, and I want to be accurate about both rather than overstate the finding:
So the realistic regression path is already covered. What remains uncovered is the eval binary itself asserting nothing about how much work it did. A one-line For completeness on the related error path: API failures are not silently skipped — a throw from 4. The eval arms itself by side effect. The gate is the existence of A separate repo variable as the arming switch ( On finding #2 (the duplicated model literal): worth converting into a key-free unit test rather than a review note — assert that the eval's Not concerns, checked and cleared: the workflow already has One operational note: cron-failure notifications go to whoever last modified the workflow file. Post-merge that is @sneumannb5, not the maintainer team — worth a deliberate owner once the key is configured, otherwise a red canary reaches nobody. |
…lders audit row (#820) Two findings from the same review wave, one branch because both are small and sharp. #764 — CI never ran the workspace packages' own test suites. `npm run test` globs only `middleware/test/**`; canvas-core (vitest), conductor-core (vitest) and plugin-api (node:test) ran nowhere. Not hypothetical twice over: #759 broke two conductor-core tests while every required check stayed green, and #725's 15 canvas-core validator tests were only ever run by hand during review. New CI step runs all three. `@omadia/plugin-ui-helpers` is deliberately absent: it declares a test script but contains zero test files, and papering over that with --passWithNoTests would create a permanently green no-op — the exact failure family this repo keeps finding (#640, #752). If it gains tests, add it to the step. #775 — the `conductor.role_holders_change` audit entry never landed: the index.ts closure passed the session sub (an EMAIL under local auth) as `actor.id`, and `admin_audit.actor_id` is a uuid column, so every insert threw. Loud in the log, empty in the audit trail — the entire point of #759. Fix: the conductor entry additionally threads the session's `omadia_user_id` (a real uuid), and the mapping lives in an exported `roleChangeAuditEntry` (adminAuditLog.ts): uuid to `actor_id` only when the session carries one, the sub always to the free-text `actor_email` — the same treatment the adminUsers routes give it, and for the `'operator'` fallback the only place the actor survives at all. Tests: 3 mapper units; the existing route test now proves the uuid threading end-to-end (harness session carries omadia_user_id, deep-equal asserts it); and a pg-gated suite runs the mapper output through the real `AdminAuditLog.record` against the REAL migration DDL (read from the migration file, not copied — a hand-duplicated schema could drift and green-light what production rejects). Its second case pins the regression permanently: the OLD mapping must keep failing on the real column with `invalid input syntax for type uuid`. Mutation checks: removing the uuid threading turns the route test red; reverting the closure to the old inline mapping reproduces exactly the insert the pg test asserts the database refuses. Verified against an ephemeral postgres:16-alpine (2/2). Full suite: 7048 tests, 1 pre-existing-shape failure fixed by extending the existing expectation (the new field), then green; typecheck:test ratchet held with no regressions; lint clean.
What
Golden-set regression eval for LLM verifier behaviour (Closes #129). A frozen corpus runs through a real
VerifierPipeline(realClaimExtractor+EvidenceJudgeon the pinnedVERIFIER_MODEL, fixture-backed sources) and fails when a known-good input regresses to a worse verdict class (approved/approved_with_disclaimer/blocked). Newgolden-eval.ymlruns it on main push + dispatch + weekly cron.Why
CI verifies plumbing (lint / typecheck / unit tests) but never LLM behaviour against the pinned model — a model bump or prompt edit can silently regress agent quality (weakness #13, version drift). Asserting the verdict class rather than the raw string gives a stable signal despite generation stochasticity; a first-sample miss re-runs up to 3× and decides by majority to absorb jitter.
Test plan
npm run typecheck:golden(new — putstest/golden/**undertsc) — cleannpx tsx --test test/goldenRunner.test.ts test/goldenModel.test.ts— 22/22 passrunEntryguard → 3 red; dropknowledgeGraphToolsCalled/toolPostconditionViolationsin
toVerifierInput→ wiring tests red; revert → green)shouldTriggerVerifierover the whole corpus — 0 broken judge-dependent fixtures (every disclaimer/contradiction answer now trips atrigger signal)
npm run eval:goldenwith a real keynpm run lint && npm run typecheck && npm run testRisk / blast radius
golden-eval.ymlworkflow (main push +workflow_dispatch+ weekly cron, not per-PR). Skips cleanly with a notice whenANTHROPIC_API_KEYis absent, so an unconfigured repo/fork stays green.ANTHROPIC_API_KEYGitHub Actions secret + a cost budget — maintainer action, not yet configured.typecheck:golden, chained intotypecheck(runs per-PR). Test-only; nosrc/change, no migration, no public API, no new env-var in the runtime.npm teststays green key-free (eval lives outside thetest/**/*.test.tsglob).