Skip to content

eval: second/third servers + baseline regression gate - #7

Open
jeremy wants to merge 5 commits into
eval-loop-v0from
eval-multiserver
Open

eval: second/third servers + baseline regression gate#7
jeremy wants to merge 5 commits into
eval-loop-v0from
eval-multiserver

Conversation

@jeremy

@jeremy jeremy commented Aug 30, 2026

Copy link
Copy Markdown
Member

Stacked on #6 (eval-loop-v0). Delivers the ranked top of the hillclimb backlog — 2nd & 3rd servers — plus the catch-regressions-fast lever, both on the unchanged eval package.

Multi-server (backlog #1)

Product-specific launch details move into a server registry in the eval command (cmd/eval/main.go), so a new server is one map entry, not code.

Server Launch Hermetic?
fake in-process catalog yes (CI fixture)
fizzy fizzy-mcp stdio --writes yes — dummy token, never hits a backend
hey hey-mcp stdio yes — catalog from vendored SDK model
basecamp basecamp-mcp stdio no — see boundary below

hey lands as a real second product server with a committed corpus (testdata/scenarios/hey.json, generated straight from hey's own describe surface — reads/writes/idempotent/destructive across four domains) and a real haiku run: 12/12 pass, $0.0183 (results/hey-v0.jsonl). Zero eval-package changes — that is the proof of product-agnosticism.

Honest basecamp boundary: basecamp-mcp's stdio authenticates eagerly (it fetches authorization.json before serving the transport), so unlike fizzy/hey it cannot list+describe hermetically. It is wired into the registry as a credentialed/--live target and marked non-hermetic; making it hermetic is the cassette player (hillclimb #2).

Regression gate (backlog #4/#5)

--baseline <prior.jsonl> compares a fresh run cell by cell, keyed on (model, scenario_id):

  • newly-failing / score-drop / safety → prints a diff table and exits nonzero (merge-blocking signal).
  • improved / added / removed → reported, never gated.

make eval-smoke now gates the hermetic fake+oracle run against a committed baseline (testdata/results/fake-oracle.jsonl), so CI catches a shifted catalog surface at $0.

The runs

server  model   pass    params  safety  cost_usd
fizzy   haiku   12/12   12/12   12/12   $0.0165   results/fizzy-v0.jsonl
hey     haiku   12/12   12/12   12/12   $0.0183   results/hey-v0.jsonl

Tests / CI

Hermetic, $0. New unit tests cover the baseline comparison (each regression kind, per-model keying, corpus edits never gating) and the server registry (env override, dummy-token injection that never overwrites a real token, hermeticity flags). make vet test-race build green; go mod tidy -diff clean (no new deps).

Do not merge — shared toolkit, human merge.

Deliver hillclimb #1 (2nd & 3rd servers) and the "catch regressions
fast" lever from the ranked backlog, both on the unchanged eval package.

Multi-server: the product-specific launch details move into a server
registry in the eval command (fizzy, hey, basecamp), so a new server is
one map entry, not code. fizzy and hey list+describe hermetically from
their vendored catalogs; hey lands with a committed corpus
(testdata/scenarios/hey.json) generated straight from its own describe
surface and a real haiku run (12/12, $0.0183, results/hey-v0.jsonl) —
zero eval-package changes. basecamp-mcp's stdio authenticates eagerly
(it fetches authorization.json before serving), so it is wired as a
credentialed/--live target and marked non-hermetic until the cassette
player can stub its startup.

Regression gate: --baseline compares a fresh run to a prior JSONL cell
by cell, keyed on (model, scenario_id), and exits nonzero on a score
drop, newly-failing, or safety regression; improvements and corpus edits
are reported, never gated. The hermetic eval-smoke now gates against a
committed fake baseline, so CI catches a shifted catalog surface at zero
cost.
Copilot AI balanced review requested due to automatic review settings August 30, 2026 18:43
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 30, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-08-30T19:14:38.172881Z e0d1ff8 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds multi-server eval configuration and baseline regression comparisons while keeping the core eval package product-agnostic.

Changes:

  • Adds Hey and Basecamp server profiles plus a Hey corpus/result.
  • Adds JSONL baseline comparison and regression reporting.
  • Gates the fake/oracle smoke run against a committed baseline.

Tip

If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
Makefile Adds baseline comparison to eval smoke.
eval/testdata/scenarios/hey.json Adds the Hey scenario corpus.
eval/testdata/results/fake-oracle.jsonl Adds the fake oracle baseline.
eval/results/hey-v0.jsonl Records the Hey Haiku run.
eval/README.md Documents servers, runs, and regression gating.
eval/cmd/eval/main.go Adds server registry and baseline CLI support.
eval/cmd/eval/main_test.go Tests registry and environment behavior.
eval/baseline.go Implements baseline loading, comparison, and rendering.
eval/baseline_test.go Tests regression classifications and keying.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread Makefile Outdated
Comment on lines +37 to +39
$(GO) run ./eval/cmd/eval --server fake --backend oracle --n 12 --seed 1 \
--out /tmp/eval-smoke.jsonl \
--baseline eval/testdata/results/fake-oracle.jsonl

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f7c1f08: the smoke now runs a pinned corpus (testdata/scenarios/fake.json) graded against the current fake catalog, so a renamed action, an added required param, or a changed enum invalidates a committed gold and fails as a newly-failing regression. Verified by renaming a fake action -> nonzero exit.

Comment thread eval/cmd/eval/main.go Outdated
if err != nil {
return ""
}
return strings.Join(append([]string{path}, prof.args...), " ")

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f7c1f08: registry defaults now return an argv slice (serverFields) instead of joining the resolved path into a string and re-splitting it, so a binary under a directory with spaces spawns correctly. Explicit --server-cmd/EVAL_*_CMD are still tokenized with quote handling. Test added.

Comment thread eval/cmd/eval/main.go Outdated
Comment thread eval/README.md Outdated
model pass params safety in_tok out_tok cost_usd
haiku 12/12 12/12 12/12 19199 291 $0.0165
server tools/actions model pass params safety cost_usd
fizzy boards, cards haiku 12/12 12/12 12/12 $0.0165 results/fizzy-v0.jsonl

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f7c1f08: the runs table now lists the domains the fizzy corpus actually samples — boards, cards, columns, comments, steps, users.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e33793a929

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread eval/cmd/eval/main.go Outdated
Comment on lines +121 to +122
if *baseline != "" {
bf, err := os.Open(*baseline)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Load the baseline before overwriting the output

When --baseline names the same file as --out—including the natural case where --out is omitted and its default eval/results/<server>-v0.jsonl matches the supplied baseline—the earlier os.Create(outPath) truncates and rewrites the baseline before this open. The comparison consequently reads the current run as its own baseline, silently reports no regression, and destroys the prior results; load the baseline before creating the output file.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 060ad1c: the baseline is now loaded before os.Create(outPath). Verified with --out == --baseline: the prior run is read first, the regression is detected (exit 1), and the file then holds the new run.

Comment thread Makefile Outdated
Comment on lines +37 to +39
$(GO) run ./eval/cmd/eval --server fake --backend oracle --n 12 --seed 1 \
--out /tmp/eval-smoke.jsonl \
--baseline eval/testdata/results/fake-oracle.jsonl

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pin the smoke corpus before claiming a surface gate

This target still generates scenarios from the current fake catalog, while CompareToBaseline deliberately treats removed and added scenario keys as non-gating. If an action is renamed or removed, the oracle passes the newly generated corpus and the comparison merely reports the old key as removed and the replacement as added, so this command exits successfully despite the comment's promise that a silent fake-catalog surface change fails CI. Use a pinned scenario corpus here or make structural additions/removals gate this smoke target.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f7c1f08: pinned corpus (testdata/scenarios/fake.json) so the committed golds grade against the current catalog. Verified a renamed fake action fires the gate.

Comment thread eval/README.md Outdated

# Real cheap-model run against a product's stdio server. fizzy and hey are
# hermetic (structural only; the server never reaches a backend):
go build -o /tmp/hey-mcp github.com/basecamp/hey-mcp-server/cmd/hey-mcp

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Install HEY outside the toolkit module

Running this documented command from /workspace/mcp fails with no required module provides package github.com/basecamp/hey-mcp-server/cmd/hey-mcp because that product module is intentionally absent from this toolkit's module graph. The Go CLI does not resolve and add an unrequired module for go build; use a versioned go install ...@<version> with an appropriate GOBIN, or run the build from a checkout of the HEY repository instead.

AGENTS.md reference: AGENTS.md:L10-L11

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 060ad1c: the README now builds the HEY binary from a checkout of its own repo; the product module is intentionally absent from the toolkit graph, as you note.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

f, err := os.Create(outPath)

P1 Badge Read the baseline before truncating the output

When --baseline resolves to outPath—notably --baseline eval/results/hey-v0.jsonl while leaving --out at its default—os.Create truncates the prior baseline and writes the current run before the baseline is opened. The comparison then reads the new results, always reports no regression, and destroys the committed prior run; load the baseline before creating the output or reject paths that reference the same file.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Makefile Outdated
Comment on lines +37 to +39
$(GO) run ./eval/cmd/eval --server fake --backend oracle --n 12 --seed 1 \
--out /tmp/eval-smoke.jsonl \
--baseline eval/testdata/results/fake-oracle.jsonl

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Gate catalog drift in the smoke evaluation

In the eval-smoke workflow, this command regenerates both the oracle's gold answers and the evaluated scenarios from the current fake catalog. The oracle therefore gives every retained action a score of 1; renamed, added, or removed actions surface only as added/removed cells, which CompareToBaseline deliberately does not gate, while schema or annotation changes retain the same IDs and still score 1. Consequently, the catalog changes this target claims to block will pass CI; use committed scenarios or a catalog fingerprint, or make corpus drift gating for this smoke run.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f7c1f08 — same as the pinned-corpus change above: eval-smoke now grades a committed corpus against the live catalog, so schema/rename/enum drift under an unchanged id scores 0 and gates.

Comment thread eval/baseline.go
Comment on lines +35 to +39
func LoadBaseline(r io.Reader) (*Baseline, error) {
b := &Baseline{cells: map[string]Record{}}
sc := bufio.NewScanner(r)
sc.Buffer(make([]byte, 0, 64*1024), 4*1024*1024)
for sc.Scan() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Extract the baseline only after two product implementations

This introduces the baseline store and comparison as new shared toolkit machinery, but this repository only accepts shared code after equivalent implementations have been proven by duplication in two product instances and then extracted with minimal adaptation. The reviewed change contains the first implementation rather than an extraction, so prove it independently in two product servers before moving the duplicated code here.

AGENTS.md reference: AGENTS.md:L8-L12

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declining this one, with reasoning. The extract-after-duplication rule governs product-domain machinery that products implement (gateway, catalog, mcptest). The eval package is not that: it is deliberately product-agnostic, toolkit-native infrastructure established by #6 and the program design doc — it reads any server's wire surface and is inherited by every server for free, with no product-side implementation to duplicate first. baseline.go extends that eval loop (a --baseline compare over its own JSONL), so there is nothing to prove-by-duplication in two products. Happy to revisit if we decide the eval loop itself should move out of the toolkit.

Comment thread eval/baseline.go
Comment on lines +121 to +125
case rec.Score < prev.Score:
cmp.Regressions = append(cmp.Regressions, Regression{
Model: rec.Model, ScenarioID: rec.ScenarioID, Kind: KindScoreDrop,
OldScore: prev.Score, NewScore: rec.Score,
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Compare failing-cell dimensions instead of unreachable score drops

For records produced by Grade, Score is always either 0 or 1, so after the preceding prev.Score >= 1 && rec.Score < 1 case, this score-drop branch cannot be reached by a real evaluation. A scenario that was already failing can therefore lose additional correctness dimensions—for example, going from valid to invalid parameters—without the advertised score-drop gate noticing; compare the individual grading dimensions or store a genuinely fractional score.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 060ad1c: added a dimension-regression gate — an already-failing cell (score stays 0) that loses tool/action/params true->false now gates. The fractional score-drop branch is kept for the future judge layer, with a comment noting it is unreachable under today's binary grader.

jeremy added 2 commits August 30, 2026 11:52
…racy

- eval-smoke now runs a PINNED corpus (testdata/scenarios/fake.json), so
  the committed golds are graded against the current fake catalog: a
  renamed action, an added required param, or a changed enum invalidates
  a gold and fails as a newly-failing regression (nonzero exit).
  Regenerating the corpus each run let the oracle drift with the schema
  and gated nothing. Verified: renaming a fake action fires the gate.
- Resolve the server argv as a slice (serverFields) instead of joining
  the PATH-resolved binary into a string and re-splitting it, so a binary
  under a directory with spaces still spawns. Explicit --server-cmd /
  EVAL_<PRODUCT>_CMD are still tokenized (quotes honored).
- --baseline help now names safety regressions; README fizzy row lists
  the domains its corpus actually samples (boards, cards, columns,
  comments, steps, users).
- Load --baseline BEFORE creating the --out file. When they name the same
  path (compare-then-overwrite, or the default out coinciding with the
  baseline), os.Create truncated the baseline first, so the comparison
  read the just-written run and reported no regression while destroying
  the prior results. Verified: same-file run now detects the regression.
- Add a dimension regression: today's grader scores a binary 0/1, so an
  already-failing cell that loses a correctness dimension (tool/action/
  params true->false) kept score 0 and slipped past the score compare.
  Such a cell now gates. The fractional score-drop branch stays for a
  future judge layer.
- Fix the README HEY build command (the product module is absent from the
  toolkit graph; build from the product checkout).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

f, err := os.Create(outPath)

P1 Badge Preserve a same-path baseline when the run regresses

When --baseline and --out resolve to the same file, a failing run is written over the known-good baseline before CompareToBaseline returns errRegression; the next invocation then compares against the regressed data and can pass, while the prior results are lost. Fresh evidence in this revision is that the baseline is now loaded before this call, fixing self-comparison, but os.Create still truncates it before the regression decision; compare first and preserve the file on failure, or reject identical paths.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread eval/baseline.go
Model: rec.Model, ScenarioID: rec.ScenarioID, Kind: KindDimension,
Detail: dim, OldScore: prev.Score, NewScore: rec.Score,
})
} else if rec.Score > prev.Score {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Report dimension-only improvements

When an already-failing cell fixes one correctness dimension but still fails another—for example, tool_match changes from false to true while action_match remains false—both scores remain 0, so this condition does not add the cell to Improved and the comparison reports no change. Since regressions at an unchanged score are handled dimension-by-dimension, the symmetric improvement case also needs to inspect those dimensions for the advertised comparison report to be accurate.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d4877f6: added dimensionImproved as the mirror of dimensionRegressed, so an already-failing cell that fixes one correctness dimension (e.g. tool_match false->true) now appears in the non-gating improvement report. Improvements still never gate.

Comment thread eval/baseline.go
// same-cell drop — surfaced, not silently dropped.

// baselineKey identifies one comparable cell.
func baselineKey(model, scenarioID string) string { return model + "\x00" + scenarioID }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include scenario content in the baseline identity

When a pinned corpus edits nl_framing, gold parameters, or readonly_framed while retaining its scenario_id, this key treats the changed task as the same comparison cell. A model failure caused by the new framing can therefore be reported as a regression even though corpus edits are documented as non-gating, while two successful but materially different tasks can be reported as unchanged; persist and key on a scenario-content fingerprint, or reject records whose scenario identity metadata differs.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, and this is exactly the next backlog increment — scenario-set / three-SHA rows: stamping a scenario-content (and catalog/SDK) fingerprint on each record so a same-id-but-edited task is keyed as a distinct cell (and a catalog-driven regeneration floats to its layer). It needs a new field on Record, so I'm keeping it out of this PR rather than half-adding it. In the intended workflow it doesn't bite: you compare runs of the same committed corpus (the smoke pins it), and the corpus is regenerated only on deliberate catalog drift. Tracked as the follow-up.

Comment thread eval/cmd/eval/main.go Outdated
Comment on lines +108 to +113
if *baseline != "" {
bf, err := os.Open(*baseline)
if err != nil {
return err
}
base, err = eval.LoadBaseline(bf)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate the baseline before running paid models

When --backend api or cli is used with a missing, unreadable, or malformed --baseline, the entire model run has already completed before this open is attempted, so the command incurs every model call and only then exits without producing the requested comparison. Loading and validating this input immediately after flag parsing would fail before any paid work or server startup.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d4877f6: --baseline is now loaded and validated right after flag parsing, before the server spawns or any model call — a missing/unreadable/empty baseline fails immediately, not after a billable run. Verified with a nonexistent path (exits before the run).

Comment thread eval/baseline.go
Comment on lines +50 to +53
if err := sc.Err(); err != nil {
return nil, err
}
return b, nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject an empty baseline instead of disabling the gate

When the supplied baseline file is empty, LoadBaseline returns a valid baseline with zero cells; the subsequent comparison classifies every current record as added, HasRegression remains false, and the merge-blocking command exits successfully without comparing anything. An explicitly requested baseline with no records cannot provide a regression signal and should be rejected as invalid.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d4877f6: LoadBaseline now errors on zero records, so an empty or truncated baseline fails loudly instead of vacuously passing the gate (every cell would otherwise be 'added').

- Load and validate --baseline right after flag parsing, before spawning a
  server or making any paid model call, so a missing/unreadable/empty
  baseline fails immediately instead of after a full billable run.
- Reject an empty baseline: a file with zero records would classify every
  cell as "added" and pass a merge gate vacuously, silently disarming it.
- Report dimension-only improvements symmetrically with the dimension
  regression gate, so the (non-gating) improvement list is accurate when
  an already-failing cell fixes one correctness dimension.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d4877f66f5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread eval/baseline.go
Comment on lines +45 to +48
if err := json.Unmarshal([]byte(line), &rec); err != nil {
return nil, fmt.Errorf("decode baseline record: %w", err)
}
b.cells[baselineKey(rec.Model, rec.ScenarioID)] = rec

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate decoded baseline records

When a baseline contains syntactically valid but structurally invalid JSON such as {}, unmarshalling succeeds and inserts a record under the blank model/scenario key. That makes len(b.cells) nonzero, so the empty-baseline check is bypassed; every real result is then classified as added and the regression gate exits successfully. Validate required record fields before inserting each baseline cell so a damaged or wrong JSONL file cannot silently disable the gate.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e0d1ff8: LoadBaseline now rejects any record missing model or scenario_id at parse time, so a stray {} or truncated line fails loudly instead of inserting a blank-key cell and slipping past the empty-baseline guard. Test added.

A syntactically valid but identity-less line (a stray {} or a truncated
record) unmarshalled to a blank model/scenario_id, inserted under a blank
key, and made len(cells) nonzero — bypassing the empty-baseline guard so
every real result classified as "added" and the gate passed vacuously.
Reject any baseline record missing model or scenario_id at parse time, so
a corrupt JSONL fails loudly instead of silently disarming the gate.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e0d1ff805a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread eval/baseline.go
Comment on lines +121 to +124
prev, ok := base.cells[key]
if !ok {
cmp.Added = append(cmp.Added, rec.Model+"/"+rec.ScenarioID)
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject baseline comparisons with no matching cells

When the current run has an empty scenario file or no (model, scenario_id) overlap with the baseline—for example, the baseline contains haiku but the run accidentally uses another label—every baseline cell is classified as removed and every current cell as added, so HasRegression() remains false and the command exits successfully without performing any comparison. Reject a comparison with zero matched cells, just as LoadBaseline now rejects an empty baseline, to prevent silently disabling the gate.

Useful? React with 👍 / 👎.

Comment thread Makefile
Comment on lines +40 to +43
$(GO) run ./eval/cmd/eval --server fake --backend oracle \
--scenarios eval/testdata/scenarios/fake.json \
--out /tmp/eval-smoke.jsonl \
--baseline eval/testdata/results/fake-oracle.jsonl

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate annotation-only catalog drift

After the pinned-corpus fix, the workflow in .github/workflows/eval.yml still passes when a fake action only loses its ReadOnly or Idempotent annotation. The oracle returns the same pinned gold, Grade only uses the live Destructive bit for its annotation check, and the record's class comes from the pinned scenario, so both runs retain score 1 and this target exits zero despite the safety metadata regression. Compare the live annotations with pinned metadata or include them in a catalog fingerprint.

Useful? React with 👍 / 👎.

Comment thread eval/baseline.go
}
// An explicitly requested baseline with no records cannot provide a
// regression signal: comparing against it would classify every cell as
// "added" and pass a merge gate vacuously. Reject it so an empty or

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject duplicate cells in baseline files

If a baseline contains the same (model, scenario_id) more than once—for example, because two runs were concatenated into the documented append-only JSONL store—this assignment silently keeps only the last record. A later failing duplicate can overwrite an earlier passing result, allowing the same failure in the current run to compare equal and pass the gate. Reject duplicate keys, or define and validate an explicit run-selection policy, rather than silently discarding baseline records.

Useful? React with 👍 / 👎.

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.

2 participants