Record graded runs in a store nobody can quietly rewrite

df073b9b46d4 · AtlantisPleb · · parent f688901e7288

Record graded runs in a store nobody can quietly rewrite

comparison by lane falls out for free. Neither was wired: the report printed a
number and forgot it.

`--append <store>` writes one JSON line per graded run. Each row carries a
`receipt:<sha256>` over its own fields AND the receipt of the row before it, so
editing a figure in place breaks that row's receipt and inserting or removing a
row breaks the next row's link — a `receipt_mismatch` or a `chain_broken`, each
naming the row. That is what makes the file append-only in practice rather than
by convention. A benchmark history nobody can quietly rewrite is worth more than
one that merely has not been rewritten yet. It is a hash chain and not a
signature on purpose: a signature answers "who wrote this", which needs a key
this tooling has no business holding, and a trend line is asking the other
question.

Two refusals, both reported rather than thrown. `duplicate_job` — the store
already holds this Harbor job, and re-scoring a run against different floors is
useful but does not make it a second run; two rows for one execution would
double-count it in every trend after. `chain_broken` — appending to a store that
does not verify would extend a rewritten history and bury the break a row
deeper. A refused append exits 3, its own code, so a scheduled run whose result
never reached the store is not reported as green. It only ever replaces a 0: a
breached floor or an unverifiable gate is the more important finding.

`effectiveness:compare` reads the store, verifies the chain before comparing
anything, and refuses a store that does not verify — a trend over a rewritten
history reads exactly like a trend over an honest one. It produces the trend
(one suite, one lane, over time) and the lane comparison (one suite across
lanes at their latest runs). Three rules keep both honest. Only rows sharing a
suiteKey are compared, that key being suite plus sorted task list plus rate
catalog, with lane, model, and CLI version left out because those are what a
comparison varies; other groups are reported as their own groups rather than
folded into one table with a footnote. A cost delta needs both sides priced —
against an unpriced side it is `unpriced`, not zero and not "improved", the same
refusal the report already makes about one run. And a comparison that varies two
things says so: differing CLI versions, a model that also changed, or a row
priced from placeholder rates each land as a named confounder rather than being
suppressed, because a confounded comparison is often the only one available and
is readable once labelled.

The unknowns survive the round trip. A row's cost is null with its disposition
and coverage beside it, never 0, for the same reason the report withholds it:
`gpt-5.6-luna` is deliberately unpriced upstream, and a zero in the file would
launder an unmeasured lane into a free one at exactly the point where the figure
stops being read next to its reason.

44 new tests over the same five fixture Harbor jobs, 101 in the package. No
model called, no Docker image, clock injected. bench-results/ ships with its
README and no rows: every row must come from a real Harbor run, and a seeded
example would be a fabricated measurement sitting in the file the trend reads.

scheduled runs and a live-caught regression need real amd64 Harbor runs; the
store now holds the rows those runs will produce, and the fixtures prove the
trend reads a regression as a rise in cost per accepted outcome, but a fixture
is not a schedule. The 20-30 task suite is also still unbuilt.

Refs #34

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • added bench-results/README.md
  • modified docs/assure-repo/false-green-candidates.v1.json
  • modified docs/assure-repo/surface-inventory.v1.json
  • modified package.json
  • modified packages/coder-effectiveness/README.md
  • modified packages/coder-effectiveness/src/cli.ts
  • added packages/coder-effectiveness/src/compare-cli.ts
  • added packages/coder-effectiveness/src/compare.test.ts
  • added packages/coder-effectiveness/src/compare.ts
  • modified packages/coder-effectiveness/src/index.ts
  • added packages/coder-effectiveness/src/render-compare.ts
  • added packages/coder-effectiveness/src/results-store.test.ts
  • added packages/coder-effectiveness/src/results-store.ts
  • added packages/coder-effectiveness/src/store-cli.test.ts

Diff

14 files changed, +1711 -20

bench-results/README.md added +89

@@ -0,0 +1,89 @@

1
# bench-results
2
3
The append-only record of graded coder runs, one file per suite, one JSON line
4
per run. Issue
5
[#34](https://openagents.com/OpenAgentsInc/openagents/issues/34) asks for
6
results that append here with receipts; this directory is that store.
7
8
It is empty of rows. Every row must come from a real Harbor run, and no run has
9
been recorded yet. A seeded or example row would be a fabricated measurement
10
sitting in the file that the trend line reads.
11
12
## Appending a run
13
14
```sh
15
# 1. Run the suite. Harbor grades the trials.
16
bench/run-suite.sh bench/suites/tb2-cross-section.txt \
17
  --model openai/gpt-5.6-luna --jobs-dir /tmp/gym-jobs-run
18
19
# 2. Score it and record it.
20
pnpm run effectiveness:report -- /tmp/gym-jobs-run/<job-dir> \
21
  --suite tb2-cross-section --lane proxy \
22
  --thresholds packages/coder-effectiveness/thresholds/tb2-cross-section.json \
23
  --append bench-results/tb2-cross-section.jsonl
24
```
25
26
The report's exit code is unchanged by `--append`: `0` the gate passed, `1` a
27
floor was breached, `2` the gate was unverifiable. A fourth code, `3`, means the
28
run was scored but the store refused to record it, and it only ever replaces a
29
`0`.
30
31
## Reading it
32
33
```sh
34
pnpm run effectiveness:compare -- bench-results/tb2-cross-section.jsonl
35
```
36
37
Compare verifies the chain first and refuses to compare a store that does not
38
verify. It then produces two views:
39
40
- **Lanes** — the same suite across lanes at their most recent runs, each
41
  measured against a baseline lane.
42
- **Trend** — the same suite on one lane over time, the shape #34's acceptance
43
  clause asks for and the shape a regression appears in.
44
45
## Receipts, and why they are a chain
46
47
Each row carries a `receipt`, a `receipt:<sha256>` digest over that row's own
48
fields **and** the receipt of the row before it.
49
50
- Edit a figure in an existing row and that row's contents stop matching its
51
  receipt: a `receipt_mismatch` naming the row.
52
- Insert, remove, or reorder rows and a row's `previousReceipt` stops naming the
53
  row before it: a `chain_broken` naming the row.
54
55
That is what makes the file append-only in practice rather than by convention.
56
A benchmark history nobody can quietly rewrite is worth more than one that
57
merely has not been rewritten yet.
58
59
It is a hash chain, not a signature. A signature would answer "who wrote this",
60
which needs a key the tooling has no business holding. The chain answers "has
61
this been rewritten since it was written", which is the question a trend
62
actually asks. If a signing seam arrives later, it signs the head receipt and
63
this chain still holds underneath it.
64
65
## Unknown costs stay unknown
66
67
A row's `costPerAcceptedOutcomeUsd` is `null` when the run could not be priced,
68
and it carries the `costDisposition` and `costCoverage` that say why. It is
69
never `0`. `gpt-5.6-luna` — the lane the graded runs use most — is deliberately
70
unpriced in the forge model catalog, so writing a zero here would launder an
71
unmeasured lane into a free one at exactly the point where the figure stops
72
being read next to its reason.
73
74
A comparison follows the same rule: a delta against an unpriced side is
75
`unpriced`, never `0` and never "improved".
76
77
## Row fields
78
79
| Field                         | Meaning                                                                                                                                                                                  |
80
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
81
| `runDigest`                   | The report's pin over suite, lane, tasks, CLI version, model, rates.                                                                                                                     |
82
| `suiteKey`                    | The narrower pin two rows must share to be comparable: suite, sorted task list, rate catalog. Lane, model, and CLI version are excluded, because those are the axes a comparison varies. |
83
| `jobId`                       | The Harbor job. A store refuses a job it already holds — re-scoring a run does not make it a second run.                                                                                 |
84
| `costPerAcceptedOutcomeUsd`   | Total run cost over accepted outcomes, failures included, or `null`.                                                                                                                     |
85
| `gateStatus`                  | `passed`, `failed`, `unverifiable`, or `null` when no thresholds file was given.                                                                                                         |
86
| `previousReceipt` / `receipt` | The chain.                                                                                                                                                                               |
87
88
The full type is `BenchResultRow` in
89
`packages/coder-effectiveness/src/results-store.ts`.
docs/assure-repo/false-green-candidates.v1.json modified +1 -1

@@ -4,7 +4,7 @@

4 4
  "note": "Heuristic false-green LEADS, not findings. A finding requires a demonstrated reproduction (surviving mutation via mutation-runner). Do not treat a candidate as a confirmed false green. Coverage-theater leads may include tests that delegate their assertion to a custom helper the classifier does not recognise; verify before acting.",
5 5
  "sourceDigest": "sha256:dd810dd48c5bdbc9becd7fcc01dd41a4ca2abf0b2d6f6a545907247f6e3e8361",
6 6
  "summary": {
7
    "filesScanned": 2485,
7
    "filesScanned": 2488,
8 8
    "candidateCount": 16,
9 9
    "byMode": {
10 10
      "false_green_coverage_theater": 15,
docs/assure-repo/surface-inventory.v1.json modified +2 -2

@@ -1,7 +1,7 @@

1 1
{
2 2
  "schemaVersion": "1",
3 3
  "repository": "OpenAgentsInc/openagents",
4
  "sourceDigest": "sha256:88c82b2d87bcc50ede92cc36ec6d4b69b275bb598a0645b6f611e4ab69b72bab",
4
  "sourceDigest": "sha256:65e4ac0e1a556f2bfac1a35ea147861f876a3704f4e7b8cbeeeccf8292b0aeba",
5 5
  "surfaces": [
6 6
    {
7 7
      "id": "app:@openagentsinc/acceptance-runner",

@@ -1935,7 +1935,7 @@

1935 1935
      "oracles": [
1936 1936
        {
1937 1937
          "type": "test",
1938
          "ref": "packages/coder-effectiveness (4 tracked test files)"
1938
          "ref": "packages/coder-effectiveness (7 tracked test files)"
1939 1939
        }
1940 1940
      ],
1941 1941
      "obligation": {
package.json modified +1

@@ -172,6 +172,7 @@

172 172
    "test:assure-repo": "vp test --run packages/assure-repo/test",
173 173
    "audit:assure-repo": "node --import tsx packages/assure-repo/src/cli.ts audit-generate",
174 174
    "check:assure-repo-audit": "node --import tsx packages/assure-repo/src/cli.ts audit-check",
175
    "effectiveness:compare": "node --import tsx packages/coder-effectiveness/src/compare-cli.ts",
175 176
    "effectiveness:report": "node --import tsx packages/coder-effectiveness/src/cli.ts",
176 177
    "test:coder-effectiveness": "vp test --run packages/coder-effectiveness/src",
177 178
    "test:product-spec": "vp test --run packages/product-spec",
packages/coder-effectiveness/README.md modified +76 -11

@@ -19,10 +19,14 @@ question the run was for.

19 19
bench/run-suite.sh bench/suites/tb2-cross-section.txt \
20 20
  --model openai/gpt-5.6-luna --jobs-dir /tmp/gym-jobs-run
21 21
22
# 2. Score it.
22
# 2. Score it, and record it.
23 23
pnpm run effectiveness:report -- /tmp/gym-jobs-run/<job-dir> \
24 24
  --suite tb2-cross-section --lane proxy \
25
  --thresholds packages/coder-effectiveness/thresholds/tb2-cross-section.json
25
  --thresholds packages/coder-effectiveness/thresholds/tb2-cross-section.json \
26
  --append bench-results/tb2-cross-section.jsonl
27
28
# 3. Read the trend and the lanes.
29
pnpm run effectiveness:compare -- bench-results/tb2-cross-section.jsonl
26 30
```
27 31
28 32
Nothing new has to be produced by the coder for this to work. The suite reads

@@ -108,11 +112,66 @@ A measured breach outranks an unmeasurable criterion: a run that fails the

108 112
success floor and cannot be priced is `failed`, because something _was_
109 113
measured and it broke.
110 114
111
| Exit | Meaning                                     |
112
| ---- | ------------------------------------------- |
113
| 0    | Every applicable floor passed.              |
114
| 1    | A floor was breached.                       |
115
| 2    | The gate could not be verified. Not a pass. |
115
| Exit | Meaning                                                 |
116
| ---- | ------------------------------------------------------- |
117
| 0    | Every applicable floor passed.                          |
118
| 1    | A floor was breached.                                   |
119
| 2    | The gate could not be verified. Not a pass.             |
120
| 3    | The run was scored but `--append` refused to record it. |
121
122
Code 3 only ever replaces a 0. A breach or an unverifiable gate is the more
123
important finding, so a non-zero gate always outranks a bookkeeping refusal.
124
125
## The store
126
127
`--append <file>` writes the run into an append-only JSONL store — one line per
128
run, one file per suite, under `bench-results/`. Every row carries a
129
`receipt:<sha256>` over its own fields **and** the receipt of the row before it,
130
so editing a figure in place breaks that row's receipt and inserting or removing
131
a row breaks the next row's link. Both are named findings rather than silent
132
ones, which is what makes the file append-only in practice rather than by
133
convention.
134
135
Two refusals, both reported rather than thrown:
136
137
- `duplicate_job` — the store already holds this Harbor job. Re-scoring a run
138
  against different floors is a useful thing to do, and a second row is not what
139
  it produces; two rows for one execution would double-count it in every trend
140
  that follows.
141
- `chain_broken` — the store does not verify, so appending would extend a
142
  rewritten history and bury the break one row deeper.
143
144
An unpriced run records `null`, never `0`, and carries the disposition and
145
coverage that say why. See `bench-results/README.md`.
146
147
## Comparing runs
148
149
```sh
150
pnpm run effectiveness:compare -- bench-results/tb2-cross-section.jsonl
151
```
152
153
The chain is verified first, and a store that does not verify is refused rather
154
than compared: a trend over a rewritten history reads exactly like a trend over
155
an honest one. Then two views come off the same rows.
156
157
**Trend** is one suite on one lane over time — the shape #34's acceptance clause
158
asks for, and the shape a regression appears in. **Lanes** is one suite across
159
lanes at their most recent runs, each measured against a baseline.
160
161
Three rules keep both honest:
162
163
- **Only rows sharing a `suiteKey` are compared.** That key is the suite, the
164
  sorted task list, and the rate catalog. Lane, model, and CLI version are
165
  excluded, because those are the axes a comparison varies. Rows in other groups
166
  are reported as their own groups, never folded into one table with a footnote.
167
- **A cost delta needs both sides priced.** If either row is unpriced, the delta
168
  is `unpriced` — not zero, and not "improved". It is the same refusal the report
169
  makes about a single run, applied to the difference between two.
170
- **A comparison that varies two things says so.** A lane comparison whose rows
171
  carry different CLI versions is flagged `confounded`, as is a trend that also
172
  changes model, and any comparison touching a row priced from placeholder
173
  rates. Nothing is suppressed: a confounded comparison is often the only one
174
  available, and it is readable as long as it is labelled.
116 175
117 176
## Grading
118 177

@@ -139,6 +198,9 @@ called, no Docker image runs, and no clock is read.

139 198
| `crashed-verifier` | Ungraded trials stay out of both buckets.                         |
140 199
| `regressed-lane`   | A regression raises cost per accepted outcome and trips the gate. |
141 200
201
The store and comparison cases build their rows from the same fixtures, into a
202
temporary store, with the clock injected.
203
142 204
```sh
143 205
pnpm --dir packages/coder-effectiveness test
144 206
```

@@ -165,7 +227,10 @@ leaves unpriced stays unpriced here — that omission is the signal.

165 227
  per-model-family seam, the token-economy delta it measures is readable from
166 228
  this suite's rows without a change here.
167 229
- **Two consecutive scheduled runs and a caught live regression.** Issue #34's
168
  acceptance needs real Harbor runs on amd64 hardware; the fixture runs prove
169
  the grading and the arithmetic, not the schedule.
170
- **Appending results to `bench-results` with receipts.** The report is
171
  `--json`-shaped and ready for it; the store is not wired.
230
  acceptance needs real Harbor runs on amd64 hardware. The store and the
231
  comparison now hold the rows those runs will produce, and the fixture cases
232
  prove the trend reads a regression as a rise in cost per accepted outcome —
233
  but a fixture is not a schedule, and no real run has been recorded.
234
- **The 20-30 task suite.** The floors point at
235
  `bench/suites/tb2-cross-section.txt`, twelve tasks. The wider suite and the
236
  owned set drawn from this tracker's closed issues are not built.
packages/coder-effectiveness/src/cli.ts modified +45 -6

@@ -29,6 +29,7 @@ import {

29 29
  pricingFromModelsPayload,
30 30
} from "./pricing.ts";
31 31
import { renderReport } from "./render.ts";
32
import { appendResultRow } from "./results-store.ts";
32 33
import { evaluateThresholds, parseThresholds, type ThresholdGate } from "./thresholds.ts";
33 34
34 35
const USAGE = `Usage: coder-effectiveness report <job-dir> [options]

@@ -45,12 +46,25 @@ Options:

45 46
  --models <file>        A captured GET /api/v1/models body to price from,
46 47
                         instead of the pinned rate catalog. A model the served
47 48
                         catalog leaves unpriced stays unpriced here.
49
  --append <store>       Append this run to an append-only bench-results store,
50
                         chained to the receipt of the row before it. Refuses a
51
                         Harbor job the store already holds, and refuses to
52
                         extend a store that does not verify.
48 53
  --json                 Emit the report as JSON instead of text.
49 54
  -h, --help             Show this help.
50 55
51
Exit codes: 0 gate passed, 1 a floor was breached, 2 the gate was unverifiable.
52
An unverifiable gate is not a pass: a criterion could not be measured, most
53
often because the lane carries no published rate.`;
56
Exit codes: 0 gate passed, 1 a floor was breached, 2 the gate was unverifiable,
57
3 the run was scored but --append refused to record it. An unverifiable gate is
58
not a pass: a criterion could not be measured, most often because the lane
59
carries no published rate. A non-zero gate always outranks 3 — a breach matters
60
more than a bookkeeping refusal.`;
61
62
/**
63
 * Its own code, so a scheduled run whose result never reached the store is not
64
 * reported as a clean pass. It only ever replaces a 0: a gate that failed or
65
 * could not be verified is the more important finding.
66
 */
67
const APPEND_REFUSED_EXIT = 3;
54 68
55 69
interface Arguments {
56 70
  readonly jobDir: string;

@@ -58,6 +72,7 @@ interface Arguments {

58 72
  readonly lane: string;
59 73
  readonly thresholdsPath: string | null;
60 74
  readonly modelsPath: string | null;
75
  readonly appendPath: string | null;
61 76
  readonly json: boolean;
62 77
}
63 78

@@ -68,6 +83,7 @@ const parseArguments = (argv: ReadonlyArray<string>): Arguments | "help" => {

68 83
  let lane = "proxy";
69 84
  let thresholdsPath: string | null = null;
70 85
  let modelsPath: string | null = null;
86
  let appendPath: string | null = null;
71 87
  let json = false;
72 88
73 89
  for (let index = 0; index < argv.length; index += 1) {

@@ -94,6 +110,10 @@ const parseArguments = (argv: ReadonlyArray<string>): Arguments | "help" => {

94 110
      modelsPath = expectValue(argv, (index += 1), "--models");
95 111
      continue;
96 112
    }
113
    if (argument === "--append") {
114
      appendPath = expectValue(argv, (index += 1), "--append");
115
      continue;
116
    }
97 117
    if (argument.startsWith("-")) {
98 118
      throw new Error(`unknown option: ${argument}`);
99 119
    }

@@ -107,7 +127,7 @@ const parseArguments = (argv: ReadonlyArray<string>): Arguments | "help" => {

107 127
  if (lane !== "proxy" && lane !== "local") {
108 128
    throw new Error(`--lane must be proxy or local, got: ${lane}`);
109 129
  }
110
  return { jobDir, suite, lane, thresholdsPath, modelsPath, json };
130
  return { jobDir, suite, lane, thresholdsPath, modelsPath, appendPath, json };
111 131
};
112 132
113 133
const expectValue = (argv: ReadonlyArray<string>, index: number, option: string): string => {

@@ -161,10 +181,29 @@ const main = (argv: ReadonlyArray<string>): number => {

161 181
            parseThresholds(JSON.parse(readFileSync(parsed.thresholdsPath, "utf8"))),
162 182
          );
163 183
184
    const appended =
185
      parsed.appendPath === null
186
        ? null
187
        : appendResultRow(parsed.appendPath, report, gate, {
188
            recordedAt: new Date().toISOString(),
189
          });
190
164 191
    process.stdout.write(
165
      parsed.json ? `${JSON.stringify({ report, gate }, null, 2)}\n` : renderReport(report, gate),
192
      parsed.json
193
        ? `${JSON.stringify({ report, gate, appended }, null, 2)}\n`
194
        : renderReport(report, gate),
166 195
    );
167
    return exitCodeFor(gate);
196
    if (appended !== null && !parsed.json) {
197
      process.stdout.write(
198
        appended.appended
199
          ? `\nAppended to ${parsed.appendPath!} as ${appended.row.receipt}\n`
200
          : `\nNot appended (${appended.refusal}): ${appended.reason}\n`,
201
      );
202
    }
203
204
    const gateCode = exitCodeFor(gate);
205
    if (gateCode !== 0) return gateCode;
206
    return appended !== null && !appended.appended ? APPEND_REFUSED_EXIT : 0;
168 207
  } catch (error) {
169 208
    process.stderr.write(`coder-effectiveness: ${(error as Error).message}\n`);
170 209
    return 1;
packages/coder-effectiveness/src/compare-cli.ts added +125

@@ -0,0 +1,125 @@

1
/**
2
 * `coder-effectiveness compare` — read a bench-results store and say what its
3
 * rows do and do not prove about each other.
4
 *
5
 *     pnpm run effectiveness:compare -- bench-results/tb2-cross-section.jsonl
6
 *
7
 * The chain is verified before anything is compared, and a store that does not
8
 * verify is refused rather than compared. A trend line computed over a history
9
 * that has been rewritten is worse than no trend line, because it reads exactly
10
 * like one that has not.
11
 *
12
 * EXIT CODES. 0 the store verified and was compared, 1 the store could not be
13
 * read, 2 the store does not verify. The second and third are different
14
 * findings: a missing file is an operator typo, a broken chain is evidence.
15
 */
16
17
import { compareRuns } from "./compare.ts";
18
import { renderComparison } from "./render-compare.ts";
19
import { readResultRows, verifyResultChain } from "./results-store.ts";
20
21
const USAGE = `Usage: coder-effectiveness compare <store> [options]
22
23
Arguments:
24
  <store>                An append-only bench-results JSONL store, the file
25
                         \`effectiveness:report --append\` writes.
26
27
Options:
28
  --suite <name>         Compare only rows recorded under this suite name.
29
  --baseline-lane <lane> Lane every other lane is measured against.
30
                         Default: proxy, or the first lane in the group.
31
  --json                 Emit the comparison as JSON instead of text.
32
  -h, --help             Show this help.
33
34
Exit codes: 0 compared, 1 the store could not be read, 2 the store does not
35
verify. A store that does not verify is not compared: its history was rewritten
36
after it was written, and a trend over a rewritten history reads exactly like a
37
trend over an honest one.`;
38
39
interface Arguments {
40
  readonly storePath: string;
41
  readonly suite: string | null;
42
  readonly baselineLane: string | null;
43
  readonly json: boolean;
44
}
45
46
const parseArguments = (argv: ReadonlyArray<string>): Arguments | "help" => {
47
  let storePath: string | null = null;
48
  let suite: string | null = null;
49
  let baselineLane: string | null = null;
50
  let json = false;
51
52
  for (let index = 0; index < argv.length; index += 1) {
53
    const argument = argv[index]!;
54
    if (argument === "--") continue;
55
    if (argument === "-h" || argument === "--help") return "help";
56
    if (argument === "--json") {
57
      json = true;
58
      continue;
59
    }
60
    if (argument === "--suite") {
61
      suite = expectValue(argv, (index += 1), "--suite");
62
      continue;
63
    }
64
    if (argument === "--baseline-lane") {
65
      baselineLane = expectValue(argv, (index += 1), "--baseline-lane");
66
      continue;
67
    }
68
    if (argument.startsWith("-")) throw new Error(`unknown option: ${argument}`);
69
    if (storePath !== null) throw new Error(`unexpected extra argument: ${argument}`);
70
    storePath = argument;
71
  }
72
73
  if (storePath === null) throw new Error("missing required <store> argument");
74
  return { storePath, suite, baselineLane, json };
75
};
76
77
const expectValue = (argv: ReadonlyArray<string>, index: number, option: string): string => {
78
  const value = argv[index];
79
  if (value === undefined || value.startsWith("-")) throw new Error(`${option} needs a value`);
80
  return value;
81
};
82
83
const main = (argv: ReadonlyArray<string>): number => {
84
  let parsed: Arguments | "help";
85
  try {
86
    parsed = parseArguments(argv);
87
  } catch (error) {
88
    process.stderr.write(`${(error as Error).message}\n\n${USAGE}\n`);
89
    return 1;
90
  }
91
  if (parsed === "help") {
92
    process.stdout.write(`${USAGE}\n`);
93
    return 0;
94
  }
95
96
  let rows;
97
  try {
98
    rows = readResultRows(parsed.storePath);
99
  } catch (error) {
100
    process.stderr.write(`coder-effectiveness: ${(error as Error).message}\n`);
101
    return 1;
102
  }
103
104
  const verdict = verifyResultChain(rows);
105
  if (!verdict.ok) {
106
    process.stderr.write(
107
      `coder-effectiveness: ${parsed.storePath} does not verify (${verdict.break.kind}).\n  ${verdict.break.detail}\nNothing was compared.\n`,
108
    );
109
    return 2;
110
  }
111
112
  const comparison = compareRuns(rows, {
113
    ...(parsed.suite === null ? {} : { suite: parsed.suite }),
114
    ...(parsed.baselineLane === null ? {} : { baselineLane: parsed.baselineLane }),
115
  });
116
117
  process.stdout.write(
118
    parsed.json
119
      ? `${JSON.stringify({ verified: verdict, comparison }, null, 2)}\n`
120
      : renderComparison(comparison),
121
  );
122
  return 0;
123
};
124
125
process.exitCode = main(process.argv.slice(2));
packages/coder-effectiveness/src/compare.test.ts added +211

@@ -0,0 +1,211 @@

1
/**
2
 * Comparison: trends over time, lanes against each other, and the deltas this
3
 * suite refuses to state.
4
 *
5
 * Rows are built from the same checked-in fixture Harbor jobs the rest of the
6
 * suite reads, with the clock injected. No model is called and no Docker image
7
 * runs.
8
 */
9
10
import { fileURLToPath } from "node:url";
11
import { describe, expect, test } from "vite-plus/test";
12
13
import { compareRuns } from "./compare.ts";
14
import { summarizeRun } from "./effectiveness.ts";
15
import { readHarborJob } from "./harbor-job.ts";
16
import { CODER_RATE_CATALOG_VERSION } from "./pricing.ts";
17
import { renderComparison } from "./render-compare.ts";
18
import { buildResultRow, type BenchResultRow } from "./results-store.ts";
19
20
const fixture = (name: string): string =>
21
  fileURLToPath(new URL(`../fixtures/${name}`, import.meta.url));
22
23
const row = (name: string, lane: string, recordedAt: string): BenchResultRow =>
24
  buildResultRow(
25
    summarizeRun(
26
      readHarborJob(fixture(name), {
27
        suite: "tb2-cross-section",
28
        lane,
29
        rateCatalogVersion: CODER_RATE_CATALOG_VERSION,
30
      }),
31
    ),
32
    null,
33
    null,
34
    { recordedAt },
35
  );
36
37
describe("trend on one lane", () => {
38
  test("reads a regression between two consecutive runs as cost rising", () => {
39
    const first = row("priced-lane", "proxy", "2026-08-25T10:00:00.000Z");
40
    const second = row("regressed-lane", "proxy", "2026-08-25T11:00:00.000Z");
41
42
    const trends = compareRuns([first, second]).trends;
43
44
    expect(trends).toHaveLength(1);
45
    const step = trends[0]!.steps[0]!;
46
    expect(step.costDelta.direction).toBe("worse");
47
    expect(step.costDelta.absolute).toBeGreaterThan(0);
48
    expect(step.successRateDelta.direction).toBe("worse");
49
  });
50
51
  test("orders by when a run was recorded, not by the order rows were passed", () => {
52
    const later = row("regressed-lane", "proxy", "2026-08-25T11:00:00.000Z");
53
    const earlier = row("priced-lane", "proxy", "2026-08-25T10:00:00.000Z");
54
55
    const trend = compareRuns([later, earlier]).trends[0]!;
56
57
    expect(trend.rows.map((entry) => entry.recordedAt)).toEqual([
58
      "2026-08-25T10:00:00.000Z",
59
      "2026-08-25T11:00:00.000Z",
60
    ]);
61
    expect(trend.steps[0]!.costDelta.direction).toBe("worse");
62
  });
63
64
  test("produces no trend from a single run", () => {
65
    expect(compareRuns([row("priced-lane", "proxy", "2026-08-25T10:00:00.000Z")]).trends).toEqual(
66
      [],
67
    );
68
  });
69
});
70
71
describe("lane against lane", () => {
72
  test("measures every other lane against the baseline", () => {
73
    const proxy = row("priced-lane", "proxy", "2026-08-25T10:00:00.000Z");
74
    const local = row("regressed-lane", "local", "2026-08-25T10:05:00.000Z");
75
76
    const comparison = compareRuns([proxy, local]).laneComparisons[0]!;
77
78
    expect(comparison.baselineLane).toBe("proxy");
79
    const lane = comparison.lanes.find((entry) => entry.lane === "local")!;
80
    expect(lane.costDelta?.direction).toBe("worse");
81
    expect(comparison.lanes.find((entry) => entry.lane === "proxy")!.costDelta).toBeNull();
82
  });
83
84
  test("honours an explicit baseline lane", () => {
85
    const proxy = row("priced-lane", "proxy", "2026-08-25T10:00:00.000Z");
86
    const local = row("regressed-lane", "local", "2026-08-25T10:05:00.000Z");
87
88
    const comparison = compareRuns([proxy, local], { baselineLane: "local" }).laneComparisons[0]!;
89
90
    expect(comparison.baselineLane).toBe("local");
91
    expect(comparison.lanes.find((entry) => entry.lane === "proxy")!.costDelta?.direction).toBe(
92
      "better",
93
    );
94
  });
95
96
  test("compares each lane at its most recent run", () => {
97
    const stale = row("regressed-lane", "local", "2026-08-25T09:00:00.000Z");
98
    const fresh = row("priced-lane", "local", "2026-08-25T12:00:00.000Z");
99
    const proxy = row("priced-lane", "proxy", "2026-08-25T10:00:00.000Z");
100
101
    const comparison = compareRuns([stale, fresh, proxy], { baselineLane: "proxy" })
102
      .laneComparisons[0]!;
103
104
    expect(comparison.lanes.find((entry) => entry.lane === "local")!.row.recordedAt).toBe(
105
      "2026-08-25T12:00:00.000Z",
106
    );
107
  });
108
});
109
110
describe("deltas this suite refuses to state", () => {
111
  test("refuses a cost delta when either side could not be priced", () => {
112
    const unpriced = row("unpriced-lane", "local", "2026-08-25T10:00:00.000Z");
113
    const mixed = row("mixed-lane", "proxy", "2026-08-25T10:05:00.000Z");
114
115
    const comparison = compareRuns([unpriced, mixed]).laneComparisons[0]!;
116
    const delta = comparison.lanes.find((entry) => entry.lane === "local")!.costDelta!;
117
118
    expect(delta.direction).toBe("unpriced");
119
    expect(delta.absolute).toBeNull();
120
    expect(delta.reason).toContain("unmeasured cost");
121
  });
122
123
  test("refuses a success-rate delta when no verifier ran on one side", () => {
124
    const graded = row("priced-lane", "proxy", "2026-08-25T10:00:00.000Z");
125
    const crashed = { ...row("crashed-verifier", "local", "2026-08-25T10:05:00.000Z") };
126
    const forced: BenchResultRow = { ...crashed, suiteKey: graded.suiteKey, successRate: null };
127
128
    const comparison = compareRuns([graded, forced]).laneComparisons[0]!;
129
    const delta = comparison.lanes.find((entry) => entry.lane === "local")!.successRateDelta!;
130
131
    expect(delta.direction).toBe("unknown");
132
    expect(delta.absolute).toBeNull();
133
  });
134
});
135
136
describe("what a comparison will not fold together", () => {
137
  test("keeps runs of different task lists in different groups", () => {
138
    const four = row("priced-lane", "proxy", "2026-08-25T10:00:00.000Z");
139
    const three = row("unpriced-lane", "local", "2026-08-25T10:05:00.000Z");
140
141
    const comparison = compareRuns([four, three]);
142
143
    expect(comparison.laneComparisons).toEqual([]);
144
    expect(comparison.isolatedGroups).toBe(2);
145
  });
146
147
  test("scopes to one suite when asked", () => {
148
    const kept = row("priced-lane", "proxy", "2026-08-25T10:00:00.000Z");
149
    const other: BenchResultRow = {
150
      ...row("regressed-lane", "local", "2026-08-25T10:05:00.000Z"),
151
      suite: "other",
152
    };
153
154
    const comparison = compareRuns([kept, other], { suite: "tb2-cross-section" });
155
156
    expect(comparison.laneComparisons).toEqual([]);
157
    expect(comparison.isolatedGroups).toBe(1);
158
  });
159
160
  test("flags a lane comparison whose CLI version also varies", () => {
161
    const proxy = row("priced-lane", "proxy", "2026-08-25T10:00:00.000Z");
162
    const local: BenchResultRow = {
163
      ...row("regressed-lane", "local", "2026-08-25T10:05:00.000Z"),
164
      agentVersions: ["9.9.9"],
165
    };
166
167
    const comparison = compareRuns([proxy, local]).laneComparisons[0]!;
168
169
    expect(comparison.confounders.some((note) => note.includes("CLI version also varies"))).toBe(
170
      true,
171
    );
172
  });
173
174
  test("flags a trend priced from placeholder rates", () => {
175
    const first = row("priced-lane", "proxy", "2026-08-25T10:00:00.000Z");
176
    const second = row("regressed-lane", "proxy", "2026-08-25T11:00:00.000Z");
177
178
    const step = compareRuns([first, second]).trends[0]!.steps[0]!;
179
180
    expect(step.confounders.some((note) => note.includes("operator placeholder"))).toBe(true);
181
  });
182
});
183
184
describe("rendering", () => {
185
  test("prints an unpriced lane as unpriced rather than as a blank cell", () => {
186
    const unpriced = row("unpriced-lane", "local", "2026-08-25T10:00:00.000Z");
187
    const mixed = row("mixed-lane", "proxy", "2026-08-25T10:05:00.000Z");
188
189
    const text = renderComparison(compareRuns([unpriced, mixed]));
190
191
    expect(text).toContain("unpriced");
192
    expect(text).not.toContain("$0.0000");
193
  });
194
195
  test("says why an empty store has nothing to compare", () => {
196
    const text = renderComparison(compareRuns([]));
197
198
    expect(text).toContain("Nothing to compare");
199
    expect(text).toContain("The store holds no rows");
200
  });
201
202
  test("prints the trend a regression shows up in", () => {
203
    const first = row("priced-lane", "proxy", "2026-08-25T10:00:00.000Z");
204
    const second = row("regressed-lane", "proxy", "2026-08-25T11:00:00.000Z");
205
206
    const text = renderComparison(compareRuns([first, second]));
207
208
    expect(text).toContain("Trend — tb2-cross-section on the proxy lane");
209
    expect(text).toContain("worse");
210
  });
211
});
packages/coder-effectiveness/src/compare.ts added +296

@@ -0,0 +1,296 @@

1
/**
2
 * Comparison: what two rows in the results store may honestly be said about
3
 * each other, and what they may not.
4
 *
5
 * #34 asks for two things off the same store. **Trend** is the same suite on
6
 * the same lane over time — the shape "two consecutive scheduled runs producing
7
 * comparable rows" needs, and the shape a regression shows up in. **Lane
8
 * comparison** is the same suite across lanes at one moment: the same tasks
9
 * through the proxy and through a local model, which is the comparative view
10
 * the issue says falls out for free.
11
 *
12
 * The rules that keep both honest:
13
 *
14
 * 1. **Only rows sharing a `suiteKey` are compared.** Different task lists are
15
 *    different measurements. Rows in other groups are reported as their own
16
 *    groups, never folded into one table with a footnote.
17
 * 2. **A cost delta needs both sides priced.** If either row's cost per
18
 *    accepted outcome is `null`, the delta is `unpriced`, not zero and not
19
 *    "improved". This is the same refusal the report makes about a single run,
20
 *    applied to the difference between two.
21
 * 3. **A comparison that varies two things says so.** Lane comparison assumes
22
 *    the CLI is held still while the lane changes; a group whose rows carry
23
 *    different agent versions is flagged `confounded`. Trend assumes the lane
24
 *    is held still while the CLI changes, so a trend that also changes model is
25
 *    flagged the same way. Neither is suppressed — a confounded comparison is
26
 *    often the only one available, and it is readable as long as it is labelled.
27
 * 4. **Deltas are directional facts, not verdicts.** Cost per accepted outcome
28
 *    going up is `worse` and success rate going up is `better`, and this module
29
 *    says which without deciding whether the run passes. That is the gate's
30
 *    job, and it already has a third verdict for the cases nothing measured.
31
 */
32
33
import type { BenchResultRow } from "./results-store.ts";
34
35
export type DeltaDirection = "better" | "worse" | "unchanged" | "unpriced" | "unknown";
36
37
export interface Delta {
38
  readonly from: number | null;
39
  readonly to: number | null;
40
  readonly absolute: number | null;
41
  /** Change as a fraction of `from`. `null` when `from` is 0 or unknown. */
42
  readonly relative: number | null;
43
  readonly direction: DeltaDirection;
44
  readonly reason: string;
45
}
46
47
export interface LaneRow {
48
  readonly lane: string;
49
  readonly row: BenchResultRow;
50
  /** `null` on the baseline lane itself. */
51
  readonly costDelta: Delta | null;
52
  readonly successRateDelta: Delta | null;
53
}
54
55
export interface LaneComparison {
56
  readonly suiteKey: string;
57
  readonly suite: string;
58
  readonly tasks: ReadonlyArray<string>;
59
  readonly baselineLane: string;
60
  readonly lanes: ReadonlyArray<LaneRow>;
61
  /** Set when something other than the lane also varies across these rows. */
62
  readonly confounders: ReadonlyArray<string>;
63
}
64
65
export interface TrendStep {
66
  readonly from: BenchResultRow;
67
  readonly to: BenchResultRow;
68
  readonly costDelta: Delta;
69
  readonly successRateDelta: Delta;
70
  readonly confounders: ReadonlyArray<string>;
71
}
72
73
export interface LaneTrend {
74
  readonly suiteKey: string;
75
  readonly suite: string;
76
  readonly lane: string;
77
  readonly rows: ReadonlyArray<BenchResultRow>;
78
  readonly steps: ReadonlyArray<TrendStep>;
79
}
80
81
export interface Comparison {
82
  readonly laneComparisons: ReadonlyArray<LaneComparison>;
83
  readonly trends: ReadonlyArray<LaneTrend>;
84
  /** Suite keys the store holds that no other row is comparable to. */
85
  readonly isolatedGroups: number;
86
}
87
88
/**
89
 * Compare a store's rows.
90
 *
91
 * `lower is better` is passed per metric rather than inferred, because the two
92
 * metrics here disagree: cost per accepted outcome improves downward and
93
 * success rate improves upward, and a single "delta" helper that guessed would
94
 * eventually guess wrong on a third metric.
95
 */
96
export const compareRuns = (
97
  rows: ReadonlyArray<BenchResultRow>,
98
  options: { readonly suite?: string; readonly baselineLane?: string } = {},
99
): Comparison => {
100
  const scoped =
101
    options.suite === undefined ? rows : rows.filter((row) => row.suite === options.suite);
102
  const groups = groupBy(scoped, (row) => row.suiteKey);
103
104
  const laneComparisons: Array<LaneComparison> = [];
105
  const trends: Array<LaneTrend> = [];
106
  let isolatedGroups = 0;
107
108
  for (const group of groups) {
109
    const latestByLane = latestPerLane(group);
110
    if (latestByLane.length < 2) {
111
      isolatedGroups += 1;
112
    } else {
113
      laneComparisons.push(laneComparisonOf(latestByLane, options.baselineLane));
114
    }
115
116
    for (const laneRows of groupBy(group, (row) => row.lane)) {
117
      if (laneRows.length < 2) continue;
118
      trends.push(trendOf(laneRows));
119
    }
120
  }
121
122
  return { laneComparisons, trends, isolatedGroups };
123
};
124
125
/**
126
 * One row per lane: the most recently recorded.
127
 *
128
 * A lane with several rows in the store is a lane that has been run more than
129
 * once, and the lane comparison wants where it stands now. Its history is not
130
 * discarded — that is what the trend reads.
131
 */
132
const latestPerLane = (rows: ReadonlyArray<BenchResultRow>): ReadonlyArray<BenchResultRow> =>
133
  groupBy(rows, (row) => row.lane)
134
    .map((laneRows) =>
135
      laneRows.reduce((latest, row) => (row.recordedAt >= latest.recordedAt ? row : latest)),
136
    )
137
    .toSorted((left, right) => left.lane.localeCompare(right.lane));
138
139
const laneComparisonOf = (
140
  rows: ReadonlyArray<BenchResultRow>,
141
  requestedBaseline: string | undefined,
142
): LaneComparison => {
143
  const baseline =
144
    rows.find((row) => row.lane === requestedBaseline) ??
145
    rows.find((row) => row.lane === "proxy") ??
146
    rows[0]!;
147
148
  return {
149
    suiteKey: baseline.suiteKey,
150
    suite: baseline.suite,
151
    tasks: baseline.tasks,
152
    baselineLane: baseline.lane,
153
    lanes: rows.map((row) => ({
154
      lane: row.lane,
155
      row,
156
      costDelta: row === baseline ? null : costDelta(baseline, row),
157
      successRateDelta: row === baseline ? null : successRateDelta(baseline, row),
158
    })),
159
    confounders: confoundersOf(rows, "lane"),
160
  };
161
};
162
163
const trendOf = (laneRows: ReadonlyArray<BenchResultRow>): LaneTrend => {
164
  const ordered = laneRows.toSorted((left, right) =>
165
    left.recordedAt.localeCompare(right.recordedAt),
166
  );
167
  const steps: Array<TrendStep> = [];
168
  for (let index = 1; index < ordered.length; index += 1) {
169
    const from = ordered[index - 1]!;
170
    const to = ordered[index]!;
171
    steps.push({
172
      from,
173
      to,
174
      costDelta: costDelta(from, to),
175
      successRateDelta: successRateDelta(from, to),
176
      confounders: confoundersOf([from, to], "recordedAt"),
177
    });
178
  }
179
  return {
180
    suiteKey: ordered[0]!.suiteKey,
181
    suite: ordered[0]!.suite,
182
    lane: ordered[0]!.lane,
183
    rows: ordered,
184
    steps,
185
  };
186
};
187
188
/**
189
 * Cost per accepted outcome, where up is worse.
190
 *
191
 * An unpriced side makes the delta `unpriced` and carries the disposition that
192
 * explains why, so a reader sees "the local lane bills no metered tokens"
193
 * rather than an empty cell.
194
 */
195
const costDelta = (from: BenchResultRow, to: BenchResultRow): Delta => {
196
  if (from.costPerAcceptedOutcomeUsd === null || to.costPerAcceptedOutcomeUsd === null) {
197
    const unpriced = from.costPerAcceptedOutcomeUsd === null ? from : to;
198
    return {
199
      from: from.costPerAcceptedOutcomeUsd,
200
      to: to.costPerAcceptedOutcomeUsd,
201
      absolute: null,
202
      relative: null,
203
      direction: "unpriced",
204
      reason: `no cost delta: the ${unpriced.lane} lane reports ${unpriced.costDisposition}, and a delta against an unmeasured cost would be an invention`,
205
    };
206
  }
207
  return numericDelta(from.costPerAcceptedOutcomeUsd, to.costPerAcceptedOutcomeUsd, "lower", {
208
    better: "cost per accepted outcome fell",
209
    worse: "cost per accepted outcome rose",
210
  });
211
};
212
213
/** Success rate, where up is better. `null` on either side is unknown. */
214
const successRateDelta = (from: BenchResultRow, to: BenchResultRow): Delta => {
215
  if (from.successRate === null || to.successRate === null) {
216
    return {
217
      from: from.successRate,
218
      to: to.successRate,
219
      absolute: null,
220
      relative: null,
221
      direction: "unknown",
222
      reason:
223
        "no success-rate delta: a run with no graded trials has no success rate rather than a zero one",
224
    };
225
  }
226
  return numericDelta(from.successRate, to.successRate, "higher", {
227
    better: "success rate rose",
228
    worse: "success rate fell",
229
  });
230
};
231
232
const numericDelta = (
233
  from: number,
234
  to: number,
235
  betterWhen: "lower" | "higher",
236
  reasons: { readonly better: string; readonly worse: string },
237
): Delta => {
238
  const absolute = to - from;
239
  const relative = from === 0 ? null : absolute / from;
240
  const improved = betterWhen === "lower" ? absolute < 0 : absolute > 0;
241
  return {
242
    from,
243
    to,
244
    absolute,
245
    relative,
246
    direction: absolute === 0 ? "unchanged" : improved ? "better" : "worse",
247
    reason: absolute === 0 ? "unchanged" : improved ? reasons.better : reasons.worse,
248
  };
249
};
250
251
/**
252
 * What else varies across the rows being compared, besides the axis being
253
 * compared on.
254
 *
255
 * Named rather than counted, so the reader can decide whether the confound
256
 * matters. Comparing two lanes on different CLI versions is still worth doing;
257
 * reading it as a clean lane comparison is not.
258
 */
259
const confoundersOf = (
260
  rows: ReadonlyArray<BenchResultRow>,
261
  axis: "lane" | "recordedAt",
262
): ReadonlyArray<string> => {
263
  const notes: Array<string> = [];
264
  const versions = distinct(rows.flatMap((row) => row.agentVersions));
265
  const models = distinct(rows.flatMap((row) => row.models));
266
  const bases = distinct(rows.map((row) => row.rateBasis));
267
268
  if (versions.length > 1) {
269
    notes.push(`CLI version also varies (${versions.join(", ")})`);
270
  }
271
  if (axis === "recordedAt" && models.length > 1) {
272
    notes.push(`model also varies (${models.join(", ")})`);
273
  }
274
  if (bases.includes("operator_placeholder")) {
275
    notes.push(
276
      "at least one row is priced from operator placeholder rates, so its cost is provisional",
277
    );
278
  }
279
  return notes;
280
};
281
282
const distinct = (values: ReadonlyArray<string | null>): ReadonlyArray<string> =>
283
  [...new Set(values.filter((value): value is string => value !== null))].toSorted();
284
285
const groupBy = <T>(
286
  values: ReadonlyArray<T>,
287
  key: (value: T) => string,
288
): ReadonlyArray<ReadonlyArray<T>> => {
289
  const buckets = new Map<string, Array<T>>();
290
  for (const value of values) {
291
    const bucket = buckets.get(key(value));
292
    if (bucket === undefined) buckets.set(key(value), [value]);
293
    else bucket.push(value);
294
  }
295
  return [...buckets.values()];
296
};
packages/coder-effectiveness/src/index.ts modified +25

@@ -1,3 +1,13 @@

1
export {
2
  type Comparison,
3
  compareRuns,
4
  type Delta,
5
  type DeltaDirection,
6
  type LaneComparison,
7
  type LaneRow,
8
  type LaneTrend,
9
  type TrendStep,
10
} from "./compare.ts";
1 11
export {
2 12
  type CostAggregate,
3 13
  type CostPerAcceptedOutcome,

@@ -20,7 +30,22 @@ export {

20 30
  pricingFromModelsPayload,
21 31
  type RateBasis,
22 32
} from "./pricing.ts";
33
export { renderComparison } from "./render-compare.ts";
23 34
export { renderReport } from "./render.ts";
35
export {
36
  type AppendRefusal,
37
  type AppendResult,
38
  appendResultRow,
39
  BENCH_RESULT_SCHEMA,
40
  type BenchResultRow,
41
  buildResultRow,
42
  type ChainBreak,
43
  type ChainVerdict,
44
  readResultRows,
45
  receiptOf,
46
  suiteKeyOf,
47
  verifyResultChain,
48
} from "./results-store.ts";
24 49
export {
25 50
  type EffectivenessThresholds,
26 51
  evaluateThresholds,
packages/coder-effectiveness/src/render-compare.ts added +107

@@ -0,0 +1,107 @@

1
/**
2
 * Render a comparison as text.
3
 *
4
 * Same rendering rule as `render.ts`: an unmeasured figure prints as `unknown`
5
 * with its reason beside it, never as `$0.0000` and never as a blank cell. A
6
 * lane comparison is exactly where a blank would be read as "the same", so the
7
 * unpriced lane says `unpriced` and says why on the next line.
8
 */
9
10
import type { Comparison, Delta, LaneComparison, LaneTrend } from "./compare.ts";
11
12
const usd = (value: number | null): string => (value === null ? "unknown" : `$${value.toFixed(4)}`);
13
14
const pct = (value: number | null): string =>
15
  value === null ? "unknown" : `${(value * 100).toFixed(1)}%`;
16
17
const signed = (value: number, digits: number): string =>
18
  `${value >= 0 ? "+" : ""}${value.toFixed(digits)}`;
19
20
const deltaLine = (label: string, delta: Delta, digits: number, money: boolean): string => {
21
  if (delta.absolute === null) {
22
    return `    ${label.padEnd(22)} ${delta.direction}`;
23
  }
24
  const shown = money
25
    ? `${delta.absolute >= 0 ? "+" : "-"}$${Math.abs(delta.absolute).toFixed(digits)}`
26
    : signed(delta.absolute, digits);
27
  const relative = delta.relative === null ? "" : ` (${signed(delta.relative * 100, 1)}%)`;
28
  return `    ${label.padEnd(22)} ${shown}${relative}  ${delta.direction}`;
29
};
30
31
const renderLaneComparison = (comparison: LaneComparison): ReadonlyArray<string> => {
32
  const lines: Array<string> = [];
33
  lines.push(`Lanes — ${comparison.suite}, ${String(comparison.tasks.length)} tasks`);
34
  lines.push(`  suite key       ${comparison.suiteKey}`);
35
  lines.push(`  baseline lane   ${comparison.baselineLane}`);
36
  for (const note of comparison.confounders) {
37
    lines.push(`  confounded      ${note}`);
38
  }
39
  lines.push("");
40
  for (const lane of comparison.lanes) {
41
    const marker = lane.lane === comparison.baselineLane ? " (baseline)" : "";
42
    lines.push(`  ${lane.lane}${marker}`);
43
    lines.push(
44
      `    ${"cost per accepted".padEnd(22)} ${usd(lane.row.costPerAcceptedOutcomeUsd)}${lane.row.costPerAcceptedOutcomeUsd === null ? ` (${lane.row.costDisposition})` : ""}`,
45
    );
46
    lines.push(`    ${"success rate".padEnd(22)} ${pct(lane.row.successRate)}`);
47
    lines.push(
48
      `    ${"outcomes".padEnd(22)} ${String(lane.row.accepted)} accepted, ${String(lane.row.rejected)} rejected, ${String(lane.row.ungraded)} ungraded`,
49
    );
50
    if (lane.costDelta !== null) {
51
      lines.push(deltaLine("Δ cost per accepted", lane.costDelta, 4, true));
52
      if (lane.costDelta.absolute === null) lines.push(`      ${lane.costDelta.reason}`);
53
    }
54
    if (lane.successRateDelta !== null) {
55
      lines.push(deltaLine("Δ success rate", lane.successRateDelta, 3, false));
56
    }
57
    lines.push("");
58
  }
59
  return lines;
60
};
61
62
const renderTrend = (trend: LaneTrend): ReadonlyArray<string> => {
63
  const lines: Array<string> = [];
64
  lines.push(`Trend — ${trend.suite} on the ${trend.lane} lane, ${String(trend.rows.length)} runs`);
65
  for (const step of trend.steps) {
66
    lines.push(`  ${step.from.recordedAt} → ${step.to.recordedAt}`);
67
    lines.push(deltaLine("Δ cost per accepted", step.costDelta, 4, true));
68
    if (step.costDelta.absolute === null) lines.push(`      ${step.costDelta.reason}`);
69
    lines.push(deltaLine("Δ success rate", step.successRateDelta, 3, false));
70
    for (const note of step.confounders) {
71
      lines.push(`    confounded             ${note}`);
72
    }
73
  }
74
  lines.push("");
75
  return lines;
76
};
77
78
export const renderComparison = (comparison: Comparison): string => {
79
  const lines: Array<string> = [];
80
81
  if (comparison.laneComparisons.length === 0 && comparison.trends.length === 0) {
82
    lines.push("Nothing to compare.");
83
    lines.push("");
84
    lines.push(
85
      comparison.isolatedGroups === 0
86
        ? "  The store holds no rows. Append one with `effectiveness:report --append <store>`."
87
        : `  The store holds ${String(comparison.isolatedGroups)} run shape(s), none with a second comparable row.`,
88
    );
89
    lines.push("  Two rows are comparable when they share a suite key: the same suite, the same");
90
    lines.push("  task list, and the same rate catalog. Lane, model, and CLI version may differ —");
91
    lines.push("  those are the axes a comparison varies.");
92
    return `${lines.join("\n")}\n`;
93
  }
94
95
  for (const laneComparison of comparison.laneComparisons) {
96
    lines.push(...renderLaneComparison(laneComparison));
97
  }
98
  for (const trend of comparison.trends) {
99
    lines.push(...renderTrend(trend));
100
  }
101
  if (comparison.isolatedGroups > 0) {
102
    lines.push(
103
      `${String(comparison.isolatedGroups)} run shape(s) in this store had no comparable second row and were left out.`,
104
    );
105
  }
106
  return `${lines.join("\n")}\n`;
107
};
packages/coder-effectiveness/src/results-store.test.ts added +233

@@ -0,0 +1,233 @@

1
/**
2
 * The append-only store and its receipt chain.
3
 *
4
 * Every case builds its rows from the same checked-in fixture Harbor jobs the
5
 * rest of the suite reads, into a temporary store. No model is called, no
6
 * Docker image runs, and the clock is injected, so the receipts a run produces
7
 * here are the same ones it produces in CI.
8
 */
9
10
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
11
import { tmpdir } from "node:os";
12
import { join } from "node:path";
13
import { fileURLToPath } from "node:url";
14
import { afterEach, beforeEach, describe, expect, test } from "vite-plus/test";
15
16
import { summarizeRun, type EffectivenessReport } from "./effectiveness.ts";
17
import { readHarborJob } from "./harbor-job.ts";
18
import { CODER_RATE_CATALOG_VERSION } from "./pricing.ts";
19
import {
20
  appendResultRow,
21
  BENCH_RESULT_SCHEMA,
22
  buildResultRow,
23
  readResultRows,
24
  suiteKeyOf,
25
  verifyResultChain,
26
  type BenchResultRow,
27
} from "./results-store.ts";
28
import { evaluateThresholds, parseThresholds } from "./thresholds.ts";
29
30
const fixture = (name: string): string =>
31
  fileURLToPath(new URL(`../fixtures/${name}`, import.meta.url));
32
33
const report = (name: string, lane = "proxy"): EffectivenessReport =>
34
  summarizeRun(
35
    readHarborJob(fixture(name), {
36
      suite: "tb2-cross-section",
37
      lane,
38
      rateCatalogVersion: CODER_RATE_CATALOG_VERSION,
39
    }),
40
  );
41
42
const floors = parseThresholds(
43
  JSON.parse(
44
    readFileSync(
45
      fileURLToPath(new URL("../fixtures/floors-fixture-scale.json", import.meta.url)),
46
      "utf8",
47
    ),
48
  ),
49
);
50
51
let directory: string;
52
let store: string;
53
54
beforeEach(() => {
55
  directory = mkdtempSync(join(tmpdir(), "coder-effectiveness-store-"));
56
  store = join(directory, "tb2-cross-section.jsonl");
57
});
58
59
afterEach(() => {
60
  rmSync(directory, { recursive: true, force: true });
61
});
62
63
const append = (name: string, lane: string, recordedAt: string) =>
64
  appendResultRow(store, report(name, lane), null, { recordedAt });
65
66
const rows = (): ReadonlyArray<BenchResultRow> => readResultRows(store);
67
68
describe("appending a run", () => {
69
  test("writes one JSON line per run and chains each to the one before it", () => {
70
    const first = append("priced-lane", "proxy", "2026-08-25T10:00:00.000Z");
71
    const second = append("regressed-lane", "proxy", "2026-08-25T11:00:00.000Z");
72
73
    expect(first.appended).toBe(true);
74
    expect(second.appended).toBe(true);
75
76
    const written = rows();
77
    expect(written).toHaveLength(2);
78
    expect(written[0]!.schema).toBe(BENCH_RESULT_SCHEMA);
79
    expect(written[0]!.previousReceipt).toBeNull();
80
    expect(written[1]!.previousReceipt).toBe(written[0]!.receipt);
81
    expect(verifyResultChain(written)).toMatchObject({ ok: true, rows: 2 });
82
  });
83
84
  test("creates the store directory rather than requiring it to exist", () => {
85
    const nested = join(directory, "does", "not", "exist", "suite.jsonl");
86
    const result = appendResultRow(nested, report("priced-lane"), null, {
87
      recordedAt: "2026-08-25T10:00:00.000Z",
88
    });
89
90
    expect(result.appended).toBe(true);
91
    expect(readResultRows(nested)).toHaveLength(1);
92
  });
93
94
  test("reads a store that does not exist yet as no rows, not as a corruption", () => {
95
    expect(readResultRows(join(directory, "absent.jsonl"))).toEqual([]);
96
  });
97
98
  test("records the gate verdict when the run was scored against floors", () => {
99
    const result = appendResultRow(
100
      store,
101
      report("priced-lane"),
102
      evaluateThresholds(report("priced-lane"), floors),
103
      {
104
        recordedAt: "2026-08-25T10:00:00.000Z",
105
      },
106
    );
107
108
    expect(result.appended).toBe(true);
109
    expect(rows()[0]!.gateStatus).toBe(evaluateThresholds(report("priced-lane"), floors).status);
110
    expect(rows()[0]!.thresholdsId).toBe(floors.id);
111
  });
112
113
  test("leaves the gate null when no thresholds file was given", () => {
114
    append("priced-lane", "proxy", "2026-08-25T10:00:00.000Z");
115
116
    expect(rows()[0]!.gateStatus).toBeNull();
117
    expect(rows()[0]!.thresholdsId).toBeNull();
118
  });
119
});
120
121
describe("what the store refuses", () => {
122
  test("refuses a Harbor job it already holds, because re-scoring is not a second run", () => {
123
    append("priced-lane", "proxy", "2026-08-25T10:00:00.000Z");
124
    const again = append("priced-lane", "proxy", "2026-08-25T12:00:00.000Z");
125
126
    expect(again).toMatchObject({ appended: false, refusal: "duplicate_job" });
127
    expect(rows()).toHaveLength(1);
128
  });
129
130
  test("refuses to extend a store whose chain is already broken", () => {
131
    append("priced-lane", "proxy", "2026-08-25T10:00:00.000Z");
132
    const tampered = { ...rows()[0]!, accepted: 4 };
133
    writeFileSync(store, `${JSON.stringify(tampered)}\n`, "utf8");
134
135
    const result = append("regressed-lane", "proxy", "2026-08-25T11:00:00.000Z");
136
137
    expect(result).toMatchObject({ appended: false, refusal: "chain_broken" });
138
    expect(rows()).toHaveLength(1);
139
  });
140
141
  test("throws on a malformed line rather than silently reading a shorter history", () => {
142
    append("priced-lane", "proxy", "2026-08-25T10:00:00.000Z");
143
    writeFileSync(store, `${readFileSync(store, "utf8")}{ not json\n`, "utf8");
144
145
    expect(() => readResultRows(store)).toThrow(/line 2 is not JSON/u);
146
  });
147
148
  test("throws on a row written under another schema", () => {
149
    writeFileSync(store, `${JSON.stringify({ schema: "something.else.v1" })}\n`, "utf8");
150
151
    expect(() => readResultRows(store)).toThrow(/expected openagents\.bench_result\.v1/u);
152
  });
153
});
154
155
describe("the receipt chain", () => {
156
  test("names the row when a figure was edited in place", () => {
157
    append("priced-lane", "proxy", "2026-08-25T10:00:00.000Z");
158
    append("regressed-lane", "proxy", "2026-08-25T11:00:00.000Z");
159
    const written = [...rows()];
160
    written[0] = { ...written[0]!, costPerAcceptedOutcomeUsd: 0.0001 };
161
162
    const verdict = verifyResultChain(written);
163
164
    expect(verdict.ok).toBe(false);
165
    if (verdict.ok) throw new Error("unreachable");
166
    expect(verdict.break.kind).toBe("receipt_mismatch");
167
    expect(verdict.break.index).toBe(0);
168
  });
169
170
  test("names the row when a row was removed from the middle", () => {
171
    append("priced-lane", "proxy", "2026-08-25T10:00:00.000Z");
172
    append("regressed-lane", "proxy", "2026-08-25T11:00:00.000Z");
173
    append("unpriced-lane", "local", "2026-08-25T12:00:00.000Z");
174
175
    const verdict = verifyResultChain([rows()[0]!, rows()[2]!]);
176
177
    expect(verdict.ok).toBe(false);
178
    if (verdict.ok) throw new Error("unreachable");
179
    expect(verdict.break.kind).toBe("chain_broken");
180
    expect(verdict.break.index).toBe(1);
181
  });
182
183
  test("verifies an empty store", () => {
184
    expect(verifyResultChain([])).toEqual({ ok: true, rows: 0, head: null });
185
  });
186
187
  test("is stable across two builds of the same row", () => {
188
    const first = buildResultRow(report("priced-lane"), null, null, {
189
      recordedAt: "2026-08-25T10:00:00.000Z",
190
    });
191
    const second = buildResultRow(report("priced-lane"), null, null, {
192
      recordedAt: "2026-08-25T10:00:00.000Z",
193
    });
194
195
    expect(second.receipt).toBe(first.receipt);
196
  });
197
});
198
199
describe("what a row records", () => {
200
  test("keeps an unpriced run unknown rather than writing it as zero", () => {
201
    append("unpriced-lane", "local", "2026-08-25T10:00:00.000Z");
202
203
    const row = rows()[0]!;
204
    expect(row.costPerAcceptedOutcomeUsd).toBeNull();
205
    expect(row.costDisposition).toBe("cost_unknown");
206
    expect(row.costCoverage).toBe("unknown");
207
  });
208
209
  test("keeps a partly priced run unknown and records the coverage that explains it", () => {
210
    append("mixed-lane", "proxy", "2026-08-25T10:00:00.000Z");
211
212
    const row = rows()[0]!;
213
    expect(row.costPerAcceptedOutcomeUsd).toBeNull();
214
    expect(row.costDisposition).toBe("cost_partial");
215
    expect(row.costCoverage).toBe("partial");
216
  });
217
218
  test("gives two runs of the same tasks the same suite key across different lanes", () => {
219
    expect(suiteKeyOf(report("priced-lane", "proxy"))).toBe(
220
      suiteKeyOf(report("regressed-lane", "local")),
221
    );
222
  });
223
224
  test("gives two runs of different task lists different suite keys", () => {
225
    expect(suiteKeyOf(report("priced-lane"))).not.toBe(suiteKeyOf(report("unpriced-lane")));
226
  });
227
228
  test("gives two lanes of the same tasks different run digests", () => {
229
    expect(report("priced-lane", "proxy").runDigest).not.toBe(
230
      report("priced-lane", "local").runDigest,
231
    );
232
  });
233
});
packages/coder-effectiveness/src/results-store.ts added +320

@@ -0,0 +1,320 @@

1
/**
2
 * The append-only `bench-results` store, and the receipts that make it one.
3
 *
4
 * Issue #34's contract says results append into `bench-results` with receipts.
5
 * A file you can append to is not yet append-only — nothing stops a later hand
6
 * from editing a row that made a release look bad, and a trend line whose
7
 * history can be quietly rewritten is worth less than no trend line. So every
8
 * row carries a receipt: a digest over that row's own facts AND the receipt of
9
 * the row before it. Change one figure in row 3 and rows 3..n stop verifying.
10
 * Delete row 3 and row 4's `previousReceipt` names a receipt that is no longer
11
 * in the file. Both are named breaks, not silent ones.
12
 *
13
 * This is deliberately a hash chain and not a signature. A signature would
14
 * answer "who wrote this", which needs a key this package has no business
15
 * holding; the chain answers "has this history been rewritten since it was
16
 * written", which is the question a benchmark trend actually asks. When a
17
 * signing seam exists, it signs the head receipt and the chain underneath it
18
 * still holds.
19
 *
20
 * THE UNKNOWNS SURVIVE THE ROUND TRIP. A row's cost is `null` when the run
21
 * could not be priced, exactly as the report says it. Writing 0 into the store
22
 * would launder an unpriced lane into a free one at the moment the figure stops
23
 * being read next to its reason, which is the whole failure this suite exists
24
 * to prevent. The disposition and the coverage travel with every row so a later
25
 * reader can tell "we did not measure this" from "this cost nothing".
26
 */
27
28
import { createHash } from "node:crypto";
29
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
30
import { dirname } from "node:path";
31
32
import type {
33
  CostCoverage,
34
  CostPerAcceptedOutcomeDisposition,
35
  EffectivenessReport,
36
} from "./effectiveness.ts";
37
import type { RateBasis } from "./pricing.ts";
38
import type { CriterionVerdict, ThresholdGate } from "./thresholds.ts";
39
40
export const BENCH_RESULT_SCHEMA = "openagents.bench_result.v1";
41
42
/**
43
 * One graded run, flattened to the columns a trend or a lane comparison reads.
44
 *
45
 * The per-trial detail deliberately stays out. A results store is a row per
46
 * run, and a row that carries its whole job directory invites reading the store
47
 * as the archive of record, which it is not — the Harbor job directory is. What
48
 * is here is what two runs can be compared on.
49
 */
50
export interface BenchResultRow {
51
  readonly schema: typeof BENCH_RESULT_SCHEMA;
52
  readonly recordedAt: string;
53
54
  readonly suite: string;
55
  readonly lane: string;
56
  /** The report's pin over suite, lane, tasks, CLI version, model, and rates. */
57
  readonly runDigest: string;
58
  /**
59
   * The narrower pin two rows must share to be comparable at all: the suite,
60
   * the sorted task list, and the rate catalog. Lane, model, and CLI version
61
   * are excluded on purpose — those are the axes a comparison varies.
62
   */
63
  readonly suiteKey: string;
64
  readonly jobId: string | null;
65
66
  readonly models: ReadonlyArray<string>;
67
  readonly agentVersions: ReadonlyArray<string>;
68
  readonly rateCatalogVersion: string;
69
  readonly tasks: ReadonlyArray<string>;
70
71
  readonly trialsTotal: number;
72
  readonly accepted: number;
73
  readonly rejected: number;
74
  readonly ungraded: number;
75
  readonly graded: number;
76
  readonly successRate: number | null;
77
  readonly ungradedRatio: number;
78
79
  /** `null` when the run could not be priced. Never 0 for an unpriced lane. */
80
  readonly costPerAcceptedOutcomeUsd: number | null;
81
  readonly costDisposition: CostPerAcceptedOutcomeDisposition;
82
  readonly totalCostUsd: number | null;
83
  readonly costCoverage: CostCoverage;
84
  readonly rateBasis: RateBasis | null;
85
86
  readonly promptTokens: number | null;
87
  readonly completionTokens: number | null;
88
  readonly cachedInputTokens: number;
89
  readonly toolCalls: number | null;
90
  readonly wallClockSeconds: number | null;
91
92
  /** `null` when the run was scored without a thresholds file. */
93
  readonly gateStatus: CriterionVerdict | null;
94
  readonly thresholdsId: string | null;
95
96
  /** The receipt of the row before this one, or `null` for the first row. */
97
  readonly previousReceipt: string | null;
98
  /** `receipt:<sha256>` over every field above, this row's chain link. */
99
  readonly receipt: string;
100
}
101
102
/** The row minus its own receipt: what the receipt is computed over. */
103
type UnreceiptedRow = Omit<BenchResultRow, "receipt">;
104
105
export interface AppendOptions {
106
  /** The clock is injected so a test never reads one. */
107
  readonly recordedAt: string;
108
}
109
110
/**
111
 * The suite key: the run shape two rows must share before comparing them is
112
 * meaningful at all.
113
 *
114
 * Lane, model, and CLI version are left out because those are exactly what a
115
 * comparison varies. The rate catalog version is in, because a cost figure
116
 * computed from one catalog and a cost figure computed from another are not the
117
 * same measurement even when the tasks match.
118
 */
119
export const suiteKeyOf = (report: EffectivenessReport): string => {
120
  const source = JSON.stringify({
121
    suite: report.suite,
122
    tasks: report.perTrial.map((trial) => trial.task).toSorted(),
123
    rateCatalogVersion: report.rateCatalogVersion,
124
  });
125
  return `suite:${createHash("sha256").update(source).digest("hex")}`;
126
};
127
128
/** Build the row a report and its gate would append. Pure. */
129
export const buildResultRow = (
130
  report: EffectivenessReport,
131
  gate: ThresholdGate | null,
132
  previousReceipt: string | null,
133
  options: AppendOptions,
134
): BenchResultRow => {
135
  const unreceipted: UnreceiptedRow = {
136
    schema: BENCH_RESULT_SCHEMA,
137
    recordedAt: options.recordedAt,
138
139
    suite: report.suite,
140
    lane: report.lane,
141
    runDigest: report.runDigest,
142
    suiteKey: suiteKeyOf(report),
143
    jobId: report.jobId,
144
145
    models: report.models,
146
    agentVersions: report.agentVersions,
147
    rateCatalogVersion: report.rateCatalogVersion,
148
    tasks: report.perTrial.map((trial) => trial.task).toSorted(),
149
150
    trialsTotal: report.trialsTotal,
151
    accepted: report.accepted,
152
    rejected: report.rejected,
153
    ungraded: report.ungraded,
154
    graded: report.graded,
155
    successRate: report.successRate,
156
    ungradedRatio: report.ungradedRatio,
157
158
    costPerAcceptedOutcomeUsd: report.costPerAcceptedOutcome.usd,
159
    costDisposition: report.costPerAcceptedOutcome.disposition,
160
    totalCostUsd: report.cost.totalUsd,
161
    costCoverage: report.cost.coverage,
162
    rateBasis: report.cost.rateBasis,
163
164
    promptTokens: report.promptTokens,
165
    completionTokens: report.completionTokens,
166
    cachedInputTokens: report.cachedInputTokens,
167
    toolCalls: report.toolCalls,
168
    wallClockSeconds: report.wallClockSeconds,
169
170
    gateStatus: gate === null ? null : gate.status,
171
    thresholdsId: gate === null ? null : gate.thresholdsId,
172
173
    previousReceipt,
174
  };
175
  return { ...unreceipted, receipt: receiptOf(unreceipted) };
176
};
177
178
/**
179
 * The receipt.
180
 *
181
 * Serialised through an explicit key order rather than `JSON.stringify` on the
182
 * object, so a future field reordering does not silently invalidate every
183
 * receipt already written. Adding a field does invalidate them, which is
184
 * correct: a new column changes what the row asserts, and the schema string
185
 * carries the version that says so.
186
 */
187
export const receiptOf = (row: UnreceiptedRow): string => {
188
  const ordered = Object.keys(row)
189
    .toSorted()
190
    .map((key) => [key, (row as unknown as Record<string, unknown>)[key]] as const);
191
  const source = JSON.stringify(ordered);
192
  return `receipt:${createHash("sha256").update(source).digest("hex")}`;
193
};
194
195
export type ChainBreak =
196
  | { readonly kind: "receipt_mismatch"; readonly index: number; readonly detail: string }
197
  | { readonly kind: "chain_broken"; readonly index: number; readonly detail: string };
198
199
export type ChainVerdict =
200
  | { readonly ok: true; readonly rows: number; readonly head: string | null }
201
  | { readonly ok: false; readonly break: ChainBreak };
202
203
/**
204
 * Verify that a store has not been rewritten since it was written.
205
 *
206
 * Two distinct findings, because they mean different things. A
207
 * `receipt_mismatch` says a row's own contents no longer match its receipt —
208
 * somebody edited a figure in place. A `chain_broken` says a row's
209
 * `previousReceipt` does not name the row before it — somebody inserted,
210
 * removed, or reordered rows.
211
 */
212
export const verifyResultChain = (rows: ReadonlyArray<BenchResultRow>): ChainVerdict => {
213
  let previous: string | null = null;
214
  for (const [index, row] of rows.entries()) {
215
    const { receipt, ...unreceipted } = row;
216
    const expected = receiptOf(unreceipted);
217
    if (receipt !== expected) {
218
      return {
219
        ok: false,
220
        break: {
221
          kind: "receipt_mismatch",
222
          index,
223
          detail: `row ${String(index)} (${row.suite} on ${row.lane}, recorded ${row.recordedAt}) carries ${receipt} but its contents digest to ${expected}, so it was edited after it was written`,
224
        },
225
      };
226
    }
227
    if (row.previousReceipt !== previous) {
228
      return {
229
        ok: false,
230
        break: {
231
          kind: "chain_broken",
232
          index,
233
          detail: `row ${String(index)} follows ${String(row.previousReceipt)} but the row before it is ${String(previous)}, so rows were inserted, removed, or reordered`,
234
        },
235
      };
236
    }
237
    previous = receipt;
238
  }
239
  return { ok: true, rows: rows.length, head: previous };
240
};
241
242
/**
243
 * Read a store.
244
 *
245
 * A malformed line throws rather than being skipped. Skipping would turn a
246
 * corrupted store into a shorter, apparently valid one, and a trend that
247
 * silently drops the rows it could not parse is the worst of both worlds.
248
 * A store that does not exist yet reads as no rows, which is not a corruption.
249
 */
250
export const readResultRows = (storePath: string): ReadonlyArray<BenchResultRow> => {
251
  if (!existsSync(storePath)) return [];
252
  const lines = readFileSync(storePath, "utf8")
253
    .split("\n")
254
    .map((line) => line.trim())
255
    .filter((line) => line !== "");
256
  return lines.map((line, index) => {
257
    let parsed: unknown;
258
    try {
259
      parsed = JSON.parse(line);
260
    } catch {
261
      throw new Error(`${storePath} line ${String(index + 1)} is not JSON`);
262
    }
263
    const row = parsed as BenchResultRow;
264
    if (row.schema !== BENCH_RESULT_SCHEMA) {
265
      throw new Error(
266
        `${storePath} line ${String(index + 1)} has schema ${String(row.schema)}, expected ${BENCH_RESULT_SCHEMA}`,
267
      );
268
    }
269
    return row;
270
  });
271
};
272
273
export type AppendRefusal = "duplicate_job" | "chain_broken";
274
275
export type AppendResult =
276
  | { readonly appended: true; readonly row: BenchResultRow }
277
  | { readonly appended: false; readonly refusal: AppendRefusal; readonly reason: string };
278
279
/**
280
 * Append one graded run to a store.
281
 *
282
 * Two refusals, both returned rather than thrown, because both are ordinary
283
 * operator situations rather than programming errors:
284
 *
285
 * - `duplicate_job` — this Harbor job is already in the store. Re-scoring a job
286
 *   with a different thresholds file is a useful thing to do and a second row
287
 *   is not what it produces; two rows for one execution would double-count it
288
 *   in every trend that follows.
289
 * - `chain_broken` — the existing store does not verify, so appending to it
290
 *   would extend a history that has already been rewritten and bury the break
291
 *   one row deeper.
292
 */
293
export const appendResultRow = (
294
  storePath: string,
295
  report: EffectivenessReport,
296
  gate: ThresholdGate | null,
297
  options: AppendOptions,
298
): AppendResult => {
299
  const rows = readResultRows(storePath);
300
  const verdict = verifyResultChain(rows);
301
  if (!verdict.ok) {
302
    return {
303
      appended: false,
304
      refusal: "chain_broken",
305
      reason: `${storePath} does not verify, so nothing was appended: ${verdict.break.detail}`,
306
    };
307
  }
308
  if (report.jobId !== null && rows.some((row) => row.jobId === report.jobId)) {
309
    return {
310
      appended: false,
311
      refusal: "duplicate_job",
312
      reason: `harbor job ${report.jobId} is already recorded in ${storePath}; re-scoring a run does not make it a second run`,
313
    };
314
  }
315
316
  const row = buildResultRow(report, gate, verdict.head, options);
317
  mkdirSync(dirname(storePath), { recursive: true });
318
  appendFileSync(storePath, `${JSON.stringify(row)}\n`, "utf8");
319
  return { appended: true, row };
320
};
packages/coder-effectiveness/src/store-cli.test.ts added +180

@@ -0,0 +1,180 @@

1
/**
2
 * The store and comparison commands, run the way a scheduled job runs them.
3
 *
4
 * These cases spawn the entry points rather than importing them, so they prove
5
 * the commands are invocable and that their exit codes mean what
6
 * `bench-results/README.md` says they mean. A run that was scored but never
7
 * recorded must not exit 0, and a store that does not verify must not be
8
 * compared.
9
 */
10
11
import { spawnSync } from "node:child_process";
12
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
13
import { tmpdir } from "node:os";
14
import { join } from "node:path";
15
import { fileURLToPath } from "node:url";
16
import { afterEach, beforeEach, describe, expect, test } from "vite-plus/test";
17
18
import { readResultRows } from "./results-store.ts";
19
20
const reportCli = fileURLToPath(new URL("./cli.ts", import.meta.url));
21
const compareCli = fileURLToPath(new URL("./compare-cli.ts", import.meta.url));
22
const fixture = (name: string): string =>
23
  fileURLToPath(new URL(`../fixtures/${name}`, import.meta.url));
24
25
const spawn = (
26
  cli: string,
27
  args: ReadonlyArray<string>,
28
): { status: number; stdout: string; stderr: string } => {
29
  const result = spawnSync(process.execPath, ["--import", "tsx", cli, ...args], {
30
    encoding: "utf8",
31
  });
32
  return { status: result.status ?? -1, stdout: result.stdout, stderr: result.stderr };
33
};
34
35
let directory: string;
36
let store: string;
37
38
beforeEach(() => {
39
  directory = mkdtempSync(join(tmpdir(), "coder-effectiveness-cli-"));
40
  store = join(directory, "tb2-cross-section.jsonl");
41
});
42
43
afterEach(() => {
44
  rmSync(directory, { recursive: true, force: true });
45
});
46
47
const record = (job: string, lane: string) =>
48
  spawn(reportCli, [
49
    fixture(job),
50
    "--suite",
51
    "tb2-cross-section",
52
    "--lane",
53
    lane,
54
    "--append",
55
    store,
56
  ]);
57
58
describe("report --append", () => {
59
  test("records the run and names the receipt it wrote", () => {
60
    const result = record("priced-lane", "proxy");
61
62
    expect(result.status).toBe(0);
63
    expect(result.stdout).toContain("Appended to");
64
    expect(result.stdout).toContain("receipt:");
65
    expect(readResultRows(store)).toHaveLength(1);
66
  });
67
68
  test("exits 3 when the run was scored but the store refused it", () => {
69
    expect(record("priced-lane", "proxy").status).toBe(0);
70
    const again = record("priced-lane", "proxy");
71
72
    expect(again.status).toBe(3);
73
    expect(again.stdout).toContain("Not appended (duplicate_job)");
74
    expect(readResultRows(store)).toHaveLength(1);
75
  });
76
77
  test("lets a breached floor outrank a refused append", () => {
78
    const floors = fileURLToPath(new URL("../fixtures/floors-fixture-scale.json", import.meta.url));
79
    expect(record("regressed-lane", "proxy").status).toBe(0);
80
81
    const again = spawn(reportCli, [
82
      fixture("regressed-lane"),
83
      "--suite",
84
      "tb2-cross-section",
85
      "--thresholds",
86
      floors,
87
      "--append",
88
      store,
89
    ]);
90
91
    // The gate failed and the append was refused. 1 is the finding that matters.
92
    expect(again.status).toBe(1);
93
  });
94
95
  test("carries the append result into --json", () => {
96
    const result = spawn(reportCli, [fixture("priced-lane"), "--append", store, "--json"]);
97
    const parsed = JSON.parse(result.stdout) as {
98
      appended: { appended: boolean; row: { receipt: string } };
99
    };
100
101
    expect(result.status).toBe(0);
102
    expect(parsed.appended.appended).toBe(true);
103
    expect(parsed.appended.row.receipt).toMatch(/^receipt:[0-9a-f]{64}$/u);
104
  });
105
});
106
107
describe("compare", () => {
108
  test("prints the trend two consecutive runs on one lane produce", () => {
109
    record("priced-lane", "proxy");
110
    record("regressed-lane", "proxy");
111
112
    const result = spawn(compareCli, [store]);
113
114
    expect(result.status).toBe(0);
115
    expect(result.stdout).toContain("Trend — tb2-cross-section on the proxy lane");
116
    expect(result.stdout).toContain("worse");
117
  });
118
119
  test("prints a lane comparison and its baseline", () => {
120
    record("priced-lane", "proxy");
121
    record("regressed-lane", "local");
122
123
    const result = spawn(compareCli, [store]);
124
125
    expect(result.status).toBe(0);
126
    expect(result.stdout).toContain("Lanes —");
127
    expect(result.stdout).toContain("baseline lane   proxy");
128
  });
129
130
  test("refuses to compare a store that was edited after it was written", () => {
131
    record("priced-lane", "proxy");
132
    record("regressed-lane", "proxy");
133
    const lines = readFileSync(store, "utf8").trim().split("\n");
134
    const tampered = { ...(JSON.parse(lines[0]!) as Record<string, unknown>), accepted: 4 };
135
    writeFileSync(store, [JSON.stringify(tampered), lines[1]!].join("\n") + "\n", "utf8");
136
137
    const result = spawn(compareCli, [store]);
138
139
    expect(result.status).toBe(2);
140
    expect(result.stderr).toContain("does not verify");
141
    expect(result.stderr).toContain("Nothing was compared");
142
  });
143
144
  test("exits 1 on a store it cannot read", () => {
145
    writeFileSync(store, "{ not json\n", "utf8");
146
147
    const result = spawn(compareCli, [store]);
148
149
    expect(result.status).toBe(1);
150
    expect(result.stderr).toContain("is not JSON");
151
  });
152
153
  test("says why an empty store has nothing to compare", () => {
154
    const result = spawn(compareCli, [join(directory, "absent.jsonl")]);
155
156
    expect(result.status).toBe(0);
157
    expect(result.stdout).toContain("The store holds no rows");
158
  });
159
160
  test("emits the verified chain alongside the comparison under --json", () => {
161
    record("priced-lane", "proxy");
162
    record("regressed-lane", "proxy");
163
164
    const result = spawn(compareCli, [store, "--json"]);
165
    const parsed = JSON.parse(result.stdout) as {
166
      verified: { ok: boolean; rows: number };
167
      comparison: { trends: ReadonlyArray<unknown> };
168
    };
169
170
    expect(parsed.verified).toMatchObject({ ok: true, rows: 2 });
171
    expect(parsed.comparison.trends).toHaveLength(1);
172
  });
173
174
  test("prints usage on --help", () => {
175
    const result = spawn(compareCli, ["--help"]);
176
177
    expect(result.status).toBe(0);
178
    expect(result.stdout).toContain("Exit codes");
179
  });
180
});

This page updates live while a promote is in flight · changelog