@tangle-network/agent-bench 0.3.7 → 0.3.8

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.
Files changed (191) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/HARNESS.md +43 -0
  3. package/dist/adapters.js +23 -23
  4. package/dist/benchmarks/_harness.d.ts +1 -1
  5. package/dist/benchmarks/_harness.js +1 -1
  6. package/dist/benchmarks/aec-bench.js +2 -2
  7. package/dist/benchmarks/agentbench.js +2 -2
  8. package/dist/benchmarks/appworld.js +2 -2
  9. package/dist/benchmarks/bfcl.js +2 -2
  10. package/dist/benchmarks/commit0.js +2 -2
  11. package/dist/benchmarks/crag.js +2 -2
  12. package/dist/benchmarks/dabstep.js +2 -2
  13. package/dist/benchmarks/enterpriseops-gym.js +2 -2
  14. package/dist/benchmarks/finresearchbench.js +2 -2
  15. package/dist/benchmarks/nomiracl.js +2 -2
  16. package/dist/benchmarks/open-rag-bench.js +2 -2
  17. package/dist/benchmarks/programbench.js +2 -2
  18. package/dist/benchmarks/ragbench.js +2 -2
  19. package/dist/benchmarks/swe-bench.js +2 -2
  20. package/dist/benchmarks/t2-ragbench.js +2 -2
  21. package/dist/benchmarks/tau-bench-shared.js +2 -2
  22. package/dist/benchmarks/tau2-bench.js +3 -3
  23. package/dist/benchmarks/tau3-banking.js +3 -3
  24. package/dist/benchmarks/terminal-bench.js +2 -2
  25. package/dist/benchmarks/toollm.js +2 -2
  26. package/dist/benchmarks/webarena-verified.js +2 -2
  27. package/dist/{chunk-CKUVRZ2T.js → chunk-3U5TXJZS.js} +2 -2
  28. package/dist/{chunk-YCGY7UIZ.js → chunk-7GRVHU22.js} +2 -2
  29. package/dist/{chunk-Z7ML6L77.js → chunk-HWST3SED.js} +2 -2
  30. package/dist/{chunk-SYDW647C.js → chunk-IA2FBTWC.js} +2 -2
  31. package/dist/{chunk-R67DFVLO.js → chunk-IFVINJ4B.js} +2 -2
  32. package/dist/{chunk-R36V2VP7.js → chunk-IZ5M6OAC.js} +2 -2
  33. package/dist/{chunk-ODT47UAY.js → chunk-K3BQGZCT.js} +2 -2
  34. package/dist/{chunk-IFAV6KEM.js → chunk-KP5KD6EN.js} +2 -2
  35. package/dist/{chunk-ZEWMTR5M.js → chunk-MQMRLGOG.js} +2 -2
  36. package/dist/{chunk-TSWPNOYM.js → chunk-NQG5XDSB.js} +2 -2
  37. package/dist/{chunk-7WSD27QQ.js → chunk-PB64GYIG.js} +2 -2
  38. package/dist/{chunk-HBSWHQNJ.js → chunk-RCYQEFNX.js} +3 -3
  39. package/dist/{chunk-J3KDJNX2.js → chunk-RH5F53JT.js} +2 -2
  40. package/dist/{chunk-UAIOHCUK.js → chunk-SFLA7OH3.js} +3 -3
  41. package/dist/{chunk-Y6O2OCUO.js → chunk-SHM6MRRF.js} +2 -2
  42. package/dist/{chunk-KDIKRJGB.js → chunk-SHYIRB7I.js} +2 -2
  43. package/dist/{chunk-HHXFIHXC.js → chunk-SVR2LKYI.js} +2 -2
  44. package/dist/{chunk-PWQVGAJB.js → chunk-V7AEBY6U.js} +21 -21
  45. package/dist/{chunk-LRRD7NAG.js → chunk-WSKWVEQB.js} +18 -2
  46. package/dist/chunk-WSKWVEQB.js.map +1 -0
  47. package/dist/{chunk-2PVVP7GN.js → chunk-XKEFIFIC.js} +2 -2
  48. package/dist/{chunk-JRWWGMK7.js → chunk-XYA4XSNU.js} +2 -2
  49. package/dist/{chunk-X5YKXC6V.js → chunk-YSMEKBTD.js} +2 -2
  50. package/dist/{chunk-2XU6OGEN.js → chunk-Z4TZ76N7.js} +2 -2
  51. package/dist/index.js +23 -23
  52. package/package.json +4 -4
  53. package/scripts/verify-packed-consumer.mjs +1 -1
  54. package/src/benchmarks/_harness.ts +20 -2
  55. package/src/benchmarks/humaneval.test.mts +2 -2
  56. package/src/quant-arena/README.md +144 -0
  57. package/src/quant-arena/backtest.test.mts +135 -0
  58. package/src/quant-arena/backtest.ts +218 -0
  59. package/src/quant-arena/data.test.mts +44 -0
  60. package/src/quant-arena/data.ts +141 -0
  61. package/src/quant-arena/driver.test.mts +253 -0
  62. package/src/quant-arena/driver.ts +219 -0
  63. package/src/quant-arena/fixtures/data/PROVENANCE.md +26 -0
  64. package/src/quant-arena/fixtures/data/holdout/IDX.csv +523 -0
  65. package/src/quant-arena/fixtures/data/holdout/S01.csv +523 -0
  66. package/src/quant-arena/fixtures/data/holdout/S02.csv +523 -0
  67. package/src/quant-arena/fixtures/data/holdout/S03.csv +523 -0
  68. package/src/quant-arena/fixtures/data/holdout/S04.csv +523 -0
  69. package/src/quant-arena/fixtures/data/holdout/S05.csv +523 -0
  70. package/src/quant-arena/fixtures/data/holdout/S06.csv +523 -0
  71. package/src/quant-arena/fixtures/data/holdout/S07.csv +523 -0
  72. package/src/quant-arena/fixtures/data/holdout/S08.csv +523 -0
  73. package/src/quant-arena/fixtures/data/holdout/S09.csv +523 -0
  74. package/src/quant-arena/fixtures/data/holdout/S10.csv +523 -0
  75. package/src/quant-arena/fixtures/data/insample/IDX.csv +2087 -0
  76. package/src/quant-arena/fixtures/data/insample/S01.csv +2087 -0
  77. package/src/quant-arena/fixtures/data/insample/S02.csv +2087 -0
  78. package/src/quant-arena/fixtures/data/insample/S03.csv +2087 -0
  79. package/src/quant-arena/fixtures/data/insample/S04.csv +2087 -0
  80. package/src/quant-arena/fixtures/data/insample/S05.csv +2087 -0
  81. package/src/quant-arena/fixtures/data/insample/S06.csv +2087 -0
  82. package/src/quant-arena/fixtures/data/insample/S07.csv +2087 -0
  83. package/src/quant-arena/fixtures/data/insample/S08.csv +2087 -0
  84. package/src/quant-arena/fixtures/data/insample/S09.csv +2087 -0
  85. package/src/quant-arena/fixtures/data/insample/S10.csv +2087 -0
  86. package/src/quant-arena/fixtures/demo-campaign/cost-ledger.jsonl +16 -0
  87. package/src/quant-arena/fixtures/demo-campaign/notebook.jsonl +5 -0
  88. package/src/quant-arena/fixtures/demo-campaign/rollout-manifest.json +171 -0
  89. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-001-default-author/strategy.ts +119 -0
  90. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-002-default-author/strategy.ts +119 -0
  91. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-003-quant-researcher/strategy.ts +105 -0
  92. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-004-quant-researcher/strategy.ts +102 -0
  93. package/src/quant-arena/fixtures/demo-campaign-v2/cost-ledger.jsonl +4 -0
  94. package/src/quant-arena/fixtures/demo-campaign-v2/notebook.jsonl +2 -0
  95. package/src/quant-arena/fixtures/demo-campaign-v2/rollout-manifest.json +84 -0
  96. package/src/quant-arena/fixtures/demo-campaign-v2/strategies/cand-001-quant-researcher/strategy.ts +117 -0
  97. package/src/quant-arena/holdout-certify.mts +206 -0
  98. package/src/quant-arena/holdout-certify.test.mts +82 -0
  99. package/src/quant-arena/leak-audit.test.mts +79 -0
  100. package/src/quant-arena/leak-audit.ts +95 -0
  101. package/src/quant-arena/make-fixtures.mts +161 -0
  102. package/src/quant-arena/multiplicity.test.mts +68 -0
  103. package/src/quant-arena/multiplicity.ts +87 -0
  104. package/src/quant-arena/nautilus-certify.ts +31 -0
  105. package/src/quant-arena/oms.ts +90 -0
  106. package/src/quant-arena/profiles/quant-researcher.profile.json +7 -0
  107. package/src/quant-arena/python/pyproject.toml +8 -0
  108. package/src/quant-arena/python/uv.lock +1297 -0
  109. package/src/quant-arena/python/vbt-worker.py +192 -0
  110. package/src/quant-arena/quant-loop.mts +813 -0
  111. package/src/quant-arena/quant-loop.test.mts +75 -0
  112. package/src/quant-arena/strategies/buy-hold-index/strategy.ts +11 -0
  113. package/src/quant-arena/strategies/equal-weight/strategy.ts +20 -0
  114. package/src/quant-arena/strategies/sma-crossover/strategy.ts +42 -0
  115. package/src/quant-arena/types.ts +133 -0
  116. package/src/quant-arena/vbt-client.ts +321 -0
  117. package/src/quant-arena/vbt-parity.test.mts +183 -0
  118. package/src/quant-arena/windows.test.mts +45 -0
  119. package/src/quant-arena/windows.ts +54 -0
  120. package/src/rollout-ledger/backfill-swe-arena.mts +606 -0
  121. package/src/rollout-ledger/backfill-swe-arena.test.mts +338 -0
  122. package/src/rollout-ledger/settle-capture.mts +442 -0
  123. package/src/rollout-ledger/settle-capture.test.mts +270 -0
  124. package/src/swe-arena/activation.mts +228 -0
  125. package/src/swe-arena/activation.test.mts +303 -0
  126. package/src/swe-arena/arms.ts +30 -14
  127. package/src/swe-arena/briefing.mts +217 -0
  128. package/src/swe-arena/briefing.test.mts +178 -0
  129. package/src/swe-arena/calibrate.ts +102 -1
  130. package/src/swe-arena/cell-evidence.mts +41 -9
  131. package/src/swe-arena/factory-command-container.ts +284 -0
  132. package/src/swe-arena/factory-judge-child.mts +228 -0
  133. package/src/swe-arena/factory.test.mts +643 -0
  134. package/src/swe-arena/fixtures/factory/agent-eval-309/calibration.md +51 -0
  135. package/src/swe-arena/fixtures/factory/agent-eval-309/manifest.json +29 -0
  136. package/src/swe-arena/fixtures/factory/agent-eval-309/spec.md +64 -0
  137. package/src/swe-arena/fixtures/factory/agent-runtime-232/calibration.md +48 -0
  138. package/src/swe-arena/fixtures/factory/agent-runtime-232/manifest.json +29 -0
  139. package/src/swe-arena/fixtures/factory/agent-runtime-232/spec.md +48 -0
  140. package/src/swe-arena/fixtures/factory/loops-28/calibration.md +47 -0
  141. package/src/swe-arena/fixtures/factory/loops-28/manifest.json +30 -0
  142. package/src/swe-arena/fixtures/factory/loops-28/spec.md +50 -0
  143. package/src/swe-arena/fixtures/run-report/README.md +43 -0
  144. package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.json +173 -0
  145. package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.md +100 -0
  146. package/src/swe-arena/fixtures/run-report/gen3-rollup.json +551 -0
  147. package/src/swe-arena/fixtures/run-report/gen3-rollup.md +64 -0
  148. package/src/swe-arena/fixtures.ts +135 -2
  149. package/src/swe-arena/gepa-seat.mts +583 -0
  150. package/src/swe-arena/gepa-seat.test.mts +635 -0
  151. package/src/swe-arena/ledger-orphans.mts +77 -0
  152. package/src/swe-arena/ledger-orphans.test.mts +147 -0
  153. package/src/swe-arena/lineage-record.mts +164 -0
  154. package/src/swe-arena/lineage-record.test.mts +115 -0
  155. package/src/swe-arena/outer-loop.mts +677 -27
  156. package/src/swe-arena/premeasured-from-cells.mts +281 -0
  157. package/src/swe-arena/premeasured-from-cells.test.mts +180 -0
  158. package/src/swe-arena/proposer-fanout.mts +295 -14
  159. package/src/swe-arena/proposer-fanout.test.mts +247 -0
  160. package/src/swe-arena/proposer-provenance.mts +177 -0
  161. package/src/swe-arena/proposer-provenance.test.mts +106 -0
  162. package/src/swe-arena/run-experiment.mts +367 -1
  163. package/src/swe-arena/run-report.mts +75 -0
  164. package/src/swe-arena/run-supervisor.test.mts +5 -3
  165. package/src/swe-arena/score-split.mts +140 -0
  166. package/src/swe-arena/score-split.test.mts +123 -0
  167. package/src/swe-arena/types.ts +52 -0
  168. package/src/swe-structural.mts +245 -837
  169. package/dist/chunk-LRRD7NAG.js.map +0 -1
  170. /package/dist/{chunk-CKUVRZ2T.js.map → chunk-3U5TXJZS.js.map} +0 -0
  171. /package/dist/{chunk-YCGY7UIZ.js.map → chunk-7GRVHU22.js.map} +0 -0
  172. /package/dist/{chunk-Z7ML6L77.js.map → chunk-HWST3SED.js.map} +0 -0
  173. /package/dist/{chunk-SYDW647C.js.map → chunk-IA2FBTWC.js.map} +0 -0
  174. /package/dist/{chunk-R67DFVLO.js.map → chunk-IFVINJ4B.js.map} +0 -0
  175. /package/dist/{chunk-R36V2VP7.js.map → chunk-IZ5M6OAC.js.map} +0 -0
  176. /package/dist/{chunk-ODT47UAY.js.map → chunk-K3BQGZCT.js.map} +0 -0
  177. /package/dist/{chunk-IFAV6KEM.js.map → chunk-KP5KD6EN.js.map} +0 -0
  178. /package/dist/{chunk-ZEWMTR5M.js.map → chunk-MQMRLGOG.js.map} +0 -0
  179. /package/dist/{chunk-TSWPNOYM.js.map → chunk-NQG5XDSB.js.map} +0 -0
  180. /package/dist/{chunk-7WSD27QQ.js.map → chunk-PB64GYIG.js.map} +0 -0
  181. /package/dist/{chunk-HBSWHQNJ.js.map → chunk-RCYQEFNX.js.map} +0 -0
  182. /package/dist/{chunk-J3KDJNX2.js.map → chunk-RH5F53JT.js.map} +0 -0
  183. /package/dist/{chunk-UAIOHCUK.js.map → chunk-SFLA7OH3.js.map} +0 -0
  184. /package/dist/{chunk-Y6O2OCUO.js.map → chunk-SHM6MRRF.js.map} +0 -0
  185. /package/dist/{chunk-KDIKRJGB.js.map → chunk-SHYIRB7I.js.map} +0 -0
  186. /package/dist/{chunk-HHXFIHXC.js.map → chunk-SVR2LKYI.js.map} +0 -0
  187. /package/dist/{chunk-PWQVGAJB.js.map → chunk-V7AEBY6U.js.map} +0 -0
  188. /package/dist/{chunk-2PVVP7GN.js.map → chunk-XKEFIFIC.js.map} +0 -0
  189. /package/dist/{chunk-JRWWGMK7.js.map → chunk-XYA4XSNU.js.map} +0 -0
  190. /package/dist/{chunk-X5YKXC6V.js.map → chunk-YSMEKBTD.js.map} +0 -0
  191. /package/dist/{chunk-2XU6OGEN.js.map → chunk-Z4TZ76N7.js.map} +0 -0
@@ -0,0 +1,281 @@
1
+ /**
2
+ * Premeasured-baseline artifact builder — reconstruct the lib's
3
+ * `PremeasuredOptimizationBaseline` ({surfaceHash, campaign}) from a PRIOR
4
+ * run's on-disk baseline campaign cells.
5
+ *
6
+ * Why this exists: the gen-3 run (r4-mrwc0awe) measured the full 6-instance ×
7
+ * 2-rep baseline but never wrote the artifact — its bootstrap writer keyed on
8
+ * the Pareto frontier's generation −1 entry, and the frontier the lib returned
9
+ * held no such entry, so the measurement survives ONLY as per-cell
10
+ * `cached-result.json` caches under `<gen3>/improve-run/baseline/`. Those
11
+ * caches cannot be replayed into a NEW runDir: the lib's cache-hit path
12
+ * requires every `costCallIds` receipt in the CURRENT run's ledger (tagged
13
+ * with the current runDir), which a fresh outDir cannot satisfy. The
14
+ * premeasured-artifact path has no such coupling — `validatedPremeasuredBaseline`
15
+ * checks surface hash, seed, reps, and split digest, then uses the campaign
16
+ * as-is — so rebuilding the artifact is the designed way to pin a prior run's
17
+ * measured baseline.
18
+ *
19
+ * Everything identity-bearing is REAL, never fabricated:
20
+ * - cells: verbatim `cached-result.json` records (verdicts, spend, receipts);
21
+ * - surfaceHash: recomputed from the loops repo's base-ref tip (identical
22
+ * incumbent surface shape the lib builds: baseCommit == candidateCommit,
23
+ * empty patch) — verified equal to gen-3's recorded baseline hash;
24
+ * - splitDigest/scenarios: the lib's own `campaignSplitDigest` /
25
+ * `campaignScenarioIdentity` over the exact scenario payloads;
26
+ * - seed/manifestHash: carried from the cells, uniformity asserted;
27
+ * - run window: reconstructed from cache-file mtimes (endedAt = last cell
28
+ * write; startedAt = earliest write minus that cell's duration).
29
+ *
30
+ * tsx src/swe-arena/premeasured-from-cells.mts <config.json> --from <priorBaselineDir>
31
+ */
32
+
33
+ import { createHash } from 'node:crypto'
34
+ import { readdir, readFile, stat, writeFile } from 'node:fs/promises'
35
+ import { join } from 'node:path'
36
+ import process from 'node:process'
37
+ import { pathToFileURL } from 'node:url'
38
+ import {
39
+ assertCampaignSplitIdentity,
40
+ campaignScenarioIdentity,
41
+ campaignSplitDigest,
42
+ surfaceHash,
43
+ type CampaignCellResult,
44
+ type CampaignResult,
45
+ type CodeSurface,
46
+ type PremeasuredOptimizationBaseline,
47
+ type Scenario,
48
+ } from '@tangle-network/agent-eval/campaign'
49
+ import type { R4Artifact } from './cell-evidence.mts'
50
+ import type { OuterLoopConfig } from './outer-loop.mts'
51
+ import { runOk } from './proc.ts'
52
+
53
+ export type FullCell = CampaignCellResult<R4Artifact> & { mtimeMs?: number }
54
+
55
+ /** Read every `<dir>/<cell>/cached-result.json` as a FULL lib cell record. */
56
+ export async function loadFullCampaignCells(campaignDir: string): Promise<FullCell[]> {
57
+ const entries = await readdir(campaignDir, { withFileTypes: true })
58
+ const cells: FullCell[] = []
59
+ for (const entry of entries) {
60
+ if (!entry.isDirectory()) continue
61
+ const path = join(campaignDir, entry.name, 'cached-result.json')
62
+ const raw = await readFile(path, 'utf8').catch(() => null)
63
+ if (raw === null) continue
64
+ const cell = JSON.parse(raw) as FullCell
65
+ if (typeof cell.scenarioId !== 'string' || typeof cell.rep !== 'number') {
66
+ throw new Error(`premeasured-from-cells: ${path} is not a campaign cell`)
67
+ }
68
+ cell.mtimeMs = (await stat(path)).mtimeMs
69
+ cells.push(cell)
70
+ }
71
+ return cells
72
+ }
73
+
74
+ /** The incumbent surface hash the lib will compute for `baseRef`'s tip: an
75
+ * unchanged code surface (candidate == base, empty patch). Identity material
76
+ * is {baseCommit, baseTree, candidateTree, patch} — worktree/ref excluded. */
77
+ export async function incumbentSurfaceHash(loopsRepo: string, baseRef: string): Promise<string> {
78
+ const baseCommit = (await runOk('git', ['-C', loopsRepo, 'rev-parse', `${baseRef}^{commit}`])).stdout.trim()
79
+ const tree = (await runOk('git', ['-C', loopsRepo, 'rev-parse', `${baseRef}^{tree}`])).stdout.trim()
80
+ const surface: CodeSurface = {
81
+ kind: 'code',
82
+ worktreeRef: loopsRepo,
83
+ baseRef,
84
+ baseCommit,
85
+ baseTree: tree,
86
+ candidateCommit: baseCommit,
87
+ candidateTree: tree,
88
+ patch: {
89
+ format: 'git-diff-binary',
90
+ sha256: `sha256:${createHash('sha256').update(Buffer.alloc(0)).digest('hex')}`,
91
+ byteLength: 0,
92
+ },
93
+ }
94
+ return surfaceHash(surface)
95
+ }
96
+
97
+ function meanStdCi(values: number[]): { mean: number; stdev: number; ci95: [number, number]; n: number } {
98
+ const n = values.length
99
+ const mean = n === 0 ? 0 : values.reduce((s, v) => s + v, 0) / n
100
+ const variance = n <= 1 ? 0 : values.reduce((s, v) => s + (v - mean) ** 2, 0) / (n - 1)
101
+ const stdev = Math.sqrt(variance)
102
+ const half = n === 0 ? 0 : (1.96 * stdev) / Math.sqrt(n)
103
+ return { mean, stdev, ci95: [mean - half, mean + half], n }
104
+ }
105
+
106
+ /** Assemble the artifact from full cells. Pure over inputs; every check fails
107
+ * loud so a wrong artifact can never validate downstream by accident. */
108
+ export function buildPremeasuredFromCells(input: {
109
+ cells: FullCell[]
110
+ instances: string[]
111
+ reps: number
112
+ surfaceHash: string
113
+ /** Provenance label recorded as the campaign's runDir (the SOURCE dir). */
114
+ sourceDir: string
115
+ }): PremeasuredOptimizationBaseline<R4Artifact, Scenario> {
116
+ const { cells, instances, reps } = input
117
+ if (cells.length === 0) throw new Error('premeasured-from-cells: no cells')
118
+ for (const iid of instances) {
119
+ for (let rep = 0; rep < reps; rep++) {
120
+ const mine = cells.filter((c) => c.scenarioId === iid && c.rep === rep)
121
+ if (mine.length !== 1) {
122
+ throw new Error(`premeasured-from-cells: expected exactly 1 cell for ${iid} rep ${rep}, got ${mine.length}`)
123
+ }
124
+ }
125
+ }
126
+ const extras = cells.filter((c) => !instances.includes(c.scenarioId) || c.rep >= reps)
127
+ if (extras.length > 0) {
128
+ throw new Error(
129
+ `premeasured-from-cells: ${extras.length} cell(s) outside the ${instances.length}x${reps} design ` +
130
+ `(e.g. ${extras[0]!.scenarioId}#r${extras[0]!.rep})`,
131
+ )
132
+ }
133
+ // Per-cell seeds derive from the campaign base seed (run-campaign:
134
+ // `cellSeed = seed + groupIndex * reps + rep`), so a complete design carries
135
+ // the contiguous range [base, base + cells). The campaign-level seed the lib
136
+ // validates is the base.
137
+ const seeds = cells.map((c) => c.seed).sort((a, b) => a - b)
138
+ const baseSeed = seeds[0]!
139
+ for (let i = 0; i < seeds.length; i++) {
140
+ if (seeds[i] !== baseSeed + i) {
141
+ throw new Error(
142
+ `premeasured-from-cells: cell seeds are not the contiguous derived range from base ${baseSeed} ` +
143
+ `(got ${seeds.join(', ')})`,
144
+ )
145
+ }
146
+ }
147
+ const manifests = new Set(cells.map((c) => c.manifestHash ?? 'absent'))
148
+ if (manifests.size !== 1) {
149
+ throw new Error(`premeasured-from-cells: non-uniform manifestHash across cells (${[...manifests].join(', ')})`)
150
+ }
151
+ for (const cell of cells) {
152
+ if (cell.error !== undefined) throw new Error(`premeasured-from-cells: cell ${cell.cellId} carries an error`)
153
+ if (cell.artifact === null || cell.artifact.kind !== 'swe-arm') {
154
+ throw new Error(`premeasured-from-cells: cell ${cell.cellId} has no swe-arm artifact`)
155
+ }
156
+ }
157
+
158
+ const scenarios: Scenario[] = instances.map((iid) => ({ id: iid, kind: 'swe-instance' }))
159
+ const splitDigest = campaignSplitDigest(scenarios, reps)
160
+ const identities = scenarios.map((s) => campaignScenarioIdentity(s))
161
+ assertCampaignSplitIdentity(identities, reps, splitDigest)
162
+
163
+ const sorted = [...cells].sort(
164
+ (a, b) => instances.indexOf(a.scenarioId) - instances.indexOf(b.scenarioId) || a.rep - b.rep,
165
+ )
166
+ const composites = (cell: FullCell): number[] => Object.values(cell.judgeScores).map((s) => s.composite)
167
+ const judgeNames = [...new Set(sorted.flatMap((c) => Object.keys(c.judgeScores)))]
168
+ const byJudge = Object.fromEntries(
169
+ judgeNames.map((name) => {
170
+ const { mean, stdev, ci95, n } = meanStdCi(
171
+ sorted.filter((c) => c.judgeScores[name]).map((c) => c.judgeScores[name]!.composite),
172
+ )
173
+ return [name, { mean, stdev, ci95, n }]
174
+ }),
175
+ )
176
+ const byScenario = Object.fromEntries(
177
+ instances.map((iid) => {
178
+ const { mean, ci95, n } = meanStdCi(sorted.filter((c) => c.scenarioId === iid).flatMap(composites))
179
+ return [iid, { meanComposite: mean, ci95, n }]
180
+ }),
181
+ )
182
+ const totalCostUsd = sorted.reduce((s, c) => s + c.costUsd, 0)
183
+ const inputTokens = sorted.reduce((s, c) => s + c.tokenUsage.input, 0)
184
+ const outputTokens = sorted.reduce((s, c) => s + c.tokenUsage.output, 0)
185
+ const totalCalls = sorted.reduce((s, c) => s + (c.costCallIds?.length ?? 0), 0)
186
+ const models = [...new Set(sorted.map((c) => c.resolvedModel).filter((m): m is string => typeof m === 'string'))]
187
+
188
+ const mtimes = sorted.filter((c) => typeof c.mtimeMs === 'number')
189
+ const endedMs = mtimes.length > 0 ? Math.max(...mtimes.map((c) => c.mtimeMs!)) : 0
190
+ const startedMs = mtimes.length > 0 ? Math.min(...mtimes.map((c) => c.mtimeMs! - c.durationMs)) : 0
191
+
192
+ const campaign: CampaignResult<R4Artifact, Scenario> = {
193
+ manifestHash: sorted[0]!.manifestHash ?? '',
194
+ splitDigest,
195
+ seed: baseSeed,
196
+ reps,
197
+ startedAt: new Date(startedMs).toISOString(),
198
+ endedAt: new Date(endedMs).toISOString(),
199
+ durationMs: Math.max(0, endedMs - startedMs),
200
+ // Verbatim prior-run cells (marked cached — they were measured, and this
201
+ // artifact replays them without dispatch). The transient mtime rider is
202
+ // dropped from the persisted record.
203
+ cells: sorted.map(({ mtimeMs: _mtimeMs, ...cell }) => ({ ...cell, cached: true })),
204
+ aggregates: {
205
+ byJudge,
206
+ byScenario,
207
+ cost: {
208
+ totalCalls,
209
+ pendingCalls: 0,
210
+ unresolvedCalls: 0,
211
+ reservedCostUsd: 0,
212
+ inputTokens,
213
+ outputTokens,
214
+ cachedTokens: 0,
215
+ totalCostUsd,
216
+ byChannel: [
217
+ {
218
+ channel: 'agent',
219
+ calls: totalCalls,
220
+ inputTokens,
221
+ outputTokens,
222
+ cachedTokens: 0,
223
+ costUsd: totalCostUsd,
224
+ unpricedCalls: 0,
225
+ unknownUsageCalls: 0,
226
+ },
227
+ ],
228
+ unpricedModels: [],
229
+ fullyPriced: true,
230
+ usageComplete: true,
231
+ accountingComplete: true,
232
+ incompleteReasons: [],
233
+ },
234
+ totalCostUsd,
235
+ cellsExecuted: sorted.length,
236
+ cellsSkipped: 0,
237
+ cellsCached: sorted.length,
238
+ cellsFailed: 0,
239
+ },
240
+ runDir: input.sourceDir,
241
+ artifactsByPath: {},
242
+ scenarios: identities,
243
+ }
244
+ void models
245
+ return { surfaceHash: input.surfaceHash, campaign }
246
+ }
247
+
248
+ // ---------------------------------------------------------------------------
249
+ // CLI.
250
+ // ---------------------------------------------------------------------------
251
+
252
+ const isMain = process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href
253
+
254
+ if (isMain) {
255
+ const argv = process.argv.slice(2)
256
+ const configPath = argv[0]
257
+ const fromIdx = argv.indexOf('--from')
258
+ const fromDir = fromIdx !== -1 ? argv[fromIdx + 1] : undefined
259
+ if (!configPath || configPath.startsWith('--') || !fromDir) {
260
+ console.error('usage: tsx src/swe-arena/premeasured-from-cells.mts <config.json> --from <priorBaselineCampaignDir>')
261
+ process.exit(2)
262
+ }
263
+ const config = JSON.parse(await readFile(configPath, 'utf8')) as OuterLoopConfig
264
+ const reps = config.repsPerInstance ?? 1
265
+ const cells = await loadFullCampaignCells(fromDir)
266
+ const hash = await incumbentSurfaceHash(config.loopsRepo, config.loopsBaseRef)
267
+ const artifact = buildPremeasuredFromCells({
268
+ cells,
269
+ instances: config.instances,
270
+ reps,
271
+ surfaceHash: hash,
272
+ sourceDir: fromDir,
273
+ })
274
+ await writeFile(config.premeasuredBaselinePath, JSON.stringify(artifact, null, 1))
275
+ const resolved = artifact.campaign.cells.map(
276
+ (c) => `${c.scenarioId}#r${c.rep}=${(c.artifact as R4Artifact).resolved}`,
277
+ )
278
+ console.log(`premeasured baseline artifact → ${config.premeasuredBaselinePath}`)
279
+ console.log(`surfaceHash=${hash} splitDigest=${artifact.campaign.splitDigest} seed=${artifact.campaign.seed} reps=${reps}`)
280
+ console.log(resolved.join('\n'))
281
+ }
@@ -0,0 +1,180 @@
1
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
2
+ import { tmpdir } from 'node:os'
3
+ import { join } from 'node:path'
4
+ import { campaignSplitDigest, assertCampaignSplitIdentity, type Scenario } from '@tangle-network/agent-eval/campaign'
5
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
6
+ import { cellsFromCampaign, replicateRunsFromCells, resolvedInstanceCount } from './cell-evidence.mts'
7
+ import type { R4Artifact } from './cell-evidence.mts'
8
+ import {
9
+ buildPremeasuredFromCells,
10
+ incumbentSurfaceHash,
11
+ loadFullCampaignCells,
12
+ type FullCell,
13
+ } from './premeasured-from-cells.mts'
14
+ import { runOk } from './proc.ts'
15
+
16
+ const artifact = (iid: string, resolved: boolean): R4Artifact => ({
17
+ kind: 'swe-arm',
18
+ iid,
19
+ commit: 'c0ffee',
20
+ resolved,
21
+ verifyPass: resolved,
22
+ patchLines: 3,
23
+ wallS: 100,
24
+ spentTokens: 10,
25
+ spentUsd: 0.01,
26
+ recoveredTokens: 20,
27
+ workerTokIn: 5,
28
+ workerTokOut: 5,
29
+ judgeAttempts: 1,
30
+ judgeWallS: 9,
31
+ runDir: '/runs/x',
32
+ patchPath: '/patches/x.patch',
33
+ })
34
+
35
+ // Per-cell seeds mirror run-campaign's derivation: base 42 + groupIndex*reps + rep.
36
+ const derivedSeed = (iid: string, rep: number): number => 42 + (iid === 'inst-a' ? 0 : 2) + rep
37
+
38
+ const cell = (iid: string, rep: number, resolved: boolean, over: Partial<FullCell> = {}): FullCell => ({
39
+ manifestHash: 'm1',
40
+ cellId: `${iid}:${rep}`,
41
+ scenarioId: iid,
42
+ rep,
43
+ artifact: artifact(iid, resolved),
44
+ judgeScores: {
45
+ 'swe-arena-official-judge': {
46
+ composite: resolved ? 1 : 0,
47
+ dimensions: { resolved: resolved ? 1 : 0 },
48
+ notes: `official judge: ${iid} resolved=${resolved}`,
49
+ },
50
+ },
51
+ costUsd: 0.05,
52
+ costCallIds: [`call-${iid}-${rep}`],
53
+ tokenUsage: { input: 100, output: 50 },
54
+ resolvedModel: 'zai-coding-plan/glm-5.2',
55
+ durationMs: 60_000,
56
+ seed: derivedSeed(iid, rep),
57
+ cached: false,
58
+ mtimeMs: 1_000_000 + rep * 1000,
59
+ ...over,
60
+ })
61
+
62
+ const INSTANCES = ['inst-a', 'inst-b']
63
+
64
+ describe('buildPremeasuredFromCells', () => {
65
+ const cells = [cell('inst-a', 0, false), cell('inst-a', 1, true), cell('inst-b', 0, true), cell('inst-b', 1, true)]
66
+
67
+ it('assembles a campaign whose split identity passes the lib validation checks', () => {
68
+ const out = buildPremeasuredFromCells({
69
+ cells,
70
+ instances: INSTANCES,
71
+ reps: 2,
72
+ surfaceHash: 'abc123',
73
+ sourceDir: '/prior/baseline',
74
+ })
75
+ expect(out.surfaceHash).toBe('abc123')
76
+ const scenarios: Scenario[] = INSTANCES.map((id) => ({ id, kind: 'swe-instance' }))
77
+ // The exact checks validatedPremeasuredBaseline runs (minus surface):
78
+ expect(out.campaign.reps).toBe(2)
79
+ expect(out.campaign.seed).toBe(42)
80
+ expect(out.campaign.splitDigest).toBe(campaignSplitDigest(scenarios, 2))
81
+ expect(() => assertCampaignSplitIdentity(out.campaign.scenarios, 2, out.campaign.splitDigest)).not.toThrow()
82
+ // Cells replay verbatim into the harness scoring (AND rule: a=F, b=T → 1/2).
83
+ const evidence = cellsFromCampaign(out.campaign)
84
+ expect(resolvedInstanceCount(replicateRunsFromCells(evidence), INSTANCES, 2)).toBe(1)
85
+ expect(out.campaign.cells.every((c) => c.cached)).toBe(true)
86
+ expect(out.campaign.cells.every((c) => !('mtimeMs' in c))).toBe(true)
87
+ // Honest spend rollup from the real cells.
88
+ expect(out.campaign.aggregates.totalCostUsd).toBeCloseTo(0.2)
89
+ expect(out.campaign.aggregates.cost.inputTokens).toBe(400)
90
+ expect(out.campaign.runDir).toBe('/prior/baseline')
91
+ // Window reconstructed from mtimes: ends at the last cell write.
92
+ expect(out.campaign.endedAt).toBe(new Date(1_001_000).toISOString())
93
+ })
94
+
95
+ it('fails loud on a missing replicate, an extra cell, or a design mismatch', () => {
96
+ expect(() =>
97
+ buildPremeasuredFromCells({
98
+ cells: cells.slice(0, 3),
99
+ instances: INSTANCES,
100
+ reps: 2,
101
+ surfaceHash: 'x',
102
+ sourceDir: '/p',
103
+ }),
104
+ ).toThrow(/expected exactly 1 cell for inst-b rep 1/)
105
+ expect(() =>
106
+ buildPremeasuredFromCells({
107
+ cells: [...cells, cell('inst-c', 0, true)],
108
+ instances: INSTANCES,
109
+ reps: 2,
110
+ surfaceHash: 'x',
111
+ sourceDir: '/p',
112
+ }),
113
+ ).toThrow(/outside the 2x2 design/)
114
+ })
115
+
116
+ it('fails loud on broken seed derivation or manifest drift, an errored cell, or a missing artifact', () => {
117
+ const seedDrift = [cell('inst-a', 0, false, { seed: 7 }), cell('inst-a', 1, true), cell('inst-b', 0, true), cell('inst-b', 1, true)]
118
+ expect(() =>
119
+ buildPremeasuredFromCells({ cells: seedDrift, instances: INSTANCES, reps: 2, surfaceHash: 'x', sourceDir: '/p' }),
120
+ ).toThrow(/contiguous derived range/)
121
+ const manifestDrift = [cell('inst-a', 0, false, { manifestHash: 'OTHER' }), cell('inst-a', 1, true), cell('inst-b', 0, true), cell('inst-b', 1, true)]
122
+ expect(() =>
123
+ buildPremeasuredFromCells({ cells: manifestDrift, instances: INSTANCES, reps: 2, surfaceHash: 'x', sourceDir: '/p' }),
124
+ ).toThrow(/non-uniform manifestHash/)
125
+ const errored = [cell('inst-a', 0, false, { error: 'boom' }), cell('inst-a', 1, true), cell('inst-b', 0, true), cell('inst-b', 1, true)]
126
+ expect(() =>
127
+ buildPremeasuredFromCells({ cells: errored, instances: INSTANCES, reps: 2, surfaceHash: 'x', sourceDir: '/p' }),
128
+ ).toThrow(/carries an error/)
129
+ })
130
+ })
131
+
132
+ describe('loadFullCampaignCells + incumbentSurfaceHash (real fs/git)', () => {
133
+ let dir: string
134
+ let repo: string
135
+
136
+ beforeEach(async () => {
137
+ dir = await mkdtemp(join(tmpdir(), 'premeasured-'))
138
+ repo = await mkdtemp(join(tmpdir(), 'premeasured-repo-'))
139
+ })
140
+
141
+ afterEach(async () => {
142
+ await rm(dir, { recursive: true, force: true })
143
+ await rm(repo, { recursive: true, force: true })
144
+ })
145
+
146
+ it('round-trips full cells (costCallIds and manifest preserved) from disk', async () => {
147
+ const c = cell('inst-a', 0, true)
148
+ await mkdir(join(dir, 'inst-a_0'), { recursive: true })
149
+ const { mtimeMs: _m, ...persisted } = c
150
+ await writeFile(join(dir, 'inst-a_0', 'cached-result.json'), JSON.stringify(persisted))
151
+ const loaded = await loadFullCampaignCells(dir)
152
+ expect(loaded).toHaveLength(1)
153
+ expect(loaded[0]).toMatchObject({
154
+ scenarioId: 'inst-a',
155
+ rep: 0,
156
+ manifestHash: 'm1',
157
+ costCallIds: ['call-inst-a-0'],
158
+ seed: 42,
159
+ })
160
+ expect(typeof loaded[0]!.mtimeMs).toBe('number')
161
+ })
162
+
163
+ it('computes the same incumbent hash for the unchanged tip (baseCommit == candidateCommit, empty patch)', async () => {
164
+ await runOk('git', ['init', '-q', '-b', 'main', repo])
165
+ await runOk('git', ['-C', repo, 'config', 'user.email', 't@t.dev'])
166
+ await runOk('git', ['-C', repo, 'config', 'user.name', 'T'])
167
+ await writeFile(join(repo, 'a.txt'), 'x\n')
168
+ await runOk('git', ['-C', repo, 'add', '-A'])
169
+ await runOk('git', ['-C', repo, 'commit', '-q', '-m', 'init'])
170
+ const h1 = await incumbentSurfaceHash(repo, 'main')
171
+ const h2 = await incumbentSurfaceHash(repo, 'main')
172
+ expect(h1).toBe(h2)
173
+ expect(h1).toMatch(/^[0-9a-f]{16}$/)
174
+ // A moved tip changes the hash — the fail-loud property the artifact rides.
175
+ await writeFile(join(repo, 'a.txt'), 'y\n')
176
+ await runOk('git', ['-C', repo, 'add', '-A'])
177
+ await runOk('git', ['-C', repo, 'commit', '-q', '-m', 'move'])
178
+ expect(await incumbentSurfaceHash(repo, 'main')).not.toBe(h1)
179
+ })
180
+ })