@tangle-network/agent-bench 0.1.0 → 0.3.5
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 +17 -0
- package/HARNESS.md +302 -0
- package/README.md +26 -1
- package/fixtures/aec-bench.json +18 -0
- package/fixtures/agentbench-dbbench.json +22 -0
- package/fixtures/bfcl.json +45 -0
- package/fixtures/commit0.json +72 -0
- package/fixtures/crag.json +10 -0
- package/fixtures/dabstep.json +22 -0
- package/fixtures/enterpriseops-gym.json +103 -0
- package/fixtures/finresearchbench.json +21 -0
- package/fixtures/finsearchcomp.json +66 -0
- package/fixtures/frames.json +26 -0
- package/fixtures/hotpotqa.json +182 -0
- package/fixtures/nomiracl.json +26 -0
- package/fixtures/open-rag-bench.json +16 -0
- package/fixtures/pier-agent/no-model-task/environment/Dockerfile +16 -0
- package/fixtures/pier-agent/no-model-task/environment/seed/src/status.txt +1 -0
- package/fixtures/pier-agent/no-model-task/instruction.md +6 -0
- package/fixtures/pier-agent/no-model-task/pre_artifacts.sh +6 -0
- package/fixtures/pier-agent/no-model-task/task.toml +35 -0
- package/fixtures/pier-agent/no-model-task/tests/Dockerfile +17 -0
- package/fixtures/pier-agent/no-model-task/tests/seed/src/status.txt +1 -0
- package/fixtures/pier-agent/no-model-task/tests/test.sh +19 -0
- package/fixtures/programbench.json +17 -0
- package/fixtures/ragbench.json +21 -0
- package/fixtures/simpleqa.json +121 -0
- package/fixtures/t2-ragbench.json +13 -0
- package/fixtures/tau2-bench.json +16 -0
- package/fixtures/tau3-banking.json +16 -0
- package/fixtures/toollm.json +28 -0
- package/fixtures/webarena-verified.json +20 -0
- package/package.json +39 -15
- package/pier_agents/__init__.py +18 -0
- package/pier_agents/candidate_contract.py +755 -0
- package/pier_agents/process_boundary.py +321 -0
- package/pier_agents/tangle_candidate.py +907 -0
- package/pier_agents/workspace_boundary.py +368 -0
- package/scripts/appworld_driver.py +359 -0
- package/scripts/cadbench_prepare.py +22 -0
- package/scripts/cadgenbench_hard_parts.py +48 -0
- package/scripts/clbench_codebase_judge.py +73 -0
- package/scripts/commit0_judge.py +170 -0
- package/scripts/dabstep_judge.py +42 -0
- package/scripts/enterpriseops_gym_judge.py +281 -0
- package/scripts/programbench_judge.py +120 -0
- package/scripts/render-gate-chart.mjs +176 -0
- package/scripts/run-package-tests.mjs +56 -0
- package/scripts/terminate-pier-trial.mts +66 -0
- package/scripts/trata-hedge/README.md +56 -0
- package/scripts/trata-hedge/run.sh +60 -0
- package/scripts/trata-hedge/solve.py +83 -0
- package/scripts/verify-packed-consumer.mjs +224 -0
- package/scripts/verify-pier-agent.mts +715 -0
- package/scripts/verify-pier-pair.mts +74 -0
- package/scripts/verify-pier-recovery.mts +139 -0
- package/src/adapters.ts +26 -0
- package/src/benchmarks/_harness.test.mts +178 -0
- package/src/benchmarks/_harness.ts +239 -16
- package/src/benchmarks/agentbench.ts +163 -0
- package/src/benchmarks/appworld.test.mts +15 -9
- package/src/benchmarks/bfcl.ts +346 -0
- package/src/benchmarks/crag.ts +137 -0
- package/src/benchmarks/dabstep.test.mts +70 -0
- package/src/benchmarks/dabstep.ts +212 -0
- package/src/benchmarks/external-adapters.test.mts +150 -0
- package/src/benchmarks/finresearchbench.ts +269 -0
- package/src/benchmarks/humaneval.ts +20 -8
- package/src/benchmarks/nomiracl.ts +180 -0
- package/src/benchmarks/open-rag-bench.ts +153 -0
- package/src/benchmarks/rag-benchmarks.test.mts +138 -0
- package/src/benchmarks/rag-shared.ts +327 -0
- package/src/benchmarks/ragbench.ts +171 -0
- package/src/benchmarks/swe-bench.test.mts +61 -0
- package/src/benchmarks/swe-bench.ts +201 -19
- package/src/benchmarks/t2-ragbench.ts +166 -0
- package/src/benchmarks/tau-bench-shared.ts +214 -0
- package/src/benchmarks/tau2-bench.ts +30 -0
- package/src/benchmarks/tau3-banking.ts +29 -0
- package/src/benchmarks/terminal-bench.test.mts +33 -0
- package/src/benchmarks/terminal-bench.ts +23 -8
- package/src/benchmarks/toollm.ts +254 -0
- package/src/benchmarks/types.ts +42 -0
- package/src/benchmarks/webarena-verified.ts +200 -0
- package/src/commit0-prereqs.sh +0 -0
- package/src/coordination-mcp-container-reach.mts +181 -0
- package/src/decoder-live.mts +1 -1
- package/src/examples/README.md +103 -39
- package/src/examples/benchmark-matrix.mts +101 -0
- package/src/examples/lean-proof-gate.README.md +77 -0
- package/src/examples/lean-proof-gate.mts +162 -0
- package/src/examples/lean-verify.ts +95 -0
- package/src/examples/lean.Dockerfile +12 -0
- package/src/examples/math-demo.mts +9 -7
- package/src/examples/strategy-demo.mts +10 -12
- package/src/gate.ts +3 -2
- package/src/hev-eval.mts +69 -0
- package/src/hev-improve.mts +169 -0
- package/src/hev-structural.mts +688 -0
- package/src/index.ts +73 -0
- package/src/mbpp-structural.mts +662 -0
- package/src/pier-agent.test-fixtures.mts +19 -0
- package/src/pier-agent.test.mts +363 -0
- package/src/pier-agent.ts +657 -0
- package/src/pier-result-grader.mjs +30 -0
- package/src/pier-result-grader.test.mts +62 -0
- package/src/pier-result-grader.ts +108 -0
- package/src/pier-task-outcome.test.mts +117 -0
- package/src/pier-task-outcome.ts +240 -0
- package/src/pier-trial-controller.test.mts +412 -0
- package/src/pier-trial-controller.ts +858 -0
- package/src/pier-trial-supervisor.mjs +352 -0
- package/src/resolve-client.ts +25 -2
- package/src/run-benchmarks-cli.mts +72 -0
- package/src/run-benchmarks-report.ts +66 -0
- package/src/run-benchmarks.test.mts +231 -0
- package/src/run-benchmarks.ts +589 -0
- package/src/smoke-structural-rollout.mts +393 -0
- package/src/swe-bench-env.test.ts +207 -0
- package/src/swe-bench-env.ts +554 -0
- package/src/swe-jail.ts +293 -0
- package/src/swe-self-improve.mts +84 -0
- package/src/swe-structural-judge-policy.test.ts +117 -0
- package/src/swe-structural-judge-policy.ts +133 -0
- package/src/swe-structural-policy.test.ts +124 -0
- package/src/swe-structural-policy.ts +132 -0
- package/src/swe-structural-provenance.test.ts +93 -0
- package/src/swe-structural-provenance.ts +138 -0
- package/src/swe-structural.mts +1260 -0
- package/src/swe-temp.ts +14 -0
- package/src/tb-container-executor.mts +234 -0
- package/src/tb-container-executor.test.mts +99 -0
- package/src/tb-supervisor-sidecar.mts +222 -0
- package/src/trata-gepa.mts +1 -1
- package/steerers/eops-itsm-population.json +1 -0
- package/tb_agents/opencode_refine_agent.py +117 -0
- package/tb_agents/opencode_router_agent.py +406 -0
- package/tb_agents/opencode_supervisor_agent.py +239 -0
- package/tb_agents/script_agent.py +66 -0
package/src/swe-jail.ts
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared SWE execution-jail + zai-client primitives — extracted from swe-repro-calibrate.mts
|
|
3
|
+
* (the Stage-0 calibrator, commit 5102c43b) so Stage 1/2 drivers (swe-structural.mts) run on the
|
|
4
|
+
* SAME canary-verified jail instead of forking one. Behavior is byte-identical to the calibrator's
|
|
5
|
+
* in-file originals; the calibrator now imports these.
|
|
6
|
+
*
|
|
7
|
+
* The jail is the byte-identical invocation pattern of swe-bench-env's `run` tool: the instance's
|
|
8
|
+
* cached swebench Docker image, conda testbed, cwd=/testbed, --network none, dual timeout.
|
|
9
|
+
*/
|
|
10
|
+
import { execFile } from 'node:child_process'
|
|
11
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
12
|
+
import { join } from 'node:path'
|
|
13
|
+
import { promisify } from 'node:util'
|
|
14
|
+
import { absoluteSweTempDir } from './swe-temp'
|
|
15
|
+
|
|
16
|
+
const exec = promisify(execFile)
|
|
17
|
+
|
|
18
|
+
export const tail = (s: string, n: number): string => (s.length > n ? `…${s.slice(s.length - n)}` : s)
|
|
19
|
+
|
|
20
|
+
// ---------- zai chat client (plain fetch; patient 429 ladder) ----------
|
|
21
|
+
|
|
22
|
+
export interface ZaiCfg {
|
|
23
|
+
base: string
|
|
24
|
+
key: string
|
|
25
|
+
timeoutMs: number
|
|
26
|
+
/** Optional hard wall-clock stop (epoch ms). No HTTP attempt starts — and no retry-ladder sleep
|
|
27
|
+
* runs — past this instant, so a per-instance deadline reaches INTO the 429 ladder instead of
|
|
28
|
+
* letting a doomed retry sleep for another 240s after the instance was already written off. */
|
|
29
|
+
deadlineAt?: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ZaiRaw {
|
|
33
|
+
/** The parsed /chat/completions JSON, verbatim. */
|
|
34
|
+
json: Record<string, unknown>
|
|
35
|
+
/** HTTP attempts spent (retries included). */
|
|
36
|
+
attempts: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* One OpenAI-shape chat completion against the zai coding endpoint, with the calibrator's retry
|
|
41
|
+
* discipline: zai 429s arrive in sustained bursts (measured: an overnight code-1305 storm zeroed
|
|
42
|
+
* 6/23 instances even on a 30s ladder), so rate-limit retries climb a long ladder (60s → 120s →
|
|
43
|
+
* 240s cap) while transient errors retry on a 2s base. An "empty" completion — no content AND no
|
|
44
|
+
* tool_calls — is the glm reasoning path starving `content` when reasoning eats max_tokens, and is
|
|
45
|
+
* retried too (a tool_calls turn with empty content is a NORMAL tool-loop turn, not starvation).
|
|
46
|
+
*/
|
|
47
|
+
export async function zaiChatRaw(cfg: ZaiCfg, body: Record<string, unknown>): Promise<ZaiRaw> {
|
|
48
|
+
let lastErr = ''
|
|
49
|
+
let delayBase = 2_000
|
|
50
|
+
for (let attempt = 1; attempt <= 7; attempt += 1) {
|
|
51
|
+
if (attempt > 1) {
|
|
52
|
+
const delay = Math.min(delayBase * 2 ** (attempt - 2), 240_000)
|
|
53
|
+
if (cfg.deadlineAt !== undefined && Date.now() + delay >= cfg.deadlineAt) {
|
|
54
|
+
throw new Error(`completion abandoned at deadline (attempt ${attempt}): ${lastErr}`)
|
|
55
|
+
}
|
|
56
|
+
await new Promise((r) => setTimeout(r, delay))
|
|
57
|
+
}
|
|
58
|
+
if (cfg.deadlineAt !== undefined && Date.now() >= cfg.deadlineAt) {
|
|
59
|
+
throw new Error(`completion abandoned at deadline (attempt ${attempt}): ${lastErr || 'no attempt made'}`)
|
|
60
|
+
}
|
|
61
|
+
const perCallTimeout =
|
|
62
|
+
cfg.deadlineAt !== undefined ? Math.max(1, Math.min(cfg.timeoutMs, cfg.deadlineAt - Date.now())) : cfg.timeoutMs
|
|
63
|
+
const ctl = new AbortController()
|
|
64
|
+
const timer = setTimeout(() => ctl.abort(), perCallTimeout)
|
|
65
|
+
try {
|
|
66
|
+
const res = await fetch(`${cfg.base}/chat/completions`, {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
headers: { Authorization: `Bearer ${cfg.key}`, 'Content-Type': 'application/json' },
|
|
69
|
+
body: JSON.stringify(body),
|
|
70
|
+
signal: ctl.signal,
|
|
71
|
+
})
|
|
72
|
+
if (!res.ok) {
|
|
73
|
+
lastErr = `HTTP ${res.status}: ${(await res.text()).slice(0, 200)}`
|
|
74
|
+
delayBase = res.status === 429 ? 60_000 : 2_000
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
const json = (await res.json()) as Record<string, unknown>
|
|
78
|
+
const msg = ((json.choices as Array<{ message?: Record<string, unknown> }> | undefined)?.[0]?.message ?? {}) as {
|
|
79
|
+
content?: string
|
|
80
|
+
tool_calls?: unknown[]
|
|
81
|
+
}
|
|
82
|
+
const hasToolCalls = Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0
|
|
83
|
+
if (!hasToolCalls && String(msg.content ?? '').trim() === '') {
|
|
84
|
+
lastErr = 'empty content'
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
return { json, attempts: attempt }
|
|
88
|
+
} catch (e) {
|
|
89
|
+
lastErr = e instanceof Error ? e.message : String(e)
|
|
90
|
+
} finally {
|
|
91
|
+
clearTimeout(timer)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
throw new Error(`completion failed after retries: ${lastErr}`)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ---------- judge-separation guard ----------
|
|
98
|
+
|
|
99
|
+
/** Refuse any outbound request whose SYSTEM/USER messages (the strings the harness authors) carry
|
|
100
|
+
* hidden-judge content marks (gold patch / test patch lines). Assistant and tool messages are
|
|
101
|
+
* exempt by construction: they are the model's own text and reads of the visible base tree — a
|
|
102
|
+
* model that independently authors the gold line is a success, not a leak. Returns the number of
|
|
103
|
+
* messages checked; throws on contact. */
|
|
104
|
+
export function assertNoHiddenLeak(marks: readonly string[], msgs: ReadonlyArray<{ role?: string; content?: unknown }>): number {
|
|
105
|
+
let checked = 0
|
|
106
|
+
for (const m of msgs) {
|
|
107
|
+
if (m.role !== 'system' && m.role !== 'user') continue
|
|
108
|
+
const c = typeof m.content === 'string' ? m.content : JSON.stringify(m.content ?? '')
|
|
109
|
+
for (const mark of marks) {
|
|
110
|
+
if (mark && c.includes(mark)) {
|
|
111
|
+
throw new Error('REFUSED: hidden judge content (gold/test patch) leaked into a model message')
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
checked += 1
|
|
115
|
+
}
|
|
116
|
+
return checked
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---------- jailed python execution ----------
|
|
120
|
+
|
|
121
|
+
export interface JailRun {
|
|
122
|
+
code: number
|
|
123
|
+
out: string
|
|
124
|
+
timedOut: boolean
|
|
125
|
+
infraError?: string
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Echoed by the in-container shell after a successful `git apply` of the ride-along patch —
|
|
129
|
+
* unambiguously separates "patch failed to apply" (no sentinel, git's error in the output) from
|
|
130
|
+
* "patched code still fails" (sentinel present, the script's own nonzero exit). */
|
|
131
|
+
export const APPLY_SENTINEL = '__PATCH_APPLIED__'
|
|
132
|
+
|
|
133
|
+
let runSeq = 0
|
|
134
|
+
|
|
135
|
+
/** `treeDir === null` ⇒ run against the image's OWN built /testbed (the `image` substrate); a string
|
|
136
|
+
* ⇒ mount that host tree :ro over /testbed (the `mount` substrate — the run tool's exact pattern).
|
|
137
|
+
* `applyPatch` (image-substrate patch runs) applies a patch to the container's writable layer before
|
|
138
|
+
* running; --rm discards it, so every run still starts from the pristine image. */
|
|
139
|
+
export async function runPyInJail(
|
|
140
|
+
imageTag: string,
|
|
141
|
+
treeDir: string | null,
|
|
142
|
+
pyScript: string,
|
|
143
|
+
applyPatch?: string,
|
|
144
|
+
opts: { timeoutS?: number } = {},
|
|
145
|
+
): Promise<JailRun> {
|
|
146
|
+
const scriptDir = mkdtempSync(join(absoluteSweTempDir(), 'swe-repro-'))
|
|
147
|
+
writeFileSync(join(scriptDir, 'repro.py'), pyScript)
|
|
148
|
+
if (applyPatch) writeFileSync(join(scriptDir, 'ride.patch'), applyPatch.endsWith('\n') ? applyPatch : `${applyPatch}\n`)
|
|
149
|
+
const T = opts.timeoutS ?? 120
|
|
150
|
+
const containerName = `swe-repro-${process.pid}-${Date.now()}-${runSeq++}`
|
|
151
|
+
// Identical shape to swe-bench-env's run tool; the command is the fixed `python /repro/repro.py`,
|
|
152
|
+
// so no SWE_CMD env ride-along is needed. The script rides in on a SECOND :ro mount.
|
|
153
|
+
// PYTHONPATH=/testbed is LOAD-BEARING: `python /repro/repro.py` puts /repro (the script's dir), not
|
|
154
|
+
// the cwd, at sys.path[0], so without it `import <pkg>` resolves to the image's baked-in install —
|
|
155
|
+
// the UNPATCHED code — and the patched tree is never exercised (verified on psf__requests-1142:
|
|
156
|
+
// post-gold run kept failing until PYTHONPATH pinned the mounted tree). The run tool never hits this
|
|
157
|
+
// because `python -c`/`python -m` put the cwd on sys.path.
|
|
158
|
+
const shell =
|
|
159
|
+
'{ source /opt/miniconda3/etc/profile.d/conda.sh && conda activate testbed && cd /testbed && ' +
|
|
160
|
+
(applyPatch ? `git apply --whitespace=nowarn /repro/ride.patch && echo ${APPLY_SENTINEL} && ` : '') +
|
|
161
|
+
'timeout -s KILL "$SWE_T"s env PYTHONPATH=/testbed python /repro/repro.py; } 2>&1'
|
|
162
|
+
const dockerArgs = [
|
|
163
|
+
'run', '--rm', '--name', containerName, '--network', 'none',
|
|
164
|
+
...(treeDir ? ['-v', `${treeDir}:/testbed:ro`] : []),
|
|
165
|
+
'-v', `${scriptDir}:/repro:ro`, '-w', '/testbed',
|
|
166
|
+
'-e', 'PYTHONDONTWRITEBYTECODE=1', '-e', `SWE_T=${T}`,
|
|
167
|
+
imageTag, 'bash', '-lc', shell,
|
|
168
|
+
]
|
|
169
|
+
let code = 0
|
|
170
|
+
let out = ''
|
|
171
|
+
// docker exit 125 (the daemon refused to start the container — transient resource pressure,
|
|
172
|
+
// a momentary daemon hiccup) is NOT a test result. Retry a few times with backoff before
|
|
173
|
+
// surfacing it as an infra error, so one bad daemon window can't nuke an instance (or, when it
|
|
174
|
+
// hits the canary of every instance in a fast error-loop, the whole stream).
|
|
175
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
176
|
+
code = 0
|
|
177
|
+
out = ''
|
|
178
|
+
try {
|
|
179
|
+
const r = await exec('docker', dockerArgs, { timeout: (T + 20) * 1000, killSignal: 'SIGKILL', maxBuffer: 20_000_000 })
|
|
180
|
+
out = r.stdout
|
|
181
|
+
} catch (e) {
|
|
182
|
+
const err = e as { code?: number; killed?: boolean; stdout?: string; stderr?: string; message?: string }
|
|
183
|
+
if (typeof err.code === 'number') {
|
|
184
|
+
code = err.code
|
|
185
|
+
out = `${err.stdout ?? ''}${err.stderr ?? ''}`
|
|
186
|
+
} else if (err.killed) {
|
|
187
|
+
code = 124
|
|
188
|
+
out = `${err.stdout ?? ''}\n[host timeout: docker run exceeded ${T + 20}s and was killed]`
|
|
189
|
+
} else {
|
|
190
|
+
rmSync(scriptDir, { recursive: true, force: true })
|
|
191
|
+
return { code: -1, out: '', timedOut: false, infraError: `run failed to execute (${String(err.message ?? e).slice(0, 200)})` }
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const transient = code === 125 || /Cannot connect to the Docker daemon|error creating overlay mount|no space left/i.test(out)
|
|
195
|
+
if (!transient || attempt >= 4) break
|
|
196
|
+
await exec('docker', ['rm', '-f', containerName], { timeout: 20_000 }).catch(() => {})
|
|
197
|
+
await new Promise((r) => setTimeout(r, 2000 * 2 ** attempt))
|
|
198
|
+
}
|
|
199
|
+
rmSync(scriptDir, { recursive: true, force: true })
|
|
200
|
+
if (code === 125 || /Cannot connect to the Docker daemon/i.test(out)) {
|
|
201
|
+
return { code, out, timedOut: false, infraError: `docker: ${out.slice(0, 300)} (persisted through 5 attempts)` }
|
|
202
|
+
}
|
|
203
|
+
return { code, out, timedOut: code === 124 || code === 137 }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ---------- execution canary ----------
|
|
207
|
+
|
|
208
|
+
/** Import name of the repo's primary package, used by the per-instance EXECUTION CANARY. The canary
|
|
209
|
+
* decides whether this substrate can grade this instance AT ALL: with the gold patch applied to the
|
|
210
|
+
* tree under test, `import <pkg>` must succeed AND resolve INTO that tree (/testbed — not the image's
|
|
211
|
+
* site-packages install, which never receives the patch). A canary failure means any grade produced
|
|
212
|
+
* here measures the harness, not the model, so the instance is marked env-unresolvable before a
|
|
213
|
+
* single model call is spent. Never shown to the model. */
|
|
214
|
+
export const IMPORT_NAME: Record<string, string> = {
|
|
215
|
+
'astropy/astropy': 'astropy',
|
|
216
|
+
'django/django': 'django',
|
|
217
|
+
'matplotlib/matplotlib': 'matplotlib',
|
|
218
|
+
'mwaskom/seaborn': 'seaborn',
|
|
219
|
+
'pallets/flask': 'flask',
|
|
220
|
+
'pydata/xarray': 'xarray',
|
|
221
|
+
'psf/requests': 'requests',
|
|
222
|
+
'pylint-dev/pylint': 'pylint',
|
|
223
|
+
'pytest-dev/pytest': 'pytest',
|
|
224
|
+
'scikit-learn/scikit-learn': 'sklearn',
|
|
225
|
+
'sphinx-doc/sphinx': 'sphinx',
|
|
226
|
+
'sympy/sympy': 'sympy',
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** The canary script: exit 0 iff `import <pkg>` succeeds AND resolves into /testbed. */
|
|
230
|
+
export const importCanaryScript = (pkg: string): string =>
|
|
231
|
+
`import sys\nimport ${pkg}\nf = getattr(${pkg}, '__file__', '') or ''\nprint(f)\n` +
|
|
232
|
+
"sys.exit(0 if f.startswith('/testbed') else 3)\n"
|
|
233
|
+
|
|
234
|
+
// ---------- repro-authoring protocol (extracted from swe-repro-calibrate.mts, byte-identical
|
|
235
|
+
// behavior, so the stream driver authors fresh repros with the SAME calibrated protocol instead
|
|
236
|
+
// of forking one) ----------
|
|
237
|
+
|
|
238
|
+
/** The authoring system prompt, parameterized only on the jail's script timeout. */
|
|
239
|
+
export const reproAuthorSystem = (reproTimeoutS: number): string =>
|
|
240
|
+
'You are an expert Python engineer writing a REPRODUCTION script for a reported bug in an open-source repository.\n\n' +
|
|
241
|
+
'Contract for the script you produce:\n' +
|
|
242
|
+
'- A single self-contained Python file, executed as: python /repro/repro.py with cwd=/testbed, where /testbed is the ' +
|
|
243
|
+
"repository checkout. The project's own environment is active, so the repository package and its dependencies are importable.\n" +
|
|
244
|
+
'- Exit code 0 means the bug is FIXED. A nonzero exit (sys.exit(1), a failing assert, or an uncaught exception) means the ' +
|
|
245
|
+
'bug is PRESENT. The script must exercise the EXACT behavior described in the issue.\n' +
|
|
246
|
+
'- The repository tree is READ-ONLY: never modify, create, or delete files inside it. If you genuinely need a scratch ' +
|
|
247
|
+
'file, use the tempfile module (system tmp is writable).\n' +
|
|
248
|
+
`- No network access. Deterministic. Must finish well under ${reproTimeoutS} seconds.\n` +
|
|
249
|
+
"- Prefer plain Python with assert statements. Do not invoke the repository's test-suite runner and do not depend on " +
|
|
250
|
+
'pytest fixtures; importing the repository package directly is the way.\n' +
|
|
251
|
+
'- Print one short line describing what was checked before exiting.\n\n' +
|
|
252
|
+
'Be precise: the script must FAIL on the current buggy code and PASS once the underlying bug is properly fixed. Test the ' +
|
|
253
|
+
'observable behavior the issue describes, not incidental implementation details that a legitimate fix might change.'
|
|
254
|
+
|
|
255
|
+
/** All-READ-lines detector: models sometimes wrap their read requests in a python fence, which must
|
|
256
|
+
* NOT be mistaken for a script (observed live: a "script" of `READ: astropy/timeseries/core.py`
|
|
257
|
+
* reached the jail and crashed with NameError — measuring the parser, not the model). */
|
|
258
|
+
export function asReadRequests(block: string): string[] | null {
|
|
259
|
+
const lines = block.split('\n').map((l) => l.trim()).filter(Boolean)
|
|
260
|
+
if (lines.length > 0 && lines.length <= 3 && lines.every((l) => /^READ:\s*\S+$/.test(l))) {
|
|
261
|
+
return lines.map((l) => l.replace(/^READ:\s*/, ''))
|
|
262
|
+
}
|
|
263
|
+
return null
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function extractReproScript(text: string): string | null {
|
|
267
|
+
// Collapse a doubled fence OPENER (observed live: "```python\n```python\nimport os…" — the
|
|
268
|
+
// non-greedy fence match otherwise captures the empty span between the two openers and a
|
|
269
|
+
// complete script is thrown away as authoring-failed).
|
|
270
|
+
const cleaned = text.replace(/```(?:python|py)?[ \t]*\n(?=```(?:python|py)?[ \t]*\n)/g, '')
|
|
271
|
+
const fences = [...cleaned.matchAll(/```(?:python|py)?\s*\n([\s\S]*?)```/g)]
|
|
272
|
+
const last = fences.at(-1)?.[1]?.trim()
|
|
273
|
+
if (!last || asReadRequests(last)) return null
|
|
274
|
+
return last
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function extractReadRequests(text: string): string[] {
|
|
278
|
+
return [...text.matchAll(/^READ:\s*(\S+)\s*$/gm)].map((m) => m[1]).slice(0, 3)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ---------- backbone enumeration ----------
|
|
282
|
+
|
|
283
|
+
/** The cached-instance backbone: every instance whose swebench eval image is cached locally.
|
|
284
|
+
* Tag → id mapping inverts make_test_spec's `__` → `_1776_` name mangling. */
|
|
285
|
+
export async function cachedInstanceIds(): Promise<string[]> {
|
|
286
|
+
const { stdout } = await exec('docker', ['images', '--format', '{{.Repository}}'], { timeout: 30_000 })
|
|
287
|
+
const ids = new Set<string>()
|
|
288
|
+
for (const line of stdout.split('\n')) {
|
|
289
|
+
const m = /(?:^|\/)sweb\.eval\.x86_64\.(.+)$/.exec(line.trim())
|
|
290
|
+
if (m) ids.add(m[1].replace('_1776_', '__'))
|
|
291
|
+
}
|
|
292
|
+
return [...ids].sort()
|
|
293
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SWE-bench self-improvement — the PROPER, no-cheating run: a frontier worker over the SWE-bench
|
|
3
|
+
* `Environment`, with `runStrategyEvolution` enforcing the train→freeze→holdout split (the substrate
|
|
4
|
+
* draws a disjoint holdout slice and gates once — adaptive reuse is impossible). CONTAMINATION CAVEAT
|
|
5
|
+
* applies (public fixes may be memorized) — reported, never claimed clean.
|
|
6
|
+
*
|
|
7
|
+
* CALIBRATE first (cost gate): TANGLE_API_KEY=… CALIBRATE=1 N=3 tsx bench/src/swe-self-improve.mts
|
|
8
|
+
* Full run: TANGLE_API_KEY=… TRAIN_N=6 HOLDOUT_N=8 GENERATIONS=2 tsx bench/src/swe-self-improve.mts
|
|
9
|
+
*/
|
|
10
|
+
import { mkdtempSync, rmSync } from 'node:fs'
|
|
11
|
+
import { join } from 'node:path'
|
|
12
|
+
import { createChatClient } from '@tangle-network/agent-eval'
|
|
13
|
+
import { refine, runAgentic, runStrategyEvolution, sample } from '@tangle-network/agent-runtime/loops'
|
|
14
|
+
import { createSweBenchEnvironment } from './swe-bench-env'
|
|
15
|
+
|
|
16
|
+
async function main(): Promise<void> {
|
|
17
|
+
const routerKey = process.env.TANGLE_API_KEY
|
|
18
|
+
if (!routerKey) throw new Error('TANGLE_API_KEY required (worker + author call the router)')
|
|
19
|
+
const routerBaseUrl = process.env.ROUTER_BASE ?? 'https://router.tangle.tools/v1'
|
|
20
|
+
const workerModel = process.env.WORKER_MODEL ?? 'gemini-2.5-pro'
|
|
21
|
+
const authorModel = process.env.AUTHOR_MODEL ?? 'gemini-2.5-pro'
|
|
22
|
+
const innerTurns = Number(process.env.INNER_TURNS ?? 40)
|
|
23
|
+
const { environment, tasks } = await createSweBenchEnvironment(Number(process.env.POOL_N ?? 80))
|
|
24
|
+
|
|
25
|
+
if (process.env.CALIBRATE === '1') {
|
|
26
|
+
const n = Number(process.env.N ?? 3)
|
|
27
|
+
const ts = await tasks(0, n)
|
|
28
|
+
console.log(`═══ SWE-bench CALIBRATION — ${workerModel}, baseline=refine, ${n} real bugs ═══`)
|
|
29
|
+
let resolved = 0
|
|
30
|
+
for (const t of ts) {
|
|
31
|
+
const t0 = Date.now()
|
|
32
|
+
const r = await runAgentic({ surface: environment, task: t, strategy: refine, routerBaseUrl, routerKey, model: workerModel, maxTokens: 8000, innerTurns, budget: 1 })
|
|
33
|
+
if (r.resolved) resolved++
|
|
34
|
+
console.log(` ${t.id.padEnd(32)} resolved=${r.resolved} completions=${r.completions} shots=${r.shots} (${Math.round((Date.now() - t0) / 1000)}s)`)
|
|
35
|
+
}
|
|
36
|
+
const band = resolved > 0 && resolved < n
|
|
37
|
+
console.log(`\n>>> baseline resolved ${resolved}/${n}. ${band ? 'HEADROOM — the loop has room to improve. PROCEED.' : resolved === 0 ? 'TOO HARD / env issue — inspect before the loop.' : 'saturated at this small n — raise N.'}`)
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const report = await (async () => {
|
|
42
|
+
const outDir = mkdtempSync(join(process.cwd(), '.swe-run-'))
|
|
43
|
+
try {
|
|
44
|
+
return await runStrategyEvolution({
|
|
45
|
+
environment,
|
|
46
|
+
tasks,
|
|
47
|
+
trainN: Number(process.env.TRAIN_N ?? 6),
|
|
48
|
+
holdoutN: Number(process.env.HOLDOUT_N ?? 8),
|
|
49
|
+
worker: { routerBaseUrl, routerKey, model: workerModel, maxTokens: 8000, innerTurns },
|
|
50
|
+
author: {
|
|
51
|
+
chat: createChatClient({ transport: 'router', baseUrl: routerBaseUrl, apiKey: routerKey, defaultModel: authorModel }),
|
|
52
|
+
model: authorModel,
|
|
53
|
+
maxTokens: 8000,
|
|
54
|
+
fallbackModel: process.env.AUTHOR_FALLBACK ?? 'deepseek-v4-flash',
|
|
55
|
+
},
|
|
56
|
+
baselines: [sample, refine],
|
|
57
|
+
budget: Number(process.env.BUDGET ?? 2),
|
|
58
|
+
generations: Number(process.env.GENERATIONS ?? 2),
|
|
59
|
+
populationSize: Number(process.env.POP ?? 2),
|
|
60
|
+
outDir,
|
|
61
|
+
})
|
|
62
|
+
} finally {
|
|
63
|
+
rmSync(outDir, { recursive: true, force: true })
|
|
64
|
+
}
|
|
65
|
+
})()
|
|
66
|
+
|
|
67
|
+
const v = report.verdict
|
|
68
|
+
console.log('\n═══ SWE-bench SELF-IMPROVEMENT — certified on a FROZEN holdout (CONTAMINATION-flagged) ═══')
|
|
69
|
+
console.log(`worker=${workerModel} author=${authorModel}`)
|
|
70
|
+
console.log(`gen0 champion: ${report.gen0Champion.name}`)
|
|
71
|
+
console.log(`final champion: ${report.finalChampion.name}`)
|
|
72
|
+
console.log(`PROMOTED: ${v.promoted} (${v.reason})`)
|
|
73
|
+
console.log(`held-out lift: mean ${v.lift.mean.toFixed(3)} 95% CI [${v.lift.low.toFixed(3)}, ${v.lift.high.toFixed(3)}] n=${v.n}`)
|
|
74
|
+
console.log(
|
|
75
|
+
v.promoted
|
|
76
|
+
? '\n>>> The search taught the agent a strategy that resolves MORE real bugs it never trained on, beyond luck. (Report the contamination caveat: public fixes may be memorized.)'
|
|
77
|
+
: '\n>>> No promotion: the evolved strategy did not beat gen0 on the fresh holdout beyond noise (honest null).',
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
main().catch((e) => {
|
|
82
|
+
console.error(e instanceof Error ? (e.stack ?? e.message) : String(e))
|
|
83
|
+
process.exit(1)
|
|
84
|
+
})
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { linkSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join, relative } from 'node:path'
|
|
5
|
+
import { describe, it } from 'node:test'
|
|
6
|
+
import { createFingerprints } from './swe-structural-provenance'
|
|
7
|
+
import {
|
|
8
|
+
assertCompleteTaskSet,
|
|
9
|
+
assertDistinctArtifactPaths,
|
|
10
|
+
assertJudgeCompletionMatchesInput,
|
|
11
|
+
assertJudgeResumeFingerprints,
|
|
12
|
+
assertPairedExecutionFingerprint,
|
|
13
|
+
assertPairedFingerprints,
|
|
14
|
+
completeJudgeScore,
|
|
15
|
+
} from './swe-structural-judge-policy'
|
|
16
|
+
|
|
17
|
+
const inputs = {
|
|
18
|
+
source: 'source',
|
|
19
|
+
config: { arm: 'independent-2' },
|
|
20
|
+
commonConfig: { model: 'worker' },
|
|
21
|
+
repro: 'repro',
|
|
22
|
+
prompt: 'prompt',
|
|
23
|
+
tools: ['run'],
|
|
24
|
+
task: 'task',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('judge-only admission', () => {
|
|
28
|
+
it('requires a complete Phase-A task set before scoring', () => {
|
|
29
|
+
assert.doesNotThrow(() => assertCompleteTaskSet(['a', 'b'], ['a', 'b'], 'left'))
|
|
30
|
+
assert.throws(() => assertCompleteTaskSet(['a'], ['a', 'b'], 'left'), /incomplete Phase A/)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('accepts arm-specific config hashes but rejects any shared-input mismatch', () => {
|
|
34
|
+
const left = createFingerprints(inputs)
|
|
35
|
+
const right = createFingerprints({ ...inputs, config: { arm: 'persistent-refine-2' } })
|
|
36
|
+
assert.doesNotThrow(() => assertPairedFingerprints(left, right, 'task'))
|
|
37
|
+
const changedTools = createFingerprints({ ...inputs, config: { arm: 'persistent-refine-2' }, tools: ['edit'] })
|
|
38
|
+
assert.throws(() => assertPairedFingerprints(left, changedTools, 'task'), /paired tools fingerprints/)
|
|
39
|
+
assert.doesNotThrow(() => assertPairedExecutionFingerprint('execution', 'execution', 'task'))
|
|
40
|
+
assert.throws(() => assertPairedExecutionFingerprint('left', 'right', 'task'), /paired execution/)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('rejects a judge resume row tied to different Phase-A bytes', () => {
|
|
44
|
+
const expected = {
|
|
45
|
+
pairFingerprint: 'pair',
|
|
46
|
+
phaseFileFingerprint: 'phase-new',
|
|
47
|
+
inputFingerprint: 'input',
|
|
48
|
+
executionFingerprint: 'execution',
|
|
49
|
+
finalDiffHash: 'diff',
|
|
50
|
+
}
|
|
51
|
+
assert.doesNotThrow(() => assertJudgeResumeFingerprints(expected, expected, 'row'))
|
|
52
|
+
assert.throws(() =>
|
|
53
|
+
assertJudgeResumeFingerprints({ ...expected, phaseFileFingerprint: 'phase-old' }, expected, 'row'),
|
|
54
|
+
/phaseFileFingerprint mismatch/,
|
|
55
|
+
)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('returns only completed scores and propagates scorer failures for retry', async () => {
|
|
59
|
+
assert.deepEqual(await completeJudgeScore('', async () => ({ resolved: true })), {
|
|
60
|
+
hiddenResolved: false,
|
|
61
|
+
judgeDetail: null,
|
|
62
|
+
judgeSkipped: 'empty-patch',
|
|
63
|
+
})
|
|
64
|
+
await assert.rejects(
|
|
65
|
+
completeJudgeScore('diff --git a/a b/a', async () => {
|
|
66
|
+
throw new Error('transient scorer failure')
|
|
67
|
+
}),
|
|
68
|
+
/transient scorer failure/,
|
|
69
|
+
)
|
|
70
|
+
assert.deepEqual(await completeJudgeScore('diff', async () => ({ resolved: true, detail: 'ok' })), {
|
|
71
|
+
hiddenResolved: true,
|
|
72
|
+
judgeDetail: 'ok',
|
|
73
|
+
judgeSkipped: null,
|
|
74
|
+
})
|
|
75
|
+
assert.doesNotThrow(() =>
|
|
76
|
+
assertJudgeCompletionMatchesInput(
|
|
77
|
+
{ hiddenResolved: false, judgeDetail: null, judgeSkipped: 'empty-patch' },
|
|
78
|
+
'',
|
|
79
|
+
'row',
|
|
80
|
+
),
|
|
81
|
+
)
|
|
82
|
+
assert.throws(() =>
|
|
83
|
+
assertJudgeCompletionMatchesInput(
|
|
84
|
+
{ hiddenResolved: false, judgeDetail: null, judgeSkipped: 'empty-patch' },
|
|
85
|
+
'diff',
|
|
86
|
+
'row',
|
|
87
|
+
),
|
|
88
|
+
/non-empty patch cannot skip/,
|
|
89
|
+
)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('rejects relative, symlink, and hardlink aliases before scoring', () => {
|
|
93
|
+
const dir = mkdtempSync(join(tmpdir(), 'swe-judge-paths-'))
|
|
94
|
+
try {
|
|
95
|
+
const phase = join(dir, 'phase-a.jsonl')
|
|
96
|
+
const other = join(dir, 'phase-b.jsonl')
|
|
97
|
+
const symlink = join(dir, 'phase-link.jsonl')
|
|
98
|
+
const hardlink = join(dir, 'phase-hard.jsonl')
|
|
99
|
+
writeFileSync(phase, '{}\n')
|
|
100
|
+
writeFileSync(other, '{}\n')
|
|
101
|
+
symlinkSync(phase, symlink)
|
|
102
|
+
linkSync(phase, hardlink)
|
|
103
|
+
|
|
104
|
+
assert.throws(() =>
|
|
105
|
+
assertDistinctArtifactPaths({ phase, alias: relative(process.cwd(), phase), out: join(dir, 'out.jsonl') }),
|
|
106
|
+
/alias the same artifact file/,
|
|
107
|
+
)
|
|
108
|
+
assert.throws(() => assertDistinctArtifactPaths({ phase, symlink, out: join(dir, 'out.jsonl') }),
|
|
109
|
+
/alias the same artifact file/,
|
|
110
|
+
)
|
|
111
|
+
assert.throws(() => assertDistinctArtifactPaths({ phase, hardlink, other }), /alias the same artifact file/)
|
|
112
|
+
assert.doesNotThrow(() => assertDistinctArtifactPaths({ phase, other, out: join(dir, 'out.jsonl') }))
|
|
113
|
+
} finally {
|
|
114
|
+
rmSync(dir, { recursive: true, force: true })
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
})
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { realpathSync, statSync } from 'node:fs'
|
|
2
|
+
import { basename, dirname, join, resolve } from 'node:path'
|
|
3
|
+
import type { Fingerprints } from './swe-structural-provenance'
|
|
4
|
+
|
|
5
|
+
export const PAIRED_FINGERPRINT_KEYS = [
|
|
6
|
+
'source',
|
|
7
|
+
'commonConfig',
|
|
8
|
+
'repro',
|
|
9
|
+
'prompt',
|
|
10
|
+
'tools',
|
|
11
|
+
'task',
|
|
12
|
+
] as const satisfies ReadonlyArray<keyof Fingerprints>
|
|
13
|
+
|
|
14
|
+
export function assertCompleteTaskSet(
|
|
15
|
+
rowIds: Iterable<string>,
|
|
16
|
+
taskIds: readonly string[],
|
|
17
|
+
context: string,
|
|
18
|
+
): void {
|
|
19
|
+
const rows = new Set(rowIds)
|
|
20
|
+
if (rows.size !== taskIds.length || taskIds.some((id) => !rows.has(id))) {
|
|
21
|
+
throw new Error(`${context}: incomplete Phase A (${rows.size}/${taskIds.length} required rows)`)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function assertPairedFingerprints(
|
|
26
|
+
left: Fingerprints,
|
|
27
|
+
right: Fingerprints,
|
|
28
|
+
context: string,
|
|
29
|
+
): void {
|
|
30
|
+
for (const key of PAIRED_FINGERPRINT_KEYS) {
|
|
31
|
+
if (left[key] !== right[key]) throw new Error(`${context}: paired ${key} fingerprints do not match`)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function assertPairedExecutionFingerprint(left: string, right: string, context: string): void {
|
|
36
|
+
if (!left || left !== right) throw new Error(`${context}: paired execution fingerprints do not match`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface ArtifactPathIdentity {
|
|
40
|
+
canonicalPath: string
|
|
41
|
+
device: number | null
|
|
42
|
+
inode: number | null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function artifactPathIdentity(path: string): ArtifactPathIdentity {
|
|
46
|
+
const absolute = resolve(path)
|
|
47
|
+
try {
|
|
48
|
+
const canonicalPath = realpathSync(absolute)
|
|
49
|
+
const stat = statSync(canonicalPath)
|
|
50
|
+
return { canonicalPath, device: stat.dev, inode: stat.ino }
|
|
51
|
+
} catch (error) {
|
|
52
|
+
const code = (error as NodeJS.ErrnoException).code
|
|
53
|
+
if (code !== 'ENOENT') throw error
|
|
54
|
+
const canonicalParent = realpathSync(dirname(absolute))
|
|
55
|
+
return { canonicalPath: join(canonicalParent, basename(absolute)), device: null, inode: null }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Reject relative/absolute, symlink, and hardlink aliases before any judge output is opened. */
|
|
60
|
+
export function assertDistinctArtifactPaths(paths: Record<string, string>): void {
|
|
61
|
+
const entries = Object.entries(paths).map(([name, path]) => ({ name, ...artifactPathIdentity(path) }))
|
|
62
|
+
for (let leftIndex = 0; leftIndex < entries.length; leftIndex += 1) {
|
|
63
|
+
for (let rightIndex = leftIndex + 1; rightIndex < entries.length; rightIndex += 1) {
|
|
64
|
+
const left = entries[leftIndex]!
|
|
65
|
+
const right = entries[rightIndex]!
|
|
66
|
+
const sameCanonicalPath = left.canonicalPath === right.canonicalPath
|
|
67
|
+
const sameExistingFile =
|
|
68
|
+
left.device !== null &&
|
|
69
|
+
right.device !== null &&
|
|
70
|
+
left.device === right.device &&
|
|
71
|
+
left.inode === right.inode
|
|
72
|
+
if (sameCanonicalPath || sameExistingFile) {
|
|
73
|
+
throw new Error(`${left.name} and ${right.name} alias the same artifact file`)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface CompletedJudgeScore {
|
|
80
|
+
hiddenResolved: boolean
|
|
81
|
+
judgeDetail: string | null
|
|
82
|
+
judgeSkipped: 'empty-patch' | null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** A scorer error rejects the promise, so callers cannot serialize an incomplete judge row. */
|
|
86
|
+
export async function completeJudgeScore(
|
|
87
|
+
finalDiff: string,
|
|
88
|
+
judge: () => Promise<{ resolved?: boolean; detail?: unknown }>,
|
|
89
|
+
): Promise<CompletedJudgeScore> {
|
|
90
|
+
if (!finalDiff.trim()) {
|
|
91
|
+
return { hiddenResolved: false, judgeDetail: null, judgeSkipped: 'empty-patch' }
|
|
92
|
+
}
|
|
93
|
+
const score = await judge()
|
|
94
|
+
return {
|
|
95
|
+
hiddenResolved: score.resolved ?? false,
|
|
96
|
+
judgeDetail: String(score.detail ?? '').slice(0, 1_000),
|
|
97
|
+
judgeSkipped: null,
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function assertJudgeCompletionMatchesInput(
|
|
102
|
+
row: CompletedJudgeScore,
|
|
103
|
+
finalDiff: string,
|
|
104
|
+
context: string,
|
|
105
|
+
): void {
|
|
106
|
+
if (!finalDiff.trim()) {
|
|
107
|
+
if (row.hiddenResolved !== false || row.judgeSkipped !== 'empty-patch') {
|
|
108
|
+
throw new Error(`${context}: empty patch has an invalid completed-judge receipt`)
|
|
109
|
+
}
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
if (row.judgeSkipped !== null) {
|
|
113
|
+
throw new Error(`${context}: non-empty patch cannot skip official scoring`)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface JudgeResumeFingerprintReceipt {
|
|
118
|
+
pairFingerprint: string
|
|
119
|
+
phaseFileFingerprint: string
|
|
120
|
+
inputFingerprint: string
|
|
121
|
+
executionFingerprint: string
|
|
122
|
+
finalDiffHash: string
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function assertJudgeResumeFingerprints(
|
|
126
|
+
actual: JudgeResumeFingerprintReceipt,
|
|
127
|
+
expected: JudgeResumeFingerprintReceipt,
|
|
128
|
+
context: string,
|
|
129
|
+
): void {
|
|
130
|
+
for (const key of Object.keys(expected) as Array<keyof JudgeResumeFingerprintReceipt>) {
|
|
131
|
+
if (actual[key] !== expected[key]) throw new Error(`${context}: judge resume ${key} mismatch`)
|
|
132
|
+
}
|
|
133
|
+
}
|