@tangle-network/agent-bench 0.1.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.
Files changed (111) hide show
  1. package/README.md +15 -0
  2. package/package.json +35 -0
  3. package/src/adapters.ts +60 -0
  4. package/src/aec-gate.mts +217 -0
  5. package/src/atom-humaneval.mts +197 -0
  6. package/src/atom-mcp-e2e.mts +223 -0
  7. package/src/benchmarks/_harness.ts +206 -0
  8. package/src/benchmarks/aec-bench.test.mts +53 -0
  9. package/src/benchmarks/aec-bench.ts +319 -0
  10. package/src/benchmarks/appworld.test.mts +45 -0
  11. package/src/benchmarks/appworld.ts +426 -0
  12. package/src/benchmarks/cad-design.ts +429 -0
  13. package/src/benchmarks/cadbench.ts +135 -0
  14. package/src/benchmarks/cadgenbench.ts +121 -0
  15. package/src/benchmarks/commit0.test.mts +71 -0
  16. package/src/benchmarks/commit0.ts +266 -0
  17. package/src/benchmarks/enterpriseops-gym.test.mts +77 -0
  18. package/src/benchmarks/enterpriseops-gym.ts +343 -0
  19. package/src/benchmarks/finsearchcomp.ts +371 -0
  20. package/src/benchmarks/frames.ts +520 -0
  21. package/src/benchmarks/hotpotqa.ts +320 -0
  22. package/src/benchmarks/humaneval.ts +251 -0
  23. package/src/benchmarks/mind2web.ts +311 -0
  24. package/src/benchmarks/programbench.test.mts +60 -0
  25. package/src/benchmarks/programbench.ts +210 -0
  26. package/src/benchmarks/simpleqa.ts +390 -0
  27. package/src/benchmarks/swe-bench.ts +152 -0
  28. package/src/benchmarks/terminal-bench.ts +182 -0
  29. package/src/benchmarks/trata-hedge.ts +496 -0
  30. package/src/benchmarks/types.ts +58 -0
  31. package/src/browser/adapters/bad-design-audit.ts +153 -0
  32. package/src/browser/adapters/bad-design-audit.verify.ts +85 -0
  33. package/src/browser/adapters/bad.ts +165 -0
  34. package/src/browser/agent-adapter.ts +145 -0
  35. package/src/browser/process-adapter.ts +146 -0
  36. package/src/browser/run-to-spans.ts +105 -0
  37. package/src/browser/run-to-spans.verify.ts +89 -0
  38. package/src/browser/ui-reviewer.ts +200 -0
  39. package/src/browser/ui-reviewer.verify.ts +94 -0
  40. package/src/browser/verify.ts +96 -0
  41. package/src/clbench-codebase-gate.mts +314 -0
  42. package/src/clbench-context-gate.mts +305 -0
  43. package/src/cloud-loop.mts +138 -0
  44. package/src/coding-skills/minimal-diff.md +9 -0
  45. package/src/coding-skills/read-before-edit.md +9 -0
  46. package/src/coding-skills/reproduce-first.md +10 -0
  47. package/src/coding-skills/run-tests-after-edit.md +9 -0
  48. package/src/coding-skills/trace-the-failure.md +9 -0
  49. package/src/commit0-env-run.mts +59 -0
  50. package/src/commit0-env.ts +173 -0
  51. package/src/commit0-gate.mts +529 -0
  52. package/src/commit0-prereqs.sh +48 -0
  53. package/src/corpus-replay.mts +300 -0
  54. package/src/corpus-report.mts +504 -0
  55. package/src/corpus.test.mts +273 -0
  56. package/src/corpus.ts +325 -0
  57. package/src/decoder-live.mts +133 -0
  58. package/src/directives.ts +84 -0
  59. package/src/diverse-gate.mjs +112 -0
  60. package/src/egress-probe.mts +26 -0
  61. package/src/eops-skills/address-every-subtask.md +5 -0
  62. package/src/eops-skills/exact-tools-and-args.md +5 -0
  63. package/src/eops-skills/full-sequence-to-goal.md +5 -0
  64. package/src/eops-skills/ground-every-value.md +5 -0
  65. package/src/eops-skills/honor-the-policies.md +5 -0
  66. package/src/examples/README.md +58 -0
  67. package/src/examples/math-demo.mts +110 -0
  68. package/src/examples/strategy-demo.mts +119 -0
  69. package/src/fleet.mts +121 -0
  70. package/src/gate-cli.mts +101 -0
  71. package/src/gate.test.mts +129 -0
  72. package/src/gate.ts +460 -0
  73. package/src/generate-eval/certify.ts +178 -0
  74. package/src/generate-eval/schema.ts +78 -0
  75. package/src/humaneval-gate.mts +204 -0
  76. package/src/humaneval-repair-gate.mts +143 -0
  77. package/src/index.ts +19 -0
  78. package/src/mcp-mount-probe.mts +126 -0
  79. package/src/profile-coordinates.ts +134 -0
  80. package/src/profiles.ts +128 -0
  81. package/src/refine-loop.test.mts +106 -0
  82. package/src/refine-loop.ts +106 -0
  83. package/src/research-gate.mts +132 -0
  84. package/src/research-shot.ts +134 -0
  85. package/src/resolve-client.ts +58 -0
  86. package/src/router-executor.ts +51 -0
  87. package/src/run-pool.ts +48 -0
  88. package/src/runtime-hook-recorder.ts +137 -0
  89. package/src/sandbox-run.ts +125 -0
  90. package/src/search-bench/bridge.ts +124 -0
  91. package/src/search-bench/export.mts +0 -0
  92. package/src/search-bench/parametric-check.mts +63 -0
  93. package/src/search-bench/profiles.ts +98 -0
  94. package/src/search-bench/run.mts +287 -0
  95. package/src/search-bench/tasks-fresh.ts +688 -0
  96. package/src/search-bench/tasks.ts +129 -0
  97. package/src/search-tool.ts +95 -0
  98. package/src/selector.test.mts +189 -0
  99. package/src/selector.ts +366 -0
  100. package/src/skill-sandbox-smoke.mts +100 -0
  101. package/src/stats.mts +90 -0
  102. package/src/terminal-compare.ts +519 -0
  103. package/src/trajectory-assemble.mjs +130 -0
  104. package/src/trata-gate.mts +243 -0
  105. package/src/trata-gepa.mts +434 -0
  106. package/src/worker-blender.ts +230 -0
  107. package/src/worker-browser.ts +102 -0
  108. package/src/worker-build123d.ts +143 -0
  109. package/src/worker-cad.ts +451 -0
  110. package/src/worker.ts +136 -0
  111. package/src/workspace-loop.mts +133 -0
@@ -0,0 +1,429 @@
1
+ /**
2
+ * CAD-Design adapter. Worker artifact = OpenSCAD source (`.scad`). Judge = the
3
+ * REAL OpenSCAD engine: compile + export STL, then measure the produced solid
4
+ * geometry against the task's spec. Fully deterministic — the authoritative CAD
5
+ * kernel is the gate, not an LLM and not the agent's own claim.
6
+ *
7
+ * This is verifiable-reward CAD: an agent is far better at *writing* exact
8
+ * parametric code than at clicking a GUI, and the kernel either produces the
9
+ * specified geometry or it doesn't. Every judged artifact also renders a PNG,
10
+ * so a refine loop's attempts become a watchable "model getting better" reel
11
+ * (run-capsule consumes the renders).
12
+ *
13
+ * Spec checks are geometric and ungameable:
14
+ * - compiles : `openscad -o out.stl` exits 0 (hard gate)
15
+ * - volumes : disconnected-solid count within [min,max]
16
+ * - bbox : overall X/Y/Z extent within bounds (the thing is the right size)
17
+ * - detail : triangle count ≥ floor (not a degenerate single cube)
18
+ * - pitchedRoof : the top band's XY footprint NARROWS vs the base — a flat
19
+ * box can't fake this; a real gabled/hipped roof tapers
20
+ * - hollow : interior cavity present (walls, not a solid block) — the
21
+ * bbox volume materially exceeds the printed solid volume
22
+ *
23
+ * Requires only `openscad` + `xvfb-run` on PATH (no venv, no Docker, no network).
24
+ */
25
+
26
+ import { execFile } from 'node:child_process'
27
+ import { mkdtemp, readFile, writeFile } from 'node:fs/promises'
28
+ import { tmpdir } from 'node:os'
29
+ import { join } from 'node:path'
30
+ import { promisify } from 'node:util'
31
+ import type { BenchmarkAdapter, BenchScore, BenchTask, LoadOptions } from './types'
32
+
33
+ const execFileAsync = promisify(execFile)
34
+
35
+ /** Spec assertions a CAD task can require. All are deterministic + geometric. */
36
+ export interface CadSpec {
37
+ volumes?: [number, number]
38
+ bbox?: { x?: [number, number]; y?: [number, number]; z?: [number, number] }
39
+ minTriangles?: number
40
+ /** Top-band XY footprint must be < this fraction of the base footprint. */
41
+ pitchedRoof?: number
42
+ /** Printed solid volume must be < this fraction of the bbox volume (hollow). */
43
+ hollowBelow?: number
44
+ }
45
+
46
+ interface CadTaskMeta {
47
+ spec: CadSpec
48
+ /** A known-good .scad that satisfies the spec — the oracle for verify-judge. */
49
+ gold: string
50
+ }
51
+
52
+ /** Run openscad with xvfb (it needs a GL context even headless). */
53
+ async function openscad(args: string[], cwd: string): Promise<{ stdout: string; stderr: string }> {
54
+ // -a => auto-pick a free display; openscad writes Facets/Volumes to stderr.
55
+ const { stdout, stderr } = await execFileAsync('xvfb-run', ['-a', 'openscad', ...args], {
56
+ cwd,
57
+ maxBuffer: 1024 * 1024 * 64,
58
+ timeout: 120_000,
59
+ })
60
+ return { stdout, stderr }
61
+ }
62
+
63
+ interface Geometry {
64
+ triangles: number
65
+ volumes: number
66
+ bbox: { x: number; y: number; z: number }
67
+ /** XY footprint area (extent_x * extent_y) of the bottom 60% and top 25% z-bands. */
68
+ baseFootprint: number
69
+ topFootprint: number
70
+ /** Convex-ish solid volume estimate from the STL (sum of signed tetra volumes). */
71
+ solidVolume: number
72
+ bboxVolume: number
73
+ }
74
+
75
+ /** Parse an ASCII STL into the geometric measures the spec checks. Pure. */
76
+ function measureStl(stl: string, volumesFromStderr: number): Geometry {
77
+ const verts: Array<[number, number, number]> = []
78
+ const tris: Array<[[number, number, number], [number, number, number], [number, number, number]]> = []
79
+ const re = /vertex\s+(-?[\d.eE+]+)\s+(-?[\d.eE+]+)\s+(-?[\d.eE+]+)/g
80
+ let m: RegExpExecArray | null
81
+ const flat: Array<[number, number, number]> = []
82
+ while ((m = re.exec(stl))) flat.push([Number(m[1]), Number(m[2]), Number(m[3])])
83
+ for (let i = 0; i + 2 < flat.length; i += 3) tris.push([flat[i]!, flat[i + 1]!, flat[i + 2]!])
84
+ for (const v of flat) verts.push(v)
85
+
86
+ const xs = verts.map((v) => v[0])
87
+ const ys = verts.map((v) => v[1])
88
+ const zs = verts.map((v) => v[2])
89
+ const min = (a: number[]) => Math.min(...a)
90
+ const max = (a: number[]) => Math.max(...a)
91
+ const minZ = min(zs)
92
+ const maxZ = max(zs)
93
+ const h = maxZ - minZ || 1
94
+
95
+ // Footprint of a z-band = XY extent of the vertices within it.
96
+ const footprint = (lo: number, hi: number): number => {
97
+ const band = verts.filter((v) => v[2] >= minZ + lo * h && v[2] <= minZ + hi * h)
98
+ if (band.length < 3) return 0
99
+ const bx = band.map((v) => v[0])
100
+ const by = band.map((v) => v[1])
101
+ return (max(bx) - min(bx)) * (max(by) - min(by))
102
+ }
103
+
104
+ // Signed volume via the divergence theorem over triangles (|Σ v0·(v1×v2)/6|).
105
+ let vol6 = 0
106
+ for (const [a, b, c] of tris) {
107
+ vol6 +=
108
+ a[0] * (b[1] * c[2] - b[2] * c[1]) -
109
+ a[1] * (b[0] * c[2] - b[2] * c[0]) +
110
+ a[2] * (b[0] * c[1] - b[1] * c[0])
111
+ }
112
+ const bbox = { x: max(xs) - min(xs), y: max(ys) - min(ys), z: maxZ - minZ }
113
+ return {
114
+ triangles: tris.length,
115
+ volumes: volumesFromStderr,
116
+ bbox,
117
+ baseFootprint: footprint(0, 0.6),
118
+ topFootprint: footprint(0.75, 1),
119
+ solidVolume: Math.abs(vol6) / 6,
120
+ bboxVolume: bbox.x * bbox.y * bbox.z || 1,
121
+ }
122
+ }
123
+
124
+ function inRange(v: number, [lo, hi]: [number, number]): boolean {
125
+ return v >= lo && v <= hi
126
+ }
127
+
128
+ /** Score the geometry against the spec — each check is a named pass/fail. */
129
+ function scoreGeometry(g: Geometry, spec: CadSpec): { checks: Record<string, boolean>; score: number } {
130
+ const checks: Record<string, boolean> = {}
131
+ if (spec.volumes) checks.volumes = inRange(g.volumes, spec.volumes)
132
+ if (spec.minTriangles != null) checks.detail = g.triangles >= spec.minTriangles
133
+ if (spec.bbox?.x) checks.bboxX = inRange(g.bbox.x, spec.bbox.x)
134
+ if (spec.bbox?.y) checks.bboxY = inRange(g.bbox.y, spec.bbox.y)
135
+ if (spec.bbox?.z) checks.bboxZ = inRange(g.bbox.z, spec.bbox.z)
136
+ if (spec.pitchedRoof != null)
137
+ checks.pitchedRoof = g.baseFootprint > 0 && g.topFootprint < spec.pitchedRoof * g.baseFootprint
138
+ if (spec.hollowBelow != null) checks.hollow = g.solidVolume < spec.hollowBelow * g.bboxVolume
139
+ const vals = Object.values(checks)
140
+ const score = vals.length ? vals.filter(Boolean).length / vals.length : 0
141
+ return { checks, score }
142
+ }
143
+
144
+ /** A parametric multi-story gabled building — the gold generator for the
145
+ * building-family tasks. They share geometry (hollow stacked shells + a gabled
146
+ * roof that tapers to a ridge + cut openings) and differ only in dimensions, so
147
+ * one verified generator yields a spec-passing oracle for each. */
148
+ function buildingGold(w: number, d: number, sh: number, ns: number, rh: number, t = 3): string {
149
+ return `
150
+ w=${w}; d=${d}; sh=${sh}; ns=${ns}; rh=${rh}; t=${t};
151
+ module shell(w,d,h,t){ difference(){ cube([w,d,h]); translate([t,t,-1]) cube([w-2*t,d-2*t,h+2]); } }
152
+ module roof(w,d,h){ rotate([90,0,90]) linear_extrude(height=w) polygon([[0,0],[d,0],[d/2,h]]); }
153
+ union(){
154
+ for(s=[0:ns-1]) translate([0,0,s*sh]) shell(w,d,sh,t);
155
+ for(s=[0:ns]) translate([0,0,s*sh-0.5]) cube([w,d,1]);
156
+ translate([0,0,ns*sh]) roof(w,d,rh);
157
+ for(s=[0:ns-1]) for(i=[0:floor((w-28)/22)]) translate([14+i*22,-1,s*sh+10]) cube([10,t+2,12]);
158
+ translate([w/2-7,-1,2]) cube([14,t+2,20]);
159
+ }
160
+ `.trim()
161
+ }
162
+
163
+ /** Build a building-family task (prompt + spec + gold) from its dimensions.
164
+ * The spec's bbox is derived from the dims with a ±~18% tolerance so the agent
165
+ * must respect the brief's size; the gold (buildingGold) lands exactly on the
166
+ * nominal dims, so it passes by construction. Keep rh > ns*sh/3 (else the roof
167
+ * base sits inside the top-25% band and pitchedRoof fails). */
168
+ function buildingTask(o: { id: string; desc: string; w: number; d: number; sh: number; ns: number; rh: number }): {
169
+ id: string
170
+ prompt: string
171
+ meta: CadTaskMeta
172
+ } {
173
+ const { id, desc, w, d, sh, ns, rh } = o
174
+ const totalWall = ns * sh
175
+ const totalH = totalWall + rh
176
+ const r = Math.round
177
+ const spec: CadSpec = {
178
+ volumes: [1, ns * 4 + 6],
179
+ bbox: {
180
+ x: [r(w * 0.82), r(w * 1.18)],
181
+ y: [r(d * 0.82), r((d + 1) * 1.2)],
182
+ z: [r(totalH * 0.82), r((totalH + 1) * 1.2)],
183
+ },
184
+ minTriangles: 50,
185
+ pitchedRoof: 0.55,
186
+ hollowBelow: 0.72,
187
+ }
188
+ const prompt = [
189
+ `Write OpenSCAD source for ${desc}.`,
190
+ 'Requirements:',
191
+ `- footprint roughly ${w} (X) by ${d} (Y) units`,
192
+ ns > 1 ? `- ${ns} stories, total wall height ~${totalWall} units` : `- one story, wall height ~${sh} units`,
193
+ '- a PITCHED / gabled roof on top that tapers toward a ridge (NOT flat)',
194
+ '- hollow shell (walls with an interior cavity), not a solid block',
195
+ '- a door opening and several windows',
196
+ 'Output ONLY the .scad source, no prose, no code fences.',
197
+ ].join('\n')
198
+ return { id, prompt, meta: { spec, gold: buildingGold(w, d, sh, ns, rh) } }
199
+ }
200
+
201
+ /** The building family — one parametric skill (hit the bbox + pitched roof +
202
+ * hollow shell), sampled across footprints, heights, story counts, roof rises. */
203
+ const BUILDING_CONFIGS: Array<{ id: string; desc: string; w: number; d: number; sh: number; ns: number; rh: number }> = [
204
+ { id: 'cottage', desc: 'a single-story cottage', w: 50, d: 40, sh: 30, ns: 1, rh: 18 },
205
+ { id: 'cabin', desc: 'a small one-room cabin', w: 44, d: 36, sh: 26, ns: 1, rh: 22 },
206
+ { id: 'bungalow', desc: 'a wide single-story bungalow', w: 70, d: 52, sh: 28, ns: 1, rh: 20 },
207
+ { id: 'barn', desc: 'a long deep barn', w: 60, d: 96, sh: 42, ns: 1, rh: 26 },
208
+ { id: 'warehouse', desc: 'a large warehouse', w: 110, d: 80, sh: 40, ns: 1, rh: 26 },
209
+ { id: 'chapel', desc: 'a narrow tall chapel with a steep roof', w: 40, d: 72, sh: 48, ns: 1, rh: 34 },
210
+ { id: 'farmhouse', desc: 'a two-story farmhouse', w: 72, d: 56, sh: 30, ns: 2, rh: 26 },
211
+ { id: 'two-story-manor', desc: 'a wide two-story manor', w: 96, d: 64, sh: 32, ns: 2, rh: 28 },
212
+ { id: 'townhouse', desc: 'a narrow three-story townhouse', w: 42, d: 52, sh: 30, ns: 3, rh: 34 },
213
+ { id: 'watchtower', desc: 'a tall narrow three-story watchtower', w: 34, d: 34, sh: 30, ns: 3, rh: 32 },
214
+ ]
215
+
216
+ /** A hollow cylindrical water tower with a conical roof — a non-box shape that
217
+ * still exercises pitched-roof (the cone tapers to an apex) + hollow + bbox. */
218
+ const WATER_TOWER_GOLD = `
219
+ $fn=48; r=24; bh=72; t=3; ch=26;
220
+ union(){
221
+ difference(){ cylinder(h=bh, r=r); translate([0,0,t]) cylinder(h=bh, r=r-t); }
222
+ translate([0,0,bh]) cylinder(h=ch, r1=r, r2=0);
223
+ }
224
+ `.trim()
225
+
226
+ /** An A-frame: the steep roof IS the walls — a hollow triangular prism. The most
227
+ * extreme taper in the set; tests that the agent can build a non-rectangular shell. */
228
+ const AFRAME_GOLD = `
229
+ w=44; d=60; h=66; t=3;
230
+ module tri(w,d,h){ rotate([90,0,90]) linear_extrude(height=w) polygon([[0,0],[d,0],[d/2,h]]); }
231
+ union(){
232
+ difference(){ tri(w,d,h); translate([t,0,0]) tri(w-2*t,d,h-2*t*h/d); }
233
+ translate([w/2-7,-1,0]) cube([14,t+2,24]);
234
+ }
235
+ `.trim()
236
+
237
+ const TASKS: Array<{ id: string; prompt: string; meta: CadTaskMeta }> = [
238
+ {
239
+ id: 'two-story-house',
240
+ prompt: [
241
+ 'Write OpenSCAD source for a two-story house.',
242
+ 'Requirements:',
243
+ '- footprint roughly 80 (X) by 60 (Y) units',
244
+ '- two stories, total wall height ~68 units',
245
+ '- a PITCHED / gabled roof on top (NOT a flat roof) — the roof must taper toward a ridge',
246
+ '- hollow shell (walls with an interior cavity), not a solid block',
247
+ '- at least one door opening and several windows',
248
+ 'Output ONLY the .scad source, no prose, no code fences.',
249
+ ].join('\n'),
250
+ meta: {
251
+ spec: {
252
+ volumes: [1, 12],
253
+ bbox: { x: [70, 110], y: [50, 80], z: [80, 130] },
254
+ minTriangles: 60,
255
+ pitchedRoof: 0.55,
256
+ hollowBelow: 0.7,
257
+ },
258
+ gold: `
259
+ w=80; d=60; sh=34; t=3; rh=28;
260
+ module shell(w,d,h,t){ difference(){ cube([w,d,h]); translate([t,t,-1]) cube([w-2*t,d-2*t,h+2]); } }
261
+ module roof(w,d,h){ translate([0,0,0]) rotate([90,0,90]) linear_extrude(height=w) polygon([[0,0],[d,0],[d/2,h]]); }
262
+ union(){
263
+ for(s=[0,1]) translate([0,0,s*sh]) shell(w,d,sh,t);
264
+ for(s=[0,1,2]) translate([0,0,s*sh-1]) cube([w,d,1]);
265
+ translate([0,0,2*sh]) roof(w,d,rh);
266
+ for(s=[0,1]) for(i=[0:2]) translate([16+i*22,-1,s*sh+10]) cube([12,t+2,14]);
267
+ translate([w/2-8,-1,2]) cube([16,t+2,22]);
268
+ }
269
+ `.trim(),
270
+ },
271
+ },
272
+ ...BUILDING_CONFIGS.map(buildingTask),
273
+ {
274
+ id: 'water-tower',
275
+ prompt: [
276
+ 'Write OpenSCAD source for a water tower: a tall hollow cylindrical tank with a conical roof.',
277
+ 'Requirements:',
278
+ '- a hollow cylindrical tank, ~48 units in diameter, ~72 units tall, with an interior cavity',
279
+ '- a CONICAL roof on top that tapers to a point (a pitched roof)',
280
+ '- wall thickness around 3 units',
281
+ 'Output ONLY the .scad source, no prose, no code fences.',
282
+ ].join('\n'),
283
+ meta: {
284
+ spec: {
285
+ volumes: [1, 4],
286
+ bbox: { x: [40, 56], y: [40, 56], z: [88, 110] },
287
+ minTriangles: 60,
288
+ pitchedRoof: 0.55,
289
+ hollowBelow: 0.8,
290
+ },
291
+ gold: WATER_TOWER_GOLD,
292
+ },
293
+ },
294
+ {
295
+ id: 'a-frame-cabin',
296
+ prompt: [
297
+ 'Write OpenSCAD source for an A-frame cabin: a steep triangular shell where the roof forms the walls.',
298
+ 'Requirements:',
299
+ '- footprint roughly 44 (X) by 60 (Y) units',
300
+ '- a STEEP triangular cross-section ~66 units tall that tapers to a ridge at the top',
301
+ '- hollow inside (an interior cavity), not a solid wedge',
302
+ '- a door opening at the front',
303
+ 'Output ONLY the .scad source, no prose, no code fences.',
304
+ ].join('\n'),
305
+ meta: {
306
+ spec: {
307
+ volumes: [1, 4],
308
+ bbox: { x: [36, 52], y: [50, 74], z: [56, 80] },
309
+ minTriangles: 30,
310
+ pitchedRoof: 0.45,
311
+ hollowBelow: 0.8,
312
+ },
313
+ gold: AFRAME_GOLD,
314
+ },
315
+ },
316
+ {
317
+ id: 'hex-planter',
318
+ prompt: [
319
+ 'Write OpenSCAD source for a hexagonal planter pot.',
320
+ 'Requirements:',
321
+ '- a hollow hexagonal prism (6-sided), outer width ~50 units across, height ~45',
322
+ '- open at the top, closed at the bottom (a cavity for soil)',
323
+ '- a wall thickness around 3 units',
324
+ 'Output ONLY the .scad source, no prose, no code fences.',
325
+ ].join('\n'),
326
+ meta: {
327
+ spec: {
328
+ volumes: [1, 3],
329
+ bbox: { x: [40, 60], y: [40, 60], z: [40, 55] },
330
+ minTriangles: 30,
331
+ hollowBelow: 0.7,
332
+ },
333
+ gold: `
334
+ $fn=6; or=25; h=45; t=3;
335
+ difference(){
336
+ cylinder(h=h, r=or);
337
+ translate([0,0,t]) cylinder(h=h, r=or-t);
338
+ }
339
+ `.trim(),
340
+ },
341
+ },
342
+ ]
343
+
344
+ export function createCadDesignAdapter(): BenchmarkAdapter {
345
+ return {
346
+ name: 'cad-design',
347
+
348
+ async preflight() {
349
+ try {
350
+ await execFileAsync('xvfb-run', ['-a', 'openscad', '--version'], { timeout: 30_000 })
351
+ } catch (err) {
352
+ const msg = err instanceof Error ? err.message : String(err)
353
+ throw new Error(
354
+ `cad-design preflight failed: ${msg}\n` +
355
+ `Fix: install OpenSCAD + Xvfb (Debian/Ubuntu: sudo apt-get install -y openscad xvfb). ` +
356
+ `The judge runs \`xvfb-run -a openscad -o out.stl model.scad\` — both must be on PATH.`,
357
+ )
358
+ }
359
+ },
360
+
361
+ async loadTasks(opts: LoadOptions = {}) {
362
+ let tasks = TASKS
363
+ if (opts.ids) tasks = tasks.filter((t) => opts.ids!.includes(t.id))
364
+ if (opts.limit != null) tasks = tasks.slice(0, opts.limit)
365
+ return tasks.map((t): BenchTask => ({ id: t.id, prompt: t.prompt, metadata: t.meta as unknown as Record<string, unknown> }))
366
+ },
367
+
368
+ async goldArtifact(task: BenchTask) {
369
+ const meta = task.metadata as unknown as CadTaskMeta | undefined
370
+ return meta?.gold
371
+ },
372
+
373
+ async judge(task: BenchTask, artifact: string): Promise<BenchScore> {
374
+ const spec = (task.metadata as unknown as CadTaskMeta).spec
375
+ const src = artifact.trim()
376
+ if (!src) return { resolved: false, score: 0, detail: 'empty artifact' }
377
+
378
+ const dir = await mkdtemp(join(tmpdir(), 'cad-'))
379
+ const scad = join(dir, 'model.scad')
380
+ const stlPath = join(dir, 'model.stl')
381
+ const pngPath = join(dir, 'model.png')
382
+ await writeFile(scad, src)
383
+
384
+ // GATE 1: compile + export STL. Nonzero exit / CGAL error => does not compile.
385
+ let stderr = ''
386
+ try {
387
+ const r = await openscad(['-o', stlPath, scad], dir)
388
+ stderr = r.stderr
389
+ } catch (err) {
390
+ const msg = err instanceof Error ? err.message : String(err)
391
+ return { resolved: false, score: 0, detail: `compile failed: ${msg.slice(0, 300)}` }
392
+ }
393
+
394
+ // Render a PNG alongside (the artifact run-capsule animates). Non-fatal.
395
+ try {
396
+ await openscad(
397
+ ['-o', pngPath, '--imgsize=1100,850', '--camera=40,30,40,55,0,25,260', '--colorscheme=Tomorrow', scad],
398
+ dir,
399
+ )
400
+ } catch {
401
+ /* render is for the reel; geometry gate already has the STL */
402
+ }
403
+
404
+ let stl: string
405
+ try {
406
+ stl = await readFile(stlPath, 'utf8')
407
+ } catch {
408
+ return { resolved: false, score: 0, detail: 'compiled but produced no STL (empty geometry)' }
409
+ }
410
+ const volumes = Number(/Volumes:\s*(\d+)/.exec(stderr)?.[1] ?? '1')
411
+ const geo = measureStl(stl, volumes)
412
+ const { checks, score } = scoreGeometry(geo, spec)
413
+ const resolved = score === 1
414
+ const detail = JSON.stringify({
415
+ checks,
416
+ geo: {
417
+ triangles: geo.triangles,
418
+ volumes: geo.volumes,
419
+ bbox: { x: +geo.bbox.x.toFixed(1), y: +geo.bbox.y.toFixed(1), z: +geo.bbox.z.toFixed(1) },
420
+ hollowRatio: +(geo.solidVolume / geo.bboxVolume).toFixed(3),
421
+ topVsBaseFootprint: geo.baseFootprint ? +(geo.topFootprint / geo.baseFootprint).toFixed(3) : null,
422
+ },
423
+ renderPath: pngPath,
424
+ stlPath,
425
+ })
426
+ return { resolved, score, detail }
427
+ },
428
+ }
429
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * CADBench / BlenderLLM adapter (FreedomIntelligence/CADBench, arXiv:2412.14203).
3
+ * Task = NL instruction → a Blender `bpy` script. Score = the paper's criteria
4
+ * eval: render the produced model to standardized views, then a vision judge
5
+ * (GPT-4o-class) marks each per-task criterion bullet pass/fail against the
6
+ * rendered images + the script text. score = fraction of criteria satisfied.
7
+ *
8
+ * Data: the published dataset's `criteria` flattened to a bullet list (700 tasks,
9
+ * 500 Simulative + 200 Wild). Point CADBENCH_PATH at the cleaned JSONL
10
+ * ({id,name,instruction,type,criteria:string[]} per line). Judge creds from
11
+ * TANGLE_API_KEY / ROUTER_BASE / JUDGE_MODEL (default gpt-4o).
12
+ */
13
+
14
+ import { readFile } from 'node:fs/promises'
15
+ import type { BenchScore, BenchTask, BenchmarkAdapter, LoadOptions } from './types'
16
+ import { renderBpy } from '../worker-blender'
17
+
18
+ interface CadBenchMeta {
19
+ name: string
20
+ type: string
21
+ criteria: string[]
22
+ }
23
+
24
+ function must(name: string): string {
25
+ const v = process.env[name]
26
+ if (!v) throw new Error(`env ${name} is required for the CADBench judge`)
27
+ return v
28
+ }
29
+
30
+ /** One batched vision call: rendered views + the bpy script + the numbered
31
+ * criteria → a JSON array of booleans (true = satisfied). Faithful to the
32
+ * paper's combined image+script evaluation. Throws on transport failure (never
33
+ * a silent zero); a parse miss falls back to "all fail" with a note. */
34
+ async function judgeCriteria(
35
+ instruction: string,
36
+ script: string,
37
+ criteria: string[],
38
+ renders: string[],
39
+ ): Promise<{ passed: boolean[]; note: string }> {
40
+ const base = (process.env.ROUTER_BASE ?? 'https://router.tangle.tools/v1').replace(/\/$/, '')
41
+ const key = must('TANGLE_API_KEY')
42
+ const model = process.env.JUDGE_MODEL ?? 'deepseek-v4-flash'
43
+ const numbered = criteria.map((c, i) => `${i + 1}. ${c}`).join('\n')
44
+ const text =
45
+ `You are strictly grading a 3D model that was built by a Blender bpy script for this instruction:\n"${instruction}"\n\n` +
46
+ `Below are ${renders.length} rendered views of the produced model, and the script that built it. ` +
47
+ `For EACH numbered criterion, decide whether it is satisfied (judge geometry/shape/proportion/structure from the IMAGES; judge color/size/material reasonableness from the SCRIPT where the images are ambiguous). ` +
48
+ `Return ONLY a JSON array of exactly ${criteria.length} booleans (true=satisfied, false=not), in order, no prose.\n\nCRITERIA:\n${numbered}\n\nSCRIPT:\n\`\`\`python\n${script.slice(0, 6000)}\n\`\`\``
49
+ const content: unknown[] = [{ type: 'text', text }]
50
+ for (const url of renders) content.push({ type: 'image_url', image_url: { url } })
51
+ const res = await fetch(`${base}/chat/completions`, {
52
+ method: 'POST',
53
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${key}` },
54
+ body: JSON.stringify({ model, max_tokens: 1500, temperature: 0, messages: [{ role: 'user', content }] }),
55
+ })
56
+ if (!res.ok) throw new Error(`judge ${model} ${res.status}: ${(await res.text()).slice(0, 200)}`)
57
+ const data = (await res.json()) as { choices?: Array<{ message?: { content?: string } }> }
58
+ const raw = data.choices?.[0]?.message?.content ?? ''
59
+ const m = /\[\s*(?:true|false)[\s\S]*?\]/i.exec(raw)
60
+ if (!m) return { passed: criteria.map(() => false), note: `judge returned no parseable verdict: ${raw.slice(0, 80)}` }
61
+ let arr: unknown
62
+ try {
63
+ arr = JSON.parse(m[0].toLowerCase())
64
+ } catch {
65
+ return { passed: criteria.map(() => false), note: 'judge verdict not valid JSON' }
66
+ }
67
+ const bools = Array.isArray(arr) ? arr.map((x) => x === true) : []
68
+ // Pad/truncate to criteria length (a short array scores the missing as fail).
69
+ const passed = criteria.map((_, i) => bools[i] === true)
70
+ return { passed, note: `${passed.filter(Boolean).length}/${criteria.length} criteria` }
71
+ }
72
+
73
+ export function createCadBenchAdapter(): BenchmarkAdapter {
74
+ let cache: Array<{ id: string; instruction: string; meta: CadBenchMeta }> | null = null
75
+
76
+ async function load(): Promise<typeof cache & object> {
77
+ if (cache) return cache
78
+ const path = process.env.CADBENCH_PATH
79
+ if (!path) throw new Error('CADBENCH_PATH must point at the cleaned CADBench JSONL ({id,instruction,type,criteria:[]} per line)')
80
+ const text = await readFile(path, 'utf8')
81
+ cache = text
82
+ .split('\n')
83
+ .filter((l) => l.trim())
84
+ .map((l) => {
85
+ const r = JSON.parse(l) as { id: string; name?: string; instruction: string; type?: string; criteria: string[] }
86
+ return { id: r.id, instruction: r.instruction, meta: { name: r.name ?? '', type: r.type ?? '', criteria: r.criteria } }
87
+ })
88
+ return cache
89
+ }
90
+
91
+ return {
92
+ name: 'cadbench',
93
+
94
+ async preflight() {
95
+ const { execFile } = await import('node:child_process')
96
+ const { promisify } = await import('node:util')
97
+ const exec = promisify(execFile)
98
+ try {
99
+ await exec('xvfb-run', ['-a', 'blender', '--version'], { timeout: 30_000 })
100
+ } catch (err) {
101
+ throw new Error(
102
+ `cadbench preflight failed: ${(err instanceof Error ? err.message : String(err)).slice(0, 200)}\n` +
103
+ `Fix: install Blender + Xvfb (sudo apt-get install -y blender xvfb). The judge runs \`xvfb-run -a blender --background --python\`.`,
104
+ )
105
+ }
106
+ await load()
107
+ },
108
+
109
+ async loadTasks(opts: LoadOptions = {}) {
110
+ let rows = await load()
111
+ if (opts.ids) rows = rows.filter((r) => opts.ids!.includes(r.id))
112
+ // TYPE filter (Simulative|Wild) via env, applied before limit.
113
+ const t = process.env.CADBENCH_TYPE
114
+ if (t) rows = rows.filter((r) => r.meta.type.toLowerCase() === t.toLowerCase())
115
+ if (opts.limit != null) rows = rows.slice(0, opts.limit)
116
+ return rows.map((r): BenchTask => ({ id: r.id, prompt: r.instruction, metadata: r.meta as unknown as Record<string, unknown> }))
117
+ },
118
+
119
+ async goldArtifact() {
120
+ return undefined // no reference bpy script ships with the benchmark
121
+ },
122
+
123
+ async judge(task: BenchTask, artifact: string): Promise<BenchScore> {
124
+ const meta = task.metadata as unknown as CadBenchMeta
125
+ const criteria = meta.criteria ?? []
126
+ if (!artifact.trim()) return { resolved: false, score: 0, detail: 'empty artifact' }
127
+ if (criteria.length === 0) return { resolved: false, score: 0, detail: 'task has no criteria' }
128
+ const r = await renderBpy(artifact, { views: 4 })
129
+ if (!r.built) return { resolved: false, score: 0, detail: `did not build/render: ${r.error ?? 'no mesh'}` }
130
+ const { passed, note } = await judgeCriteria(task.prompt, artifact, criteria, r.renders)
131
+ const score = passed.filter(Boolean).length / criteria.length
132
+ return { resolved: score === 1, score, detail: note }
133
+ },
134
+ }
135
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * CADGenBench adapter (huggingface/cadgenbench, Apache-2.0). Task = a part
3
+ * description → a STEP B-rep solid (output.step). Score = the benchmark's OWN
4
+ * deterministic geometric metric (cad_score): validity gate → PCA/ICP align to
5
+ * the ground truth → point-cloud F1 + volume IoU + edge F1 + topology match.
6
+ * NOT an LLM judge, NOT self-defined checks — the published CAD kernel decides.
7
+ *
8
+ * The official task set (private GT, server-side graded) isn't released yet, so
9
+ * tasks here are seeded from the repo's dimension-named geometry fixtures (real
10
+ * GT STEPs scored by the real scorer). When CADGENBENCH_DATA_DIR is set, swap
11
+ * loadTasks to read the published fixtures' description.yaml + ground_truth.step.
12
+ *
13
+ * Requires the CADGenBench venv (CADGENBENCH_VENV) + clone (CADGENBENCH_DIR) +
14
+ * xvfb (the scorer's alignment renders need a display).
15
+ */
16
+
17
+ import { execFile } from 'node:child_process'
18
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises'
19
+ import { tmpdir } from 'node:os'
20
+ import { join } from 'node:path'
21
+ import { promisify } from 'node:util'
22
+ import type { BenchScore, BenchTask, BenchmarkAdapter, LoadOptions } from './types'
23
+ import { CGB_DIR, CGB_VENV_PY } from '../worker-build123d'
24
+
25
+ const execFileAsync = promisify(execFile)
26
+
27
+ /** Self-contained scorer wrapper (written to a temp file, run in the venv).
28
+ * Scores a candidate STEP against a ground-truth STEP via the benchmark's own
29
+ * evaluate_result, printing the cad_score line. */
30
+ const SCORE_PY = `
31
+ import sys, json, tempfile, shutil
32
+ from pathlib import Path
33
+ from cadgenbench.eval.evaluate import evaluate_result
34
+ cand, gt = Path(sys.argv[1]), Path(sys.argv[2])
35
+ with tempfile.TemporaryDirectory() as rd, tempfile.TemporaryDirectory() as gd:
36
+ rd, gd = Path(rd), Path(gd)
37
+ (rd / 'result.json').write_text('{}')
38
+ shutil.copy(gt, gd / 'ground_truth.step')
39
+ try:
40
+ evaluate_result(rd, gd, candidate_step=cand)
41
+ d = json.loads((rd / 'result.json').read_text())
42
+ print('CGB_SCORE ' + json.dumps({'cad_score': d.get('cad_score', 0.0), 'status': d.get('status', 'unknown')}))
43
+ except Exception as e:
44
+ print('CGB_SCORE ' + json.dumps({'cad_score': 0.0, 'status': 'error', 'error': str(e)[:200]}))
45
+ `.trim()
46
+
47
+ interface CgbMeta {
48
+ gtStep: string
49
+ resolveThreshold: number
50
+ }
51
+
52
+ /** Fixture-seeded tasks (real GT STEPs from the repo, dim-named so the spec is
53
+ * exact). Replaced by the published dataset when CADGENBENCH_DATA_DIR is set. */
54
+ function fixtureTasks(): Array<{ id: string; prompt: string; gtStep: string }> {
55
+ const g = join(CGB_DIR, 'tests/fixtures/geometry')
56
+ return [
57
+ { id: 'box-10x20x30', prompt: 'A rectangular solid box, 10 units wide (X), 20 units deep (Y), and 30 units tall (Z).', gtStep: join(g, 'box_10_20_30.step') },
58
+ { id: 'cube-10', prompt: 'A cube, 10 units on every side.', gtStep: join(g, 'box_10_10_10.step') },
59
+ { id: 'sphere-10', prompt: 'A sphere of radius 10 units, centered at the origin.', gtStep: join(g, 'sphere_10.step') },
60
+ ]
61
+ }
62
+
63
+ export function createCadGenBenchAdapter(): BenchmarkAdapter {
64
+ return {
65
+ name: 'cadgenbench',
66
+
67
+ async preflight() {
68
+ const r = await execFileAsync(CGB_VENV_PY, ['-c', 'import cadgenbench.eval.evaluate, build123d, trimesh, manifold3d; print("ok")'], { timeout: 60_000 }).catch(
69
+ (e) => ({ stdout: '', stderr: e instanceof Error ? e.message : String(e) }),
70
+ )
71
+ if (!/ok/.test(r.stdout)) {
72
+ throw new Error(
73
+ `cadgenbench preflight failed (venv=${CGB_VENV_PY}): ${r.stderr.slice(0, 200)}\n` +
74
+ `Fix: git clone https://github.com/huggingface/cadgenbench ${CGB_DIR}; python3 -m venv $CADGENBENCH_VENV; $CADGENBENCH_VENV/bin/pip install -e ${CGB_DIR}`,
75
+ )
76
+ }
77
+ },
78
+
79
+ async loadTasks(opts: LoadOptions = {}) {
80
+ // CGB_HARD_DIR (a dir with tasks.json = [{id,prompt,gtStep}]) overrides the
81
+ // trivial fixture primitives with hard multi-feature parts (real headroom).
82
+ let tasks = fixtureTasks()
83
+ const hard = process.env.CGB_HARD_DIR
84
+ if (hard) {
85
+ const { readFile } = await import('node:fs/promises')
86
+ tasks = JSON.parse(await readFile(join(hard, 'tasks.json'), 'utf8')) as Array<{ id: string; prompt: string; gtStep: string }>
87
+ }
88
+ if (opts.ids) tasks = tasks.filter((t) => opts.ids!.includes(t.id))
89
+ if (opts.limit != null) tasks = tasks.slice(0, opts.limit)
90
+ const meta = (gtStep: string): CgbMeta => ({ gtStep, resolveThreshold: Number(process.env.CGB_RESOLVE_THRESHOLD ?? 0.9) })
91
+ return tasks.map((t): BenchTask => ({ id: t.id, prompt: t.prompt, metadata: meta(t.gtStep) as unknown as Record<string, unknown> }))
92
+ },
93
+
94
+ async goldArtifact() {
95
+ return undefined // GT is a STEP file scored by the kernel, not a returnable artifact
96
+ },
97
+
98
+ async judge(task: BenchTask, artifact: string): Promise<BenchScore> {
99
+ const { gtStep, resolveThreshold } = task.metadata as unknown as CgbMeta
100
+ if (!artifact.includes('ISO-10303-21')) return { resolved: false, score: 0, detail: 'artifact is not a STEP file' }
101
+ const dir = await mkdtemp(join(tmpdir(), 'cgb-judge-'))
102
+ const cand = join(dir, 'candidate.step')
103
+ const scorer = join(dir, 'score.py')
104
+ try {
105
+ await writeFile(cand, artifact)
106
+ await writeFile(scorer, SCORE_PY)
107
+ // xvfb: the scorer's alignment step renders; needs a display.
108
+ const r = await execFileAsync('xvfb-run', ['-a', CGB_VENV_PY, scorer, cand, gtStep], { maxBuffer: 1 << 26, timeout: 180_000 }).catch(
109
+ (e) => ({ stdout: (e as { stdout?: string }).stdout ?? '', stderr: e instanceof Error ? e.message : String(e) }),
110
+ )
111
+ const m = /CGB_SCORE (\{.*\})/.exec(r.stdout)
112
+ if (!m) return { resolved: false, score: 0, detail: `scorer produced no verdict: ${(r.stderr || r.stdout).slice(0, 160)}` }
113
+ const v = JSON.parse(m[1]) as { cad_score: number; status: string; error?: string }
114
+ const score = typeof v.cad_score === 'number' ? v.cad_score : 0
115
+ return { resolved: score >= resolveThreshold, score, detail: `cad_score=${score.toFixed(3)} status=${v.status}${v.error ? ` (${v.error})` : ''}` }
116
+ } finally {
117
+ await rm(dir, { recursive: true, force: true }).catch(() => {})
118
+ }
119
+ },
120
+ }
121
+ }