@tangle-network/agent-bench 0.3.6 → 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 (265) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/HARNESS.md +43 -0
  3. package/dist/adapters.js +24 -24
  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/humaneval.d.ts +10 -1
  16. package/dist/benchmarks/humaneval.js +5 -3
  17. package/dist/benchmarks/nomiracl.js +2 -2
  18. package/dist/benchmarks/open-rag-bench.js +2 -2
  19. package/dist/benchmarks/programbench.js +2 -2
  20. package/dist/benchmarks/ragbench.js +2 -2
  21. package/dist/benchmarks/swe-bench.js +2 -2
  22. package/dist/benchmarks/t2-ragbench.js +2 -2
  23. package/dist/benchmarks/tau-bench-shared.js +2 -2
  24. package/dist/benchmarks/tau2-bench.js +3 -3
  25. package/dist/benchmarks/tau3-banking.js +3 -3
  26. package/dist/benchmarks/terminal-bench.js +2 -2
  27. package/dist/benchmarks/toollm.js +2 -2
  28. package/dist/benchmarks/webarena-verified.js +2 -2
  29. package/dist/{chunk-CKUVRZ2T.js → chunk-3U5TXJZS.js} +2 -2
  30. package/dist/{chunk-PPYSEKFM.js → chunk-5H5XV76F.js} +73 -15
  31. package/dist/chunk-5H5XV76F.js.map +1 -0
  32. package/dist/{chunk-YCGY7UIZ.js → chunk-7GRVHU22.js} +2 -2
  33. package/dist/{chunk-Z7ML6L77.js → chunk-HWST3SED.js} +2 -2
  34. package/dist/{chunk-SYDW647C.js → chunk-IA2FBTWC.js} +2 -2
  35. package/dist/{chunk-R67DFVLO.js → chunk-IFVINJ4B.js} +2 -2
  36. package/dist/{chunk-R36V2VP7.js → chunk-IZ5M6OAC.js} +2 -2
  37. package/dist/{chunk-ODT47UAY.js → chunk-K3BQGZCT.js} +2 -2
  38. package/dist/{chunk-IFAV6KEM.js → chunk-KP5KD6EN.js} +2 -2
  39. package/dist/{chunk-ZEWMTR5M.js → chunk-MQMRLGOG.js} +2 -2
  40. package/dist/{chunk-TSWPNOYM.js → chunk-NQG5XDSB.js} +2 -2
  41. package/dist/{chunk-7WSD27QQ.js → chunk-PB64GYIG.js} +2 -2
  42. package/dist/{chunk-HBSWHQNJ.js → chunk-RCYQEFNX.js} +3 -3
  43. package/dist/{chunk-J3KDJNX2.js → chunk-RH5F53JT.js} +2 -2
  44. package/dist/{chunk-UAIOHCUK.js → chunk-SFLA7OH3.js} +3 -3
  45. package/dist/{chunk-Y6O2OCUO.js → chunk-SHM6MRRF.js} +2 -2
  46. package/dist/{chunk-KDIKRJGB.js → chunk-SHYIRB7I.js} +2 -2
  47. package/dist/{chunk-HHXFIHXC.js → chunk-SVR2LKYI.js} +2 -2
  48. package/dist/{chunk-5SBJCB6W.js → chunk-V7AEBY6U.js} +22 -22
  49. package/dist/{chunk-LRRD7NAG.js → chunk-WSKWVEQB.js} +18 -2
  50. package/dist/chunk-WSKWVEQB.js.map +1 -0
  51. package/dist/{chunk-2PVVP7GN.js → chunk-XKEFIFIC.js} +2 -2
  52. package/dist/{chunk-JRWWGMK7.js → chunk-XYA4XSNU.js} +2 -2
  53. package/dist/{chunk-X5YKXC6V.js → chunk-YSMEKBTD.js} +2 -2
  54. package/dist/{chunk-2XU6OGEN.js → chunk-Z4TZ76N7.js} +2 -2
  55. package/dist/index.js +24 -24
  56. package/package.json +6 -5
  57. package/scripts/run-package-tests.mjs +30 -8
  58. package/scripts/verify-packed-consumer.mjs +1 -1
  59. package/scripts/verify-pier-agent.mts +1 -0
  60. package/src/benchmarks/_harness.ts +20 -2
  61. package/src/benchmarks/humaneval.test.mts +122 -0
  62. package/src/benchmarks/humaneval.ts +100 -27
  63. package/src/david-attribution.mts +78 -0
  64. package/src/david-goliath.mts +149 -0
  65. package/src/hev-improve.mts +25 -6
  66. package/src/humaneval-object-ablation.mts +201 -0
  67. package/src/live-improve-campaign-mbpp.mts +641 -0
  68. package/src/live-improve-campaign.mts +500 -0
  69. package/src/mbpp-structural.mts +12 -7
  70. package/src/quant-arena/README.md +144 -0
  71. package/src/quant-arena/backtest.test.mts +135 -0
  72. package/src/quant-arena/backtest.ts +218 -0
  73. package/src/quant-arena/data.test.mts +44 -0
  74. package/src/quant-arena/data.ts +141 -0
  75. package/src/quant-arena/driver.test.mts +253 -0
  76. package/src/quant-arena/driver.ts +219 -0
  77. package/src/quant-arena/fixtures/data/PROVENANCE.md +26 -0
  78. package/src/quant-arena/fixtures/data/holdout/IDX.csv +523 -0
  79. package/src/quant-arena/fixtures/data/holdout/S01.csv +523 -0
  80. package/src/quant-arena/fixtures/data/holdout/S02.csv +523 -0
  81. package/src/quant-arena/fixtures/data/holdout/S03.csv +523 -0
  82. package/src/quant-arena/fixtures/data/holdout/S04.csv +523 -0
  83. package/src/quant-arena/fixtures/data/holdout/S05.csv +523 -0
  84. package/src/quant-arena/fixtures/data/holdout/S06.csv +523 -0
  85. package/src/quant-arena/fixtures/data/holdout/S07.csv +523 -0
  86. package/src/quant-arena/fixtures/data/holdout/S08.csv +523 -0
  87. package/src/quant-arena/fixtures/data/holdout/S09.csv +523 -0
  88. package/src/quant-arena/fixtures/data/holdout/S10.csv +523 -0
  89. package/src/quant-arena/fixtures/data/insample/IDX.csv +2087 -0
  90. package/src/quant-arena/fixtures/data/insample/S01.csv +2087 -0
  91. package/src/quant-arena/fixtures/data/insample/S02.csv +2087 -0
  92. package/src/quant-arena/fixtures/data/insample/S03.csv +2087 -0
  93. package/src/quant-arena/fixtures/data/insample/S04.csv +2087 -0
  94. package/src/quant-arena/fixtures/data/insample/S05.csv +2087 -0
  95. package/src/quant-arena/fixtures/data/insample/S06.csv +2087 -0
  96. package/src/quant-arena/fixtures/data/insample/S07.csv +2087 -0
  97. package/src/quant-arena/fixtures/data/insample/S08.csv +2087 -0
  98. package/src/quant-arena/fixtures/data/insample/S09.csv +2087 -0
  99. package/src/quant-arena/fixtures/data/insample/S10.csv +2087 -0
  100. package/src/quant-arena/fixtures/demo-campaign/cost-ledger.jsonl +16 -0
  101. package/src/quant-arena/fixtures/demo-campaign/notebook.jsonl +5 -0
  102. package/src/quant-arena/fixtures/demo-campaign/rollout-manifest.json +171 -0
  103. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-001-default-author/strategy.ts +119 -0
  104. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-002-default-author/strategy.ts +119 -0
  105. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-003-quant-researcher/strategy.ts +105 -0
  106. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-004-quant-researcher/strategy.ts +102 -0
  107. package/src/quant-arena/fixtures/demo-campaign-v2/cost-ledger.jsonl +4 -0
  108. package/src/quant-arena/fixtures/demo-campaign-v2/notebook.jsonl +2 -0
  109. package/src/quant-arena/fixtures/demo-campaign-v2/rollout-manifest.json +84 -0
  110. package/src/quant-arena/fixtures/demo-campaign-v2/strategies/cand-001-quant-researcher/strategy.ts +117 -0
  111. package/src/quant-arena/holdout-certify.mts +206 -0
  112. package/src/quant-arena/holdout-certify.test.mts +82 -0
  113. package/src/quant-arena/leak-audit.test.mts +79 -0
  114. package/src/quant-arena/leak-audit.ts +95 -0
  115. package/src/quant-arena/make-fixtures.mts +161 -0
  116. package/src/quant-arena/multiplicity.test.mts +68 -0
  117. package/src/quant-arena/multiplicity.ts +87 -0
  118. package/src/quant-arena/nautilus-certify.ts +31 -0
  119. package/src/quant-arena/oms.ts +90 -0
  120. package/src/quant-arena/profiles/quant-researcher.profile.json +7 -0
  121. package/src/quant-arena/python/pyproject.toml +8 -0
  122. package/src/quant-arena/python/uv.lock +1297 -0
  123. package/src/quant-arena/python/vbt-worker.py +192 -0
  124. package/src/quant-arena/quant-loop.mts +813 -0
  125. package/src/quant-arena/quant-loop.test.mts +75 -0
  126. package/src/quant-arena/strategies/buy-hold-index/strategy.ts +11 -0
  127. package/src/quant-arena/strategies/equal-weight/strategy.ts +20 -0
  128. package/src/quant-arena/strategies/sma-crossover/strategy.ts +42 -0
  129. package/src/quant-arena/types.ts +133 -0
  130. package/src/quant-arena/vbt-client.ts +321 -0
  131. package/src/quant-arena/vbt-parity.test.mts +183 -0
  132. package/src/quant-arena/windows.test.mts +45 -0
  133. package/src/quant-arena/windows.ts +54 -0
  134. package/src/rollout-ledger/backfill-swe-arena.mts +606 -0
  135. package/src/rollout-ledger/backfill-swe-arena.test.mts +338 -0
  136. package/src/rollout-ledger/settle-capture.mts +442 -0
  137. package/src/rollout-ledger/settle-capture.test.mts +270 -0
  138. package/src/stream-observe.py +45 -0
  139. package/src/stream-observe.tpl.html +247 -0
  140. package/src/supervisor-arena.mts +816 -0
  141. package/src/swe-arena/activation.mts +228 -0
  142. package/src/swe-arena/activation.test.mts +303 -0
  143. package/src/swe-arena/analyze.ts +211 -0
  144. package/src/swe-arena/arms.ts +804 -0
  145. package/src/swe-arena/bootstrap-meta.mts +188 -0
  146. package/src/swe-arena/bootstrap-meta.test.mts +51 -0
  147. package/src/swe-arena/briefing.mts +217 -0
  148. package/src/swe-arena/briefing.test.mts +178 -0
  149. package/src/swe-arena/calibrate.ts +217 -0
  150. package/src/swe-arena/capabilities.mts +76 -0
  151. package/src/swe-arena/capabilities.test.mts +57 -0
  152. package/src/swe-arena/capacity.ts +194 -0
  153. package/src/swe-arena/cell-evidence.mts +437 -0
  154. package/src/swe-arena/cell-evidence.test.mts +248 -0
  155. package/src/swe-arena/diagnosis-ensemble.test.mts +210 -0
  156. package/src/swe-arena/diagnosis-ensemble.ts +520 -0
  157. package/src/swe-arena/execution.test.mts +1170 -0
  158. package/src/swe-arena/factory-command-container.ts +284 -0
  159. package/src/swe-arena/factory-judge-child.mts +228 -0
  160. package/src/swe-arena/factory.test.mts +643 -0
  161. package/src/swe-arena/fixtures/analyze.py +80 -0
  162. package/src/swe-arena/fixtures/excludes.txt +8 -0
  163. package/src/swe-arena/fixtures/factory/agent-eval-309/calibration.md +51 -0
  164. package/src/swe-arena/fixtures/factory/agent-eval-309/manifest.json +29 -0
  165. package/src/swe-arena/fixtures/factory/agent-eval-309/spec.md +64 -0
  166. package/src/swe-arena/fixtures/factory/agent-runtime-232/calibration.md +48 -0
  167. package/src/swe-arena/fixtures/factory/agent-runtime-232/manifest.json +29 -0
  168. package/src/swe-arena/fixtures/factory/agent-runtime-232/spec.md +48 -0
  169. package/src/swe-arena/fixtures/factory/loops-28/calibration.md +47 -0
  170. package/src/swe-arena/fixtures/factory/loops-28/manifest.json +30 -0
  171. package/src/swe-arena/fixtures/factory/loops-28/spec.md +50 -0
  172. package/src/swe-arena/fixtures/gen1-salvage/README.md +45 -0
  173. package/src/swe-arena/fixtures/gen1-salvage/cand0-e6d7361.diff +116 -0
  174. package/src/swe-arena/fixtures/gen1-salvage/cand1-76a8590.diff +293 -0
  175. package/src/swe-arena/fixtures/holdout-preregister.log +12 -0
  176. package/src/swe-arena/fixtures/holdout.json +44 -0
  177. package/src/swe-arena/fixtures/instances.json +146 -0
  178. package/src/swe-arena/fixtures/ledger.jsonl +12 -0
  179. package/src/swe-arena/fixtures/patches/pallets__flask-5014.solo.patch +36 -0
  180. package/src/swe-arena/fixtures/patches/pydata__xarray-4687.sup.patch +33 -0
  181. package/src/swe-arena/fixtures/rejudge.jsonl +15 -0
  182. package/src/swe-arena/fixtures/rematch.jsonl +3 -0
  183. package/src/swe-arena/fixtures/rematch2.jsonl +3 -0
  184. package/src/swe-arena/fixtures/rematch3.jsonl +3 -0
  185. package/src/swe-arena/fixtures/run-report/README.md +43 -0
  186. package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.json +173 -0
  187. package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.md +100 -0
  188. package/src/swe-arena/fixtures/run-report/gen3-rollup.json +551 -0
  189. package/src/swe-arena/fixtures/run-report/gen3-rollup.md +64 -0
  190. package/src/swe-arena/fixtures/sup-journal-true.json +19 -0
  191. package/src/swe-arena/fixtures/verify/astropy__astropy-13033.sh +48 -0
  192. package/src/swe-arena/fixtures/verify/django__django-11532.sh +50 -0
  193. package/src/swe-arena/fixtures/verify/matplotlib__matplotlib-20826.sh +76 -0
  194. package/src/swe-arena/fixtures/verify/pydata__xarray-4687.sh +44 -0
  195. package/src/swe-arena/fixtures/verify/pytest-dev__pytest-6197.sh +32 -0
  196. package/src/swe-arena/fixtures/verify/sphinx-doc__sphinx-9658.sh +51 -0
  197. package/src/swe-arena/fixtures/worker-tokens.json +42 -0
  198. package/src/swe-arena/fixtures.ts +237 -0
  199. package/src/swe-arena/gepa-seat.mts +583 -0
  200. package/src/swe-arena/gepa-seat.test.mts +635 -0
  201. package/src/swe-arena/holdout-certify.mts +408 -0
  202. package/src/swe-arena/holdout-certify.test.mts +160 -0
  203. package/src/swe-arena/judge-child.mts +37 -0
  204. package/src/swe-arena/ledger-orphans.mts +77 -0
  205. package/src/swe-arena/ledger-orphans.test.mts +147 -0
  206. package/src/swe-arena/lineage-record.mts +164 -0
  207. package/src/swe-arena/lineage-record.test.mts +115 -0
  208. package/src/swe-arena/manifest.mts +293 -0
  209. package/src/swe-arena/manifest.test.mts +169 -0
  210. package/src/swe-arena/materialize.ts +142 -0
  211. package/src/swe-arena/outer-loop.mts +2795 -0
  212. package/src/swe-arena/outer-loop.test.mts +696 -0
  213. package/src/swe-arena/parity.test.mts +87 -0
  214. package/src/swe-arena/premeasured-from-cells.mts +281 -0
  215. package/src/swe-arena/premeasured-from-cells.test.mts +180 -0
  216. package/src/swe-arena/proc.test.mts +174 -0
  217. package/src/swe-arena/proc.ts +260 -0
  218. package/src/swe-arena/profiles/default-author.profile.json +4 -0
  219. package/src/swe-arena/proposer-fanout.mts +770 -0
  220. package/src/swe-arena/proposer-fanout.test.mts +619 -0
  221. package/src/swe-arena/proposer-provenance.mts +177 -0
  222. package/src/swe-arena/proposer-provenance.test.mts +106 -0
  223. package/src/swe-arena/reconcile.ts +0 -0
  224. package/src/swe-arena/replay.mts +183 -0
  225. package/src/swe-arena/replay.test.mts +300 -0
  226. package/src/swe-arena/run-experiment.mts +727 -0
  227. package/src/swe-arena/run-report.mts +75 -0
  228. package/src/swe-arena/run-supervisor.mjs +297 -0
  229. package/src/swe-arena/run-supervisor.test.mts +500 -0
  230. package/src/swe-arena/score-split.mts +140 -0
  231. package/src/swe-arena/score-split.test.mts +123 -0
  232. package/src/swe-arena/serialized-judge.ts +414 -0
  233. package/src/swe-arena/types.ts +218 -0
  234. package/src/swe-code-improve.mts +328 -0
  235. package/src/swe-emit-patch.mts +104 -0
  236. package/src/swe-improve.mts +232 -0
  237. package/src/swe-jail.ts +2 -2
  238. package/src/swe-local-proof.mts +169 -0
  239. package/src/swe-repro-calibrate.mts +446 -0
  240. package/src/swe-stream.mts +1497 -0
  241. package/src/swe-structural.mts +245 -837
  242. package/dist/chunk-LRRD7NAG.js.map +0 -1
  243. package/dist/chunk-PPYSEKFM.js.map +0 -1
  244. /package/dist/{chunk-CKUVRZ2T.js.map → chunk-3U5TXJZS.js.map} +0 -0
  245. /package/dist/{chunk-YCGY7UIZ.js.map → chunk-7GRVHU22.js.map} +0 -0
  246. /package/dist/{chunk-Z7ML6L77.js.map → chunk-HWST3SED.js.map} +0 -0
  247. /package/dist/{chunk-SYDW647C.js.map → chunk-IA2FBTWC.js.map} +0 -0
  248. /package/dist/{chunk-R67DFVLO.js.map → chunk-IFVINJ4B.js.map} +0 -0
  249. /package/dist/{chunk-R36V2VP7.js.map → chunk-IZ5M6OAC.js.map} +0 -0
  250. /package/dist/{chunk-ODT47UAY.js.map → chunk-K3BQGZCT.js.map} +0 -0
  251. /package/dist/{chunk-IFAV6KEM.js.map → chunk-KP5KD6EN.js.map} +0 -0
  252. /package/dist/{chunk-ZEWMTR5M.js.map → chunk-MQMRLGOG.js.map} +0 -0
  253. /package/dist/{chunk-TSWPNOYM.js.map → chunk-NQG5XDSB.js.map} +0 -0
  254. /package/dist/{chunk-7WSD27QQ.js.map → chunk-PB64GYIG.js.map} +0 -0
  255. /package/dist/{chunk-HBSWHQNJ.js.map → chunk-RCYQEFNX.js.map} +0 -0
  256. /package/dist/{chunk-J3KDJNX2.js.map → chunk-RH5F53JT.js.map} +0 -0
  257. /package/dist/{chunk-UAIOHCUK.js.map → chunk-SFLA7OH3.js.map} +0 -0
  258. /package/dist/{chunk-Y6O2OCUO.js.map → chunk-SHM6MRRF.js.map} +0 -0
  259. /package/dist/{chunk-KDIKRJGB.js.map → chunk-SHYIRB7I.js.map} +0 -0
  260. /package/dist/{chunk-HHXFIHXC.js.map → chunk-SVR2LKYI.js.map} +0 -0
  261. /package/dist/{chunk-5SBJCB6W.js.map → chunk-V7AEBY6U.js.map} +0 -0
  262. /package/dist/{chunk-2PVVP7GN.js.map → chunk-XKEFIFIC.js.map} +0 -0
  263. /package/dist/{chunk-JRWWGMK7.js.map → chunk-XYA4XSNU.js.map} +0 -0
  264. /package/dist/{chunk-X5YKXC6V.js.map → chunk-YSMEKBTD.js.map} +0 -0
  265. /package/dist/{chunk-2XU6OGEN.js.map → chunk-Z4TZ76N7.js.map} +0 -0
@@ -0,0 +1,437 @@
1
+ /**
2
+ * Cell-derived scoring evidence — the round's ground truth read from the
3
+ * LIB's campaign cells, never from in-process dispatch-order bookkeeping.
4
+ *
5
+ * Root cause this kills (r4-mroh3rkt): `improve()` resumes its campaign from
6
+ * runDir and replays cached cells WITHOUT dispatching them, so any recorder
7
+ * keyed on "what this process dispatched" mislabels arms — the resumed run
8
+ * published candidate b08d31c910's cells as "baseline 0/3" while the measured
9
+ * baseline was 1/3. Campaign cells carry their own attribution instead:
10
+ *
11
+ * - the campaign DIRECTORY names the arm (`baseline/` vs
12
+ * `gen-<g>/candidate-<i>/` under the improve runDir — run-campaign.ts
13
+ * writes one `<cellId>/cached-result.json` per conclusive cell), and
14
+ * - each cell's artifact names its loops commit (`R4Artifact.commit`).
15
+ *
16
+ * Everything here is pure over cells (plus the two disk readers), so the
17
+ * aggregation is unit-testable against a synthetic cell set reproducing the
18
+ * resume-replay shape with zero dispatch.
19
+ */
20
+
21
+ import { readdir, readFile } from 'node:fs/promises'
22
+ import { join } from 'node:path'
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // The evaluated artifact. One cell = one (surface × scenario × rep); every
26
+ // cell carries the official-judge outcome + recovered spend. (The `kind`
27
+ // discriminant stays: cached cells on disk carry it, and it keeps replayed
28
+ // artifacts distinguishable from a null/errored cell.)
29
+ // ---------------------------------------------------------------------------
30
+
31
+ export interface R4Artifact {
32
+ kind: 'swe-arm'
33
+ iid: string
34
+ commit: string
35
+ resolved: boolean
36
+ verifyPass: boolean
37
+ patchLines: number
38
+ wallS: number
39
+ /** Runtime spend-tree total (state.json `result.spentTokens`, winner AND
40
+ * no-winner arms). `null` = state.json unreadable, a telemetry gap. */
41
+ spentTokens: number | null
42
+ spentUsd: number | null
43
+ /** spentTokens + opencode-sqlite worker-session tokens. */
44
+ recoveredTokens: number | null
45
+ /** Worker-session token split from the opencode sqlite join — the
46
+ * usage the campaign CostLedger receipt reports. */
47
+ workerTokIn: number | null
48
+ workerTokOut: number | null
49
+ judgeAttempts: number | null
50
+ judgeWallS: number | null
51
+ runDir: string
52
+ patchPath: string
53
+ }
54
+
55
+ /** The minimal slice of a lib `CampaignCellResult<R4Artifact>` the scoring
56
+ * reads. Structural so both in-memory campaign results and parsed
57
+ * `cached-result.json` files satisfy it. */
58
+ export interface EvidenceCell {
59
+ scenarioId: string
60
+ rep: number
61
+ /** `null` on an errored cell (the lib records failed cells with a null
62
+ * artifact; it never caches them). */
63
+ artifact: R4Artifact | null
64
+ error?: string
65
+ costUsd?: number
66
+ tokenUsage?: { input: number; output: number }
67
+ cached?: boolean
68
+ }
69
+
70
+ /** Adapt a lib campaign's cells (in-memory result) to `EvidenceCell`s. */
71
+ export function cellsFromCampaign(campaign: {
72
+ cells: Array<{
73
+ scenarioId: string
74
+ rep: number
75
+ artifact: unknown
76
+ error?: string
77
+ costUsd: number
78
+ tokenUsage: { input: number; output: number }
79
+ cached: boolean
80
+ }>
81
+ }): EvidenceCell[] {
82
+ return campaign.cells.map((cell) => ({
83
+ scenarioId: cell.scenarioId,
84
+ rep: cell.rep,
85
+ artifact: (cell.artifact ?? null) as R4Artifact | null,
86
+ ...(cell.error ? { error: cell.error } : {}),
87
+ costUsd: cell.costUsd,
88
+ tokenUsage: { input: cell.tokenUsage.input, output: cell.tokenUsage.output },
89
+ cached: cell.cached,
90
+ }))
91
+ }
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // Replicate semantics — repsPerInstance. Single-rep scoring provably flips
95
+ // instance outcomes run-to-run (judge flake + capacity noise both observed),
96
+ // so an instance counts RESOLVED only when EVERY replicate cell resolved (AND
97
+ // — fail-closed for keep-if-better), and coverage requires every replicate of
98
+ // every instance to hold a real boolean verdict.
99
+ // ---------------------------------------------------------------------------
100
+
101
+ export interface ReplicateRun {
102
+ iid: string
103
+ resolved: boolean | null
104
+ }
105
+
106
+ /** Instances where ALL `reps` replicates resolved (missing replicates never count). */
107
+ export function resolvedInstanceCount(runs: ReplicateRun[], iids: string[], reps: number): number {
108
+ let count = 0
109
+ for (const iid of iids) {
110
+ const mine = runs.filter((r) => r.iid === iid)
111
+ if (mine.length === reps && mine.every((r) => r.resolved === true)) count += 1
112
+ }
113
+ return count
114
+ }
115
+
116
+ /** Every instance has exactly `reps` replicates, each with a conclusive verdict. */
117
+ export function replicateCoverageComplete(runs: ReplicateRun[], iids: string[], reps: number): boolean {
118
+ return iids.every((iid) => {
119
+ const mine = runs.filter((r) => r.iid === iid)
120
+ return mine.length === reps && mine.every((r) => r.resolved !== null)
121
+ })
122
+ }
123
+
124
+ /** One `ReplicateRun` per swe cell. An errored/artifact-less cell is an
125
+ * inconclusive replicate (`resolved: null`) — never a fabricated boolean. */
126
+ export function replicateRunsFromCells(cells: EvidenceCell[]): ReplicateRun[] {
127
+ return cells
128
+ .filter((c) => c.artifact === null || c.artifact.kind === 'swe-arm')
129
+ .map((c) => ({
130
+ iid: c.scenarioId,
131
+ resolved: c.artifact !== null && c.artifact.kind === 'swe-arm' && !c.error ? c.artifact.resolved : null,
132
+ }))
133
+ }
134
+
135
+ /** Σ wall seconds across the swe cells (errored cells contribute 0). */
136
+ export function sumWallSFromCells(cells: EvidenceCell[]): number {
137
+ return cells.reduce(
138
+ (s, c) => s + (c.artifact !== null && c.artifact.kind === 'swe-arm' ? c.artifact.wallS : 0),
139
+ 0,
140
+ )
141
+ }
142
+
143
+ /** Per-replicate staircase row — one per swe cell, straight off the artifact. */
144
+ export interface StaircasePerInstance {
145
+ iid: string
146
+ /** Replicate index (0-based) — repsPerInstance cells per instance. */
147
+ rep: number
148
+ resolved: boolean | null
149
+ verify_pass: boolean | null
150
+ patch_lines: number | null
151
+ wall_s: number | null
152
+ spentTokens: number | null
153
+ recoveredTokens: number | null
154
+ judgeAttempts: number | null
155
+ /** Campaign-cell CostLedger spend for this replicate (worker receipt). */
156
+ costUsd: number | null
157
+ error?: string
158
+ }
159
+
160
+ export function perInstanceFromCells(cells: EvidenceCell[]): StaircasePerInstance[] {
161
+ const rows: StaircasePerInstance[] = []
162
+ for (const cell of cells) {
163
+ const a = cell.artifact !== null && cell.artifact.kind === 'swe-arm' && !cell.error ? cell.artifact : null
164
+ rows.push({
165
+ iid: cell.scenarioId,
166
+ rep: cell.rep,
167
+ resolved: a ? a.resolved : null,
168
+ verify_pass: a ? a.verifyPass : null,
169
+ patch_lines: a ? a.patchLines : null,
170
+ wall_s: a ? a.wallS : null,
171
+ spentTokens: a ? a.spentTokens : null,
172
+ recoveredTokens: a ? a.recoveredTokens : null,
173
+ judgeAttempts: a ? a.judgeAttempts : null,
174
+ costUsd: cell.costUsd ?? null,
175
+ ...(cell.error ? { error: cell.error } : {}),
176
+ })
177
+ }
178
+ return rows
179
+ }
180
+
181
+ // ---------------------------------------------------------------------------
182
+ // Premeasured-baseline drift. The gate's denominator is the stored
183
+ // premeasured baseline artifact ({surfaceHash, campaign}) that the LIB
184
+ // validates before skipping the baseline campaign — surface hash, seed, reps,
185
+ // and split digest all fail loud on mismatch. A resumed runDir can still hold
186
+ // baseline cells cached by an OLDER run of the same surface; when those
187
+ // contradict the validated artifact, the contradiction is logged loud and the
188
+ // artifact still rules.
189
+ // ---------------------------------------------------------------------------
190
+
191
+ /** AND-verdict per instance from campaign cells. Only instances with full,
192
+ * conclusive replicate coverage produce a verdict — a partial record has no
193
+ * AND-verdict to compare. */
194
+ export function instanceVerdictsFromCells(
195
+ cells: EvidenceCell[],
196
+ iids: string[],
197
+ reps: number,
198
+ ): Record<string, boolean> {
199
+ const runs = replicateRunsFromCells(cells)
200
+ const verdicts: Record<string, boolean> = {}
201
+ for (const iid of iids) {
202
+ const mine = runs.filter((r) => r.iid === iid)
203
+ if (mine.length !== reps || mine.some((r) => r.resolved === null)) continue
204
+ verdicts[iid] = mine.every((r) => r.resolved === true)
205
+ }
206
+ return verdicts
207
+ }
208
+
209
+ /** Per-instance contradictions between the validated premeasured artifact's
210
+ * verdicts and locally cached baseline cells. Only instances with full-reps,
211
+ * conclusive coverage on BOTH sides are compared — a partial record has no
212
+ * AND-verdict to contradict with. The caller logs these loud and the
213
+ * premeasured artifact STILL rules. */
214
+ export function baselineDriftWarnings(
215
+ expected: Record<string, boolean>,
216
+ runs: ReplicateRun[],
217
+ iids: string[],
218
+ reps: number,
219
+ ): string[] {
220
+ const warnings: string[] = []
221
+ for (const iid of iids) {
222
+ const want = expected[iid]
223
+ if (typeof want !== 'boolean') continue
224
+ const mine = runs.filter((r) => r.iid === iid)
225
+ if (mine.length !== reps || mine.some((r) => r.resolved === null)) continue
226
+ const measured = mine.every((r) => r.resolved === true)
227
+ if (measured !== want) {
228
+ warnings.push(
229
+ `${iid}: premeasured=${want} but cached baseline cells measured ${measured} ` +
230
+ `(reps: ${mine.map((r) => String(r.resolved)).join('/')}) — the validated premeasured artifact rules`,
231
+ )
232
+ }
233
+ }
234
+ return warnings
235
+ }
236
+
237
+ // ---------------------------------------------------------------------------
238
+ // protocol_v2 keep-if-better.
239
+ // ---------------------------------------------------------------------------
240
+
241
+ export type StaircaseVerdict =
242
+ | 'accepted'
243
+ | 'rejected-no-gain'
244
+ | 'rejected-cost'
245
+ | 'rejected-out-of-space'
246
+ | 'rejected-incomplete'
247
+ /** Killed by the gen-3 pre-filter (change-space/tsc/smoke) BEFORE any full
248
+ * evaluation — the candidate never became a measured surface. Emitted by
249
+ * the outer loop's kill-row writer, never by `decideVerdict`. */
250
+ | 'rejected-prefilter'
251
+ /** GEN-5 activation gate: the candidate's own machine-checkable predicate
252
+ * (activation.mts) proved its mechanism NEVER fired in its campaign
253
+ * traces — recorded, never promoted, EVEN when the score improved. */
254
+ | 'quarantined-inactive'
255
+
256
+ /** protocol_v2 keep-if-better: improvement-set resolved-count must RISE and
257
+ * cost must stay within the guard. Fail-closed on unprovable cost.
258
+ * GEN-5: `activationFired === false` quarantines the candidate ahead of the
259
+ * score comparison — an improved score with an inactive mechanism is
260
+ * indistinguishable from luck (undefined/null = gate not applicable:
261
+ * disabled, baseline, or predicate unevaluated). */
262
+ export function decideVerdict(input: {
263
+ violations: string[]
264
+ coverageComplete: boolean
265
+ resolvedCount: number
266
+ parentResolvedCount: number
267
+ costRatio: number | null
268
+ costGuardRatio: number
269
+ activationFired?: boolean | null
270
+ }): StaircaseVerdict {
271
+ if (input.violations.length > 0) return 'rejected-out-of-space'
272
+ if (input.activationFired === false) return 'quarantined-inactive'
273
+ if (!input.coverageComplete) return 'rejected-incomplete'
274
+ if (input.resolvedCount <= input.parentResolvedCount) return 'rejected-no-gain'
275
+ if (input.costRatio === null || input.costRatio > input.costGuardRatio) return 'rejected-cost'
276
+ return 'accepted'
277
+ }
278
+
279
+ // ---------------------------------------------------------------------------
280
+ // Disk readers — the lib's per-cell caches. run-campaign.ts writes
281
+ // `<campaignDir>/<sanitized cellId>/cached-result.json` for every conclusive
282
+ // cell (errored cells are never cached — a missing replicate reads as
283
+ // coverage-incomplete downstream, fail-closed).
284
+ // ---------------------------------------------------------------------------
285
+
286
+ /** Every `<cellDir>/cached-result.json` under one campaign dir, verbatim and
287
+ * identity-checked. Missing dir = []. A cache that exists but carries no
288
+ * scenarioId/rep cannot be attributed to a cell, so it throws rather than
289
+ * disappearing from the campaign's coverage. */
290
+ export async function loadCampaignCellRecords(campaignDir: string): Promise<Record<string, unknown>[]> {
291
+ const entries = await readdir(campaignDir, { withFileTypes: true }).catch(() => [])
292
+ const records: Record<string, unknown>[] = []
293
+ for (const entry of entries) {
294
+ if (!entry.isDirectory()) continue
295
+ const path = join(campaignDir, entry.name, 'cached-result.json')
296
+ const raw = await readFile(path, 'utf8').catch(() => null)
297
+ if (raw === null) continue
298
+ let parsed: unknown
299
+ try {
300
+ parsed = JSON.parse(raw)
301
+ } catch {
302
+ throw new Error(`loadCampaignCells: corrupt cell cache ${path}`)
303
+ }
304
+ // null and arrays parse fine and would make the identity check below throw a
305
+ // TypeError instead of naming what is wrong with the file.
306
+ const record =
307
+ parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)
308
+ ? (parsed as Record<string, unknown>)
309
+ : null
310
+ if (record === null || typeof record.scenarioId !== 'string' || typeof record.rep !== 'number') {
311
+ throw new Error(`loadCampaignCells: ${path} is not a campaign cell (scenarioId/rep missing)`)
312
+ }
313
+ records.push(record)
314
+ }
315
+ return records
316
+ }
317
+
318
+ /** Parse every `<cellDir>/cached-result.json` under one campaign dir. Missing dir = []. */
319
+ export async function loadCampaignCells(campaignDir: string): Promise<EvidenceCell[]> {
320
+ const cells: EvidenceCell[] = []
321
+ for (const parsed of await loadCampaignCellRecords(campaignDir)) {
322
+ cells.push({
323
+ // loadCampaignCellRecords has already proven both are present and typed.
324
+ scenarioId: parsed.scenarioId as string,
325
+ rep: parsed.rep as number,
326
+ artifact: (parsed.artifact ?? null) as R4Artifact | null,
327
+ ...(typeof parsed.error === 'string' ? { error: parsed.error } : {}),
328
+ ...(typeof parsed.costUsd === 'number' ? { costUsd: parsed.costUsd } : {}),
329
+ ...(parsed.tokenUsage && typeof parsed.tokenUsage === 'object'
330
+ ? { tokenUsage: parsed.tokenUsage as { input: number; output: number } }
331
+ : {}),
332
+ cached: true,
333
+ })
334
+ }
335
+ return cells
336
+ }
337
+
338
+ export interface CandidateCellGroup {
339
+ generation: number
340
+ candidateIndex: number
341
+ dir: string
342
+ cells: EvidenceCell[]
343
+ /** The loops commit the cells' artifacts name (null when no artifact
344
+ * carries one — e.g. an all-errored, never-cached candidate). */
345
+ commit: string | null
346
+ }
347
+
348
+ /** Scan `gen-<g>/candidate-<i>/` campaign dirs under the improve runDir.
349
+ * Attribution is directory + artifact-commit — dispatch order plays no part. */
350
+ export async function loadCandidateCellGroups(improveRunDir: string): Promise<CandidateCellGroup[]> {
351
+ const groups: CandidateCellGroup[] = []
352
+ const top = await readdir(improveRunDir, { withFileTypes: true }).catch(() => [])
353
+ for (const genEntry of top) {
354
+ const genMatch = /^gen-(\d+)$/.exec(genEntry.name)
355
+ if (!genEntry.isDirectory() || !genMatch) continue
356
+ const genDir = join(improveRunDir, genEntry.name)
357
+ for (const candEntry of await readdir(genDir, { withFileTypes: true }).catch(() => [])) {
358
+ const candMatch = /^candidate-(\d+)$/.exec(candEntry.name)
359
+ if (!candEntry.isDirectory() || !candMatch) continue
360
+ const dir = join(genDir, candEntry.name)
361
+ const cells = await loadCampaignCells(dir)
362
+ const commits = new Set(
363
+ cells.map((c) => c.artifact?.commit).filter((c): c is string => typeof c === 'string'),
364
+ )
365
+ if (commits.size > 1) {
366
+ throw new Error(
367
+ `loadCandidateCellGroups: ${dir} mixes commits [${[...commits].join(', ')}] — one candidate dir must hold one surface`,
368
+ )
369
+ }
370
+ groups.push({
371
+ generation: Number(genMatch[1]),
372
+ candidateIndex: Number(candMatch[1]),
373
+ dir,
374
+ cells,
375
+ commit: [...commits][0] ?? null,
376
+ })
377
+ }
378
+ }
379
+ return groups.sort((a, b) => a.generation - b.generation || a.candidateIndex - b.candidateIndex)
380
+ }
381
+
382
+ // ---------------------------------------------------------------------------
383
+ // Gate evidence — the would-be-keep operator brief, derived from cells. The
384
+ // lib's deferred-holdout gate always holds; this evidence tells the operator
385
+ // whether the pre-registered holdout run is worth approving.
386
+ // ---------------------------------------------------------------------------
387
+
388
+ export interface GateEvidence {
389
+ candResolved: number
390
+ baseResolved: number
391
+ candWallS: number
392
+ baseWallS: number
393
+ costRatio: number | null
394
+ coverageComplete: boolean
395
+ verdict: StaircaseVerdict
396
+ }
397
+
398
+ /** Score the winner-vs-baseline comparison for the operator brief. Both sides
399
+ * come from campaign cells — the baseline side is the lib-validated
400
+ * premeasured campaign (or the bootstrap run's freshly measured one). */
401
+ export function gateEvidenceFromCells(input: {
402
+ winnerCells: EvidenceCell[]
403
+ baselineCells: EvidenceCell[]
404
+ /** Dispatch-time change-space violations of the winner's diff. */
405
+ violations: string[]
406
+ iids: string[]
407
+ reps: number
408
+ costGuardRatio: number
409
+ /** GEN-5 activation-gate outcome for the winner (see decideVerdict). */
410
+ activationFired?: boolean | null
411
+ }): GateEvidence {
412
+ const winnerRuns = replicateRunsFromCells(input.winnerCells)
413
+ const baselineRuns = replicateRunsFromCells(input.baselineCells)
414
+ const candResolved = resolvedInstanceCount(winnerRuns, input.iids, input.reps)
415
+ const baseResolved = resolvedInstanceCount(baselineRuns, input.iids, input.reps)
416
+ const candWallS = sumWallSFromCells(input.winnerCells)
417
+ const baseWallS = sumWallSFromCells(input.baselineCells)
418
+ const costRatio = baseWallS > 0 ? candWallS / baseWallS : null
419
+ const coverageComplete = replicateCoverageComplete(winnerRuns, input.iids, input.reps)
420
+ return {
421
+ candResolved,
422
+ baseResolved,
423
+ candWallS,
424
+ baseWallS,
425
+ costRatio,
426
+ coverageComplete,
427
+ verdict: decideVerdict({
428
+ violations: input.violations,
429
+ coverageComplete,
430
+ resolvedCount: candResolved,
431
+ parentResolvedCount: baseResolved,
432
+ costRatio,
433
+ costGuardRatio: input.costGuardRatio,
434
+ ...(input.activationFired !== undefined ? { activationFired: input.activationFired } : {}),
435
+ }),
436
+ }
437
+ }
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Cell-derived scoring: reps-AND aggregation as a pure function over lib
3
+ * campaign cells, and the disk readers over the lib's cached-result.json
4
+ * caches — including a reproduction of the r4-mroh3rkt resume shape (baseline
5
+ * cells replayed from cache, only a candidate dispatched in-process) proving
6
+ * attribution comes from the campaign directory + artifact commit, never from
7
+ * dispatch order. No arms, no docker, no tokens.
8
+ */
9
+
10
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
11
+ import { tmpdir } from 'node:os'
12
+ import { join } from 'node:path'
13
+ import { afterAll, describe, expect, it } from 'vitest'
14
+ import {
15
+ baselineDriftWarnings,
16
+ cellsFromCampaign,
17
+ gateEvidenceFromCells,
18
+ instanceVerdictsFromCells,
19
+ loadCampaignCells,
20
+ loadCandidateCellGroups,
21
+ perInstanceFromCells,
22
+ replicateRunsFromCells,
23
+ resolvedInstanceCount,
24
+ sumWallSFromCells,
25
+ type EvidenceCell,
26
+ type R4Artifact,
27
+ } from './cell-evidence.mts'
28
+
29
+ const IIDS = ['astropy__astropy-13033', 'django__django-11532', 'matplotlib__matplotlib-20826']
30
+
31
+ function sweArtifact(iid: string, commit: string, resolved: boolean, wallS = 100): R4Artifact {
32
+ return {
33
+ kind: 'swe-arm',
34
+ iid,
35
+ commit,
36
+ resolved,
37
+ verifyPass: resolved,
38
+ patchLines: 10,
39
+ wallS,
40
+ spentTokens: 1000,
41
+ spentUsd: 0.01,
42
+ recoveredTokens: 1500,
43
+ workerTokIn: 400,
44
+ workerTokOut: 100,
45
+ judgeAttempts: 1,
46
+ judgeWallS: 30,
47
+ runDir: `/tmp/none/${iid}`,
48
+ patchPath: `/tmp/none/${iid}.patch`,
49
+ }
50
+ }
51
+
52
+ function cell(iid: string, rep: number, artifact: R4Artifact | null, error?: string): EvidenceCell {
53
+ return {
54
+ scenarioId: iid,
55
+ rep,
56
+ artifact,
57
+ ...(error ? { error } : {}),
58
+ costUsd: 0.01,
59
+ tokenUsage: { input: 400, output: 100 },
60
+ }
61
+ }
62
+
63
+ describe('cell adapters', () => {
64
+ it('cellsFromCampaign keeps errored cells with a null artifact', () => {
65
+ const cells = cellsFromCampaign({
66
+ cells: [
67
+ { scenarioId: 'a', rep: 0, artifact: sweArtifact('a', 'c1', true), costUsd: 0.5, tokenUsage: { input: 1, output: 2 }, cached: false },
68
+ { scenarioId: 'a', rep: 1, artifact: null, error: 'dispatch timeout', costUsd: 0, tokenUsage: { input: 0, output: 0 }, cached: false },
69
+ ],
70
+ })
71
+ expect(cells).toHaveLength(2)
72
+ expect(cells[0]!.artifact?.kind).toBe('swe-arm')
73
+ expect(cells[1]!.artifact).toBeNull()
74
+ expect(cells[1]!.error).toMatch(/timeout/)
75
+ })
76
+
77
+ it('replicateRunsFromCells: error/artifact-less cells are inconclusive, never a boolean', () => {
78
+ const runs = replicateRunsFromCells([
79
+ cell('a', 0, sweArtifact('a', 'c1', true)),
80
+ cell('a', 1, sweArtifact('a', 'c1', false), 'judge inconclusive'),
81
+ cell('b', 0, null, 'change-space violation'),
82
+ ])
83
+ expect(runs).toEqual([
84
+ { iid: 'a', resolved: true },
85
+ { iid: 'a', resolved: null },
86
+ { iid: 'b', resolved: null },
87
+ ])
88
+ })
89
+
90
+ it('perInstanceFromCells maps artifact fields and carries cell cost', () => {
91
+ const rows = perInstanceFromCells([
92
+ cell('a', 0, sweArtifact('a', 'c1', true, 250)),
93
+ cell('b', 1, null, 'boom'),
94
+ ])
95
+ expect(rows).toHaveLength(2)
96
+ expect(rows[0]).toMatchObject({ iid: 'a', rep: 0, resolved: true, wall_s: 250, costUsd: 0.01, judgeAttempts: 1 })
97
+ expect(rows[1]).toMatchObject({ iid: 'b', rep: 1, resolved: null, wall_s: null, error: 'boom' })
98
+ })
99
+
100
+ it('sumWallSFromCells sums swe wall only', () => {
101
+ expect(
102
+ sumWallSFromCells([
103
+ cell('a', 0, sweArtifact('a', 'c1', true, 100)),
104
+ cell('a', 1, sweArtifact('a', 'c1', true, 40)),
105
+ cell('b', 0, null, 'err'),
106
+ ]),
107
+ ).toBe(140)
108
+ })
109
+ })
110
+
111
+ describe('disk readers + the r4-mroh3rkt resume shape', () => {
112
+ const roots: string[] = []
113
+ afterAll(async () => {
114
+ for (const root of roots) await rm(root, { recursive: true, force: true })
115
+ })
116
+
117
+ async function writeCachedCell(campaignDir: string, c: EvidenceCell): Promise<void> {
118
+ const cellDir = join(campaignDir, `cell-${c.scenarioId.replace(/[^a-zA-Z0-9_-]/g, '_')}-r${c.rep}`)
119
+ await mkdir(cellDir, { recursive: true })
120
+ await writeFile(
121
+ join(cellDir, 'cached-result.json'),
122
+ JSON.stringify({
123
+ cellId: `cell-${c.scenarioId}-r${c.rep}`,
124
+ scenarioId: c.scenarioId,
125
+ rep: c.rep,
126
+ artifact: c.artifact,
127
+ ...(c.error ? { error: c.error } : {}),
128
+ costUsd: c.costUsd ?? 0,
129
+ tokenUsage: c.tokenUsage ?? { input: 0, output: 0 },
130
+ judgeScores: {},
131
+ durationMs: 1,
132
+ seed: 42,
133
+ cached: false,
134
+ }),
135
+ )
136
+ }
137
+
138
+ /** The exact resume shape behind r4-mroh3rkt: the BASELINE campaign exists
139
+ * only as cached cells on disk (measured 1/3 — matplotlib both reps), while
140
+ * the process only ever dispatched candidate b08d31c910 (0/3). */
141
+ async function makeResumedRunDir(): Promise<string> {
142
+ const root = await mkdtemp(join(tmpdir(), 'r4-cells-'))
143
+ roots.push(root)
144
+ const improveRun = join(root, 'improve-run')
145
+ const baselineDir = join(improveRun, 'baseline')
146
+ for (const iid of IIDS) {
147
+ const resolved = iid.startsWith('matplotlib')
148
+ for (const rep of [0, 1]) {
149
+ await writeCachedCell(baselineDir, cell(iid, rep, sweArtifact(iid, 'basecommit0', resolved, 100)))
150
+ }
151
+ }
152
+ const candDir = join(improveRun, 'gen-0', 'candidate-0')
153
+ for (const iid of IIDS) {
154
+ for (const rep of [0, 1]) {
155
+ await writeCachedCell(candDir, cell(iid, rep, sweArtifact(iid, 'b08d31c910', false, 110)))
156
+ }
157
+ }
158
+ return improveRun
159
+ }
160
+
161
+ it('loadCampaignCells reads every cached cell; a missing dir is empty', async () => {
162
+ const improveRun = await makeResumedRunDir()
163
+ const cells = await loadCampaignCells(join(improveRun, 'baseline'))
164
+ expect(cells).toHaveLength(6)
165
+ expect(cells.every((c) => c.cached)).toBe(true)
166
+ expect(await loadCampaignCells(join(improveRun, 'no-such-campaign'))).toEqual([])
167
+ })
168
+
169
+ it('candidate groups attribute by directory + commit — baseline cells never leak in', async () => {
170
+ const improveRun = await makeResumedRunDir()
171
+ const groups = await loadCandidateCellGroups(improveRun)
172
+ expect(groups).toHaveLength(1)
173
+ expect(groups[0]).toMatchObject({ generation: 0, candidateIndex: 0, commit: 'b08d31c910' })
174
+ expect(groups[0]!.cells).toHaveLength(6)
175
+ expect(groups[0]!.cells.every((c) => c.artifact?.commit === 'b08d31c910')).toBe(true)
176
+ })
177
+
178
+ it('r4-mroh3rkt regression: the resumed run grades winner 0/3 vs baseline 1/3, not 0/3 vs 0/3', async () => {
179
+ const improveRun = await makeResumedRunDir()
180
+ const groups = await loadCandidateCellGroups(improveRun)
181
+ const winner = groups.find((g) => g.commit === 'b08d31c910')!
182
+ const baselineCells = await loadCampaignCells(join(improveRun, 'baseline'))
183
+
184
+ // Attribution is campaign directory + artifact commit: the candidate's
185
+ // cells can never masquerade as the baseline, dispatched or replayed.
186
+ const ev = gateEvidenceFromCells({
187
+ winnerCells: winner.cells,
188
+ baselineCells,
189
+ violations: [],
190
+ iids: IIDS,
191
+ reps: 2,
192
+ costGuardRatio: 1.2,
193
+ })
194
+ expect(ev.candResolved).toBe(0)
195
+ expect(ev.baseResolved).toBe(1) // the bug published 0/3 here
196
+ expect(ev.verdict).toBe('rejected-no-gain')
197
+ expect(ev.coverageComplete).toBe(true)
198
+ expect(ev.costRatio).toBeCloseTo(660 / 600, 5)
199
+ })
200
+
201
+ it('a contradicting cached baseline raises drift warnings and the premeasured artifact rules', async () => {
202
+ const improveRun = await makeResumedRunDir()
203
+ const cachedBaseline = await loadCampaignCells(join(improveRun, 'baseline'))
204
+ // Premeasured artifact verdicts contradicting the cached cells on astropy.
205
+ const expected = {
206
+ 'astropy__astropy-13033': true, // cached cells measured F/F
207
+ 'django__django-11532': false,
208
+ 'matplotlib__matplotlib-20826': true,
209
+ }
210
+ const warnings = baselineDriftWarnings(expected, replicateRunsFromCells(cachedBaseline), IIDS, 2)
211
+ expect(warnings).toHaveLength(1)
212
+ expect(warnings[0]).toContain('astropy__astropy-13033: premeasured=true')
213
+ expect(warnings[0]).toContain('premeasured artifact rules')
214
+ // The AND-verdicts of the cached campaign (the drift comparator source).
215
+ expect(instanceVerdictsFromCells(cachedBaseline, IIDS, 2)).toEqual({
216
+ 'astropy__astropy-13033': false,
217
+ 'django__django-11532': false,
218
+ 'matplotlib__matplotlib-20826': true,
219
+ })
220
+ })
221
+
222
+ it('a missing replicate keeps the candidate coverage-incomplete (errored cells are never cached)', async () => {
223
+ const improveRun = await makeResumedRunDir()
224
+ const partialDir = join(improveRun, 'gen-0', 'candidate-1')
225
+ await writeCachedCell(partialDir, cell(IIDS[0]!, 0, sweArtifact(IIDS[0]!, 'deadbeef01', true)))
226
+ const groups = await loadCandidateCellGroups(improveRun)
227
+ const partial = groups.find((g) => g.commit === 'deadbeef01')!
228
+ const ev = gateEvidenceFromCells({
229
+ winnerCells: partial.cells,
230
+ baselineCells: await loadCampaignCells(join(improveRun, 'baseline')),
231
+ violations: [],
232
+ iids: IIDS,
233
+ reps: 2,
234
+ costGuardRatio: 1.2,
235
+ })
236
+ expect(ev.coverageComplete).toBe(false)
237
+ expect(ev.verdict).toBe('rejected-incomplete')
238
+ expect(resolvedInstanceCount(replicateRunsFromCells(partial.cells), IIDS, 2)).toBe(0)
239
+ })
240
+
241
+ it('a candidate dir mixing two commits fails loud', async () => {
242
+ const improveRun = await makeResumedRunDir()
243
+ const dir = join(improveRun, 'gen-1', 'candidate-0')
244
+ await writeCachedCell(dir, cell(IIDS[0]!, 0, sweArtifact(IIDS[0]!, 'commitaaaa1', true)))
245
+ await writeCachedCell(dir, cell(IIDS[1]!, 0, sweArtifact(IIDS[1]!, 'commitbbbb2', true)))
246
+ await expect(loadCandidateCellGroups(improveRun)).rejects.toThrow(/mixes commits/)
247
+ })
248
+ })