@tangle-network/agent-bench 0.7.1 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/HARNESS.md +2 -2
  3. package/dist/adapters.js +6 -2
  4. package/dist/adapters.js.map +1 -1
  5. package/dist/benchmarks/appworld.d.ts +61 -2
  6. package/dist/benchmarks/appworld.js +111 -40
  7. package/dist/benchmarks/appworld.js.map +1 -1
  8. package/dist/benchmarks/cadbench.js +1 -1
  9. package/dist/benchmarks/cadgenbench.js +1 -1
  10. package/dist/benchmarks/finresearchbench.js +16 -22
  11. package/dist/benchmarks/finresearchbench.js.map +1 -1
  12. package/dist/benchmarks/finsearchcomp.js +16 -22
  13. package/dist/benchmarks/finsearchcomp.js.map +1 -1
  14. package/dist/benchmarks/frames.js +18 -20
  15. package/dist/benchmarks/frames.js.map +1 -1
  16. package/dist/benchmarks/mcad-bench.d.ts +106 -0
  17. package/dist/benchmarks/mcad-bench.js +569 -0
  18. package/dist/benchmarks/mcad-bench.js.map +1 -0
  19. package/dist/benchmarks/mcad-cq-bench.d.ts +82 -0
  20. package/dist/benchmarks/mcad-cq-bench.js +339 -0
  21. package/dist/benchmarks/mcad-cq-bench.js.map +1 -0
  22. package/dist/benchmarks/mcad-cq-golds.d.ts +36 -0
  23. package/dist/benchmarks/mcad-cq-golds.js +342 -0
  24. package/dist/benchmarks/mcad-cq-golds.js.map +1 -0
  25. package/dist/benchmarks/mcad-golds.d.ts +20 -0
  26. package/dist/benchmarks/mcad-golds.js +318 -0
  27. package/dist/benchmarks/mcad-golds.js.map +1 -0
  28. package/dist/benchmarks/mcad-tasks.d.ts +66 -0
  29. package/dist/benchmarks/mcad-tasks.js +508 -0
  30. package/dist/benchmarks/mcad-tasks.js.map +1 -0
  31. package/dist/benchmarks/simpleqa.js +15 -19
  32. package/dist/benchmarks/simpleqa.js.map +1 -1
  33. package/dist/benchmarks/trata-hedge.js +20 -23
  34. package/dist/benchmarks/trata-hedge.js.map +1 -1
  35. package/dist/{cadbench-BrpwOU6A.js → cadbench-BLSyxR1N.js} +21 -20
  36. package/dist/cadbench-BLSyxR1N.js.map +1 -0
  37. package/dist/{cadgenbench-DF7hYHdl.js → cadgenbench-x2OFkf8y.js} +2 -2
  38. package/dist/cadgenbench-x2OFkf8y.js.map +1 -0
  39. package/dist/index.d.ts +0 -1
  40. package/dist/index.js +10 -9
  41. package/dist/index.js.map +1 -1
  42. package/dist/router-turn-C2wMiDoo.js +31 -0
  43. package/dist/router-turn-C2wMiDoo.js.map +1 -0
  44. package/package.json +7 -7
  45. package/scripts/appworld_driver.py +1 -170
  46. package/scripts/run-package-tests.mjs +56 -36
  47. package/scripts/run-package-tests.test.mjs +33 -0
  48. package/scripts/trata-hedge/README.md +3 -3
  49. package/scripts/trata-hedge/run.sh +6 -2
  50. package/src/adapters.ts +11 -0
  51. package/src/aec-gate.mts +30 -9
  52. package/src/agent-graphs-gen2.mts +523 -0
  53. package/src/agent-graphs-gen3.mts +660 -0
  54. package/src/agent-graphs-improve/offline-seams.mts +128 -0
  55. package/src/agent-graphs-improve.mts +747 -0
  56. package/src/atom-humaneval.mts +76 -55
  57. package/src/atom-mcp-e2e.mts +27 -13
  58. package/src/benchmarks/appworld.test.mts +119 -1
  59. package/src/benchmarks/appworld.ts +156 -33
  60. package/src/benchmarks/cadbench.ts +21 -8
  61. package/src/benchmarks/finresearchbench.ts +20 -16
  62. package/src/benchmarks/finsearchcomp.ts +20 -20
  63. package/src/benchmarks/frames.ts +19 -18
  64. package/src/benchmarks/mcad-bench.test.mts +455 -0
  65. package/src/benchmarks/mcad-bench.ts +561 -0
  66. package/src/benchmarks/mcad-cq-bench.ts +423 -0
  67. package/src/benchmarks/mcad-cq-golds.ts +374 -0
  68. package/src/benchmarks/mcad-cq.test.mts +386 -0
  69. package/src/benchmarks/mcad-golds.ts +359 -0
  70. package/src/benchmarks/mcad-tasks.ts +490 -0
  71. package/src/benchmarks/simpleqa.ts +19 -17
  72. package/src/benchmarks/trata-hedge.ts +26 -18
  73. package/src/browser/ui-reviewer.ts +1 -1
  74. package/src/clbench-context-gate.mts +45 -9
  75. package/src/cloud-loop.mts +24 -6
  76. package/src/commit0-env-run.mts +6 -3
  77. package/src/commit0-env.ts +2 -3
  78. package/src/commit0-gate.mts +53 -24
  79. package/src/corpus.ts +3 -3
  80. package/src/david-attribution.mts +28 -9
  81. package/src/david-goliath.mts +31 -12
  82. package/src/egress-probe.mts +3 -3
  83. package/src/examples/lean-proof-gate.mts +10 -3
  84. package/src/examples/math-demo.mts +10 -3
  85. package/src/examples/strategy-demo.mts +10 -4
  86. package/src/fleet.mts +30 -8
  87. package/src/gate-cli.mts +9 -4
  88. package/src/gate.test.mts +7 -2
  89. package/src/gate.ts +23 -75
  90. package/src/generate-eval/certify.ts +13 -5
  91. package/src/hev-eval.mts +39 -10
  92. package/src/hev-improve.mts +28 -16
  93. package/src/hev-structural.mts +28 -20
  94. package/src/humaneval-gate.mts +24 -8
  95. package/src/humaneval-object-ablation.mts +52 -14
  96. package/src/humaneval-repair-gate.mts +53 -14
  97. package/src/mbpp-structural.mts +28 -20
  98. package/src/mcp-mount-probe.mts +23 -11
  99. package/src/official-optimizer-config.mts +61 -12
  100. package/src/official-optimizer-config.test.mts +72 -3
  101. package/src/quant-arena/profiles/quant-researcher.profile.json +6 -1
  102. package/src/quant-arena/quant-loop.mts +88 -61
  103. package/src/research-shot.ts +21 -11
  104. package/src/resolve-client.ts +6 -8
  105. package/src/rollout-ledger/backfill-swe-arena.test.mts +7 -2
  106. package/src/router-executor.ts +1 -1
  107. package/src/router-turn.ts +135 -0
  108. package/src/run-benchmarks.ts +7 -2
  109. package/src/sandbox-run.ts +23 -41
  110. package/src/search-bench/bridge.ts +66 -8
  111. package/src/search-bench/parametric-check.mts +19 -4
  112. package/src/search-bench/profiles.ts +24 -9
  113. package/src/search-bench/run.mts +3 -4
  114. package/src/smoke-structural-rollout.mts +21 -5
  115. package/src/supervisor-arena.mts +28 -20
  116. package/src/swe-arena/arms.ts +73 -15
  117. package/src/swe-arena/capacity.ts +44 -41
  118. package/src/swe-arena/diagnosis-ensemble.ts +51 -48
  119. package/src/swe-arena/execution.test.mts +12 -12
  120. package/src/swe-arena/gepa-seat.mts +12 -5
  121. package/src/swe-arena/gepa-seat.test.mts +45 -33
  122. package/src/swe-arena/outer-loop.mts +94 -70
  123. package/src/swe-arena/premeasured-from-cells.mts +16 -1
  124. package/src/swe-arena/premeasured-from-cells.test.mts +21 -1
  125. package/src/swe-arena/profiles/deepseek-author.profile.json +12 -0
  126. package/src/swe-arena/profiles/default-author.profile.json +9 -1
  127. package/src/swe-arena/proposer-fanout.mts +52 -99
  128. package/src/swe-arena/proposer-fanout.test.mts +92 -57
  129. package/src/swe-arena/proposer-provenance.mts +1 -1
  130. package/src/swe-code-improve.mts +30 -52
  131. package/src/swe-emit-patch.mts +26 -5
  132. package/src/swe-improve.mts +27 -8
  133. package/src/swe-jail.test.ts +128 -0
  134. package/src/swe-jail.ts +106 -22
  135. package/src/swe-local-proof.mts +27 -5
  136. package/src/swe-repro-calibrate.mts +5 -0
  137. package/src/swe-self-improve.mts +45 -10
  138. package/src/swe-stream.mts +68 -6
  139. package/src/swe-structural.mts +46 -7
  140. package/src/tb-container-executor.mts +38 -6
  141. package/src/tb-container-executor.test.mts +11 -3
  142. package/src/tb-supervisor-sidecar.mts +11 -27
  143. package/src/terminal-compare.ts +38 -18
  144. package/src/trata-gate.mts +26 -26
  145. package/src/trata-gepa.mts +25 -16
  146. package/src/trata-hedge-solve.mts +80 -0
  147. package/src/worker-blender.ts +21 -11
  148. package/src/worker-browser.ts +18 -5
  149. package/src/worker-build123d.ts +17 -8
  150. package/src/worker-cad.ts +31 -12
  151. package/src/worker.ts +1 -1
  152. package/tb_agents/opencode_router_agent.py +84 -42
  153. package/dist/cadbench-BrpwOU6A.js.map +0 -1
  154. package/dist/cadgenbench-DF7hYHdl.js.map +0 -1
  155. package/scripts/trata-hedge/solve.py +0 -83
@@ -0,0 +1,423 @@
1
+ /**
2
+ * MCAD-CQ adapter — the v2 runner that closes v1's format deviation.
3
+ *
4
+ * `mcad-tasks.ts` states the deviation in its header: the upstream text-to-cad
5
+ * benchmark asks for STEP, and OpenSCAD cannot emit it, so v1 grades an STL that
6
+ * OpenSCAD compiled. This adapter runs the SAME ten dimensioned parts against the
7
+ * SAME spec assertions, but the worker's artifact is a Python CadQuery script that
8
+ * must export BOTH `part.step` (the format upstream actually asks for) and
9
+ * `part.stl` (ASCII — what the geometry engine measures).
10
+ *
11
+ * Everything downstream of the mesh is v1's engine, IMPORTED not copied:
12
+ * `parseAsciiStl` / `measureMesh` / `pointInSolid` / `scoreAgainstSpec` /
13
+ * `stripCodeFence` all come from `./mcad-bench`, and the specs come from
14
+ * `./mcad-tasks`. Only two things differ:
15
+ * 1. the deliverable preamble (Python + CadQuery, not OpenSCAD) — applied by
16
+ * mapping at load time, so v1's prompt strings are never mutated;
17
+ * 2. one EXTRA scored check, `stepEmitted`: `part.step` exists and begins with
18
+ * `ISO-10303-21` (the ISO 10303-21 exchange-file magic every STEP file
19
+ * opens with). It is scored like any other assertion, so a script that
20
+ * emits only the mesh cannot reach 1.0 no matter how correct its geometry.
21
+ *
22
+ * Pipeline, in order:
23
+ * 1. run : `<python> model.py` in a fresh temp dir, 120 s hard deadline
24
+ * 2. deliver : `part.stl` exists and is ASCII (a binary STL is a miss, not a
25
+ * parse problem — the prompt pins the format)
26
+ * 3. watertight: every undirected edge shared by exactly two faces
27
+ * 4. measure : bbox / volume / connected components / triangle count
28
+ * 5. probe : ray-parity point-in-solid at the spec's pinned coordinates
29
+ * 6. step : the `stepEmitted` check above
30
+ * A non-watertight mesh scores 0 for the same reason as v1: an unclosed surface
31
+ * has no well-defined interior, so every downstream number would be fiction.
32
+ *
33
+ * SAFETY, stated once and deliberately. The judge EXECUTES the worker's Python on
34
+ * the host with no sandbox. That is the same trust level as v1 handing arbitrary
35
+ * source to the OpenSCAD kernel, and it carries the same rule: run this judge only
36
+ * on artifacts you would be willing to run by hand. The child process is given a
37
+ * scrubbed environment — no inherited variables beyond `PATH`, `HOME`/`TMPDIR`
38
+ * pointed at the scratch directory, and proxy variables pointed at a closed port —
39
+ * which DISCOURAGES network access and keeps host secrets out of the script's
40
+ * reach. It is not a network jail and does not claim to be one; put the judge in a
41
+ * container if you need that guarantee.
42
+ *
43
+ * Requires an interpreter that can `import cadquery` at JUDGE time only.
44
+ * Resolution order: `MCAD_CQ_PYTHON` (absolute path) → `bench/.venv-cadquery/bin/python`
45
+ * → throw with the exact install command (see `CADQUERY_INSTALL_FIX`).
46
+ */
47
+
48
+ import { execFile } from 'node:child_process'
49
+ import { existsSync } from 'node:fs'
50
+ import { mkdtemp, open, readFile, writeFile } from 'node:fs/promises'
51
+ import { tmpdir } from 'node:os'
52
+ import { isAbsolute, join } from 'node:path'
53
+ import { promisify } from 'node:util'
54
+ import { venvPythonAt } from './_harness'
55
+ import {
56
+ type McadCheck,
57
+ type McadGeometry,
58
+ type McadScoring,
59
+ type McadTaskMeta,
60
+ type Tri,
61
+ measureMesh,
62
+ parseAsciiStl,
63
+ scoreAgainstSpec,
64
+ stripCodeFence,
65
+ } from './mcad-bench'
66
+ import { MCAD_CQ_GOLDS, MCAD_CQ_UNCALIBRATED } from './mcad-cq-golds'
67
+ import { MCAD_DELIVERABLE, MCAD_DELIVERABLE_MULTIBODY, MCAD_TASKS, type McadTask } from './mcad-tasks'
68
+ import type { BenchmarkAdapter, BenchScore, BenchTask, LoadOptions } from './types'
69
+
70
+ const execFileAsync = promisify(execFile)
71
+
72
+ /** Package-relative venv, resolved through `_harness`'s bench-root lookup. */
73
+ export const CADQUERY_VENV = '.venv-cadquery'
74
+
75
+ /** Files the worker's script must leave in its working directory. */
76
+ export const STL_NAME = 'part.stl'
77
+ export const STEP_NAME = 'part.step'
78
+
79
+ /** Every ISO 10303-21 exchange file (a "STEP file") starts with this token. */
80
+ export const STEP_MAGIC = 'ISO-10303-21'
81
+
82
+ /** Hard deadline for one worker script. A hang is a miss, not a stall. */
83
+ export const MCAD_CQ_TIMEOUT_MS = 120_000
84
+
85
+ /**
86
+ * The exact, VERIFIED install command. Two pins here are load-bearing and neither
87
+ * is optional on a current host:
88
+ * - `--python 3.12`: cadquery 2.4.0 resolves to `cadquery-ocp` 7.7.2, which ships
89
+ * wheels for cp38–cp312 only. On a default CPython 3.13 the resolve fails.
90
+ * - `'numpy<2'`: cadquery 2.4.0 pins `nptyping==2.0.1`, whose module body reads
91
+ * `np.bool8` — removed in numpy 2. Without the pin `import cadquery` raises
92
+ * `AttributeError: module 'numpy' has no attribute 'bool8'` at import time.
93
+ */
94
+ export const CADQUERY_INSTALL_FIX =
95
+ 'Fix: create the isolated CadQuery venv (needs `uv`):\n' +
96
+ ' cd bench && uv venv --python 3.12 .venv-cadquery \\\n' +
97
+ " && uv pip install --python .venv-cadquery/bin/python 'cadquery==2.4.0' 'numpy<2'\n" +
98
+ 'Both pins are required: cadquery-ocp 7.7.2 has no cp313 wheel, and cadquery 2.4.0 pins ' +
99
+ "nptyping 2.0.1, which reads the numpy-1.x-only `np.bool8` at import.\n" +
100
+ 'Or set MCAD_CQ_PYTHON to the ABSOLUTE path of any interpreter that can `import cadquery`.'
101
+
102
+ /**
103
+ * Resolve the CadQuery interpreter. Never falls back to a system `python`: a
104
+ * silent fallback would either fail deep inside a worker script or — worse — run
105
+ * against a different CadQuery than the golds were calibrated on.
106
+ */
107
+ export function resolveCadqueryPython(
108
+ env: Readonly<{ MCAD_CQ_PYTHON?: string }> = process.env,
109
+ ): string {
110
+ const configured = env.MCAD_CQ_PYTHON
111
+ if (configured === undefined) return venvPythonAt(CADQUERY_VENV)
112
+ if (!isAbsolute(configured)) throw new Error(`MCAD_CQ_PYTHON must be an absolute path (got ${JSON.stringify(configured)})`)
113
+ return configured
114
+ }
115
+
116
+ /** The one message shape for "there is no usable interpreter" — HARNESS.md style. */
117
+ export function cadqueryMissingError(python: string, cause?: string): Error {
118
+ return new Error(
119
+ `mcad-cq: no CadQuery interpreter at ${python}${cause ? `\n${cause}` : ''}\n${CADQUERY_INSTALL_FIX}`,
120
+ )
121
+ }
122
+
123
+ // ---------------------------------------------------------------------------
124
+ // Prompt mapping (v1's prompt strings are read, never written)
125
+ // ---------------------------------------------------------------------------
126
+
127
+ const CQ_EXPORT_STEP = `cq.exporters.export(result, "${STEP_NAME}")`
128
+ const CQ_EXPORT_STL =
129
+ `cq.exporters.export(result, "${STL_NAME}", exportType="STL", opt={"ascii": True}, ` +
130
+ 'tolerance=0.01, angularTolerance=0.05)'
131
+
132
+ /** Shared tail: what the script must WRITE, which is the whole deliverable contract. */
133
+ const CQ_DELIVERABLE_TAIL =
134
+ 'The script must run standalone under `python model.py` and write BOTH of these files into its ' +
135
+ `current working directory: \`${STEP_NAME}\` (STEP) and \`${STL_NAME}\` (ASCII STL, not binary). ` +
136
+ 'Export with exactly these two lines:\n' +
137
+ ` ${CQ_EXPORT_STEP}\n` +
138
+ ` ${CQ_EXPORT_STL}\n` +
139
+ 'Those tessellation tolerances are what make round features accurate — do not coarsen them. ' +
140
+ 'Reply with ONLY the Python code.'
141
+
142
+ /** v2 replacement for `MCAD_DELIVERABLE`. */
143
+ export const MCAD_CQ_DELIVERABLE =
144
+ 'Author a Python script using CadQuery (units: mm) that builds exactly this part as one fused solid ' +
145
+ 'and binds it to a variable named `result`. ' +
146
+ CQ_DELIVERABLE_TAIL
147
+
148
+ /** v2 replacement for `MCAD_DELIVERABLE_MULTIBODY` — task 10 only, same reason as v1. */
149
+ export const MCAD_CQ_DELIVERABLE_MULTIBODY =
150
+ 'Author a Python script using CadQuery (units: mm) that builds exactly this assembly as the separate ' +
151
+ 'solid bodies listed below and binds them to a variable named `result` (a `cq.Compound` of those bodies, ' +
152
+ 'or a Workplane holding all of them). The bodies must stay disjoint — no two of them may touch or ' +
153
+ 'intersect. ' +
154
+ CQ_DELIVERABLE_TAIL
155
+
156
+ /**
157
+ * Swap the deliverable preamble for the CadQuery one, leaving the upstream
158
+ * dimensional text byte-identical. Fails loud rather than guessing: a task whose
159
+ * prompt does not open with one of v1's two known preambles means v1 changed
160
+ * shape, and silently shipping a prompt with two contradictory deliverables is
161
+ * exactly the failure this function exists to prevent.
162
+ */
163
+ export function toCadQueryPrompt(task: McadTask): string {
164
+ for (const [v1, v2] of [
165
+ [MCAD_DELIVERABLE_MULTIBODY, MCAD_CQ_DELIVERABLE_MULTIBODY],
166
+ [MCAD_DELIVERABLE, MCAD_CQ_DELIVERABLE],
167
+ ] as const) {
168
+ if (task.prompt.startsWith(v1)) return v2 + task.prompt.slice(v1.length)
169
+ }
170
+ throw new Error(
171
+ `mcad-cq: task ${task.id} does not open with a known mcad deliverable preamble; ` +
172
+ 'mcad-tasks.ts changed shape and the CadQuery prompt mapping must be updated with it',
173
+ )
174
+ }
175
+
176
+ // ---------------------------------------------------------------------------
177
+ // Running the worker's script
178
+ // ---------------------------------------------------------------------------
179
+
180
+ /**
181
+ * The child's whole environment. Nothing is inherited except `PATH` (the
182
+ * interpreter needs its own toolchain on it) and `LANG`. `HOME`/`TMPDIR` point at
183
+ * the scratch dir so the script cannot write to the operator's home, and the proxy
184
+ * variables point at port 9 (discard), which every mainstream HTTP client honours.
185
+ * DISCOURAGEMENT, not a jail — see the file header.
186
+ */
187
+ function scriptEnv(dir: string): NodeJS.ProcessEnv {
188
+ const blackhole = 'http://127.0.0.1:9'
189
+ return {
190
+ PATH: process.env.PATH ?? '/usr/bin:/bin',
191
+ LANG: process.env.LANG ?? 'C.UTF-8',
192
+ HOME: dir,
193
+ TMPDIR: dir,
194
+ PYTHONDONTWRITEBYTECODE: '1',
195
+ PYTHONHASHSEED: '0',
196
+ http_proxy: blackhole,
197
+ https_proxy: blackhole,
198
+ HTTP_PROXY: blackhole,
199
+ HTTPS_PROXY: blackhole,
200
+ no_proxy: '',
201
+ NO_PROXY: '',
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Reject anything that is not an ASCII STL. A binary STL also opens with the
207
+ * bytes `solid` often enough that the header alone is not a test, so the NUL scan
208
+ * carries the decision: ASCII STL is printable text end to end.
209
+ */
210
+ export function asciiStlProblem(buf: Buffer): string | undefined {
211
+ if (buf.length === 0) return `${STL_NAME} is empty`
212
+ if (buf.includes(0)) return `${STL_NAME} is not ASCII (contains NUL bytes — it looks like a binary STL)`
213
+ const head = buf.subarray(0, 64).toString('latin1')
214
+ if (!/^\s*solid\b/.test(head)) return `${STL_NAME} does not start with "solid" (got ${JSON.stringify(head.slice(0, 32))})`
215
+ if (!/\bfacet\s+normal\b/.test(buf.toString('utf8'))) return `${STL_NAME} contains no "facet normal" records`
216
+ return undefined
217
+ }
218
+
219
+ /** `part.step` exists AND opens with the ISO 10303-21 magic. Reads 12 bytes, not the file. */
220
+ export async function stepFileEmitted(path: string): Promise<boolean> {
221
+ let handle: Awaited<ReturnType<typeof open>>
222
+ try {
223
+ handle = await open(path, 'r')
224
+ } catch {
225
+ return false
226
+ }
227
+ try {
228
+ const buf = Buffer.alloc(STEP_MAGIC.length)
229
+ const { bytesRead } = await handle.read(buf, 0, buf.length, 0)
230
+ return bytesRead === buf.length && buf.toString('latin1') === STEP_MAGIC
231
+ } finally {
232
+ await handle.close()
233
+ }
234
+ }
235
+
236
+ export type McadCqBuild =
237
+ | { ok: true; geo: McadGeometry; tris: Tri[]; dir: string; stlPath: string; stepEmitted: boolean }
238
+ | { ok: false; detail: string; dir: string; stepEmitted: boolean }
239
+
240
+ /**
241
+ * Run one CadQuery script and measure what it delivered. Judge-time only.
242
+ * Throws (never scores) when there is no usable interpreter — an absent toolchain
243
+ * is an operator defect, and scoring it 0 would silently poison a whole corpus.
244
+ */
245
+ export async function runAndMeasureCadQuery(src: string, timeoutMs = MCAD_CQ_TIMEOUT_MS): Promise<McadCqBuild> {
246
+ const python = resolveCadqueryPython()
247
+ if (!existsSync(python)) throw cadqueryMissingError(python)
248
+
249
+ const dir = await mkdtemp(join(tmpdir(), 'mcad-cq-'))
250
+ const stlPath = join(dir, STL_NAME)
251
+ const stepPath = join(dir, STEP_NAME)
252
+ await writeFile(join(dir, 'model.py'), `${src}\n`)
253
+
254
+ const fail = async (detail: string): Promise<McadCqBuild> => ({
255
+ ok: false,
256
+ detail,
257
+ dir,
258
+ stepEmitted: await stepFileEmitted(stepPath),
259
+ })
260
+
261
+ try {
262
+ await execFileAsync(python, ['model.py'], {
263
+ cwd: dir,
264
+ env: scriptEnv(dir),
265
+ timeout: timeoutMs,
266
+ killSignal: 'SIGKILL',
267
+ maxBuffer: 1024 * 1024 * 64,
268
+ })
269
+ } catch (err) {
270
+ const e = err as NodeJS.ErrnoException & { killed?: boolean; stderr?: string }
271
+ if (e.killed) return fail(`script timed out after ${timeoutMs} ms and was killed`)
272
+ const stderr = (e.stderr ?? e.message ?? '').trim()
273
+ return fail(`script failed: ${stderr.slice(-800)}`)
274
+ }
275
+
276
+ let stl: Buffer
277
+ try {
278
+ stl = await readFile(stlPath)
279
+ } catch {
280
+ return fail(`script wrote no ${STL_NAME} in its working directory`)
281
+ }
282
+ const bad = asciiStlProblem(stl)
283
+ if (bad) return fail(bad)
284
+
285
+ const tris = parseAsciiStl(stl.toString('utf8'))
286
+ if (tris.length === 0) return fail(`${STL_NAME} parsed to zero triangles (empty geometry)`)
287
+
288
+ return {
289
+ ok: true,
290
+ geo: measureMesh(tris),
291
+ tris,
292
+ dir,
293
+ stlPath,
294
+ stepEmitted: await stepFileEmitted(stepPath),
295
+ }
296
+ }
297
+
298
+ // ---------------------------------------------------------------------------
299
+ // Scoring
300
+ // ---------------------------------------------------------------------------
301
+
302
+ /**
303
+ * Append the STEP-delivery check to a v1 scoring and recompute. It is one check
304
+ * among the spec's, deliberately: the upstream benchmark's deliverable IS a STEP
305
+ * file, so failing to produce one costs exactly what failing a dimension costs.
306
+ */
307
+ export function withStepCheck(scored: McadScoring, stepEmitted: boolean): McadScoring {
308
+ const check: McadCheck = {
309
+ name: 'stepEmitted',
310
+ ok: stepEmitted,
311
+ measured: stepEmitted ? `${STEP_NAME} starts with ${STEP_MAGIC}` : `no ${STEP_NAME} starting with ${STEP_MAGIC}`,
312
+ expected: `${STEP_NAME} exists and starts with ${STEP_MAGIC}`,
313
+ }
314
+ const checks = [...scored.checks, check]
315
+ const failed = checks.filter((c) => !c.ok)
316
+ return { checks, failed, score: (checks.length - failed.length) / checks.length, resolved: failed.length === 0 }
317
+ }
318
+
319
+ // ---------------------------------------------------------------------------
320
+ // Adapter
321
+ // ---------------------------------------------------------------------------
322
+
323
+ export function createMcadCqAdapter(): BenchmarkAdapter {
324
+ return {
325
+ name: 'mcad-cq',
326
+
327
+ async preflight() {
328
+ const python = resolveCadqueryPython()
329
+ if (!existsSync(python)) throw cadqueryMissingError(python)
330
+ try {
331
+ await execFileAsync(python, ['-c', 'import cadquery; print(cadquery.__version__)'], {
332
+ timeout: 300_000,
333
+ env: scriptEnv(tmpdir()),
334
+ })
335
+ } catch (err) {
336
+ const e = err as NodeJS.ErrnoException & { stderr?: string }
337
+ throw cadqueryMissingError(python, `\`import cadquery\` failed: ${(e.stderr ?? e.message ?? '').trim().slice(-800)}`)
338
+ }
339
+ },
340
+
341
+ async loadTasks(opts: LoadOptions = {}) {
342
+ let tasks: McadTask[] = MCAD_TASKS
343
+ if (opts.ids) tasks = tasks.filter((t) => opts.ids?.includes(t.id))
344
+ if (opts.limit != null) tasks = tasks.slice(0, opts.limit)
345
+ return tasks.map(
346
+ (t): BenchTask => ({
347
+ id: t.id,
348
+ prompt: toCadQueryPrompt(t),
349
+ metadata: {
350
+ spec: t.spec,
351
+ source: t.source,
352
+ // Calibration is per-ADAPTER: a task calibrated against the OpenSCAD
353
+ // gold says nothing about whether a CadQuery gold reaches 1.0 here.
354
+ calibrated: t.calibrated && !MCAD_CQ_UNCALIBRATED.has(t.id),
355
+ } satisfies McadTaskMeta,
356
+ }),
357
+ )
358
+ },
359
+
360
+ async goldArtifact(task: BenchTask) {
361
+ return MCAD_CQ_GOLDS[task.id]
362
+ },
363
+
364
+ async judge(task: BenchTask, artifact: string): Promise<BenchScore> {
365
+ const spec = (task.metadata as McadTaskMeta | undefined)?.spec
366
+ if (!spec) return { resolved: false, score: 0, detail: `task ${task.id} carries no mcad spec` }
367
+
368
+ const src = stripCodeFence(artifact)
369
+ if (!src) return { resolved: false, score: 0, detail: 'empty artifact' }
370
+
371
+ const built = await runAndMeasureCadQuery(src)
372
+ if (!built.ok) {
373
+ return { resolved: false, score: 0, detail: `${built.detail} [dir ${built.dir}, stepEmitted ${built.stepEmitted}]` }
374
+ }
375
+
376
+ const { geo, tris, stlPath, stepEmitted, dir } = built
377
+ if (!geo.watertight) {
378
+ return {
379
+ resolved: false,
380
+ score: 0,
381
+ detail: JSON.stringify({
382
+ gate: 'watertight',
383
+ failed: [
384
+ {
385
+ name: 'watertight',
386
+ measured: `${geo.openEdges} edge(s) not shared by exactly two faces`,
387
+ expected: '0 open edges (closed 2-manifold solid)',
388
+ },
389
+ ],
390
+ geo: { triangles: geo.triangles, degenerateFaces: geo.degenerateFaces },
391
+ stepEmitted,
392
+ stlPath,
393
+ dir,
394
+ }),
395
+ }
396
+ }
397
+
398
+ const scored = withStepCheck(scoreAgainstSpec(geo, tris, spec), stepEmitted)
399
+ return {
400
+ resolved: scored.resolved,
401
+ score: scored.score,
402
+ detail: JSON.stringify({
403
+ failed: scored.failed.map((c) => ({ name: c.name, measured: c.measured, expected: c.expected })),
404
+ checks: Object.fromEntries(scored.checks.map((c) => [c.name, c.ok])),
405
+ geo: {
406
+ triangles: geo.triangles,
407
+ solids: geo.solids,
408
+ volume: +geo.volume.toFixed(2),
409
+ degenerateFaces: geo.degenerateFaces,
410
+ bbox: {
411
+ x: +geo.bbox.size.x.toFixed(3),
412
+ y: +geo.bbox.size.y.toFixed(3),
413
+ z: +geo.bbox.size.z.toFixed(3),
414
+ },
415
+ },
416
+ stepEmitted,
417
+ stlPath,
418
+ dir,
419
+ }),
420
+ }
421
+ },
422
+ }
423
+ }