Measured by the os-dev seat during #14974's phase 2 (session session_01D47qPfEWVPmhguWgBZCi5N, PR #15576) while choosing what score an unscorable stack should carry; it did not fold the repair in, and the shape is now documented in code rather than fixed. Re-derived and filed by the domain:cli execution PM seat (#6024). Unassigned, bare, for triage.
What
packages/cli/src/lint/metadata-eval.ts, the pre-existing throwing-generator path (locate by content, ⛔ not by line):
try {
stack = await options.generate!(c.prompt, c.id);
} catch (err: any) {
generationError = err?.message || String(err);
stack = {}; // ← the empty stack
}
const score = scoreMetadata(stack); // ← scores the EMPTY stack
and, further down:
const meanScore = results.length
? Math.round(results.reduce((sum, r) => sum + r.score.score, 0) / results.length)
: 0;
⭐ scoreMetadata({}) is 100 / grade A / valid: true — driven on the published binary during #14974's phase 2, and pinned independently in score.test.ts.
⚠️ Exactly what is and is not wrong — the passed flag is CORRECT
⛔ Do not read this as "failed cases pass". passed carries its own guard and it works:
passed: !generationError && score.score >= minScore && …
⇒ a case whose generator threw is passed: false, and ok: passed === results.length is correspondingly false. That half is right.
The wrong number is score on the failed case, and therefore meanScore, which sums r.score.score over every result with no regard for generationError. ⇒ A live eval in which every single generation threw reports meanScore: 100 alongside ok: false, passed: 0. Driven during phase 2: generationError: 'model unavailable' reported next to score 100, grade A, valid: true, meanScore: 100.
⇒ The headline metric of the eval reads perfect precisely when the model under test produced nothing. ⛔ A clean number that was never earned — and it is the number a human scanning the report looks at first.
⚠️ PR #15576 makes the two failure paths DISAGREE, on purpose
That PR adds a second failure path (a generator that returns a value nobody can score) and deliberately gives it 0 / F / valid: false, with the reason written in:
⛔ Deliberately NOT scoreMetadata({}), even though the throwing-generator path above substitutes an empty stack: the empty stack scores 100 / A / valid: true, and stamping that on a stack nobody could parse would put a benign-looking verdict next to a failure. A stack that cannot be walked is not an empty stack, and valid: true for one that was never parsed is simply false.
⇒ After #15576 lands, a generator that throws scores 100 and a generator that returns garbage scores 0, for the same class of outcome. ⛔ That asymmetry is an argument for repairing the older path, ⛔ never for making the newer one wrong to match — and the newer one is the one that states something true.
Likely resolution, ⛔ not a decision
Give the throwing-generator path the same unscorable score the new path uses, so a failed case contributes nothing to meanScore rather than contributing a perfect one. ⚠️ Alternatively, exclude generationError cases from the meanScore denominator — but that changes the metric's meaning (a mean over attempted vs over scored cases) and should be stated rather than slipped in.
⚠️ Expect this to be clause-②: it changes the numbers in an existing --json payload of a published command, which the mechanical floor treats as a contract move. ⇒ the PR will likely be built and then parked while CONTRACT_REVIEW_TIER is unavailable (measured exhausted by attempt, HTTP 429, 2026-09-04T14:40Z). A scheduling fact, ⛔ not a reason to widen or narrow the card.
Verified by this seat
Both code shapes read verbatim at origin/main 0c5e9736833: the catch substituting stack = {} before an unguarded scoreMetadata(stack), the passed guard including !generationError, and meanScore reducing over r.score.score with no generationError filter.
⛔ Not re-driven here — the scoreMetadata({}) === 100 / A / valid:true reading is the dev's measurement on the published binary, reported as such; this seat verified the code path that consumes it, not the score itself.
⛔ Not deduped — no dedup search was run.
Refs: #14974 · PR #15576 · #15547 · #15549 · #15550 · #14015.
Measured by the
os-devseat during #14974's phase 2 (sessionsession_01D47qPfEWVPmhguWgBZCi5N, PR #15576) while choosing what score an unscorable stack should carry; it did not fold the repair in, and the shape is now documented in code rather than fixed. Re-derived and filed by thedomain:cliexecution PM seat (#6024). Unassigned, bare, for triage.What
packages/cli/src/lint/metadata-eval.ts, the pre-existing throwing-generator path (locate by content, ⛔ not by line):and, further down:
⭐
scoreMetadata({})is 100 / gradeA/valid: true— driven on the published binary during #14974's phase 2, and pinned independently inscore.test.ts.passedflag is CORRECT⛔ Do not read this as "failed cases pass".
passedcarries its own guard and it works:⇒ a case whose generator threw is
passed: false, andok: passed === results.lengthis correspondinglyfalse. That half is right.The wrong number is
scoreon the failed case, and thereforemeanScore, which sumsr.score.scoreover every result with no regard forgenerationError. ⇒ A live eval in which every single generation threw reportsmeanScore: 100alongsideok: false,passed: 0. Driven during phase 2:generationError: 'model unavailable'reported next to score 100, grade A,valid: true,meanScore: 100.⇒ The headline metric of the eval reads perfect precisely when the model under test produced nothing. ⛔ A clean number that was never earned — and it is the number a human scanning the report looks at first.
That PR adds a second failure path (a generator that returns a value nobody can score) and deliberately gives it
0 / F / valid: false, with the reason written in:⇒ After #15576 lands, a generator that throws scores 100 and a generator that returns garbage scores 0, for the same class of outcome. ⛔ That asymmetry is an argument for repairing the older path, ⛔ never for making the newer one wrong to match — and the newer one is the one that states something true.
Likely resolution, ⛔ not a decision
Give the throwing-generator path the same unscorable score the new path uses, so a failed case contributes nothing to⚠️ Alternatively, exclude
meanScorerather than contributing a perfect one.generationErrorcases from themeanScoredenominator — but that changes the metric's meaning (a mean over attempted vs over scored cases) and should be stated rather than slipped in.--jsonpayload of a published command, which the mechanical floor treats as a contract move. ⇒ the PR will likely be built and then parked whileCONTRACT_REVIEW_TIERis unavailable (measured exhausted by attempt, HTTP 429, 2026-09-04T14:40Z). A scheduling fact, ⛔ not a reason to widen or narrow the card.Verified by this seat
Both code shapes read verbatim at
origin/main0c5e9736833: thecatchsubstitutingstack = {}before an unguardedscoreMetadata(stack), thepassedguard including!generationError, andmeanScorereducing overr.score.scorewith nogenerationErrorfilter.⛔ Not re-driven here — the
scoreMetadata({}) === 100 / A / valid:truereading is the dev's measurement on the published binary, reported as such; this seat verified the code path that consumes it, not the score itself.⛔ Not deduped — no dedup search was run.
Refs: #14974 · PR #15576 · #15547 · #15549 · #15550 · #14015.