simt-dev 0.6.3 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +162 -841
- package/README.md +21 -135
- package/agents/simt-agent.md +84 -0
- package/bin/simt.mjs +19 -55
- package/docs/decisions/ADR-009-skill-layout.md +1 -1
- package/docs/decisions/ADR-014-workspace-is-configuration.md +1 -1
- package/docs/decisions/ADR-086-the-stable-prefix-comes-first-and-the-lock-records-both-digests.md +1 -1
- package/docs/decisions/ADR-099-the-release-verifies-served-bytes-and-the-formula-mirrors-them.md +1 -1
- package/docs/decisions/ADR-106-the-harness-hook-runs-through-a-repo-shim.md +46 -0
- package/docs/decisions/ADR-107-an-install-can-name-one-harness-and-the-skipped-ones-are-named.md +51 -0
- package/docs/decisions/ADR-108-an-adoption-builds-the-index-and-a-command-refreshes-it-on-read.md +59 -0
- package/docs/decisions/ADR-109-simt-dev-is-its-own-consumer-and-the-guards-that-kept-it-from-being-one-are-retired.md +47 -0
- package/docs/decisions/ADR-110-the-lock-file-follows-the-manifest-name-and-the-old-name-is-read-until-the-next-write.md +43 -0
- package/docs/decisions/ADR-111-the-tool-ships-one-agent-rendered-per-harness-and-installs-it-with-init-and-a-command.md +75 -0
- package/docs/decisions/ADR-112-the-registry-is-what-simt-dev-ships-and-a-repo-owns-its-own-skills.md +81 -0
- package/docs/decisions/ADR-113-a-pre-push-that-carries-only-documentation-skips-the-test-gate-and-says-so.md +87 -0
- package/docs/decisions/ADR-114-the-bench-pairs-at-the-task-level-a-missing-cost-is-not-zero-and-an-unasked-cross-group-compare-is-refused.md +74 -0
- package/docs/decisions/ADR-115-the-index-stamps-its-schema-version-migrates-older-databases-in-place-and-serves-newer-ones-from-a-sidecar.md +69 -0
- package/docs/decisions/ADR-116-rust-support-reaches-the-example-path-the-indent-and-the-bench-runner.md +100 -0
- package/docs/decisions/ADR-117-the-darwin-release-builds-on-supported-macos-15-runners-and-a-re-run-is-idempotent.md +81 -0
- package/docs/decisions/ADR-118-the-bench-corpus-is-keyed-on-history-is-append-only-and-is-portable-as-a-sealed-document.md +91 -0
- package/docs/decisions/ADR-119-the-environment-variable-points-into-a-workspace-the-repo-declared.md +90 -0
- package/docs/decisions/ADR-120-a-pre-push-whose-tree-is-already-proven-green-skips-the-test-gate-on-that-evidence.md +96 -0
- package/docs/decisions/ADR-122-the-shims-move-into-the-state-directory-and-the-legacy-directory-removes-itself.md +94 -0
- package/docs/decisions/ADR-123-a-surplus-positional-is-not-refused-because-arity-is-not-in-the-source.md +57 -0
- package/docs/decisions/ADR-124-the-manifest-has-a-schema-in-the-tool-and-its-unknown-keys-stay-the-repositorys.md +64 -0
- package/docs/decisions/ADR-125-a-shared-context-belongs-to-a-read-and-a-writer-reads-directly.md +65 -0
- package/package.json +13 -3
- package/rules/core.md +9 -2
- package/rules/mdc/slice-commit.mdc +57 -4
- package/rules/mdc/test-gate.mdc +2 -0
- package/rules/stacks/android.md +3 -3
- package/rules/stacks/connectiq.md +2 -2
- package/rules/stacks/dart.md +1 -1
- package/rules/stacks/deno.md +1 -1
- package/rules/stacks/dotnet.md +1 -1
- package/rules/stacks/elixir.md +1 -1
- package/rules/stacks/go.md +1 -1
- package/rules/stacks/jvm.md +3 -2
- package/rules/stacks/node.md +2 -2
- package/rules/stacks/python.md +7 -6
- package/rules/stacks/ruby.md +1 -1
- package/rules/stacks/rust.md +1 -1
- package/rules/stacks/swift.md +1 -1
- package/rules/stacks/typescript.md +2 -2
- package/rules/stacks/uv.md +3 -2
- package/skills/simt-commands/SKILL.md +70 -0
- package/src/advice/advice.mjs +7 -3
- package/src/agenthooks/decide.mjs +141 -9
- package/src/agenthooks/opencode.mjs +72 -0
- package/src/agenthooks/prompt.mjs +10 -3
- package/src/agenthooks/render.mjs +186 -19
- package/src/agenthooks/shim.mjs +55 -0
- package/src/agents/render.mjs +277 -0
- package/src/api.mjs +29 -0
- package/src/argv.mjs +37 -3
- package/src/bench/ablate.mjs +31 -14
- package/src/bench/adapters.json +16 -0
- package/src/bench/corpus.mjs +150 -0
- package/src/bench/grade.mjs +23 -49
- package/src/bench/mine.mjs +247 -36
- package/src/bench/probes.mjs +104 -68
- package/src/bench/process.mjs +128 -0
- package/src/bench/report.mjs +260 -84
- package/src/bench/run.mjs +251 -150
- package/src/bench/variants.mjs +112 -40
- package/src/brief/assemble.mjs +44 -3
- package/src/ci/workflow.mjs +46 -14
- package/src/cli.mjs +45 -0
- package/src/commands/agent.mjs +205 -0
- package/src/commands/agenthook.mjs +113 -20
- package/src/commands/bench.mjs +192 -18
- package/src/commands/brief.mjs +5 -4
- package/src/commands/ci.mjs +2 -1
- package/src/commands/compare.mjs +50 -49
- package/src/commands/completions.mjs +2 -6
- package/src/commands/decide.mjs +10 -9
- package/src/commands/distill.mjs +4 -3
- package/src/commands/doctor.mjs +11 -7
- package/src/commands/evidence.mjs +19 -12
- package/src/commands/explain.mjs +0 -1
- package/src/commands/gate.mjs +98 -28
- package/src/commands/graph.mjs +20 -8
- package/src/commands/harness.mjs +3 -2
- package/src/commands/help.mjs +160 -31
- package/src/commands/hooks.mjs +163 -50
- package/src/commands/ignore.mjs +2 -1
- package/src/commands/impact.mjs +8 -9
- package/src/commands/index.mjs +278 -180
- package/src/commands/init.mjs +171 -38
- package/src/commands/lease.mjs +206 -39
- package/src/commands/lessons.mjs +72 -15
- package/src/commands/local.mjs +3 -3
- package/src/commands/map.mjs +47 -27
- package/src/commands/mcp.mjs +0 -1
- package/src/commands/pack.mjs +9 -6
- package/src/commands/plan.mjs +6 -5
- package/src/commands/plugin.mjs +3 -3
- package/src/commands/prbody.mjs +3 -3
- package/src/commands/repoindex.mjs +5 -3
- package/src/commands/review.mjs +2 -2
- package/src/commands/rules.mjs +124 -33
- package/src/commands/shared.mjs +2 -3
- package/src/commands/skills.mjs +82 -45
- package/src/commands/skillscompile.mjs +12 -12
- package/src/commands/skillsinstall.mjs +14 -2
- package/src/commands/slice.mjs +396 -26
- package/src/commands/spec.mjs +151 -0
- package/src/commands/status.mjs +4 -7
- package/src/commands/sync.mjs +50 -33
- package/src/commands/tests.mjs +11 -11
- package/src/commands/tokens.mjs +2 -2
- package/src/commands/undo.mjs +29 -31
- package/src/commands/upgrade.mjs +40 -4
- package/src/commands/why.mjs +19 -12
- package/src/compare/hooksstate.mjs +23 -1
- package/src/compare/learn.mjs +82 -57
- package/src/compare/parallel.mjs +50 -0
- package/src/compare/row.mjs +26 -3
- package/src/completions.mjs +5 -0
- package/src/context.mjs +64 -0
- package/src/decisions/dir.mjs +84 -0
- package/src/decisions/index-doc.mjs +46 -0
- package/src/decisions/index.mjs +109 -11
- package/src/diff.mjs +1 -1
- package/src/distill/distill.mjs +17 -2
- package/src/distill/engine.mjs +5 -1
- package/src/ecosystem/node-runner.mjs +144 -0
- package/src/explain.mjs +18 -6
- package/src/graph/load.mjs +42 -20
- package/src/hooks/capability.mjs +17 -4
- package/src/hooks/dispatcher.mjs +513 -36
- package/src/hooks/gates.mjs +0 -43
- package/src/hooks/resolve.mjs +11 -10
- package/src/hooks/shim.mjs +17 -9
- package/src/identity.mjs +96 -15
- package/src/index/extract.mjs +192 -11
- package/src/index/persist.mjs +307 -47
- package/src/index/store.mjs +263 -60
- package/src/index/tests.mjs +6 -35
- package/src/journal.mjs +372 -84
- package/src/kernel/card.mjs +37 -12
- package/src/kernel/checks.mjs +44 -9
- package/src/kernel/variants.mjs +27 -16
- package/src/lease.mjs +97 -15
- package/src/ledger/index.mjs +120 -25
- package/src/local/client.mjs +4 -6
- package/src/locate.mjs +33 -9
- package/src/manifest/schema.mjs +213 -0
- package/src/mcp/resources.mjs +12 -13
- package/src/mcp/server.mjs +151 -38
- package/src/mcp/tools.mjs +90 -16
- package/src/measure/session.mjs +3 -2
- package/src/memory/decide.mjs +98 -43
- package/src/memory/lessons.mjs +168 -33
- package/src/name.mjs +9 -0
- package/src/node-version.mjs +37 -0
- package/src/output/index.mjs +111 -3
- package/src/packs/manage.mjs +17 -3
- package/src/packs.mjs +20 -2
- package/src/plan/index.mjs +31 -8
- package/src/plan/template.mjs +27 -5
- package/src/plugins.mjs +6 -2
- package/src/prbody.mjs +7 -5
- package/src/projection/baselines.mjs +1 -1
- package/src/projection/disclosure.mjs +2 -2
- package/src/projection/loaders.mjs +41 -1
- package/src/projection/lock.mjs +31 -14
- package/src/projection/manifest.mjs +89 -53
- package/src/projection/markers.mjs +43 -3
- package/src/projection/mdc.mjs +34 -7
- package/src/projection/project.mjs +162 -18
- package/src/projection/rules.mjs +58 -6
- package/src/projection/surfaces.mjs +9 -1
- package/src/repo-root.mjs +88 -0
- package/src/resolve/body.mjs +4 -0
- package/src/resolve/fingerprint.mjs +24 -15
- package/src/resolve/local.mjs +58 -0
- package/src/review/critic.mjs +6 -3
- package/src/secrets.mjs +165 -22
- package/src/state.mjs +76 -0
- package/src/status.mjs +9 -4
- package/src/toolwords.mjs +8 -0
- package/src/tsconfig.mjs +39 -99
- package/src/workspace.mjs +55 -10
- package/templates/plan/tracker.json +1 -1
- package/docs/decisions/README.md +0 -591
package/src/bench/report.mjs
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
// Cost model, paired statistical tests and report generator (`simt bench report`, R03).
|
|
2
2
|
//
|
|
3
|
-
// Reads stored run records from `.simt
|
|
4
|
-
// from `.simt
|
|
3
|
+
// Reads stored run records from `.simt/local/bench/runs/` and optional token pricing
|
|
4
|
+
// from `.simt/local/bench/prices.json` (per million tokens: `{in, cache_write, cache_read, out}`).
|
|
5
5
|
// Never performs network lookups. All statistical estimators (paired bootstrap 95% CIs,
|
|
6
|
-
//
|
|
6
|
+
// exact sign test on task-mean R, Wilcoxon signed-rank test on task-mean C) are
|
|
7
|
+
// deterministic. Reps of a task are not independent tasks: pairing happens at the
|
|
8
|
+
// task level only (plan 0004 §1, review 0003 Q46).
|
|
7
9
|
|
|
8
10
|
import { readdir, readFile } from 'node:fs/promises';
|
|
9
11
|
import { join, resolve } from 'node:path';
|
|
10
12
|
|
|
11
|
-
import {
|
|
13
|
+
import { LOCAL_DIR } from '../state.mjs';
|
|
12
14
|
|
|
13
15
|
export const DEFAULT_BOOTSTRAP_SAMPLES = 1000;
|
|
14
16
|
export const DEFAULT_BOOTSTRAP_SEED = 0x51772026;
|
|
@@ -67,18 +69,26 @@ function isFlatPriceObject(obj) {
|
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
/**
|
|
70
|
-
* Load `.simt
|
|
72
|
+
* Load `.simt/local/bench/prices.json` if present.
|
|
71
73
|
* Supports either a flat `{in, cache_write, cache_read, out}` object (prices per 1M tokens)
|
|
72
74
|
* or a map keyed by model/harness name with an optional `"default"` entry.
|
|
73
75
|
*/
|
|
74
76
|
export async function loadPrices(root) {
|
|
75
|
-
|
|
77
|
+
// The pre-move path is a fallback for one release (review 0003 Q19).
|
|
78
|
+
const pricesPath = join(resolve(root), LOCAL_DIR, 'bench', 'prices.json');
|
|
79
|
+
const legacyPath = join(resolve(root), '.simt', 'bench', 'prices.json');
|
|
76
80
|
let rawText;
|
|
77
81
|
try {
|
|
78
82
|
rawText = await readFile(pricesPath, 'utf8');
|
|
79
83
|
} catch (error) {
|
|
80
|
-
if (error && error.code === 'ENOENT')
|
|
81
|
-
|
|
84
|
+
if (error && error.code === 'ENOENT') {
|
|
85
|
+
try {
|
|
86
|
+
rawText = await readFile(legacyPath, 'utf8');
|
|
87
|
+
} catch (legacyError) {
|
|
88
|
+
if (legacyError && legacyError.code === 'ENOENT') return null;
|
|
89
|
+
throw legacyError;
|
|
90
|
+
}
|
|
91
|
+
} else throw error;
|
|
82
92
|
}
|
|
83
93
|
|
|
84
94
|
let parsed;
|
|
@@ -126,10 +136,13 @@ function resolvePriceEntry(prices, run) {
|
|
|
126
136
|
|
|
127
137
|
/**
|
|
128
138
|
* Compute billed cost in USD for a single run record.
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
139
|
+
*
|
|
140
|
+
* Returns `{ usd, source }` where `usd` is null when the cost is unknown — a missing
|
|
141
|
+
* cost must not read as a zero cost (review 0003 Q46: the claude run with usage but no
|
|
142
|
+
* prices reported `C $0.0000 | CPI $0.0000`). `source` is:
|
|
143
|
+
* 'priced' — computed from the token prices table
|
|
144
|
+
* 'reported' — the harness reported `costUsd` directly
|
|
145
|
+
* 'missing' — no prices table and no reported cost; `usd` is null
|
|
133
146
|
*/
|
|
134
147
|
export function computeRunCost(run, prices = null) {
|
|
135
148
|
const usage = run?.usage ?? {};
|
|
@@ -142,17 +155,23 @@ export function computeRunCost(run, prices = null) {
|
|
|
142
155
|
const entry = resolvePriceEntry(prices, run);
|
|
143
156
|
if (entry) {
|
|
144
157
|
if (totalTokens === 0 && Number(run?.costUsd ?? 0) > 0) {
|
|
145
|
-
return Number(run.costUsd);
|
|
158
|
+
return { usd: Number(run.costUsd), source: 'reported' };
|
|
146
159
|
}
|
|
147
|
-
return
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
160
|
+
return {
|
|
161
|
+
usd:
|
|
162
|
+
(input * entry.in +
|
|
163
|
+
cacheWrite * entry.cache_write +
|
|
164
|
+
cacheRead * entry.cache_read +
|
|
165
|
+
output * entry.out) /
|
|
166
|
+
1_000_000,
|
|
167
|
+
source: 'priced',
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const reported = run?.costUsd;
|
|
171
|
+
if (typeof reported === 'number' && Number.isFinite(reported)) {
|
|
172
|
+
return { usd: reported, source: 'reported' };
|
|
154
173
|
}
|
|
155
|
-
return
|
|
174
|
+
return { usd: null, source: 'missing' };
|
|
156
175
|
}
|
|
157
176
|
|
|
158
177
|
/**
|
|
@@ -204,7 +223,7 @@ export function evaluateRunRecord(run, prices = null) {
|
|
|
204
223
|
const toolCalls = Number(usage.toolCalls ?? usage.tool_calls ?? 0);
|
|
205
224
|
const R = isRunResolved(run) ? 1 : 0;
|
|
206
225
|
const Q = computeRunQuality(run);
|
|
207
|
-
const
|
|
226
|
+
const cost = computeRunCost(run, prices);
|
|
208
227
|
const cacheDenom = input + cacheRead;
|
|
209
228
|
const cacheHitRatio = cacheDenom > 0 ? cacheRead / cacheDenom : 0;
|
|
210
229
|
|
|
@@ -213,10 +232,12 @@ export function evaluateRunRecord(run, prices = null) {
|
|
|
213
232
|
arm: String(run?.arm ?? 'default'),
|
|
214
233
|
harness: String(run?.harness ?? ''),
|
|
215
234
|
model: String(run?.model ?? ''),
|
|
235
|
+
experiment: String(run?.experiment ?? ''),
|
|
216
236
|
rep: Number(run?.rep ?? 1),
|
|
217
237
|
R,
|
|
218
238
|
Q,
|
|
219
|
-
C,
|
|
239
|
+
C: cost.usd,
|
|
240
|
+
costSource: cost.source,
|
|
220
241
|
turns,
|
|
221
242
|
toolCalls,
|
|
222
243
|
input,
|
|
@@ -236,13 +257,17 @@ function mean(values) {
|
|
|
236
257
|
|
|
237
258
|
/**
|
|
238
259
|
* Aggregate a list of task-level (or run-level) metric rows into arm summary metrics.
|
|
260
|
+
*
|
|
261
|
+
* `C` is null when any row's cost is missing — a partial cost must not read as a
|
|
262
|
+
* complete one. `CPI` is total cost divided by resolved outcomes (plan 0004 §1:
|
|
263
|
+
* "undefined when none resolve, never zero or a favourable finite score").
|
|
239
264
|
*/
|
|
240
265
|
function summarizeRows(rows) {
|
|
241
266
|
if (rows.length === 0) {
|
|
242
267
|
return {
|
|
243
268
|
R: 0,
|
|
244
269
|
Q: 0,
|
|
245
|
-
C:
|
|
270
|
+
C: null,
|
|
246
271
|
CPI: null,
|
|
247
272
|
turns: 0,
|
|
248
273
|
toolCalls: 0,
|
|
@@ -251,7 +276,8 @@ function summarizeRows(rows) {
|
|
|
251
276
|
}
|
|
252
277
|
const R = mean(rows.map((r) => r.R));
|
|
253
278
|
const Q = mean(rows.map((r) => r.Q));
|
|
254
|
-
const
|
|
279
|
+
const costComplete = rows.every((r) => r.C !== null && r.C !== undefined);
|
|
280
|
+
const C = costComplete ? mean(rows.map((r) => r.C)) : null;
|
|
255
281
|
const turns = mean(rows.map((r) => r.turns));
|
|
256
282
|
const toolCalls = mean(rows.map((r) => r.toolCalls));
|
|
257
283
|
let totalInput = 0;
|
|
@@ -262,8 +288,15 @@ function summarizeRows(rows) {
|
|
|
262
288
|
}
|
|
263
289
|
const cacheHitRatio =
|
|
264
290
|
totalInput + totalCacheRead > 0 ? totalCacheRead / (totalInput + totalCacheRead) : 0;
|
|
265
|
-
|
|
266
|
-
|
|
291
|
+
|
|
292
|
+
let totalCost = 0;
|
|
293
|
+
let resolvedCount = 0;
|
|
294
|
+
for (const r of rows) {
|
|
295
|
+
const reps = Number(r.reps ?? 1);
|
|
296
|
+
totalCost += (r.C ?? 0) * reps;
|
|
297
|
+
resolvedCount += r.R * reps;
|
|
298
|
+
}
|
|
299
|
+
const CPI = costComplete && resolvedCount > 0 ? totalCost / resolvedCount : null;
|
|
267
300
|
|
|
268
301
|
return { R, Q, C, CPI, turns, toolCalls, cacheHitRatio };
|
|
269
302
|
}
|
|
@@ -272,7 +305,7 @@ function summarizeRows(rows) {
|
|
|
272
305
|
* Group evaluated runs for one arm by task so multi-repetition tasks form one task unit
|
|
273
306
|
* for task-level means and bootstrap resampling.
|
|
274
307
|
*/
|
|
275
|
-
function collapseArmByTask(evaluatedRuns) {
|
|
308
|
+
export function collapseArmByTask(evaluatedRuns) {
|
|
276
309
|
const byTask = new Map();
|
|
277
310
|
for (const run of evaluatedRuns) {
|
|
278
311
|
const list = byTask.get(run.task) ?? [];
|
|
@@ -284,7 +317,8 @@ function collapseArmByTask(evaluatedRuns) {
|
|
|
284
317
|
const group = byTask.get(task);
|
|
285
318
|
const R = mean(group.map((r) => r.R));
|
|
286
319
|
const Q = mean(group.map((r) => r.Q));
|
|
287
|
-
const
|
|
320
|
+
const costComplete = group.every((r) => r.C !== null && r.C !== undefined);
|
|
321
|
+
const C = costComplete ? mean(group.map((r) => r.C)) : null;
|
|
288
322
|
const turns = mean(group.map((r) => r.turns));
|
|
289
323
|
const toolCalls = mean(group.map((r) => r.toolCalls));
|
|
290
324
|
const input = mean(group.map((r) => r.input));
|
|
@@ -325,11 +359,15 @@ export function bootstrapArmCi(
|
|
|
325
359
|
{ samples = DEFAULT_BOOTSTRAP_SAMPLES, seed = DEFAULT_BOOTSTRAP_SEED } = {},
|
|
326
360
|
) {
|
|
327
361
|
const n = taskRows.length;
|
|
362
|
+
// When any task's cost is missing the cost CIs are undefined, not degenerate:
|
|
363
|
+
// dropping the undefined resamples (the old behaviour) quietly narrowed the
|
|
364
|
+
// interval to the tasks that happened to price out (review 0003 Q46).
|
|
365
|
+
const costComplete = taskRows.every((r) => r.C !== null && r.C !== undefined);
|
|
328
366
|
if (n === 0) {
|
|
329
367
|
return {
|
|
330
368
|
R: [0, 0],
|
|
331
369
|
Q: [0, 0],
|
|
332
|
-
C:
|
|
370
|
+
C: null,
|
|
333
371
|
CPI: null,
|
|
334
372
|
turns: [0, 0],
|
|
335
373
|
toolCalls: [0, 0],
|
|
@@ -341,7 +379,7 @@ export function bootstrapArmCi(
|
|
|
341
379
|
return {
|
|
342
380
|
R: [s.R, s.R],
|
|
343
381
|
Q: [s.Q, s.Q],
|
|
344
|
-
C: [s.C, s.C],
|
|
382
|
+
C: s.C === null ? null : [s.C, s.C],
|
|
345
383
|
CPI: s.CPI === null ? null : [s.CPI, s.CPI],
|
|
346
384
|
turns: [s.turns, s.turns],
|
|
347
385
|
toolCalls: [s.toolCalls, s.toolCalls],
|
|
@@ -366,7 +404,7 @@ export function bootstrapArmCi(
|
|
|
366
404
|
const s = summarizeRows(draw);
|
|
367
405
|
rArr.push(s.R);
|
|
368
406
|
qArr.push(s.Q);
|
|
369
|
-
cArr.push(s.C);
|
|
407
|
+
if (costComplete) cArr.push(s.C);
|
|
370
408
|
if (s.CPI !== null) cpiArr.push(s.CPI);
|
|
371
409
|
turnsArr.push(s.turns);
|
|
372
410
|
toolsArr.push(s.toolCalls);
|
|
@@ -385,7 +423,7 @@ export function bootstrapArmCi(
|
|
|
385
423
|
return {
|
|
386
424
|
R: percentilePair(rArr),
|
|
387
425
|
Q: percentilePair(qArr),
|
|
388
|
-
C: percentilePair(cArr),
|
|
426
|
+
C: costComplete ? percentilePair(cArr) : null,
|
|
389
427
|
CPI: cpiArr.length > 0 ? percentilePair(cpiArr) : null,
|
|
390
428
|
turns: percentilePair(turnsArr),
|
|
391
429
|
toolCalls: percentilePair(toolsArr),
|
|
@@ -471,6 +509,66 @@ export function mcnemarTest(pairs) {
|
|
|
471
509
|
};
|
|
472
510
|
}
|
|
473
511
|
|
|
512
|
+
/**
|
|
513
|
+
* Non-inferiority margin for resolution rate (plan 0004 §1): the candidate is
|
|
514
|
+
* non-inferior only when the one-sided 95% lower bound of ΔR stays above -2 pp.
|
|
515
|
+
*/
|
|
516
|
+
export const NON_INFERIORITY_MARGIN = -0.02;
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Below this many paired tasks the two-sided exact sign test cannot reject at α = 0.05
|
|
520
|
+
* (its smallest achievable p-value at n = 4 is 0.125), so the verdict is
|
|
521
|
+
* `insufficient-tasks` instead of a p-value the sample cannot support (review 0003 Q46).
|
|
522
|
+
*/
|
|
523
|
+
export const MIN_PAIRD_TASKS = 5;
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Exact two-sided sign test on paired task-mean outcomes (armA, armB).
|
|
527
|
+
* Zero differences carry no evidence and are excluded, mirroring the Wilcoxon
|
|
528
|
+
* treatment of ties. This replaces McNemar on task means: the sign of a task-mean
|
|
529
|
+
* difference is the paired discrete evidence, and it does not assume the 0/1
|
|
530
|
+
* contingency McNemar requires.
|
|
531
|
+
*/
|
|
532
|
+
export function signTest(pairs) {
|
|
533
|
+
let plus = 0;
|
|
534
|
+
let minus = 0;
|
|
535
|
+
let zero = 0;
|
|
536
|
+
for (const [a, b] of pairs) {
|
|
537
|
+
const d = Number(b) - Number(a);
|
|
538
|
+
if (d > 1e-12) plus += 1;
|
|
539
|
+
else if (d < -1e-12) minus += 1;
|
|
540
|
+
else zero += 1;
|
|
541
|
+
}
|
|
542
|
+
return {
|
|
543
|
+
n: pairs.length,
|
|
544
|
+
plus,
|
|
545
|
+
minus,
|
|
546
|
+
zero,
|
|
547
|
+
statistic: Math.min(plus, minus),
|
|
548
|
+
pValue: exactBinomialTwoSided(plus, minus),
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Holm step-down p-value adjustment: conservative FWER control for the family of
|
|
554
|
+
* comparisons (ablation sections, rule-variant slots) tested in one run.
|
|
555
|
+
*
|
|
556
|
+
* @param {number[]} pValues raw p-values of the family, in any order
|
|
557
|
+
* @returns {number[]} adjusted p-values in the original order, monotone and in [0, 1]
|
|
558
|
+
*/
|
|
559
|
+
export function holmAdjust(pValues) {
|
|
560
|
+
if (pValues.length === 0) return [];
|
|
561
|
+
const m = pValues.length;
|
|
562
|
+
const indexed = pValues.map((p, i) => [i, Number(p)]).sort((a, b) => a[1] - b[1]);
|
|
563
|
+
const adjusted = new Array(m);
|
|
564
|
+
let runningMax = 0;
|
|
565
|
+
for (let j = 0; j < m; j += 1) {
|
|
566
|
+
runningMax = Math.max(runningMax, Math.min(1, indexed[j][1] * (m - j)));
|
|
567
|
+
adjusted[indexed[j][0]] = runningMax;
|
|
568
|
+
}
|
|
569
|
+
return adjusted;
|
|
570
|
+
}
|
|
571
|
+
|
|
474
572
|
/**
|
|
475
573
|
* Wilcoxon signed-rank test on paired numerical observations (armA, armB),
|
|
476
574
|
* evaluating differences d_i = b_i - a_i.
|
|
@@ -568,8 +666,20 @@ export function wilcoxonSignedRank(pairs) {
|
|
|
568
666
|
}
|
|
569
667
|
|
|
570
668
|
/**
|
|
571
|
-
* Pair two arms
|
|
572
|
-
*
|
|
669
|
+
* Pair two arms at the task level and compute deltas, paired bootstrap 95% CIs,
|
|
670
|
+
* an exact sign test on task-mean R, a Wilcoxon signed-rank test on task-mean C,
|
|
671
|
+
* and a one-sided non-inferiority verdict (plan 0004 §1, review 0003 Q46).
|
|
672
|
+
*
|
|
673
|
+
* Reps of a task are not independent tasks: the old run-level pairing paired
|
|
674
|
+
* (task, rep) records when both arms shared the same keys, so one task with ten
|
|
675
|
+
* reps produced ten "paired runs" and an inflated p-value. Pairing is now at the
|
|
676
|
+
* task level only; `pairedRuns` survives as a descriptive count.
|
|
677
|
+
*
|
|
678
|
+
* `verdict`:
|
|
679
|
+
* 'insufficient-tasks' — fewer than MIN_PAIRD_TASKS paired tasks
|
|
680
|
+
* 'non-inferior' — one-sided 95% lower bound of ΔR above NON_INFERIORITY_MARGIN
|
|
681
|
+
* 'inferior' — two-sided 95% CI of ΔR entirely below 0
|
|
682
|
+
* 'inconclusive' — otherwise (CI too wide to decide)
|
|
573
683
|
*/
|
|
574
684
|
export function compareArms(
|
|
575
685
|
armAName,
|
|
@@ -588,24 +698,10 @@ export function compareArms(
|
|
|
588
698
|
b: tasksB.get(task),
|
|
589
699
|
}));
|
|
590
700
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
const sharedRunKeys = [...runMapA.keys()].filter((k) => runMapB.has(k)).sort();
|
|
596
|
-
|
|
597
|
-
const useRunPairs =
|
|
598
|
-
sharedRunKeys.length > 0 &&
|
|
599
|
-
sharedRunKeys.length === armARuns.length &&
|
|
600
|
-
sharedRunKeys.length === armBRuns.length;
|
|
601
|
-
|
|
602
|
-
const rPairs = useRunPairs
|
|
603
|
-
? sharedRunKeys.map((k) => [runMapA.get(k).R, runMapB.get(k).R])
|
|
604
|
-
: pairedTasks.map((p) => [p.a.R, p.b.R]);
|
|
605
|
-
|
|
606
|
-
const cPairs = useRunPairs
|
|
607
|
-
? sharedRunKeys.map((k) => [runMapA.get(k).C, runMapB.get(k).C])
|
|
608
|
-
: pairedTasks.map((p) => [p.a.C, p.b.C]);
|
|
701
|
+
const rPairs = pairedTasks.map((p) => [p.a.R, p.b.R]);
|
|
702
|
+
const cPairs = pairedTasks
|
|
703
|
+
.filter((p) => p.a.C !== null && p.b.C !== null)
|
|
704
|
+
.map((p) => [p.a.C, p.b.C]);
|
|
609
705
|
|
|
610
706
|
const sumA = summarizeRows(pairedTasks.map((p) => p.a));
|
|
611
707
|
const sumB = summarizeRows(pairedTasks.map((p) => p.b));
|
|
@@ -613,7 +709,7 @@ export function compareArms(
|
|
|
613
709
|
const deltas = {
|
|
614
710
|
R: sumB.R - sumA.R,
|
|
615
711
|
Q: sumB.Q - sumA.Q,
|
|
616
|
-
C: sumB.C - sumA.C,
|
|
712
|
+
C: sumB.C !== null && sumA.C !== null ? sumB.C - sumA.C : null,
|
|
617
713
|
CPI: sumB.CPI !== null && sumA.CPI !== null ? sumB.CPI - sumA.CPI : null,
|
|
618
714
|
cpiRatio:
|
|
619
715
|
sumB.CPI !== null && sumA.CPI !== null && sumA.CPI > 0 ? sumB.CPI / sumA.CPI : null,
|
|
@@ -624,12 +720,15 @@ export function compareArms(
|
|
|
624
720
|
|
|
625
721
|
// Paired bootstrap 95% CIs on deltas (B - A).
|
|
626
722
|
const n = pairedTasks.length;
|
|
723
|
+
const costComplete =
|
|
724
|
+
pairedTasks.every((p) => p.a.C !== null && p.a.C !== undefined) &&
|
|
725
|
+
pairedTasks.every((p) => p.b.C !== null && p.b.C !== undefined);
|
|
627
726
|
let ci95;
|
|
628
727
|
if (n <= 1) {
|
|
629
728
|
ci95 = {
|
|
630
729
|
R: [deltas.R, deltas.R],
|
|
631
730
|
Q: [deltas.Q, deltas.Q],
|
|
632
|
-
C: [deltas.C, deltas.C],
|
|
731
|
+
C: deltas.C === null ? null : [deltas.C, deltas.C],
|
|
633
732
|
CPI: deltas.CPI === null ? null : [deltas.CPI, deltas.CPI],
|
|
634
733
|
turns: [deltas.turns, deltas.turns],
|
|
635
734
|
toolCalls: [deltas.toolCalls, deltas.toolCalls],
|
|
@@ -657,7 +756,7 @@ export function compareArms(
|
|
|
657
756
|
const sB = summarizeRows(drawB);
|
|
658
757
|
dR.push(sB.R - sA.R);
|
|
659
758
|
dQ.push(sB.Q - sA.Q);
|
|
660
|
-
dC.push(sB.C - sA.C);
|
|
759
|
+
if (costComplete) dC.push(sB.C - sA.C);
|
|
661
760
|
if (sB.CPI !== null && sA.CPI !== null) dCPI.push(sB.CPI - sA.CPI);
|
|
662
761
|
dTurns.push(sB.turns - sA.turns);
|
|
663
762
|
dTools.push(sB.toolCalls - sA.toolCalls);
|
|
@@ -676,7 +775,7 @@ export function compareArms(
|
|
|
676
775
|
ci95 = {
|
|
677
776
|
R: percentilePair(dR),
|
|
678
777
|
Q: percentilePair(dQ),
|
|
679
|
-
C: percentilePair(dC),
|
|
778
|
+
C: costComplete ? percentilePair(dC) : null,
|
|
680
779
|
CPI: dCPI.length > 0 ? percentilePair(dCPI) : null,
|
|
681
780
|
turns: percentilePair(dTurns),
|
|
682
781
|
toolCalls: percentilePair(dTools),
|
|
@@ -684,30 +783,59 @@ export function compareArms(
|
|
|
684
783
|
};
|
|
685
784
|
}
|
|
686
785
|
|
|
786
|
+
const rTest = signTest(rPairs);
|
|
787
|
+
const pairedRuns = pairedTasks.reduce(
|
|
788
|
+
(sum, p) => sum + Math.min(Number(p.a.reps ?? 1), Number(p.b.reps ?? 1)),
|
|
789
|
+
0,
|
|
790
|
+
);
|
|
791
|
+
|
|
792
|
+
const bound = ci95.R[0];
|
|
793
|
+
const met = n < MIN_PAIRD_TASKS ? null : bound > NON_INFERIORITY_MARGIN;
|
|
794
|
+
const verdict =
|
|
795
|
+
n < MIN_PAIRD_TASKS
|
|
796
|
+
? 'insufficient-tasks'
|
|
797
|
+
: bound > NON_INFERIORITY_MARGIN
|
|
798
|
+
? 'non-inferior'
|
|
799
|
+
: ci95.R[1] < 0
|
|
800
|
+
? 'inferior'
|
|
801
|
+
: 'inconclusive';
|
|
802
|
+
|
|
687
803
|
return {
|
|
688
804
|
baseline: armAName,
|
|
689
805
|
candidate: armBName,
|
|
690
|
-
pairedTasks:
|
|
691
|
-
pairedRuns
|
|
806
|
+
pairedTasks: n,
|
|
807
|
+
pairedRuns,
|
|
692
808
|
deltas,
|
|
693
809
|
ci95,
|
|
694
|
-
|
|
810
|
+
rTest,
|
|
695
811
|
wilcoxon: wilcoxonSignedRank(cPairs),
|
|
812
|
+
nonInferiority: {
|
|
813
|
+
margin: NON_INFERIORITY_MARGIN,
|
|
814
|
+
bound,
|
|
815
|
+
met,
|
|
816
|
+
},
|
|
817
|
+
verdict,
|
|
696
818
|
};
|
|
697
819
|
}
|
|
698
820
|
|
|
699
821
|
/**
|
|
700
|
-
* Load all stored run records under `.simt
|
|
822
|
+
* Load all stored run records under `.simt/local/bench/runs/`.
|
|
701
823
|
*/
|
|
702
824
|
export async function loadRunRecords(root) {
|
|
703
|
-
|
|
704
|
-
const
|
|
825
|
+
// The pre-move directory is a fallback for one release (review 0003 Q19).
|
|
826
|
+
const runsRoot = join(resolve(root), LOCAL_DIR, 'bench', 'runs');
|
|
827
|
+
let entries = await readdir(runsRoot, { withFileTypes: true }).catch(() => null);
|
|
828
|
+
let base = runsRoot;
|
|
829
|
+
if (entries === null) {
|
|
830
|
+
base = join(resolve(root), '.simt', 'bench', 'runs');
|
|
831
|
+
entries = await readdir(base, { withFileTypes: true }).catch(() => []);
|
|
832
|
+
}
|
|
705
833
|
const sortedEntries = [...entries].sort((a, b) => a.name.localeCompare(b.name));
|
|
706
834
|
|
|
707
835
|
const records = [];
|
|
708
836
|
for (const entry of sortedEntries) {
|
|
709
837
|
if (entry.name.startsWith('.')) continue;
|
|
710
|
-
const fullPath = join(
|
|
838
|
+
const fullPath = join(base, entry.name);
|
|
711
839
|
if (entry.isDirectory()) {
|
|
712
840
|
const files = (await readdir(fullPath).catch(() => []))
|
|
713
841
|
.filter((f) => f.endsWith('.json') && !f.startsWith('.'))
|
|
@@ -760,14 +888,21 @@ function fmtSignedNum(v, digits = 2) {
|
|
|
760
888
|
*/
|
|
761
889
|
export function formatReportMarkdown(report) {
|
|
762
890
|
const lines = [
|
|
763
|
-
'| Arm | Tasks | Runs | R | 95% CI (R) | Q | C ($) | CPI ($) | Turns | Tool calls | Cache hit |',
|
|
764
|
-
'| --- | ---: | ---: | ---: | --- | ---: | ---: | ---: | ---: | ---: | ---: |',
|
|
891
|
+
'| Arm | Harness | Model | Tasks | Runs | R | 95% CI (R) | Q | C ($) | CPI ($) | Turns | Tool calls | Cache hit |',
|
|
892
|
+
'| --- | --- | --- | ---: | ---: | ---: | --- | ---: | ---: | ---: | ---: | ---: | ---: |',
|
|
765
893
|
];
|
|
766
894
|
|
|
767
895
|
for (const arm of report.arms) {
|
|
768
896
|
const rCi = `[${fmtPct(arm.ci95.R[0])}, ${fmtPct(arm.ci95.R[1])}]`;
|
|
769
897
|
lines.push(
|
|
770
|
-
`| ${arm.arm} | ${arm.tasks} | ${arm.runs} | ${fmtPct(arm.R)} | ${rCi} | ${fmtNum(arm.Q, 3)} | ${fmtUsd(arm.C)} | ${fmtUsd(arm.CPI)} | ${fmtNum(arm.turns, 2)} | ${fmtNum(arm.toolCalls, 2)} | ${fmtPct(arm.cacheHitRatio)} |`,
|
|
898
|
+
`| ${arm.arm} | ${arm.harness} | ${arm.model} | ${arm.tasks} | ${arm.runs} | ${fmtPct(arm.R)} | ${rCi} | ${fmtNum(arm.Q, 3)} | ${fmtUsd(arm.C)} | ${fmtUsd(arm.CPI)} | ${fmtNum(arm.turns, 2)} | ${fmtNum(arm.toolCalls, 2)} | ${fmtPct(arm.cacheHitRatio)} |`,
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
if (report.excluded > 0) {
|
|
903
|
+
lines.push('');
|
|
904
|
+
lines.push(
|
|
905
|
+
`${report.excluded} run record(s) excluded: the run errored or exceeded its deadline, so it recorded no measurement`,
|
|
771
906
|
);
|
|
772
907
|
}
|
|
773
908
|
|
|
@@ -784,13 +919,13 @@ export function formatReportMarkdown(report) {
|
|
|
784
919
|
const candArm = report.arms.find((a) => a.arm === cmp.candidate);
|
|
785
920
|
|
|
786
921
|
lines.push(
|
|
787
|
-
`| R | ${baseArm ? fmtPct(baseArm.R) : 'n/a'} | ${candArm ? fmtPct(candArm.R) : 'n/a'} | ${fmtSignedPct(d.R)} | [${fmtSignedPct(cmp.ci95.R[0])}, ${fmtSignedPct(cmp.ci95.R[1])}] |
|
|
922
|
+
`| R | ${baseArm ? fmtPct(baseArm.R) : 'n/a'} | ${candArm ? fmtPct(candArm.R) : 'n/a'} | ${fmtSignedPct(d.R)} | [${fmtSignedPct(cmp.ci95.R[0])}, ${fmtSignedPct(cmp.ci95.R[1])}] | Sign | ${fmtNum(cmp.rTest.statistic, 4)} | ${fmtNum(cmp.rTest.pValue, 4)} |`,
|
|
788
923
|
);
|
|
789
924
|
lines.push(
|
|
790
925
|
`| Q | ${baseArm ? fmtNum(baseArm.Q, 3) : 'n/a'} | ${candArm ? fmtNum(candArm.Q, 3) : 'n/a'} | ${fmtSignedNum(d.Q, 3)} | [${fmtSignedNum(cmp.ci95.Q[0], 3)}, ${fmtSignedNum(cmp.ci95.Q[1], 3)}] | — | — | — |`,
|
|
791
926
|
);
|
|
792
927
|
lines.push(
|
|
793
|
-
`| C ($) | ${baseArm ? fmtUsd(baseArm.C) : 'n/a'} | ${candArm ? fmtUsd(candArm.C) : 'n/a'} | ${fmtSignedUsd(d.C)} | [${fmtSignedUsd(cmp.ci95.C[0])}, ${fmtSignedUsd(cmp.ci95.C[1])}] | Wilcoxon | ${fmtNum(cmp.wilcoxon.statistic, 4)} | ${fmtNum(cmp.wilcoxon.pValue, 4)} |`,
|
|
928
|
+
`| C ($) | ${baseArm ? fmtUsd(baseArm.C) : 'n/a'} | ${candArm ? fmtUsd(candArm.C) : 'n/a'} | ${fmtSignedUsd(d.C)} | ${cmp.ci95.C ? `[${fmtSignedUsd(cmp.ci95.C[0])}, ${fmtSignedUsd(cmp.ci95.C[1])}]` : 'n/a'} | Wilcoxon | ${fmtNum(cmp.wilcoxon.statistic, 4)} | ${fmtNum(cmp.wilcoxon.pValue, 4)} |`,
|
|
794
929
|
);
|
|
795
930
|
lines.push(
|
|
796
931
|
`| CPI ($) | ${baseArm ? fmtUsd(baseArm.CPI) : 'n/a'} | ${candArm ? fmtUsd(candArm.CPI) : 'n/a'} | ${fmtSignedUsd(d.CPI)} | ${cmp.ci95.CPI ? `[${fmtSignedUsd(cmp.ci95.CPI[0])}, ${fmtSignedUsd(cmp.ci95.CPI[1])}]` : 'n/a'} | — | — | — |`,
|
|
@@ -804,6 +939,11 @@ export function formatReportMarkdown(report) {
|
|
|
804
939
|
lines.push(
|
|
805
940
|
`| Cache hit | ${baseArm ? fmtPct(baseArm.cacheHitRatio) : 'n/a'} | ${candArm ? fmtPct(candArm.cacheHitRatio) : 'n/a'} | ${fmtSignedPct(d.cacheHitRatio)} | [${fmtSignedPct(cmp.ci95.cacheHitRatio[0])}, ${fmtSignedPct(cmp.ci95.cacheHitRatio[1])}] | — | — | — |`,
|
|
806
941
|
);
|
|
942
|
+
lines.push('');
|
|
943
|
+
lines.push(
|
|
944
|
+
`Non-inferiority (ΔR lower bound > ${fmtSignedPct(cmp.nonInferiority.margin)}): ${fmtSignedPct(cmp.nonInferiority.bound)} — ${cmp.nonInferiority.met === true ? 'met' : cmp.nonInferiority.met === false ? 'not met' : 'not assessable'}`,
|
|
945
|
+
);
|
|
946
|
+
lines.push(`Verdict: ${cmp.verdict} (${cmp.pairedTasks} paired tasks)`);
|
|
807
947
|
}
|
|
808
948
|
|
|
809
949
|
return lines.join('\n');
|
|
@@ -811,6 +951,11 @@ export function formatReportMarkdown(report) {
|
|
|
811
951
|
|
|
812
952
|
/**
|
|
813
953
|
* Build the complete benchmark report for `root`.
|
|
954
|
+
*
|
|
955
|
+
* Arms are grouped by (harness, model, arm, experiment): pooling the `gold` and
|
|
956
|
+
* `noop` harness runs of the same arm name into one row hid what each harness
|
|
957
|
+
* actually did (review 0003 Q46). A `--compare` across two groups that differ in
|
|
958
|
+
* harness, model or experiment is refused unless `crossGroup` is set.
|
|
814
959
|
*/
|
|
815
960
|
export async function buildBenchReport(
|
|
816
961
|
root,
|
|
@@ -822,6 +967,7 @@ export async function buildBenchReport(
|
|
|
822
967
|
prices: customPrices,
|
|
823
968
|
samples = DEFAULT_BOOTSTRAP_SAMPLES,
|
|
824
969
|
seed = DEFAULT_BOOTSTRAP_SEED,
|
|
970
|
+
crossGroup = false,
|
|
825
971
|
} = {},
|
|
826
972
|
) {
|
|
827
973
|
const resolvedRoot = resolve(root);
|
|
@@ -836,22 +982,31 @@ export async function buildBenchReport(
|
|
|
836
982
|
return true;
|
|
837
983
|
});
|
|
838
984
|
|
|
839
|
-
|
|
840
|
-
|
|
985
|
+
// A run that errored produced no measurement: averaging its zero tokens, zero diff and
|
|
986
|
+
// unresolved verdict into an arm reports a result nobody observed (review 0003 Q47).
|
|
987
|
+
const errored = filtered.filter((rec) => rec.status === 'error');
|
|
988
|
+
const evaluated = filtered
|
|
989
|
+
.filter((rec) => rec.status !== 'error')
|
|
990
|
+
.map((rec) => evaluateRunRecord(rec, prices));
|
|
991
|
+
const byGroup = new Map();
|
|
841
992
|
for (const row of evaluated) {
|
|
842
|
-
const
|
|
993
|
+
const key = `${row.harness}|${row.model}|${row.arm}|${row.experiment}`;
|
|
994
|
+
const list = byGroup.get(key) ?? [];
|
|
843
995
|
list.push(row);
|
|
844
|
-
|
|
996
|
+
byGroup.set(key, list);
|
|
845
997
|
}
|
|
846
998
|
|
|
847
|
-
const
|
|
848
|
-
const arms =
|
|
849
|
-
const
|
|
999
|
+
const groupKeys = [...byGroup.keys()].sort();
|
|
1000
|
+
const arms = groupKeys.map((key) => {
|
|
1001
|
+
const [harness, model, armName] = key.split('|');
|
|
1002
|
+
const runs = byGroup.get(key);
|
|
850
1003
|
const taskRows = collapseArmByTask(runs);
|
|
851
1004
|
const summary = summarizeRows(taskRows);
|
|
852
1005
|
const ci95 = bootstrapArmCi(taskRows, { samples, seed });
|
|
853
1006
|
return {
|
|
854
1007
|
arm: armName,
|
|
1008
|
+
harness,
|
|
1009
|
+
model,
|
|
855
1010
|
tasks: taskRows.length,
|
|
856
1011
|
runs: runs.length,
|
|
857
1012
|
R: summary.R,
|
|
@@ -871,21 +1026,42 @@ export async function buildBenchReport(
|
|
|
871
1026
|
throw new Error('--compare requires two arm names: --compare <arm-a> <arm-b>');
|
|
872
1027
|
}
|
|
873
1028
|
const [armA, armB] = compare;
|
|
874
|
-
const
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
1029
|
+
const pick = (name) => {
|
|
1030
|
+
const matches = groupKeys.filter((k) => k.split('|')[2] === name);
|
|
1031
|
+
if (matches.length === 0) {
|
|
1032
|
+
throw new Error(`arm "${name}" has no stored benchmark runs`);
|
|
1033
|
+
}
|
|
1034
|
+
if (matches.length > 1) {
|
|
1035
|
+
const listing = matches
|
|
1036
|
+
.map((k) => {
|
|
1037
|
+
const [h, m, a] = k.split('|');
|
|
1038
|
+
return `${a} (harness: ${h}, model: ${m})`;
|
|
1039
|
+
})
|
|
1040
|
+
.join('; ');
|
|
1041
|
+
throw new Error(
|
|
1042
|
+
`arm "${name}" is ambiguous — groups: ${listing}; filter with --harness/--model or disambiguate the arm name`,
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
return { runs: byGroup.get(matches[0]), groupKey: matches[0] };
|
|
1046
|
+
};
|
|
1047
|
+
const a = pick(armA);
|
|
1048
|
+
const b = pick(armB);
|
|
1049
|
+
const ga = a.groupKey.split('|');
|
|
1050
|
+
const gb = b.groupKey.split('|');
|
|
1051
|
+
const crosses = ga[0] !== gb[0] || ga[1] !== gb[1] || ga[3] !== gb[3];
|
|
1052
|
+
if (crosses && !crossGroup) {
|
|
1053
|
+
throw new Error(
|
|
1054
|
+
`comparing "${armA}" and "${armB}" crosses groups (harness/model differ); pass crossGroup to compare across groups anyway`,
|
|
1055
|
+
);
|
|
881
1056
|
}
|
|
882
|
-
comparison = compareArms(armA,
|
|
1057
|
+
comparison = compareArms(armA, a.runs, armB, b.runs, { samples, seed });
|
|
883
1058
|
}
|
|
884
1059
|
|
|
885
1060
|
const report = {
|
|
886
1061
|
root: resolvedRoot,
|
|
887
1062
|
pricesLoaded: prices !== null,
|
|
888
1063
|
runCount: evaluated.length,
|
|
1064
|
+
excluded: errored.length,
|
|
889
1065
|
arms,
|
|
890
1066
|
comparison,
|
|
891
1067
|
};
|