@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.
- package/CHANGELOG.md +11 -0
- package/HARNESS.md +43 -0
- package/dist/adapters.js +24 -24
- package/dist/benchmarks/_harness.d.ts +1 -1
- package/dist/benchmarks/_harness.js +1 -1
- package/dist/benchmarks/aec-bench.js +2 -2
- package/dist/benchmarks/agentbench.js +2 -2
- package/dist/benchmarks/appworld.js +2 -2
- package/dist/benchmarks/bfcl.js +2 -2
- package/dist/benchmarks/commit0.js +2 -2
- package/dist/benchmarks/crag.js +2 -2
- package/dist/benchmarks/dabstep.js +2 -2
- package/dist/benchmarks/enterpriseops-gym.js +2 -2
- package/dist/benchmarks/finresearchbench.js +2 -2
- package/dist/benchmarks/humaneval.d.ts +10 -1
- package/dist/benchmarks/humaneval.js +5 -3
- package/dist/benchmarks/nomiracl.js +2 -2
- package/dist/benchmarks/open-rag-bench.js +2 -2
- package/dist/benchmarks/programbench.js +2 -2
- package/dist/benchmarks/ragbench.js +2 -2
- package/dist/benchmarks/swe-bench.js +2 -2
- package/dist/benchmarks/t2-ragbench.js +2 -2
- package/dist/benchmarks/tau-bench-shared.js +2 -2
- package/dist/benchmarks/tau2-bench.js +3 -3
- package/dist/benchmarks/tau3-banking.js +3 -3
- package/dist/benchmarks/terminal-bench.js +2 -2
- package/dist/benchmarks/toollm.js +2 -2
- package/dist/benchmarks/webarena-verified.js +2 -2
- package/dist/{chunk-CKUVRZ2T.js → chunk-3U5TXJZS.js} +2 -2
- package/dist/{chunk-PPYSEKFM.js → chunk-5H5XV76F.js} +73 -15
- package/dist/chunk-5H5XV76F.js.map +1 -0
- package/dist/{chunk-YCGY7UIZ.js → chunk-7GRVHU22.js} +2 -2
- package/dist/{chunk-Z7ML6L77.js → chunk-HWST3SED.js} +2 -2
- package/dist/{chunk-SYDW647C.js → chunk-IA2FBTWC.js} +2 -2
- package/dist/{chunk-R67DFVLO.js → chunk-IFVINJ4B.js} +2 -2
- package/dist/{chunk-R36V2VP7.js → chunk-IZ5M6OAC.js} +2 -2
- package/dist/{chunk-ODT47UAY.js → chunk-K3BQGZCT.js} +2 -2
- package/dist/{chunk-IFAV6KEM.js → chunk-KP5KD6EN.js} +2 -2
- package/dist/{chunk-ZEWMTR5M.js → chunk-MQMRLGOG.js} +2 -2
- package/dist/{chunk-TSWPNOYM.js → chunk-NQG5XDSB.js} +2 -2
- package/dist/{chunk-7WSD27QQ.js → chunk-PB64GYIG.js} +2 -2
- package/dist/{chunk-HBSWHQNJ.js → chunk-RCYQEFNX.js} +3 -3
- package/dist/{chunk-J3KDJNX2.js → chunk-RH5F53JT.js} +2 -2
- package/dist/{chunk-UAIOHCUK.js → chunk-SFLA7OH3.js} +3 -3
- package/dist/{chunk-Y6O2OCUO.js → chunk-SHM6MRRF.js} +2 -2
- package/dist/{chunk-KDIKRJGB.js → chunk-SHYIRB7I.js} +2 -2
- package/dist/{chunk-HHXFIHXC.js → chunk-SVR2LKYI.js} +2 -2
- package/dist/{chunk-5SBJCB6W.js → chunk-V7AEBY6U.js} +22 -22
- package/dist/{chunk-LRRD7NAG.js → chunk-WSKWVEQB.js} +18 -2
- package/dist/chunk-WSKWVEQB.js.map +1 -0
- package/dist/{chunk-2PVVP7GN.js → chunk-XKEFIFIC.js} +2 -2
- package/dist/{chunk-JRWWGMK7.js → chunk-XYA4XSNU.js} +2 -2
- package/dist/{chunk-X5YKXC6V.js → chunk-YSMEKBTD.js} +2 -2
- package/dist/{chunk-2XU6OGEN.js → chunk-Z4TZ76N7.js} +2 -2
- package/dist/index.js +24 -24
- package/package.json +6 -5
- package/scripts/run-package-tests.mjs +30 -8
- package/scripts/verify-packed-consumer.mjs +1 -1
- package/scripts/verify-pier-agent.mts +1 -0
- package/src/benchmarks/_harness.ts +20 -2
- package/src/benchmarks/humaneval.test.mts +122 -0
- package/src/benchmarks/humaneval.ts +100 -27
- package/src/david-attribution.mts +78 -0
- package/src/david-goliath.mts +149 -0
- package/src/hev-improve.mts +25 -6
- package/src/humaneval-object-ablation.mts +201 -0
- package/src/live-improve-campaign-mbpp.mts +641 -0
- package/src/live-improve-campaign.mts +500 -0
- package/src/mbpp-structural.mts +12 -7
- package/src/quant-arena/README.md +144 -0
- package/src/quant-arena/backtest.test.mts +135 -0
- package/src/quant-arena/backtest.ts +218 -0
- package/src/quant-arena/data.test.mts +44 -0
- package/src/quant-arena/data.ts +141 -0
- package/src/quant-arena/driver.test.mts +253 -0
- package/src/quant-arena/driver.ts +219 -0
- package/src/quant-arena/fixtures/data/PROVENANCE.md +26 -0
- package/src/quant-arena/fixtures/data/holdout/IDX.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S01.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S02.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S03.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S04.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S05.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S06.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S07.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S08.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S09.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S10.csv +523 -0
- package/src/quant-arena/fixtures/data/insample/IDX.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S01.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S02.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S03.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S04.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S05.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S06.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S07.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S08.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S09.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S10.csv +2087 -0
- package/src/quant-arena/fixtures/demo-campaign/cost-ledger.jsonl +16 -0
- package/src/quant-arena/fixtures/demo-campaign/notebook.jsonl +5 -0
- package/src/quant-arena/fixtures/demo-campaign/rollout-manifest.json +171 -0
- package/src/quant-arena/fixtures/demo-campaign/strategies/cand-001-default-author/strategy.ts +119 -0
- package/src/quant-arena/fixtures/demo-campaign/strategies/cand-002-default-author/strategy.ts +119 -0
- package/src/quant-arena/fixtures/demo-campaign/strategies/cand-003-quant-researcher/strategy.ts +105 -0
- package/src/quant-arena/fixtures/demo-campaign/strategies/cand-004-quant-researcher/strategy.ts +102 -0
- package/src/quant-arena/fixtures/demo-campaign-v2/cost-ledger.jsonl +4 -0
- package/src/quant-arena/fixtures/demo-campaign-v2/notebook.jsonl +2 -0
- package/src/quant-arena/fixtures/demo-campaign-v2/rollout-manifest.json +84 -0
- package/src/quant-arena/fixtures/demo-campaign-v2/strategies/cand-001-quant-researcher/strategy.ts +117 -0
- package/src/quant-arena/holdout-certify.mts +206 -0
- package/src/quant-arena/holdout-certify.test.mts +82 -0
- package/src/quant-arena/leak-audit.test.mts +79 -0
- package/src/quant-arena/leak-audit.ts +95 -0
- package/src/quant-arena/make-fixtures.mts +161 -0
- package/src/quant-arena/multiplicity.test.mts +68 -0
- package/src/quant-arena/multiplicity.ts +87 -0
- package/src/quant-arena/nautilus-certify.ts +31 -0
- package/src/quant-arena/oms.ts +90 -0
- package/src/quant-arena/profiles/quant-researcher.profile.json +7 -0
- package/src/quant-arena/python/pyproject.toml +8 -0
- package/src/quant-arena/python/uv.lock +1297 -0
- package/src/quant-arena/python/vbt-worker.py +192 -0
- package/src/quant-arena/quant-loop.mts +813 -0
- package/src/quant-arena/quant-loop.test.mts +75 -0
- package/src/quant-arena/strategies/buy-hold-index/strategy.ts +11 -0
- package/src/quant-arena/strategies/equal-weight/strategy.ts +20 -0
- package/src/quant-arena/strategies/sma-crossover/strategy.ts +42 -0
- package/src/quant-arena/types.ts +133 -0
- package/src/quant-arena/vbt-client.ts +321 -0
- package/src/quant-arena/vbt-parity.test.mts +183 -0
- package/src/quant-arena/windows.test.mts +45 -0
- package/src/quant-arena/windows.ts +54 -0
- package/src/rollout-ledger/backfill-swe-arena.mts +606 -0
- package/src/rollout-ledger/backfill-swe-arena.test.mts +338 -0
- package/src/rollout-ledger/settle-capture.mts +442 -0
- package/src/rollout-ledger/settle-capture.test.mts +270 -0
- package/src/stream-observe.py +45 -0
- package/src/stream-observe.tpl.html +247 -0
- package/src/supervisor-arena.mts +816 -0
- package/src/swe-arena/activation.mts +228 -0
- package/src/swe-arena/activation.test.mts +303 -0
- package/src/swe-arena/analyze.ts +211 -0
- package/src/swe-arena/arms.ts +804 -0
- package/src/swe-arena/bootstrap-meta.mts +188 -0
- package/src/swe-arena/bootstrap-meta.test.mts +51 -0
- package/src/swe-arena/briefing.mts +217 -0
- package/src/swe-arena/briefing.test.mts +178 -0
- package/src/swe-arena/calibrate.ts +217 -0
- package/src/swe-arena/capabilities.mts +76 -0
- package/src/swe-arena/capabilities.test.mts +57 -0
- package/src/swe-arena/capacity.ts +194 -0
- package/src/swe-arena/cell-evidence.mts +437 -0
- package/src/swe-arena/cell-evidence.test.mts +248 -0
- package/src/swe-arena/diagnosis-ensemble.test.mts +210 -0
- package/src/swe-arena/diagnosis-ensemble.ts +520 -0
- package/src/swe-arena/execution.test.mts +1170 -0
- package/src/swe-arena/factory-command-container.ts +284 -0
- package/src/swe-arena/factory-judge-child.mts +228 -0
- package/src/swe-arena/factory.test.mts +643 -0
- package/src/swe-arena/fixtures/analyze.py +80 -0
- package/src/swe-arena/fixtures/excludes.txt +8 -0
- package/src/swe-arena/fixtures/factory/agent-eval-309/calibration.md +51 -0
- package/src/swe-arena/fixtures/factory/agent-eval-309/manifest.json +29 -0
- package/src/swe-arena/fixtures/factory/agent-eval-309/spec.md +64 -0
- package/src/swe-arena/fixtures/factory/agent-runtime-232/calibration.md +48 -0
- package/src/swe-arena/fixtures/factory/agent-runtime-232/manifest.json +29 -0
- package/src/swe-arena/fixtures/factory/agent-runtime-232/spec.md +48 -0
- package/src/swe-arena/fixtures/factory/loops-28/calibration.md +47 -0
- package/src/swe-arena/fixtures/factory/loops-28/manifest.json +30 -0
- package/src/swe-arena/fixtures/factory/loops-28/spec.md +50 -0
- package/src/swe-arena/fixtures/gen1-salvage/README.md +45 -0
- package/src/swe-arena/fixtures/gen1-salvage/cand0-e6d7361.diff +116 -0
- package/src/swe-arena/fixtures/gen1-salvage/cand1-76a8590.diff +293 -0
- package/src/swe-arena/fixtures/holdout-preregister.log +12 -0
- package/src/swe-arena/fixtures/holdout.json +44 -0
- package/src/swe-arena/fixtures/instances.json +146 -0
- package/src/swe-arena/fixtures/ledger.jsonl +12 -0
- package/src/swe-arena/fixtures/patches/pallets__flask-5014.solo.patch +36 -0
- package/src/swe-arena/fixtures/patches/pydata__xarray-4687.sup.patch +33 -0
- package/src/swe-arena/fixtures/rejudge.jsonl +15 -0
- package/src/swe-arena/fixtures/rematch.jsonl +3 -0
- package/src/swe-arena/fixtures/rematch2.jsonl +3 -0
- package/src/swe-arena/fixtures/rematch3.jsonl +3 -0
- package/src/swe-arena/fixtures/run-report/README.md +43 -0
- package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.json +173 -0
- package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.md +100 -0
- package/src/swe-arena/fixtures/run-report/gen3-rollup.json +551 -0
- package/src/swe-arena/fixtures/run-report/gen3-rollup.md +64 -0
- package/src/swe-arena/fixtures/sup-journal-true.json +19 -0
- package/src/swe-arena/fixtures/verify/astropy__astropy-13033.sh +48 -0
- package/src/swe-arena/fixtures/verify/django__django-11532.sh +50 -0
- package/src/swe-arena/fixtures/verify/matplotlib__matplotlib-20826.sh +76 -0
- package/src/swe-arena/fixtures/verify/pydata__xarray-4687.sh +44 -0
- package/src/swe-arena/fixtures/verify/pytest-dev__pytest-6197.sh +32 -0
- package/src/swe-arena/fixtures/verify/sphinx-doc__sphinx-9658.sh +51 -0
- package/src/swe-arena/fixtures/worker-tokens.json +42 -0
- package/src/swe-arena/fixtures.ts +237 -0
- package/src/swe-arena/gepa-seat.mts +583 -0
- package/src/swe-arena/gepa-seat.test.mts +635 -0
- package/src/swe-arena/holdout-certify.mts +408 -0
- package/src/swe-arena/holdout-certify.test.mts +160 -0
- package/src/swe-arena/judge-child.mts +37 -0
- package/src/swe-arena/ledger-orphans.mts +77 -0
- package/src/swe-arena/ledger-orphans.test.mts +147 -0
- package/src/swe-arena/lineage-record.mts +164 -0
- package/src/swe-arena/lineage-record.test.mts +115 -0
- package/src/swe-arena/manifest.mts +293 -0
- package/src/swe-arena/manifest.test.mts +169 -0
- package/src/swe-arena/materialize.ts +142 -0
- package/src/swe-arena/outer-loop.mts +2795 -0
- package/src/swe-arena/outer-loop.test.mts +696 -0
- package/src/swe-arena/parity.test.mts +87 -0
- package/src/swe-arena/premeasured-from-cells.mts +281 -0
- package/src/swe-arena/premeasured-from-cells.test.mts +180 -0
- package/src/swe-arena/proc.test.mts +174 -0
- package/src/swe-arena/proc.ts +260 -0
- package/src/swe-arena/profiles/default-author.profile.json +4 -0
- package/src/swe-arena/proposer-fanout.mts +770 -0
- package/src/swe-arena/proposer-fanout.test.mts +619 -0
- package/src/swe-arena/proposer-provenance.mts +177 -0
- package/src/swe-arena/proposer-provenance.test.mts +106 -0
- package/src/swe-arena/reconcile.ts +0 -0
- package/src/swe-arena/replay.mts +183 -0
- package/src/swe-arena/replay.test.mts +300 -0
- package/src/swe-arena/run-experiment.mts +727 -0
- package/src/swe-arena/run-report.mts +75 -0
- package/src/swe-arena/run-supervisor.mjs +297 -0
- package/src/swe-arena/run-supervisor.test.mts +500 -0
- package/src/swe-arena/score-split.mts +140 -0
- package/src/swe-arena/score-split.test.mts +123 -0
- package/src/swe-arena/serialized-judge.ts +414 -0
- package/src/swe-arena/types.ts +218 -0
- package/src/swe-code-improve.mts +328 -0
- package/src/swe-emit-patch.mts +104 -0
- package/src/swe-improve.mts +232 -0
- package/src/swe-jail.ts +2 -2
- package/src/swe-local-proof.mts +169 -0
- package/src/swe-repro-calibrate.mts +446 -0
- package/src/swe-stream.mts +1497 -0
- package/src/swe-structural.mts +245 -837
- package/dist/chunk-LRRD7NAG.js.map +0 -1
- package/dist/chunk-PPYSEKFM.js.map +0 -1
- /package/dist/{chunk-CKUVRZ2T.js.map → chunk-3U5TXJZS.js.map} +0 -0
- /package/dist/{chunk-YCGY7UIZ.js.map → chunk-7GRVHU22.js.map} +0 -0
- /package/dist/{chunk-Z7ML6L77.js.map → chunk-HWST3SED.js.map} +0 -0
- /package/dist/{chunk-SYDW647C.js.map → chunk-IA2FBTWC.js.map} +0 -0
- /package/dist/{chunk-R67DFVLO.js.map → chunk-IFVINJ4B.js.map} +0 -0
- /package/dist/{chunk-R36V2VP7.js.map → chunk-IZ5M6OAC.js.map} +0 -0
- /package/dist/{chunk-ODT47UAY.js.map → chunk-K3BQGZCT.js.map} +0 -0
- /package/dist/{chunk-IFAV6KEM.js.map → chunk-KP5KD6EN.js.map} +0 -0
- /package/dist/{chunk-ZEWMTR5M.js.map → chunk-MQMRLGOG.js.map} +0 -0
- /package/dist/{chunk-TSWPNOYM.js.map → chunk-NQG5XDSB.js.map} +0 -0
- /package/dist/{chunk-7WSD27QQ.js.map → chunk-PB64GYIG.js.map} +0 -0
- /package/dist/{chunk-HBSWHQNJ.js.map → chunk-RCYQEFNX.js.map} +0 -0
- /package/dist/{chunk-J3KDJNX2.js.map → chunk-RH5F53JT.js.map} +0 -0
- /package/dist/{chunk-UAIOHCUK.js.map → chunk-SFLA7OH3.js.map} +0 -0
- /package/dist/{chunk-Y6O2OCUO.js.map → chunk-SHM6MRRF.js.map} +0 -0
- /package/dist/{chunk-KDIKRJGB.js.map → chunk-SHYIRB7I.js.map} +0 -0
- /package/dist/{chunk-HHXFIHXC.js.map → chunk-SVR2LKYI.js.map} +0 -0
- /package/dist/{chunk-5SBJCB6W.js.map → chunk-V7AEBY6U.js.map} +0 -0
- /package/dist/{chunk-2PVVP7GN.js.map → chunk-XKEFIFIC.js.map} +0 -0
- /package/dist/{chunk-JRWWGMK7.js.map → chunk-XYA4XSNU.js.map} +0 -0
- /package/dist/{chunk-X5YKXC6V.js.map → chunk-YSMEKBTD.js.map} +0 -0
- /package/dist/{chunk-2XU6OGEN.js.map → chunk-Z4TZ76N7.js.map} +0 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PARITY GATE for M2: the typed execution path (materialize → patch apply →
|
|
3
|
+
* extractPatch → serialized-judge → official swebench verdict) must reproduce
|
|
4
|
+
* the pinned M1 fixture verdicts for two committed arm patches — one resolved
|
|
5
|
+
* (pallets__flask-5014 SOLO), one unresolved (pydata__xarray-4687 SUP) —
|
|
6
|
+
* WITHOUT executing any arm. Zero model tokens; docker time only (it pulls the
|
|
7
|
+
* two instance images if absent and runs the official judge, ~5-25 min each).
|
|
8
|
+
*
|
|
9
|
+
* Opt-in because of that docker cost:
|
|
10
|
+
*
|
|
11
|
+
* SWE_ARENA_PARITY=1 ../node_modules/.bin/vitest run src/swe-arena/parity.test.mts
|
|
12
|
+
*
|
|
13
|
+
* Expected verdicts are DERIVED from the pinned fixtures (ledger + rejudge
|
|
14
|
+
* reconciliation), not hardcoded — parity means agreeing with the record.
|
|
15
|
+
*
|
|
16
|
+
* TODO(operator approval): the full 12-instance ARM parity re-run (typed path
|
|
17
|
+
* vs the bash harness on the same 12 ids, both arms) costs ~$1 model spend +
|
|
18
|
+
* ~4h wall. Do not run without an explicit operator go — this 2-patch gate is
|
|
19
|
+
* the milestone check.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { mkdtemp, rm } from 'node:fs/promises'
|
|
23
|
+
import { tmpdir } from 'node:os'
|
|
24
|
+
import { join } from 'node:path'
|
|
25
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
|
26
|
+
import { loadLedger, loadRejudge } from './fixtures.ts'
|
|
27
|
+
import { reconcile } from './reconcile.ts'
|
|
28
|
+
import { createSerializedJudge } from './serialized-judge.ts'
|
|
29
|
+
import { PARITY_CASES, replayPatchParity, type ParityCaseResult } from './run-experiment.mts'
|
|
30
|
+
|
|
31
|
+
const RUN_PARITY = process.env.SWE_ARENA_PARITY === '1'
|
|
32
|
+
// materialize + image pull + official judge, twice; generous but finite.
|
|
33
|
+
const PARITY_BUDGET_MS = 5_400_000
|
|
34
|
+
|
|
35
|
+
describe.runIf(RUN_PARITY)('dry-run parity vs pinned M1 verdicts (docker, no tokens)', () => {
|
|
36
|
+
let results: ParityCaseResult[] = []
|
|
37
|
+
let workDir = ''
|
|
38
|
+
|
|
39
|
+
beforeAll(async () => {
|
|
40
|
+
workDir = await mkdtemp(join(tmpdir(), 'swe-arena-parity-'))
|
|
41
|
+
const judge = createSerializedJudge() // real judge child, 1800s ceiling, serialized
|
|
42
|
+
results = await replayPatchParity(PARITY_CASES, { workDir, judge })
|
|
43
|
+
for (const r of results) {
|
|
44
|
+
console.log(`PARITY ${r.iid} [${r.arm}] verdict=${JSON.stringify(r.verdict)}`)
|
|
45
|
+
}
|
|
46
|
+
}, PARITY_BUDGET_MS)
|
|
47
|
+
|
|
48
|
+
afterAll(async () => {
|
|
49
|
+
if (workDir) await rm(workDir, { recursive: true, force: true })
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('re-extraction preserves each committed patch’s changed-file set', () => {
|
|
53
|
+
expect(results).toHaveLength(2)
|
|
54
|
+
for (const r of results) {
|
|
55
|
+
expect(r.applyRc).toBe(0)
|
|
56
|
+
expect(r.extractedFiles, `${r.iid} [${r.arm}]`).toEqual(r.fixtureFiles)
|
|
57
|
+
expect(r.extractedPatchLines).toBeGreaterThan(0)
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('pallets__flask-5014 SOLO: typed judge verdict == pinned ledger verdict (resolved)', () => {
|
|
62
|
+
const pinned = loadLedger().find((r) => r.iid === 'pallets__flask-5014')
|
|
63
|
+
expect(pinned?.solo_resolved).toBe(true) // sanity: the fixture side of the parity claim
|
|
64
|
+
const r = results.find((x) => x.iid === 'pallets__flask-5014')
|
|
65
|
+
expect(r?.verdict.attempts).toBeGreaterThan(0)
|
|
66
|
+
expect(r?.verdict.resolved).toBe(pinned?.solo_resolved)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('pydata__xarray-4687 SUP: typed judge verdict == reconciled verdict (unresolved)', () => {
|
|
70
|
+
// The authoritative record for this arm is the personal re-judge
|
|
71
|
+
// (sup-final), which M1's reconciliation applies over the ledger.
|
|
72
|
+
const reconciled = reconcile(loadLedger(), loadRejudge()).valid.find(
|
|
73
|
+
(r) => r.iid === 'pydata__xarray-4687',
|
|
74
|
+
)
|
|
75
|
+
expect(reconciled?.sup.source).toBe('sup-final')
|
|
76
|
+
expect(reconciled?.sup.resolved).toBe(false)
|
|
77
|
+
const r = results.find((x) => x.iid === 'pydata__xarray-4687')
|
|
78
|
+
expect(r?.verdict.attempts).toBeGreaterThan(0)
|
|
79
|
+
expect(r?.verdict.resolved).toBe(reconciled?.sup.resolved)
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
describe.runIf(!RUN_PARITY)('dry-run parity (skipped)', () => {
|
|
84
|
+
it('is opt-in: set SWE_ARENA_PARITY=1 to run the docker-backed parity gate', () => {
|
|
85
|
+
expect(RUN_PARITY).toBe(false)
|
|
86
|
+
})
|
|
87
|
+
})
|
|
@@ -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
|
+
})
|