cyber-aced 0.3.0
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/.claude-plugin/plugin.json +18 -0
- package/.plugin/pins.json +1 -0
- package/.plugin/plugin.json +18 -0
- package/agents/aced-case-judge.md +165 -0
- package/agents/aced-impl-judge.md +55 -0
- package/agents/aced-scenario-writer.md +76 -0
- package/agents/aced-spec-validator.md +66 -0
- package/governances/README.md +52 -0
- package/governances/agent-tool-output.md +86 -0
- package/governances/cli-resolution.md +102 -0
- package/governances/skill-design.md +258 -0
- package/governances/skill-repo-structure.md +173 -0
- package/package.json +44 -0
- package/readme.md +124 -0
- package/skills/aced-architect-impl/README.md +9 -0
- package/skills/aced-architect-impl/SKILL.md +73 -0
- package/skills/aced-builder-impl/README.md +14 -0
- package/skills/aced-builder-impl/SKILL.md +46 -0
- package/skills/aced-builder-spec/README.md +15 -0
- package/skills/aced-builder-spec/SKILL.md +130 -0
- package/skills/aced-fit/README.md +21 -0
- package/skills/aced-fit/SKILL.md +105 -0
- package/skills/aced-impl-producer/SKILL.md +75 -0
- package/skills/add-scenario/README.md +25 -0
- package/skills/add-scenario/SKILL.md +75 -0
- package/skills/bench/README.md +64 -0
- package/skills/bench/SKILL.md +199 -0
- package/skills/bench/scripts/bench-claude-code.mts +187 -0
- package/skills/bench/scripts/bench-compare.mts +369 -0
- package/skills/bench/scripts/bench-record.mts +133 -0
- package/skills/bench/scripts/bench.mts +930 -0
- package/skills/check-freshness/README.md +57 -0
- package/skills/check-freshness/SKILL.md +76 -0
- package/skills/check-freshness/scripts/check-freshness.mts +399 -0
- package/skills/compare/README.md +25 -0
- package/skills/compare/SKILL.md +87 -0
- package/skills/contribute-skill/README.md +25 -0
- package/skills/contribute-skill/SKILL.md +241 -0
- package/skills/contribute-skill/scripts/source.mts +188 -0
- package/skills/define-agent/README.md +40 -0
- package/skills/define-agent/SKILL.md +279 -0
- package/skills/define-command/README.md +36 -0
- package/skills/define-command/SKILL.md +174 -0
- package/skills/define-governance/SKILL.md +151 -0
- package/skills/define-skill/README.md +37 -0
- package/skills/define-skill/SKILL.md +200 -0
- package/skills/extract-situation/README.md +32 -0
- package/skills/extract-situation/SKILL.md +54 -0
- package/skills/extract-situation/scripts/extract-situation.mts +287 -0
- package/skills/improve/README.md +25 -0
- package/skills/improve/SKILL.md +100 -0
- package/skills/improve-skill/README.md +38 -0
- package/skills/improve-skill/SKILL.md +169 -0
- package/skills/improve-skill/references/check-definitions.md +215 -0
- package/skills/improve-skill/scripts/validate.mts +1033 -0
- package/skills/init-aced/README.md +5 -0
- package/skills/init-aced/SKILL.md +76 -0
- package/skills/init-aced/scripts/ensure-results-ignored.mts +117 -0
- package/skills/list-skills/README.md +19 -0
- package/skills/list-skills/SKILL.md +55 -0
- package/skills/list-skills/scripts/list-skills.mts +210 -0
- package/skills/manage/README.md +35 -0
- package/skills/manage/SKILL.md +65 -0
- package/skills/manage-model-runners/README.md +25 -0
- package/skills/manage-model-runners/SKILL.md +113 -0
- package/skills/manage-skill-dirs/README.md +17 -0
- package/skills/manage-skill-dirs/SKILL.md +54 -0
- package/skills/manage-skill-dirs/scripts/manage-skill-dirs.mts +328 -0
- package/skills/repair-private-skills/README.md +17 -0
- package/skills/repair-private-skills/SKILL.md +59 -0
- package/skills/repair-private-skills/scripts/repair-private-skills.mts +279 -0
- package/skills/report/README.md +25 -0
- package/skills/report/SKILL.md +74 -0
- package/skills/run/README.md +25 -0
- package/skills/run/SKILL.md +220 -0
- package/skills/skillify/README.md +25 -0
- package/skills/skillify/SKILL.md +141 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
// bench-compare — the statistics between two run records, and the verdict.
|
|
2
|
+
//
|
|
3
|
+
// Per task and compared metric: each side's mean, median and min–max, the relative change, and a
|
|
4
|
+
// two-sided permutation test on the difference of means. Agent runs vary a lot from one to the
|
|
5
|
+
// next, so a mean moving by 10% is often noise; the p-value says how often relabelling the runs at
|
|
6
|
+
// random moves the mean at least as far as the real labels did. Up to 200,000 relabellings are
|
|
7
|
+
// enumerated (exact); past that, 20,000 seeded relabellings are drawn and p = (hits + 1) / (N + 1),
|
|
8
|
+
// identical on every run of the same records.
|
|
9
|
+
//
|
|
10
|
+
// The pooled row is the geometric mean of the per-task after/before ratios, so a cheap task and an
|
|
11
|
+
// expensive one weigh the same; its labels are permuted within each task, never across. A task
|
|
12
|
+
// enters a pooled row only when every one of its runs is positive on that metric on both sides,
|
|
13
|
+
// so no relabelling can make a log ratio undefined. `pass` has no pooled row: a ratio of pass rates
|
|
14
|
+
// is undefined whenever a side is 0%.
|
|
15
|
+
//
|
|
16
|
+
// THE VERDICT is a closed form (spec: eval-run/bench/engine/README.md). Let G be every row — per
|
|
17
|
+
// task and pooled — of a gated metric. Wrong-way: the after mean is worse in that metric's
|
|
18
|
+
// direction. Significant: p < 0.05. Then: incomparable > regressed (wrong-way AND significant) >
|
|
19
|
+
// inconclusive (wrong-way) > improved (significant) > unchanged. Multiplicity is DISCLOSED in the
|
|
20
|
+
// footer (test count × 0.05), never corrected.
|
|
21
|
+
|
|
22
|
+
import { GATED, METRICS, type Metric, median, type RunRecord, type RunResult, round } from './bench-record.mts'
|
|
23
|
+
|
|
24
|
+
const ALPHA = 0.05
|
|
25
|
+
const EXACT_LIMIT = 200_000
|
|
26
|
+
const SAMPLES = 20_000
|
|
27
|
+
|
|
28
|
+
// ─── permutation machinery ────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
/** mulberry32: a small seeded generator, so a sampled p-value repeats exactly. */
|
|
31
|
+
function rng(seed: number): () => number {
|
|
32
|
+
let s = seed >>> 0
|
|
33
|
+
return () => {
|
|
34
|
+
s = (s + 0x6d2b79f5) >>> 0
|
|
35
|
+
let t = s
|
|
36
|
+
t = Math.imul(t ^ (t >>> 15), t | 1)
|
|
37
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
|
|
38
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** FNV-1a, so each row draws from its own seed and one row's sampling never shifts another's. */
|
|
43
|
+
function seedOf(key: string): number {
|
|
44
|
+
let h = 0x811c9dc5
|
|
45
|
+
for (let i = 0; i < key.length; i++) {
|
|
46
|
+
h ^= key.charCodeAt(i)
|
|
47
|
+
h = Math.imul(h, 0x01000193)
|
|
48
|
+
}
|
|
49
|
+
return h >>> 0
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function choose(n: number, k: number): number {
|
|
53
|
+
let c = 1
|
|
54
|
+
for (let i = 1; i <= k; i++) c = (c * (n - k + i)) / i
|
|
55
|
+
return Math.round(c)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The smallest p any data could reach with these group sizes, one `[before, after]` pair per task.
|
|
60
|
+
* One pair is a per-task row; several are a pooled row. Exact when every task enumerates and the
|
|
61
|
+
* product of their relabellings stays within the limit: only the real split and, with equal
|
|
62
|
+
* groups, its mirror can be the most extreme. Sampled otherwise: one hit out of N + 1.
|
|
63
|
+
*/
|
|
64
|
+
export function minAttainableP(sizes: Array<[number, number]>): number {
|
|
65
|
+
let product = 1
|
|
66
|
+
let exact = true
|
|
67
|
+
for (const [na, nb] of sizes) {
|
|
68
|
+
const c = choose(na + nb, na)
|
|
69
|
+
if (c > EXACT_LIMIT) exact = false
|
|
70
|
+
product *= c
|
|
71
|
+
}
|
|
72
|
+
if (!exact || product > EXACT_LIMIT) return 1 / (SAMPLES + 1)
|
|
73
|
+
return (sizes.every(([na, nb]) => na === nb) ? 2 : 1) / product
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface Relabellings {
|
|
77
|
+
stats: number[]
|
|
78
|
+
exact: boolean
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Every split of the pooled runs into a "before" group of `a.length` and an "after" group of the
|
|
83
|
+
* rest, as `stat(beforeMean, afterMean)`. Enumerated when few enough, sampled otherwise.
|
|
84
|
+
*/
|
|
85
|
+
function relabellings(
|
|
86
|
+
a: number[],
|
|
87
|
+
b: number[],
|
|
88
|
+
stat: (before: number, after: number) => number,
|
|
89
|
+
random: () => number,
|
|
90
|
+
): Relabellings {
|
|
91
|
+
const all = [...a, ...b]
|
|
92
|
+
const na = a.length
|
|
93
|
+
const nb = b.length
|
|
94
|
+
const total = all.reduce((s, x) => s + x, 0)
|
|
95
|
+
const stats: number[] = []
|
|
96
|
+
const split = (before: number) => stat(before / na, (total - before) / nb)
|
|
97
|
+
if (choose(all.length, na) <= EXACT_LIMIT) {
|
|
98
|
+
const walk = (from: number, left: number, acc: number) => {
|
|
99
|
+
if (left === 0) {
|
|
100
|
+
stats.push(split(acc))
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
for (let i = from; i <= all.length - left; i++) walk(i + 1, left - 1, acc + (all[i] as number))
|
|
104
|
+
}
|
|
105
|
+
walk(0, na, 0)
|
|
106
|
+
return { stats, exact: true }
|
|
107
|
+
}
|
|
108
|
+
const idx = all.map((_, i) => i)
|
|
109
|
+
for (let n = 0; n < SAMPLES; n++) {
|
|
110
|
+
// A partial Fisher-Yates shuffle: the first `na` slots are a uniform random "before" group.
|
|
111
|
+
let acc = 0
|
|
112
|
+
for (let i = 0; i < na; i++) {
|
|
113
|
+
const j = i + Math.floor(random() * (idx.length - i))
|
|
114
|
+
const tmp = idx[i] as number
|
|
115
|
+
idx[i] = idx[j] as number
|
|
116
|
+
idx[j] = tmp
|
|
117
|
+
acc += all[idx[i] as number] as number
|
|
118
|
+
}
|
|
119
|
+
stats.push(split(acc))
|
|
120
|
+
}
|
|
121
|
+
return { stats, exact: false }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
interface Test {
|
|
125
|
+
p: number
|
|
126
|
+
exact: boolean
|
|
127
|
+
minP: number
|
|
128
|
+
tooFew: boolean
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Two-sided: the share of relabellings whose |statistic| reaches the observed one. */
|
|
132
|
+
function pValue(observed: number, { stats, exact }: Relabellings, minP: number): Test {
|
|
133
|
+
const tol = 1e-12 * Math.max(1, Math.abs(observed))
|
|
134
|
+
let hits = 0
|
|
135
|
+
for (const x of stats) if (Math.abs(x) >= Math.abs(observed) - tol) hits++
|
|
136
|
+
// A sampled test counts the real split once more: it is one of the possible ones.
|
|
137
|
+
const p = exact ? hits / stats.length : (hits + 1) / (stats.length + 1)
|
|
138
|
+
return { p, exact, minP, tooFew: minP > ALPHA }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const difference = (before: number, after: number) => after - before
|
|
142
|
+
const logRatio = (before: number, after: number) => Math.log(after / before)
|
|
143
|
+
const mean = (xs: number[]) => xs.reduce((s, x) => s + x, 0) / xs.length
|
|
144
|
+
|
|
145
|
+
// ─── rows ─────────────────────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
interface SideStats {
|
|
148
|
+
n: number
|
|
149
|
+
mean: number
|
|
150
|
+
median: number
|
|
151
|
+
min: number
|
|
152
|
+
max: number
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface TaskRow extends Partial<Test> {
|
|
156
|
+
scope: 'task'
|
|
157
|
+
task: string
|
|
158
|
+
metric: Metric
|
|
159
|
+
before: SideStats | null
|
|
160
|
+
after: SideStats | null
|
|
161
|
+
/** Relative change of the mean (0.1 is +10%); null when a side is empty or before is 0. */
|
|
162
|
+
change: number | null
|
|
163
|
+
wrongWay: boolean
|
|
164
|
+
significant: boolean
|
|
165
|
+
tags: Record<string, string>
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export interface PooledRow extends Test {
|
|
169
|
+
scope: 'pooled'
|
|
170
|
+
metric: Metric
|
|
171
|
+
/** The geometric mean of the per-task after/before ratios. */
|
|
172
|
+
ratio: number
|
|
173
|
+
change: number
|
|
174
|
+
/** How many tasks the row pooled. */
|
|
175
|
+
tasks: number
|
|
176
|
+
wrongWay: boolean
|
|
177
|
+
significant: boolean
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
type Row = TaskRow | PooledRow
|
|
181
|
+
|
|
182
|
+
export type Verdict = 'incomparable' | 'regressed' | 'inconclusive' | 'improved' | 'unchanged'
|
|
183
|
+
|
|
184
|
+
export interface Reason {
|
|
185
|
+
field: string
|
|
186
|
+
before?: string
|
|
187
|
+
after?: string
|
|
188
|
+
message: string
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export interface Comparison {
|
|
192
|
+
verdict: Verdict
|
|
193
|
+
incomparable: Reason[]
|
|
194
|
+
/** Tasks measured on only one side: listed, never compared. */
|
|
195
|
+
unmatched: string[]
|
|
196
|
+
/** Runs recorded as errors, left out of every metric. */
|
|
197
|
+
errorsExcluded: { before: number; after: number }
|
|
198
|
+
rows: Row[]
|
|
199
|
+
footer: { testCount: number; expectedByChance: number; alpha: number }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const value = (r: RunResult, m: Metric): number => (m === 'pass' ? (r.pass ? 1 : 0) : r[m])
|
|
203
|
+
|
|
204
|
+
function sideStats(xs: number[]): SideStats | null {
|
|
205
|
+
if (xs.length === 0) return null
|
|
206
|
+
return { n: xs.length, mean: mean(xs), median: median(xs), min: Math.min(...xs), max: Math.max(...xs) }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Worse in the metric's direction: lower pass is worse; higher is worse for every other metric. */
|
|
210
|
+
function worse(metric: Metric, before: number, after: number): boolean {
|
|
211
|
+
return metric === 'pass' ? after < before : after > before
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function taskRow(task: string, metric: Metric, a: number[], b: number[], tags: Record<string, string>): TaskRow {
|
|
215
|
+
const before = sideStats(a)
|
|
216
|
+
const after = sideStats(b)
|
|
217
|
+
const row: TaskRow = {
|
|
218
|
+
scope: 'task',
|
|
219
|
+
task,
|
|
220
|
+
metric,
|
|
221
|
+
before,
|
|
222
|
+
after,
|
|
223
|
+
change: before && after && before.mean !== 0 ? after.mean / before.mean - 1 : null,
|
|
224
|
+
wrongWay: false,
|
|
225
|
+
significant: false,
|
|
226
|
+
tags,
|
|
227
|
+
}
|
|
228
|
+
if (!before || !after) return row
|
|
229
|
+
const test = pValue(
|
|
230
|
+
difference(before.mean, after.mean),
|
|
231
|
+
relabellings(a, b, difference, rng(seedOf(`${task}\0${metric}`))),
|
|
232
|
+
minAttainableP([[a.length, b.length]]),
|
|
233
|
+
)
|
|
234
|
+
Object.assign(row, test)
|
|
235
|
+
row.wrongWay = worse(metric, before.mean, after.mean)
|
|
236
|
+
row.significant = test.p < ALPHA
|
|
237
|
+
return row
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function pooledRow(metric: Metric, pairs: Array<[number[], number[]]>): PooledRow | undefined {
|
|
241
|
+
const usable = pairs.filter(([a, b]) => a.length > 0 && b.length > 0 && [...a, ...b].every((x) => x > 0))
|
|
242
|
+
if (usable.length === 0) return undefined
|
|
243
|
+
const random = rng(seedOf(`pooled\0${metric}`))
|
|
244
|
+
const observed = mean(usable.map(([a, b]) => logRatio(mean(a), mean(b))))
|
|
245
|
+
const perTask = usable.map(([a, b]) => relabellings(a, b, logRatio, random))
|
|
246
|
+
const size = perTask.reduce((n, t) => n * t.stats.length, 1)
|
|
247
|
+
const exact = perTask.every((t) => t.exact) && size <= EXACT_LIMIT
|
|
248
|
+
const stats: number[] = []
|
|
249
|
+
if (exact) {
|
|
250
|
+
const walk = (i: number, acc: number) => {
|
|
251
|
+
if (i === perTask.length) {
|
|
252
|
+
stats.push(acc / perTask.length)
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
for (const s of (perTask[i] as Relabellings).stats) walk(i + 1, acc + s)
|
|
256
|
+
}
|
|
257
|
+
walk(0, 0)
|
|
258
|
+
} else {
|
|
259
|
+
for (let n = 0; n < SAMPLES; n++) {
|
|
260
|
+
let acc = 0
|
|
261
|
+
for (const t of perTask) acc += t.stats[Math.floor(random() * t.stats.length)] as number
|
|
262
|
+
stats.push(acc / perTask.length)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const test = pValue(observed, { stats, exact }, minAttainableP(usable.map(([a, b]) => [a.length, b.length])))
|
|
266
|
+
const ratio = Math.exp(observed)
|
|
267
|
+
return {
|
|
268
|
+
scope: 'pooled',
|
|
269
|
+
metric,
|
|
270
|
+
ratio,
|
|
271
|
+
change: ratio - 1,
|
|
272
|
+
tasks: usable.length,
|
|
273
|
+
...test,
|
|
274
|
+
wrongWay: observed > 1e-12,
|
|
275
|
+
significant: test.p < ALPHA,
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ─── comparability ────────────────────────────────────────────────────────────
|
|
280
|
+
|
|
281
|
+
const MATCHED_FIELDS: Array<[string, (r: RunRecord) => string]> = [
|
|
282
|
+
['layer', (r) => String(r.layer)],
|
|
283
|
+
['model', (r) => String(r.model)],
|
|
284
|
+
['harness', (r) => String(r.harness)],
|
|
285
|
+
['adapter', (r) => String(r.adapter)],
|
|
286
|
+
['runner', (r) => String(r.runner)],
|
|
287
|
+
['subject kind', (r) => String(r.subject?.kind)],
|
|
288
|
+
['task-set hash', (r) => String(r.taskSetHash)],
|
|
289
|
+
]
|
|
290
|
+
|
|
291
|
+
function incomparableReasons(a: RunRecord, b: RunRecord): Reason[] {
|
|
292
|
+
const reasons: Reason[] = []
|
|
293
|
+
for (const [field, get] of MATCHED_FIELDS) {
|
|
294
|
+
const x = get(a)
|
|
295
|
+
const y = get(b)
|
|
296
|
+
if (x !== y) reasons.push({ field, before: x, after: y, message: `${field} differs: before "${x}", after "${y}"` })
|
|
297
|
+
}
|
|
298
|
+
// An unknown model never matches another unknown: two such records may have run anything.
|
|
299
|
+
if (
|
|
300
|
+
(a.model === 'unknown' && b.model === 'unknown') ||
|
|
301
|
+
(a.scoring_model === 'unknown' && b.scoring_model === 'unknown')
|
|
302
|
+
)
|
|
303
|
+
reasons.push({ field: 'model', message: 'the model is unknown on both sides' })
|
|
304
|
+
return reasons
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function verdictOf(rows: Row[]): Verdict {
|
|
308
|
+
const g = rows.filter((r) => GATED.has(r.metric))
|
|
309
|
+
if (g.some((r) => r.wrongWay && r.significant)) return 'regressed'
|
|
310
|
+
if (g.some((r) => r.wrongWay)) return 'inconclusive'
|
|
311
|
+
if (g.some((r) => r.significant)) return 'improved'
|
|
312
|
+
return 'unchanged'
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Compares `after` against `before`. Never refuses: a mismatch is an `incomparable` verdict. */
|
|
316
|
+
export function compareRecords(before: RunRecord, after: RunRecord): Comparison {
|
|
317
|
+
const incomparable = incomparableReasons(before, after)
|
|
318
|
+
const beforeTasks = [...new Set(before.runs.map((r) => r.task))]
|
|
319
|
+
const afterTasks = [...new Set(after.runs.map((r) => r.task))]
|
|
320
|
+
const unmatched = [
|
|
321
|
+
...beforeTasks.filter((t) => !afterTasks.includes(t)),
|
|
322
|
+
...afterTasks.filter((t) => !beforeTasks.includes(t)),
|
|
323
|
+
]
|
|
324
|
+
const empty: Comparison = {
|
|
325
|
+
verdict: 'incomparable',
|
|
326
|
+
incomparable,
|
|
327
|
+
unmatched,
|
|
328
|
+
errorsExcluded: { before: 0, after: 0 },
|
|
329
|
+
rows: [],
|
|
330
|
+
footer: { testCount: 0, expectedByChance: 0, alpha: ALPHA },
|
|
331
|
+
}
|
|
332
|
+
if (incomparable.length > 0) return empty
|
|
333
|
+
|
|
334
|
+
const tasks = beforeTasks.filter((t) => afterTasks.includes(t))
|
|
335
|
+
const errorsExcluded = { before: 0, after: 0 }
|
|
336
|
+
const kept = (record: RunRecord, task: string, side: 'before' | 'after') => {
|
|
337
|
+
const rs = record.runs.filter((r) => r.task === task)
|
|
338
|
+
const ok = rs.filter((r) => !r.error)
|
|
339
|
+
errorsExcluded[side] += rs.length - ok.length
|
|
340
|
+
return ok
|
|
341
|
+
}
|
|
342
|
+
const rows: Row[] = []
|
|
343
|
+
const pairs = new Map<Metric, Array<[number[], number[]]>>()
|
|
344
|
+
for (const task of tasks) {
|
|
345
|
+
const a = kept(before, task, 'before')
|
|
346
|
+
const b = kept(after, task, 'after')
|
|
347
|
+
const tags = after.tags?.[task] ?? before.tags?.[task] ?? {}
|
|
348
|
+
for (const metric of METRICS) {
|
|
349
|
+
const va = a.map((r) => value(r, metric))
|
|
350
|
+
const vb = b.map((r) => value(r, metric))
|
|
351
|
+
rows.push(taskRow(task, metric, va, vb, { ...tags }))
|
|
352
|
+
if (metric !== 'pass') pairs.set(metric, [...(pairs.get(metric) ?? []), [va, vb]])
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
for (const metric of METRICS) {
|
|
356
|
+
if (metric === 'pass') continue
|
|
357
|
+
const row = pooledRow(metric, pairs.get(metric) ?? [])
|
|
358
|
+
if (row) rows.push(row)
|
|
359
|
+
}
|
|
360
|
+
const testCount = rows.filter((r) => r.p !== undefined).length
|
|
361
|
+
return {
|
|
362
|
+
verdict: verdictOf(rows),
|
|
363
|
+
incomparable,
|
|
364
|
+
unmatched,
|
|
365
|
+
errorsExcluded,
|
|
366
|
+
rows,
|
|
367
|
+
footer: { testCount, expectedByChance: round(testCount * ALPHA), alpha: ALPHA },
|
|
368
|
+
}
|
|
369
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// bench-record — the shapes and locations the measured layer reads and writes.
|
|
2
|
+
//
|
|
3
|
+
// A run record is schema version 3 — the first version this engine writes and the ONLY one it
|
|
4
|
+
// reads. A record another tool wrote is converted by that tool; there is no reader here for any
|
|
5
|
+
// other shape, so a consumer never silently misreads a record of another version.
|
|
6
|
+
|
|
7
|
+
import { join } from 'node:path'
|
|
8
|
+
|
|
9
|
+
export const SCHEMA_VERSION = 3
|
|
10
|
+
|
|
11
|
+
/** `^[a-z0-9]+(?:[-.][a-z0-9]+)*$` — lowercase segments joined by single `-` or `.`. */
|
|
12
|
+
export const SUITE_NAME_RULE = /^[a-z0-9]+(?:[-.][a-z0-9]+)*$/
|
|
13
|
+
|
|
14
|
+
export const BENCH_ROOT = join('.agents', 'aced', 'bench')
|
|
15
|
+
export const RESULTS_ROOT = join('.agents', 'aced', 'results', 'bench')
|
|
16
|
+
|
|
17
|
+
export function suiteDir(suite: string): string {
|
|
18
|
+
return join(BENCH_ROOT, suite)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function resultsDir(suite: string): string {
|
|
22
|
+
return join(RESULTS_ROOT, suite)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** An ISO timestamp safe in a file name: `:` and `.` become `-`. */
|
|
26
|
+
export function stamp(createdAt: string): string {
|
|
27
|
+
return createdAt.replace(/[:.]/g, '-')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The compared metrics. `pass` is 0/1 and has no pooled row. */
|
|
31
|
+
export const METRICS = [
|
|
32
|
+
'pass',
|
|
33
|
+
'inputTokens',
|
|
34
|
+
'outputTokens',
|
|
35
|
+
'cacheReadTokens',
|
|
36
|
+
'turns',
|
|
37
|
+
'toolCalls',
|
|
38
|
+
'wallMs',
|
|
39
|
+
'costUsd',
|
|
40
|
+
] as const
|
|
41
|
+
export type Metric = (typeof METRICS)[number]
|
|
42
|
+
|
|
43
|
+
/** Metrics that can make a verdict `regressed`. Cost and cache reads are compared but never gate. */
|
|
44
|
+
export const GATED: ReadonlySet<Metric> = new Set([
|
|
45
|
+
'pass',
|
|
46
|
+
'turns',
|
|
47
|
+
'toolCalls',
|
|
48
|
+
'inputTokens',
|
|
49
|
+
'outputTokens',
|
|
50
|
+
'wallMs',
|
|
51
|
+
])
|
|
52
|
+
|
|
53
|
+
export type Subject =
|
|
54
|
+
| { kind: 'git-ref'; ref: string; commit: string }
|
|
55
|
+
| { kind: 'package'; name: string; version: string }
|
|
56
|
+
| { kind: 'file'; path: string; from: string }
|
|
57
|
+
|
|
58
|
+
export interface RunResult {
|
|
59
|
+
task: string
|
|
60
|
+
run: number
|
|
61
|
+
pass: boolean
|
|
62
|
+
wallMs: number
|
|
63
|
+
inputTokens: number
|
|
64
|
+
outputTokens: number
|
|
65
|
+
cacheReadTokens: number
|
|
66
|
+
cacheCreationTokens: number
|
|
67
|
+
turns: number
|
|
68
|
+
toolCalls: number
|
|
69
|
+
costUsd: number
|
|
70
|
+
/** Stopped without a success result: budget cap, error result, timeout, or the harness dying. */
|
|
71
|
+
capped: boolean
|
|
72
|
+
/** Set only when setup failed before the agent ran. */
|
|
73
|
+
error: string | null
|
|
74
|
+
/** The gzipped transcript, repo-relative. Absent from `baseline.json`. */
|
|
75
|
+
transcript?: string | null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface EvaluatedEntry {
|
|
79
|
+
path: string
|
|
80
|
+
sha256: string
|
|
81
|
+
kind: 'file'
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface Summary {
|
|
85
|
+
runs: number
|
|
86
|
+
passes: number
|
|
87
|
+
passRate: number
|
|
88
|
+
capped: number
|
|
89
|
+
errors: number
|
|
90
|
+
medianInputTokens: number
|
|
91
|
+
medianOutputTokens: number
|
|
92
|
+
medianCacheReadTokens: number
|
|
93
|
+
medianTurns: number
|
|
94
|
+
medianToolCalls: number
|
|
95
|
+
medianWallMs: number
|
|
96
|
+
totalCostUsd: number
|
|
97
|
+
/** Total cost over passes; absent when nothing passed. */
|
|
98
|
+
costPerSuccessUsd?: number
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface RunRecord {
|
|
102
|
+
schemaVersion: typeof SCHEMA_VERSION
|
|
103
|
+
layer: 'measured'
|
|
104
|
+
suite: string
|
|
105
|
+
arm: string
|
|
106
|
+
subject: Subject
|
|
107
|
+
harness: string
|
|
108
|
+
adapter: string
|
|
109
|
+
runner: string
|
|
110
|
+
taskSetCommit: string
|
|
111
|
+
taskSetHash: string
|
|
112
|
+
tasks: string[]
|
|
113
|
+
/** Each measured task's `tags` from tasks.json, by task id. */
|
|
114
|
+
tags: Record<string, Record<string, string>>
|
|
115
|
+
model: string
|
|
116
|
+
scoring_model: string
|
|
117
|
+
createdAt: string
|
|
118
|
+
evaluated: EvaluatedEntry[]
|
|
119
|
+
runs: RunResult[]
|
|
120
|
+
summary: { overall: Summary; tasks: Array<Summary & { task: string }> }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function median(values: number[]): number {
|
|
124
|
+
if (values.length === 0) return 0
|
|
125
|
+
const sorted = [...values].sort((a, b) => a - b)
|
|
126
|
+
const mid = Math.floor(sorted.length / 2)
|
|
127
|
+
return sorted.length % 2 ? (sorted[mid] as number) : ((sorted[mid - 1] as number) + (sorted[mid] as number)) / 2
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function round(n: number, places = 4): number {
|
|
131
|
+
const f = 10 ** places
|
|
132
|
+
return Math.round(n * f) / f
|
|
133
|
+
}
|