@tangle-network/agent-bench 0.3.7 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/HARNESS.md +43 -0
- package/README.md +7 -0
- package/dist/adapters.js +23 -23
- 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/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-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-PWQVGAJB.js → chunk-V7AEBY6U.js} +21 -21
- 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 +23 -23
- package/package.json +5 -5
- package/scripts/trata-hedge/README.md +6 -5
- package/scripts/verify-packed-consumer.mjs +1 -1
- package/src/benchmarks/_harness.ts +20 -2
- package/src/benchmarks/humaneval.test.mts +2 -2
- package/src/gate.ts +1 -1
- package/src/hev-eval.mts +5 -2
- package/src/hev-improve.mts +118 -73
- package/src/official-optimizer-config.mts +89 -0
- package/src/official-optimizer-config.test.mts +88 -0
- package/src/profiles.ts +2 -2
- 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 +342 -0
- package/src/rollout-ledger/settle-capture.mts +442 -0
- package/src/rollout-ledger/settle-capture.test.mts +270 -0
- package/src/smoke-structural-rollout.mts +15 -9
- package/src/swe-arena/activation.mts +225 -0
- package/src/swe-arena/activation.test.mts +300 -0
- package/src/swe-arena/arms.ts +30 -14
- package/src/swe-arena/briefing.mts +217 -0
- package/src/swe-arena/briefing.test.mts +178 -0
- package/src/swe-arena/calibrate.ts +102 -1
- package/src/swe-arena/cell-evidence.mts +41 -9
- 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/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/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.ts +135 -2
- package/src/swe-arena/gepa-seat.mts +877 -0
- package/src/swe-arena/gepa-seat.test.mts +1059 -0
- package/src/swe-arena/implementation-ref.test.mts +64 -0
- package/src/swe-arena/implementation-ref.ts +62 -0
- package/src/swe-arena/ledger-orphans.mts +77 -0
- package/src/swe-arena/ledger-orphans.test.mts +147 -0
- package/src/swe-arena/outer-loop.mts +710 -33
- 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/proposer-fanout.mts +327 -31
- package/src/swe-arena/proposer-fanout.test.mts +246 -0
- package/src/swe-arena/proposer-provenance.mts +172 -0
- package/src/swe-arena/proposer-provenance.test.mts +106 -0
- package/src/swe-arena/run-experiment.mts +367 -1
- package/src/swe-arena/run-report.mts +75 -0
- package/src/swe-arena/run-supervisor.test.mts +5 -3
- package/src/swe-arena/score-split.mts +140 -0
- package/src/swe-arena/score-split.test.mts +123 -0
- package/src/swe-arena/scratch-worktree.test.mts +55 -0
- package/src/swe-arena/scratch-worktree.ts +34 -0
- package/src/swe-arena/types.ts +52 -0
- package/src/swe-code-improve.mts +24 -25
- package/src/swe-improve.mts +129 -96
- package/src/swe-local-proof.mts +6 -1
- package/src/swe-stream.mts +4 -2
- package/src/swe-structural.mts +245 -837
- package/src/tb-container-executor.test.mts +30 -6
- package/src/tb-supervisor-sidecar.mts +2 -1
- package/src/trata-gepa.mts +182 -245
- package/dist/chunk-LRRD7NAG.js.map +0 -1
- package/src/live-improve-campaign-mbpp.mts +0 -641
- package/src/live-improve-campaign.mts +0 -500
- /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-PWQVGAJB.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,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settle-time capture with LABEL v2: the contribution rule (delivered worker
|
|
3
|
+
* vs sibling bystander vs identity gap), baseline-relative proposer rewards,
|
|
4
|
+
* campaign-path attribution, and end-to-end line validity on a synthetic
|
|
5
|
+
* supervisor run dir (opencode store absent → labeled gap lines).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
9
|
+
import { tmpdir } from 'node:os'
|
|
10
|
+
import { join } from 'node:path'
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
12
|
+
import { readRolloutLedger } from '@tangle-network/agent-eval/rollout'
|
|
13
|
+
import {
|
|
14
|
+
campaignCoordsFromCellPath,
|
|
15
|
+
createSettleCapture,
|
|
16
|
+
deliveredWorkerLabels,
|
|
17
|
+
PROPOSER_REWARD_SOURCE_V2,
|
|
18
|
+
proposerRewardV2,
|
|
19
|
+
readWorkerEvidence,
|
|
20
|
+
WORKER_REWARD_SOURCE_V2,
|
|
21
|
+
workerRewardV2,
|
|
22
|
+
type CellCaptureArgs,
|
|
23
|
+
} from './settle-capture.mts'
|
|
24
|
+
|
|
25
|
+
describe('workerRewardV2 (contribution rule)', () => {
|
|
26
|
+
it('rewards ONLY the delivering worker in a resolved cell', () => {
|
|
27
|
+
expect(workerRewardV2({ resolved: true, isDelivered: true, identityKnown: true })).toEqual({
|
|
28
|
+
reward: 1,
|
|
29
|
+
bystander: false,
|
|
30
|
+
deliveredMatch: 'delivered',
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('marks resolved-cell siblings as bystanders with reward 0', () => {
|
|
35
|
+
expect(workerRewardV2({ resolved: true, isDelivered: false, identityKnown: true })).toEqual({
|
|
36
|
+
reward: 0,
|
|
37
|
+
bystander: true,
|
|
38
|
+
deliveredMatch: 'bystander',
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('gives every worker 0 in an unresolved cell (no bystander flag)', () => {
|
|
43
|
+
expect(workerRewardV2({ resolved: false, isDelivered: false, identityKnown: true })).toEqual({
|
|
44
|
+
reward: 0,
|
|
45
|
+
bystander: false,
|
|
46
|
+
deliveredMatch: 'unresolved',
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('labels an identity gap as reward null (never fabricated credit) and an inconclusive cell as null', () => {
|
|
51
|
+
expect(workerRewardV2({ resolved: true, isDelivered: false, identityKnown: false })).toEqual({
|
|
52
|
+
reward: null,
|
|
53
|
+
bystander: false,
|
|
54
|
+
deliveredMatch: 'unknown',
|
|
55
|
+
})
|
|
56
|
+
expect(workerRewardV2({ resolved: null, isDelivered: false, identityKnown: false }).reward).toBeNull()
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('deliveredWorkerLabels', () => {
|
|
61
|
+
const workers = [
|
|
62
|
+
{ label: 'w1', cwd: '/tmp/w1', patch: 'diff --git a/x b/x\n+fix\n' },
|
|
63
|
+
{ label: 'w2', cwd: '/tmp/w2', patch: 'diff --git a/y b/y\n+other\n' },
|
|
64
|
+
{ label: 'w3', cwd: null, patch: null },
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
it('matches by trimmed patch equality', () => {
|
|
68
|
+
expect(deliveredWorkerLabels('diff --git a/x b/x\n+fix\n\n', workers)).toEqual(['w1'])
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('returns [] for an empty delivery or no match', () => {
|
|
72
|
+
expect(deliveredWorkerLabels('', workers)).toEqual([])
|
|
73
|
+
expect(deliveredWorkerLabels('diff --git a/z b/z\n+mystery\n', workers)).toEqual([])
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe('proposerRewardV2 (baseline-relative)', () => {
|
|
78
|
+
it('is positive for an improvement, negative for a regression, zero for a tie', () => {
|
|
79
|
+
expect(proposerRewardV2(3, 1, 6)).toBeCloseTo(2 / 6)
|
|
80
|
+
expect(proposerRewardV2(0, 1, 6)).toBeCloseTo(-1 / 6)
|
|
81
|
+
expect(proposerRewardV2(1, 1, 6)).toBe(0)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('rejects a zero instance count', () => {
|
|
85
|
+
expect(() => proposerRewardV2(1, 0, 0)).toThrow(/instanceCount/)
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
describe('campaignCoordsFromCellPath (directory attribution, never dispatch order)', () => {
|
|
90
|
+
it('parses candidate and baseline cells; unknown shapes return null', () => {
|
|
91
|
+
expect(campaignCoordsFromCellPath('/x/improve-run/gen-0/candidate-2/cell-1/arm-summary.json')).toEqual({
|
|
92
|
+
generation: 0,
|
|
93
|
+
candidateIndex: 2,
|
|
94
|
+
})
|
|
95
|
+
expect(campaignCoordsFromCellPath('/x/improve-run/baseline/cell-1/arm-summary.json')).toEqual({
|
|
96
|
+
generation: -1,
|
|
97
|
+
candidateIndex: -1,
|
|
98
|
+
})
|
|
99
|
+
expect(campaignCoordsFromCellPath('/x/somewhere/else.json')).toBeNull()
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
describe('readWorkerEvidence', () => {
|
|
104
|
+
let supRunDir: string
|
|
105
|
+
|
|
106
|
+
beforeEach(async () => {
|
|
107
|
+
supRunDir = await mkdtemp(join(tmpdir(), 'sup-'))
|
|
108
|
+
const workers = join(supRunDir, 'workers')
|
|
109
|
+
await mkdir(workers, { recursive: true })
|
|
110
|
+
await writeFile(
|
|
111
|
+
join(workers, 'w1.ndjson'),
|
|
112
|
+
`${JSON.stringify({ kind: 'started', cwd: '/tmp/clone-w1' })}\n${JSON.stringify({ kind: 'settled' })}\n`,
|
|
113
|
+
)
|
|
114
|
+
await writeFile(join(workers, 'w1.patch'), 'diff --git a/x b/x\n+fix\n')
|
|
115
|
+
await writeFile(join(workers, 'w2.ndjson'), `${JSON.stringify({ kind: 'started', cwd: '/tmp/clone-w2' })}\n`)
|
|
116
|
+
// Inbox files must not create phantom workers.
|
|
117
|
+
await writeFile(join(workers, 'w1.inbox.ndjson'), '{}\n')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
afterEach(async () => {
|
|
121
|
+
await rm(supRunDir, { recursive: true, force: true })
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('joins per-worker cwd + patch; a worker without a patch reads patch:null', async () => {
|
|
125
|
+
const records = await readWorkerEvidence(supRunDir)
|
|
126
|
+
expect(records).toEqual([
|
|
127
|
+
{ label: 'w1', cwd: '/tmp/clone-w1', patch: 'diff --git a/x b/x\n+fix\n' },
|
|
128
|
+
{ label: 'w2', cwd: '/tmp/clone-w2', patch: null },
|
|
129
|
+
])
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('returns [] for a run dir without worker evidence', async () => {
|
|
133
|
+
expect(await readWorkerEvidence(join(supRunDir, 'nope'))).toEqual([])
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
describe('createSettleCapture end-to-end (opencode store absent)', () => {
|
|
138
|
+
let root: string
|
|
139
|
+
let supRunDir: string
|
|
140
|
+
let ledgerPath: string
|
|
141
|
+
|
|
142
|
+
const cellArgs = (over: Partial<CellCaptureArgs> = {}): CellCaptureArgs => ({
|
|
143
|
+
generation: 0,
|
|
144
|
+
candidateIndex: 1,
|
|
145
|
+
iid: 'pydata__xarray-4687',
|
|
146
|
+
rep: 0,
|
|
147
|
+
seed: 42,
|
|
148
|
+
splitVisibility: 'public',
|
|
149
|
+
commit: 'c'.repeat(40),
|
|
150
|
+
resolved: true,
|
|
151
|
+
judgeVerdict: { resolved: true, attempts: 1 },
|
|
152
|
+
runDir: join(root, 'runs', 'pydata__xarray-4687', 'R4'),
|
|
153
|
+
patchPath: join(root, 'delivered.patch'),
|
|
154
|
+
supRunDir,
|
|
155
|
+
deliveredPatch: 'diff --git a/x b/x\n+fix\n',
|
|
156
|
+
workerModel: 'zai-coding-plan/glm-5.2',
|
|
157
|
+
metrics: { resolved: true, verify_pass: true },
|
|
158
|
+
cost: { usd: 1.25, wallS: 900, spentTokens: 1000 },
|
|
159
|
+
...over,
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
beforeEach(async () => {
|
|
163
|
+
root = await mkdtemp(join(tmpdir(), 'settle-'))
|
|
164
|
+
ledgerPath = join(root, 'rollout-ledger.jsonl')
|
|
165
|
+
supRunDir = join(root, 'ws', '.loops', 'supervisor', 's1')
|
|
166
|
+
const workers = join(supRunDir, 'workers')
|
|
167
|
+
await mkdir(workers, { recursive: true })
|
|
168
|
+
await writeFile(join(workers, 'w1.ndjson'), `${JSON.stringify({ kind: 'started', cwd: '/tmp/clone-w1' })}\n`)
|
|
169
|
+
await writeFile(join(workers, 'w1.patch'), 'diff --git a/x b/x\n+fix\n')
|
|
170
|
+
await writeFile(join(workers, 'w2.ndjson'), `${JSON.stringify({ kind: 'started', cwd: '/tmp/clone-w2' })}\n`)
|
|
171
|
+
await writeFile(join(workers, 'w2.patch'), 'diff --git a/y b/y\n+other\n')
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
afterEach(async () => {
|
|
175
|
+
await rm(root, { recursive: true, force: true })
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
const capture = () =>
|
|
179
|
+
createSettleCapture({
|
|
180
|
+
ledgerPath,
|
|
181
|
+
runId: 'r4-test',
|
|
182
|
+
instanceCount: 6,
|
|
183
|
+
opencodeDb: join(root, 'no-such.db'),
|
|
184
|
+
now: () => new Date('2026-07-23T00:00:00Z'),
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('emits schema-valid supervisor + worker lines with v2 labels: delivered worker 1, bystander 0', async () => {
|
|
188
|
+
const { lines } = await capture().captureCell(cellArgs())
|
|
189
|
+
expect(lines).toBe(3)
|
|
190
|
+
const ledger = await readRolloutLedger(ledgerPath) // validates every line
|
|
191
|
+
expect(ledger).toHaveLength(3)
|
|
192
|
+
|
|
193
|
+
const sup = ledger.find((l) => l.role === 'supervisor')!
|
|
194
|
+
expect(sup.outcome.reward).toBe(1)
|
|
195
|
+
expect(sup.outcome.reward_source).toBe('swe-arena-official-judge')
|
|
196
|
+
expect(sup.generation).toBe(0)
|
|
197
|
+
expect(sup.candidate_index).toBe(1)
|
|
198
|
+
expect(sup.candidate_id).toBe('gen0-cand1')
|
|
199
|
+
// The canonical trainable split, not the legacy `train` alias.
|
|
200
|
+
expect(sup.task.split).toBe('search')
|
|
201
|
+
expect(sup.outcome.metrics.split_visibility).toBe('public')
|
|
202
|
+
expect(sup.provenance.capture).toBe('settle-time')
|
|
203
|
+
|
|
204
|
+
const workers = ledger.filter((l) => l.role === 'worker')
|
|
205
|
+
expect(workers).toHaveLength(2)
|
|
206
|
+
for (const w of workers) {
|
|
207
|
+
expect(w.parent_rollout_id).toBe(sup.rollout_id)
|
|
208
|
+
expect(w.candidate_id).toBe('gen0-cand1')
|
|
209
|
+
expect(w.task.split).toBe('search')
|
|
210
|
+
expect(w.outcome.reward_source).toBe(WORKER_REWARD_SOURCE_V2)
|
|
211
|
+
// Store absent → labeled gap, never a silent drop.
|
|
212
|
+
expect(w.messages).toEqual([])
|
|
213
|
+
expect(w.provenance.gap).toBeTruthy()
|
|
214
|
+
// No session at all, so no completed invocation to claim.
|
|
215
|
+
expect(w.outcome.metrics.has_session).toBe(false)
|
|
216
|
+
expect(w.outcome.is_completed).toBe(false)
|
|
217
|
+
}
|
|
218
|
+
const delivered = workers.find((w) => w.outcome.metrics.worker_label === 'w1')!
|
|
219
|
+
const bystander = workers.find((w) => w.outcome.metrics.worker_label === 'w2')!
|
|
220
|
+
expect(delivered.outcome.reward).toBe(1)
|
|
221
|
+
expect(delivered.outcome.metrics.bystander).toBe(false)
|
|
222
|
+
expect(delivered.outcome.metrics.delivered_match).toBe('delivered')
|
|
223
|
+
expect(bystander.outcome.reward).toBe(0)
|
|
224
|
+
expect(bystander.outcome.metrics.bystander).toBe(true)
|
|
225
|
+
expect(bystander.outcome.metrics.delivered_match).toBe('bystander')
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('gives all workers 0 in an unresolved cell', async () => {
|
|
229
|
+
await capture().captureCell(cellArgs({ resolved: false }))
|
|
230
|
+
const workers = (await readRolloutLedger(ledgerPath)).filter((l) => l.role === 'worker')
|
|
231
|
+
expect(workers.map((w) => w.outcome.reward)).toEqual([0, 0])
|
|
232
|
+
expect(workers.every((w) => w.outcome.metrics.bystander === false)).toBe(true)
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('labels an unmatched delivery as an identity gap: worker rewards null', async () => {
|
|
236
|
+
await capture().captureCell(cellArgs({ deliveredPatch: 'diff --git a/z b/z\n+mystery\n' }))
|
|
237
|
+
const workers = (await readRolloutLedger(ledgerPath)).filter((l) => l.role === 'worker')
|
|
238
|
+
expect(workers.map((w) => w.outcome.reward)).toEqual([null, null])
|
|
239
|
+
expect(workers.every((w) => w.outcome.metrics.delivered_match === 'unknown')).toBe(true)
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('emits a baseline-relative proposer line (improvement positive, v2 source)', async () => {
|
|
243
|
+
await capture().captureProposer({
|
|
244
|
+
generation: 0,
|
|
245
|
+
candidateIndex: 1,
|
|
246
|
+
proposer: 'glm-author',
|
|
247
|
+
harness: 'opencode',
|
|
248
|
+
commit: 'c'.repeat(40),
|
|
249
|
+
candResolved: 3,
|
|
250
|
+
baselineResolved: 1,
|
|
251
|
+
shotReceiptPaths: [],
|
|
252
|
+
diffPath: null,
|
|
253
|
+
})
|
|
254
|
+
const [line] = await readRolloutLedger(ledgerPath)
|
|
255
|
+
expect(line!.role).toBe('proposer')
|
|
256
|
+
expect(line!.outcome.reward).toBeCloseTo(2 / 6)
|
|
257
|
+
expect(line!.outcome.reward_source).toBe(PROPOSER_REWARD_SOURCE_V2)
|
|
258
|
+
expect(line!.outcome.metrics.baseline_resolved_count).toBe(1)
|
|
259
|
+
expect(line!.task.instance_id).toBe('gen0-cand1-glm-author')
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
it('appends across cells (one ledger, many flushes) and keeps every line valid', async () => {
|
|
263
|
+
const c = capture()
|
|
264
|
+
await c.captureCell(cellArgs())
|
|
265
|
+
await c.captureCell(cellArgs({ rep: 1, resolved: false, splitVisibility: 'private' }))
|
|
266
|
+
const ledger = await readRolloutLedger(ledgerPath)
|
|
267
|
+
expect(ledger).toHaveLength(6)
|
|
268
|
+
expect(ledger.filter((l) => l.outcome.metrics.split_visibility === 'private')).toHaveLength(3)
|
|
269
|
+
})
|
|
270
|
+
})
|
|
@@ -199,14 +199,25 @@ async function runTask(t: HumanEvalTask): Promise<TaskRow> {
|
|
|
199
199
|
`${t.taskId}: ${result.selection.length} receipts vs ${scored.length} scored candidates`,
|
|
200
200
|
)
|
|
201
201
|
}
|
|
202
|
-
|
|
202
|
+
const receipts = result.selection.map((receipt) => {
|
|
203
|
+
if (receipt.score === undefined || receipt.reason === undefined) {
|
|
204
|
+
throw new Error(`${t.taskId}: receipt #${receipt.candidateIndex} is missing score or reason`)
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
candidateIndex: receipt.candidateIndex,
|
|
208
|
+
selected: receipt.selected,
|
|
209
|
+
score: receipt.score,
|
|
210
|
+
reason: receipt.reason,
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
for (const r of receipts) {
|
|
203
214
|
const rec = scored[r.candidateIndex]
|
|
204
215
|
if (!rec || Math.abs(r.score - visibleCheckScore(rec.outcome)) > 1e-9) {
|
|
205
216
|
throw new Error(`${t.taskId}: receipt #${r.candidateIndex} score ${r.score} does not match the recorded outcome`)
|
|
206
217
|
}
|
|
207
218
|
}
|
|
208
219
|
|
|
209
|
-
const sampleCount =
|
|
220
|
+
const sampleCount = receipts.filter((r) => r.reason.startsWith('sample')).length
|
|
210
221
|
const samples = scored.slice(0, sampleCount)
|
|
211
222
|
if (samples.length === 0) throw new Error(`${t.taskId}: no sample candidates settled`)
|
|
212
223
|
|
|
@@ -223,7 +234,7 @@ async function runTask(t: HumanEvalTask): Promise<TaskRow> {
|
|
|
223
234
|
const sampleHidden = await Promise.all(samples.map((s) => grade(s.candidate)))
|
|
224
235
|
const selectedIdx = selectBestIndex(samples.map((s) => s.outcome))
|
|
225
236
|
const selectedHidden = sampleHidden[selectedIdx] as number
|
|
226
|
-
const winner =
|
|
237
|
+
const winner = receipts.find((r) => r.selected)
|
|
227
238
|
if (!winner) throw new Error(`${t.taskId}: no receipt marked selected`)
|
|
228
239
|
const finalIdx = winner.candidateIndex
|
|
229
240
|
const finalHidden = await grade((scored[finalIdx] as ScoredCandidate).candidate)
|
|
@@ -241,12 +252,7 @@ async function runTask(t: HumanEvalTask): Promise<TaskRow> {
|
|
|
241
252
|
finalIdx,
|
|
242
253
|
finalHidden,
|
|
243
254
|
selectedVisible: visibleCheckScore((samples[selectedIdx] as ScoredCandidate).outcome),
|
|
244
|
-
receipts
|
|
245
|
-
candidateIndex: r.candidateIndex,
|
|
246
|
-
selected: r.selected,
|
|
247
|
-
score: r.score,
|
|
248
|
-
reason: r.reason,
|
|
249
|
-
})),
|
|
255
|
+
receipts,
|
|
250
256
|
tokens: result.tokens,
|
|
251
257
|
usd: result.usd,
|
|
252
258
|
ms: result.ms,
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gen-5 activation gate (SOTA adoption #2, GSME-style "verify the mechanism
|
|
3
|
+
* fired before the score counts").
|
|
4
|
+
*
|
|
5
|
+
* Every proposer's deliverable must include a MACHINE-CHECKABLE ACTIVATION
|
|
6
|
+
* PREDICATE at `.improve/activation.json` (inside the change-space's metadata
|
|
7
|
+
* prefix, so the improvement driver's finalize commits it with the candidate):
|
|
8
|
+
* a grep pattern or script over the candidate's OWN campaign run artifacts
|
|
9
|
+
* that proves its mechanism actually fired (e.g. "the new prompt section
|
|
10
|
+
* rendered in worker prompts", "patchRiskWarnings emitted in >=1 settle").
|
|
11
|
+
*
|
|
12
|
+
* Enforcement is two-stage, both fail-closed:
|
|
13
|
+
* 1. PREFILTER — a candidate without a parseable predicate is killed before
|
|
14
|
+
* any evaluation spend (proposer-fanout.mts, stage 'activation-predicate').
|
|
15
|
+
* 2. POST-EVAL — the evaluator runs the predicate over the candidate's own
|
|
16
|
+
* cell run dirs; a candidate whose mechanism NEVER fired is QUARANTINED
|
|
17
|
+
* (staircase verdict 'quarantined-inactive': recorded, never promoted)
|
|
18
|
+
* even when its score improved — a score with an inactive mechanism is
|
|
19
|
+
* indistinguishable from luck or from gaming the visible instances.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { readFile, readdir, stat } from 'node:fs/promises'
|
|
23
|
+
import { join, relative } from 'node:path'
|
|
24
|
+
import { run } from './proc.ts'
|
|
25
|
+
|
|
26
|
+
export const ACTIVATION_PREDICATE_RELPATH = '.improve/activation.json'
|
|
27
|
+
|
|
28
|
+
export interface ActivationPredicate {
|
|
29
|
+
/** One sentence: which mechanism this proves fired. */
|
|
30
|
+
description: string
|
|
31
|
+
kind: 'grep' | 'script'
|
|
32
|
+
/** kind 'grep': JS RegExp source tested line-by-line over run artifacts. */
|
|
33
|
+
pattern?: string
|
|
34
|
+
/** kind 'grep': optional relative-path substring filters (a file is searched
|
|
35
|
+
* when its run-dir-relative path contains ANY entry). Empty/absent = all. */
|
|
36
|
+
files?: string[]
|
|
37
|
+
/** kind 'script': bash script; run once per run dir with cwd=<runDir> and
|
|
38
|
+
* $RUN_DIR set; exit 0 in ANY run dir = mechanism fired. */
|
|
39
|
+
script?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type ParsedPredicate = { ok: true; predicate: ActivationPredicate } | { ok: false; error: string }
|
|
43
|
+
|
|
44
|
+
export function parseActivationPredicate(raw: string): ParsedPredicate {
|
|
45
|
+
let value: unknown
|
|
46
|
+
try {
|
|
47
|
+
value = JSON.parse(raw)
|
|
48
|
+
} catch (cause) {
|
|
49
|
+
return { ok: false, error: `not valid JSON: ${(cause as Error).message}` }
|
|
50
|
+
}
|
|
51
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
52
|
+
return { ok: false, error: 'must be a JSON object' }
|
|
53
|
+
}
|
|
54
|
+
const p = value as Record<string, unknown>
|
|
55
|
+
if (typeof p.description !== 'string' || p.description.trim().length === 0) {
|
|
56
|
+
return { ok: false, error: 'description must be a non-empty string' }
|
|
57
|
+
}
|
|
58
|
+
if (p.kind === 'grep') {
|
|
59
|
+
if (typeof p.pattern !== 'string' || p.pattern.length === 0) {
|
|
60
|
+
return { ok: false, error: 'kind "grep" requires a non-empty pattern' }
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
new RegExp(p.pattern)
|
|
64
|
+
} catch (cause) {
|
|
65
|
+
return { ok: false, error: `pattern is not a valid RegExp: ${(cause as Error).message}` }
|
|
66
|
+
}
|
|
67
|
+
if (p.files !== undefined && (!Array.isArray(p.files) || p.files.some((f) => typeof f !== 'string'))) {
|
|
68
|
+
return { ok: false, error: 'files must be an array of strings when present' }
|
|
69
|
+
}
|
|
70
|
+
} else if (p.kind === 'script') {
|
|
71
|
+
if (typeof p.script !== 'string' || p.script.trim().length === 0) {
|
|
72
|
+
return { ok: false, error: 'kind "script" requires a non-empty script' }
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
return { ok: false, error: `kind must be "grep" or "script", got ${JSON.stringify(p.kind)}` }
|
|
76
|
+
}
|
|
77
|
+
return { ok: true, predicate: p as unknown as ActivationPredicate }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The prompt-visible contract: template + worked example. */
|
|
81
|
+
export function activationPredicateInstruction(): string {
|
|
82
|
+
return [
|
|
83
|
+
'ACTIVATION PREDICATE (required deliverable — a candidate without one is rejected before evaluation):',
|
|
84
|
+
`Write ${ACTIVATION_PREDICATE_RELPATH} in this worktree: a machine-checkable proof that YOUR mechanism`,
|
|
85
|
+
"actually fired during evaluation. The evaluator runs it over your candidate's own run artifacts",
|
|
86
|
+
'(each arm run dir: driver.log, brain.jsonl, result.json, ws/.loops/** journal + worker evidence);',
|
|
87
|
+
'if it never fires, your candidate is QUARANTINED even when its score improved.',
|
|
88
|
+
'',
|
|
89
|
+
'Template (kind "grep" — a RegExp tested over the run artifacts):',
|
|
90
|
+
' {',
|
|
91
|
+
' "description": "patchRiskWarnings emitted in at least one settle",',
|
|
92
|
+
' "kind": "grep",',
|
|
93
|
+
' "pattern": "patchRiskWarnings|patch-risk",',
|
|
94
|
+
' "files": ["journal.jsonl", "workers/"]',
|
|
95
|
+
' }',
|
|
96
|
+
'Or kind "script": {"description":"...","kind":"script","script":"grep -rq NEW_SECTION ws/.loops"}',
|
|
97
|
+
'(exit 0 in any run dir = fired).',
|
|
98
|
+
'Pick a pattern that can ONLY appear when your mechanism ran — not one that matches the diff itself.',
|
|
99
|
+
].join('\n')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Predicate execution over run dirs.
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
export interface ActivationResult {
|
|
107
|
+
fired: boolean
|
|
108
|
+
/** Bounded evidence: matching `path:line` refs (grep) or script stdout tails. */
|
|
109
|
+
evidence: string[]
|
|
110
|
+
checkedRunDirs: number
|
|
111
|
+
checkedFiles: number
|
|
112
|
+
/** Bounded notes on skipped inputs (oversized files, walk caps). */
|
|
113
|
+
warnings: string[]
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const MAX_FILES_PER_RUN_DIR = 4000
|
|
117
|
+
const MAX_FILE_BYTES = 32 * 1024 * 1024
|
|
118
|
+
const MAX_EVIDENCE = 10
|
|
119
|
+
const SCRIPT_TIMEOUT_MS = 120_000
|
|
120
|
+
|
|
121
|
+
/** Walk one run dir. The `ws/` workspace subtree (a whole checked-out repo) is
|
|
122
|
+
* skipped EXCEPT `ws/.loops/**` — the supervisor's own artifacts live there
|
|
123
|
+
* and are exactly where mechanism traces land. */
|
|
124
|
+
async function walkRunDir(runDir: string, warnings: string[]): Promise<string[]> {
|
|
125
|
+
const files: string[] = []
|
|
126
|
+
const queue: string[] = [runDir]
|
|
127
|
+
while (queue.length > 0) {
|
|
128
|
+
const dir = queue.shift()!
|
|
129
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch(() => [])
|
|
130
|
+
for (const entry of entries) {
|
|
131
|
+
const abs = join(dir, entry.name)
|
|
132
|
+
const rel = relative(runDir, abs)
|
|
133
|
+
if (entry.isDirectory()) {
|
|
134
|
+
if (rel === 'ws') {
|
|
135
|
+
queue.push(join(abs, '.loops'))
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
queue.push(abs)
|
|
139
|
+
} else if (entry.isFile()) {
|
|
140
|
+
files.push(abs)
|
|
141
|
+
if (files.length >= MAX_FILES_PER_RUN_DIR) {
|
|
142
|
+
warnings.push(`${runDir}: file walk capped at ${MAX_FILES_PER_RUN_DIR} files`)
|
|
143
|
+
return files
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return files
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Run the predicate over the candidate's run dirs. Fail-closed: an unreadable
|
|
152
|
+
* artifact contributes nothing (with a warning) — it can never count as
|
|
153
|
+
* "fired". */
|
|
154
|
+
export async function runActivationPredicate(
|
|
155
|
+
predicate: ActivationPredicate,
|
|
156
|
+
runDirs: readonly string[],
|
|
157
|
+
): Promise<ActivationResult> {
|
|
158
|
+
const result: ActivationResult = { fired: false, evidence: [], checkedRunDirs: 0, checkedFiles: 0, warnings: [] }
|
|
159
|
+
for (const runDir of runDirs) {
|
|
160
|
+
result.checkedRunDirs += 1
|
|
161
|
+
if (predicate.kind === 'script') {
|
|
162
|
+
const res = await run('bash', ['-c', predicate.script!], {
|
|
163
|
+
cwd: runDir,
|
|
164
|
+
timeoutMs: SCRIPT_TIMEOUT_MS,
|
|
165
|
+
env: { ...process.env, RUN_DIR: runDir },
|
|
166
|
+
})
|
|
167
|
+
if (res.code === 0) {
|
|
168
|
+
result.fired = true
|
|
169
|
+
if (result.evidence.length < MAX_EVIDENCE) {
|
|
170
|
+
result.evidence.push(`${runDir}: script rc=0${res.stdout.trim() ? ` — ${res.stdout.trim().slice(0, 200)}` : ''}`)
|
|
171
|
+
}
|
|
172
|
+
} else if (res.timedOut) {
|
|
173
|
+
result.warnings.push(`${runDir}: script timed out after ${SCRIPT_TIMEOUT_MS}ms (counted as not-fired)`)
|
|
174
|
+
}
|
|
175
|
+
continue
|
|
176
|
+
}
|
|
177
|
+
const regex = new RegExp(predicate.pattern!)
|
|
178
|
+
const filters = (predicate.files ?? []).filter((f) => f.length > 0)
|
|
179
|
+
for (const file of await walkRunDir(runDir, result.warnings)) {
|
|
180
|
+
const rel = relative(runDir, file)
|
|
181
|
+
if (filters.length > 0 && !filters.some((f) => rel.includes(f))) continue
|
|
182
|
+
const info = await stat(file).catch(() => null)
|
|
183
|
+
if (info === null) continue
|
|
184
|
+
if (info.size > MAX_FILE_BYTES) {
|
|
185
|
+
result.warnings.push(`${rel}: skipped (${info.size} bytes > ${MAX_FILE_BYTES})`)
|
|
186
|
+
continue
|
|
187
|
+
}
|
|
188
|
+
result.checkedFiles += 1
|
|
189
|
+
const content = await readFile(file, 'utf8').catch(() => null)
|
|
190
|
+
if (content === null) continue
|
|
191
|
+
const lines = content.split('\n')
|
|
192
|
+
for (let i = 0; i < lines.length; i++) {
|
|
193
|
+
if (regex.test(lines[i]!)) {
|
|
194
|
+
result.fired = true
|
|
195
|
+
if (result.evidence.length < MAX_EVIDENCE) {
|
|
196
|
+
result.evidence.push(`${file}:${i + 1}: ${lines[i]!.trim().slice(0, 200)}`)
|
|
197
|
+
}
|
|
198
|
+
break // one match per file is enough evidence
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return result
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Read + parse the predicate committed at a candidate's loops commit. */
|
|
207
|
+
export async function readCommittedPredicate(
|
|
208
|
+
loopsRepo: string,
|
|
209
|
+
commit: string,
|
|
210
|
+
): Promise<{ raw: string; parsed: ParsedPredicate } | null> {
|
|
211
|
+
const show = await run('git', ['-C', loopsRepo, 'show', `${commit}:${ACTIVATION_PREDICATE_RELPATH}`])
|
|
212
|
+
if (show.code !== 0) return null
|
|
213
|
+
return { raw: show.stdout, parsed: parseActivationPredicate(show.stdout) }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** The staircase row's activation record. */
|
|
217
|
+
export interface ActivationRecord {
|
|
218
|
+
/** Whether a parseable predicate was present on the candidate commit. */
|
|
219
|
+
present: boolean
|
|
220
|
+
description: string | null
|
|
221
|
+
/** null = not evaluated (no predicate / gate disabled / baseline). */
|
|
222
|
+
fired: boolean | null
|
|
223
|
+
evidence: string[]
|
|
224
|
+
warnings: string[]
|
|
225
|
+
}
|