@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.
- package/CHANGELOG.md +12 -0
- package/HARNESS.md +2 -2
- package/dist/adapters.js +6 -2
- package/dist/adapters.js.map +1 -1
- package/dist/benchmarks/appworld.d.ts +61 -2
- package/dist/benchmarks/appworld.js +111 -40
- package/dist/benchmarks/appworld.js.map +1 -1
- package/dist/benchmarks/cadbench.js +1 -1
- package/dist/benchmarks/cadgenbench.js +1 -1
- package/dist/benchmarks/finresearchbench.js +16 -22
- package/dist/benchmarks/finresearchbench.js.map +1 -1
- package/dist/benchmarks/finsearchcomp.js +16 -22
- package/dist/benchmarks/finsearchcomp.js.map +1 -1
- package/dist/benchmarks/frames.js +18 -20
- package/dist/benchmarks/frames.js.map +1 -1
- package/dist/benchmarks/mcad-bench.d.ts +106 -0
- package/dist/benchmarks/mcad-bench.js +569 -0
- package/dist/benchmarks/mcad-bench.js.map +1 -0
- package/dist/benchmarks/mcad-cq-bench.d.ts +82 -0
- package/dist/benchmarks/mcad-cq-bench.js +339 -0
- package/dist/benchmarks/mcad-cq-bench.js.map +1 -0
- package/dist/benchmarks/mcad-cq-golds.d.ts +36 -0
- package/dist/benchmarks/mcad-cq-golds.js +342 -0
- package/dist/benchmarks/mcad-cq-golds.js.map +1 -0
- package/dist/benchmarks/mcad-golds.d.ts +20 -0
- package/dist/benchmarks/mcad-golds.js +318 -0
- package/dist/benchmarks/mcad-golds.js.map +1 -0
- package/dist/benchmarks/mcad-tasks.d.ts +66 -0
- package/dist/benchmarks/mcad-tasks.js +508 -0
- package/dist/benchmarks/mcad-tasks.js.map +1 -0
- package/dist/benchmarks/simpleqa.js +15 -19
- package/dist/benchmarks/simpleqa.js.map +1 -1
- package/dist/benchmarks/trata-hedge.js +20 -23
- package/dist/benchmarks/trata-hedge.js.map +1 -1
- package/dist/{cadbench-BrpwOU6A.js → cadbench-BLSyxR1N.js} +21 -20
- package/dist/cadbench-BLSyxR1N.js.map +1 -0
- package/dist/{cadgenbench-DF7hYHdl.js → cadgenbench-x2OFkf8y.js} +2 -2
- package/dist/cadgenbench-x2OFkf8y.js.map +1 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +10 -9
- package/dist/index.js.map +1 -1
- package/dist/router-turn-C2wMiDoo.js +31 -0
- package/dist/router-turn-C2wMiDoo.js.map +1 -0
- package/package.json +7 -7
- package/scripts/appworld_driver.py +1 -170
- package/scripts/run-package-tests.mjs +56 -36
- package/scripts/run-package-tests.test.mjs +33 -0
- package/scripts/trata-hedge/README.md +3 -3
- package/scripts/trata-hedge/run.sh +6 -2
- package/src/adapters.ts +11 -0
- package/src/aec-gate.mts +30 -9
- package/src/agent-graphs-gen2.mts +523 -0
- package/src/agent-graphs-gen3.mts +660 -0
- package/src/agent-graphs-improve/offline-seams.mts +128 -0
- package/src/agent-graphs-improve.mts +747 -0
- package/src/atom-humaneval.mts +76 -55
- package/src/atom-mcp-e2e.mts +27 -13
- package/src/benchmarks/appworld.test.mts +119 -1
- package/src/benchmarks/appworld.ts +156 -33
- package/src/benchmarks/cadbench.ts +21 -8
- package/src/benchmarks/finresearchbench.ts +20 -16
- package/src/benchmarks/finsearchcomp.ts +20 -20
- package/src/benchmarks/frames.ts +19 -18
- package/src/benchmarks/mcad-bench.test.mts +455 -0
- package/src/benchmarks/mcad-bench.ts +561 -0
- package/src/benchmarks/mcad-cq-bench.ts +423 -0
- package/src/benchmarks/mcad-cq-golds.ts +374 -0
- package/src/benchmarks/mcad-cq.test.mts +386 -0
- package/src/benchmarks/mcad-golds.ts +359 -0
- package/src/benchmarks/mcad-tasks.ts +490 -0
- package/src/benchmarks/simpleqa.ts +19 -17
- package/src/benchmarks/trata-hedge.ts +26 -18
- package/src/browser/ui-reviewer.ts +1 -1
- package/src/clbench-context-gate.mts +45 -9
- package/src/cloud-loop.mts +24 -6
- package/src/commit0-env-run.mts +6 -3
- package/src/commit0-env.ts +2 -3
- package/src/commit0-gate.mts +53 -24
- package/src/corpus.ts +3 -3
- package/src/david-attribution.mts +28 -9
- package/src/david-goliath.mts +31 -12
- package/src/egress-probe.mts +3 -3
- package/src/examples/lean-proof-gate.mts +10 -3
- package/src/examples/math-demo.mts +10 -3
- package/src/examples/strategy-demo.mts +10 -4
- package/src/fleet.mts +30 -8
- package/src/gate-cli.mts +9 -4
- package/src/gate.test.mts +7 -2
- package/src/gate.ts +23 -75
- package/src/generate-eval/certify.ts +13 -5
- package/src/hev-eval.mts +39 -10
- package/src/hev-improve.mts +28 -16
- package/src/hev-structural.mts +28 -20
- package/src/humaneval-gate.mts +24 -8
- package/src/humaneval-object-ablation.mts +52 -14
- package/src/humaneval-repair-gate.mts +53 -14
- package/src/mbpp-structural.mts +28 -20
- package/src/mcp-mount-probe.mts +23 -11
- package/src/official-optimizer-config.mts +61 -12
- package/src/official-optimizer-config.test.mts +72 -3
- package/src/quant-arena/profiles/quant-researcher.profile.json +6 -1
- package/src/quant-arena/quant-loop.mts +88 -61
- package/src/research-shot.ts +21 -11
- package/src/resolve-client.ts +6 -8
- package/src/rollout-ledger/backfill-swe-arena.test.mts +7 -2
- package/src/router-executor.ts +1 -1
- package/src/router-turn.ts +135 -0
- package/src/run-benchmarks.ts +7 -2
- package/src/sandbox-run.ts +23 -41
- package/src/search-bench/bridge.ts +66 -8
- package/src/search-bench/parametric-check.mts +19 -4
- package/src/search-bench/profiles.ts +24 -9
- package/src/search-bench/run.mts +3 -4
- package/src/smoke-structural-rollout.mts +21 -5
- package/src/supervisor-arena.mts +28 -20
- package/src/swe-arena/arms.ts +73 -15
- package/src/swe-arena/capacity.ts +44 -41
- package/src/swe-arena/diagnosis-ensemble.ts +51 -48
- package/src/swe-arena/execution.test.mts +12 -12
- package/src/swe-arena/gepa-seat.mts +12 -5
- package/src/swe-arena/gepa-seat.test.mts +45 -33
- package/src/swe-arena/outer-loop.mts +94 -70
- package/src/swe-arena/premeasured-from-cells.mts +16 -1
- package/src/swe-arena/premeasured-from-cells.test.mts +21 -1
- package/src/swe-arena/profiles/deepseek-author.profile.json +12 -0
- package/src/swe-arena/profiles/default-author.profile.json +9 -1
- package/src/swe-arena/proposer-fanout.mts +52 -99
- package/src/swe-arena/proposer-fanout.test.mts +92 -57
- package/src/swe-arena/proposer-provenance.mts +1 -1
- package/src/swe-code-improve.mts +30 -52
- package/src/swe-emit-patch.mts +26 -5
- package/src/swe-improve.mts +27 -8
- package/src/swe-jail.test.ts +128 -0
- package/src/swe-jail.ts +106 -22
- package/src/swe-local-proof.mts +27 -5
- package/src/swe-repro-calibrate.mts +5 -0
- package/src/swe-self-improve.mts +45 -10
- package/src/swe-stream.mts +68 -6
- package/src/swe-structural.mts +46 -7
- package/src/tb-container-executor.mts +38 -6
- package/src/tb-container-executor.test.mts +11 -3
- package/src/tb-supervisor-sidecar.mts +11 -27
- package/src/terminal-compare.ts +38 -18
- package/src/trata-gate.mts +26 -26
- package/src/trata-gepa.mts +25 -16
- package/src/trata-hedge-solve.mts +80 -0
- package/src/worker-blender.ts +21 -11
- package/src/worker-browser.ts +18 -5
- package/src/worker-build123d.ts +17 -8
- package/src/worker-cad.ts +31 -12
- package/src/worker.ts +1 -1
- package/tb_agents/opencode_router_agent.py +84 -42
- package/dist/cadbench-BrpwOU6A.js.map +0 -1
- package/dist/cadgenbench-DF7hYHdl.js.map +0 -1
- package/scripts/trata-hedge/solve.py +0 -83
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
import { MCAD_GOLDS } from "./mcad-golds.js";
|
|
2
|
+
import { MCAD_TASKS } from "./mcad-tasks.js";
|
|
3
|
+
import { mkdtemp, readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
//#region src/benchmarks/mcad-bench.ts
|
|
9
|
+
/**
|
|
10
|
+
* MCAD adapter. Worker artifact = OpenSCAD source for a dimensioned mechanical
|
|
11
|
+
* part; judge = the REAL OpenSCAD kernel (compile + ASCII-STL export) followed by
|
|
12
|
+
* pure-TS mesh measurement against the task's `McadSpec`. No LLM, no self-report:
|
|
13
|
+
* the part either has the stated extents, volume, body count and holes, or it does
|
|
14
|
+
* not.
|
|
15
|
+
*
|
|
16
|
+
* Pipeline, in order, and every stage is a hard fact about the produced mesh:
|
|
17
|
+
* 1. compile : `xvfb-run -a openscad --export-format=asciistl -o out.stl` exits 0
|
|
18
|
+
* 2. watertight : every undirected edge shared by exactly two faces (2-manifold)
|
|
19
|
+
* 3. measure : bbox extents, enclosed volume (divergence theorem), connected
|
|
20
|
+
* -component count ("solids"), triangle count
|
|
21
|
+
* 4. probe : ray-parity point-in-solid at the spec's pinned hole/material
|
|
22
|
+
* coordinates — this is what makes a missing or misplaced bore
|
|
23
|
+
* fail, which no aggregate measure can catch
|
|
24
|
+
*
|
|
25
|
+
* Scoring: compile failure or a non-watertight mesh is 0 / unresolved — a mesh that
|
|
26
|
+
* is not a closed solid has no well-defined volume or interior, so every downstream
|
|
27
|
+
* number would be fiction. Otherwise the score is the fraction of ASSERTED spec
|
|
28
|
+
* checks passed (each bbox axis, the volume band, the solids band, the triangle
|
|
29
|
+
* floor, and EACH probe count as one check), and `resolved` requires all of them.
|
|
30
|
+
*
|
|
31
|
+
* Requires `openscad` + `xvfb-run` on PATH at JUDGE time only; `loadTasks` and the
|
|
32
|
+
* geometry engine below are dependency-free and run anywhere.
|
|
33
|
+
*
|
|
34
|
+
* Geometry checks 1-3 are ported from supervisor-lab's `bench/cad-stl-gate.ts`
|
|
35
|
+
* (edge-key manifold parity + signed-tetrahedron volume). Ported, not imported:
|
|
36
|
+
* these packages do not share a dependency edge.
|
|
37
|
+
*/
|
|
38
|
+
const execFileAsync = promisify(execFile);
|
|
39
|
+
/** Coincident-vertex quantisation, in model units (mm). */
|
|
40
|
+
const VERTEX_QUANTUM = 1e-6;
|
|
41
|
+
const AREA_EPS = 1e-9;
|
|
42
|
+
/** Parse an ASCII STL into triangles by reading `vertex x y z` lines in groups of three. */
|
|
43
|
+
function parseAsciiStl(stl) {
|
|
44
|
+
const verts = [];
|
|
45
|
+
for (const line of stl.split("\n")) {
|
|
46
|
+
const m = line.trim().match(/^vertex\s+(\S+)\s+(\S+)\s+(\S+)/i);
|
|
47
|
+
if (!m) continue;
|
|
48
|
+
const x = Number(m[1]);
|
|
49
|
+
const y = Number(m[2]);
|
|
50
|
+
const z = Number(m[3]);
|
|
51
|
+
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) continue;
|
|
52
|
+
verts.push({
|
|
53
|
+
x,
|
|
54
|
+
y,
|
|
55
|
+
z
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const tris = [];
|
|
59
|
+
for (let i = 0; i + 2 < verts.length; i += 3) tris.push([
|
|
60
|
+
verts[i],
|
|
61
|
+
verts[i + 1],
|
|
62
|
+
verts[i + 2]
|
|
63
|
+
]);
|
|
64
|
+
return tris;
|
|
65
|
+
}
|
|
66
|
+
/** Quantise a vertex so shared edges match despite float authoring noise. */
|
|
67
|
+
function vertexKey(v) {
|
|
68
|
+
const r = (n) => (Math.round(n / VERTEX_QUANTUM) * VERTEX_QUANTUM).toFixed(6);
|
|
69
|
+
return `${r(v.x)},${r(v.y)},${r(v.z)}`;
|
|
70
|
+
}
|
|
71
|
+
function sub(a, b) {
|
|
72
|
+
return {
|
|
73
|
+
x: a.x - b.x,
|
|
74
|
+
y: a.y - b.y,
|
|
75
|
+
z: a.z - b.z
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function cross(a, b) {
|
|
79
|
+
return {
|
|
80
|
+
x: a.y * b.z - a.z * b.y,
|
|
81
|
+
y: a.z * b.x - a.x * b.z,
|
|
82
|
+
z: a.x * b.y - a.y * b.x
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function dot(a, b) {
|
|
86
|
+
return a.x * b.x + a.y * b.y + a.z * b.z;
|
|
87
|
+
}
|
|
88
|
+
function norm(a) {
|
|
89
|
+
return Math.sqrt(dot(a, a));
|
|
90
|
+
}
|
|
91
|
+
function normalize(a) {
|
|
92
|
+
const n = norm(a);
|
|
93
|
+
return {
|
|
94
|
+
x: a.x / n,
|
|
95
|
+
y: a.y / n,
|
|
96
|
+
z: a.z / n
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/** Disjoint-set over triangle indices — the "how many separate bodies" primitive. */
|
|
100
|
+
function findRoot(parent, i) {
|
|
101
|
+
let r = i;
|
|
102
|
+
while (parent[r] !== r) r = parent[r];
|
|
103
|
+
let c = i;
|
|
104
|
+
while (parent[c] !== c) {
|
|
105
|
+
const next = parent[c];
|
|
106
|
+
parent[c] = r;
|
|
107
|
+
c = next;
|
|
108
|
+
}
|
|
109
|
+
return r;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Measure the closed-solid properties of a triangle soup. Pure: no I/O, no deps.
|
|
113
|
+
*
|
|
114
|
+
* `solids` is the number of connected components of the graph whose nodes are
|
|
115
|
+
* triangles and whose edges join two triangles that share a quantised mesh edge.
|
|
116
|
+
* On a watertight mesh that count IS the number of disconnected bodies (an
|
|
117
|
+
* enclosed internal cavity is its own shell and counts, which is the behaviour
|
|
118
|
+
* the specs want — a "hollow" body that is really two nested shells is not one
|
|
119
|
+
* fused solid).
|
|
120
|
+
*/
|
|
121
|
+
function measureMesh(tris) {
|
|
122
|
+
if (tris.length === 0) {
|
|
123
|
+
const zero = {
|
|
124
|
+
x: 0,
|
|
125
|
+
y: 0,
|
|
126
|
+
z: 0
|
|
127
|
+
};
|
|
128
|
+
return {
|
|
129
|
+
triangles: 0,
|
|
130
|
+
openEdges: 0,
|
|
131
|
+
watertight: false,
|
|
132
|
+
degenerateFaces: 0,
|
|
133
|
+
volume: 0,
|
|
134
|
+
solids: 0,
|
|
135
|
+
bbox: {
|
|
136
|
+
min: zero,
|
|
137
|
+
max: zero,
|
|
138
|
+
size: zero
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const min = {
|
|
143
|
+
x: Number.POSITIVE_INFINITY,
|
|
144
|
+
y: Number.POSITIVE_INFINITY,
|
|
145
|
+
z: Number.POSITIVE_INFINITY
|
|
146
|
+
};
|
|
147
|
+
const max = {
|
|
148
|
+
x: Number.NEGATIVE_INFINITY,
|
|
149
|
+
y: Number.NEGATIVE_INFINITY,
|
|
150
|
+
z: Number.NEGATIVE_INFINITY
|
|
151
|
+
};
|
|
152
|
+
const edgeFaces = /* @__PURE__ */ new Map();
|
|
153
|
+
const parent = new Int32Array(tris.length);
|
|
154
|
+
for (let i = 0; i < tris.length; i++) parent[i] = i;
|
|
155
|
+
let degenerateFaces = 0;
|
|
156
|
+
let volume6 = 0;
|
|
157
|
+
for (let i = 0; i < tris.length; i++) {
|
|
158
|
+
const [a, b, c] = tris[i];
|
|
159
|
+
for (const v of [
|
|
160
|
+
a,
|
|
161
|
+
b,
|
|
162
|
+
c
|
|
163
|
+
]) {
|
|
164
|
+
if (v.x < min.x) min.x = v.x;
|
|
165
|
+
if (v.y < min.y) min.y = v.y;
|
|
166
|
+
if (v.z < min.z) min.z = v.z;
|
|
167
|
+
if (v.x > max.x) max.x = v.x;
|
|
168
|
+
if (v.y > max.y) max.y = v.y;
|
|
169
|
+
if (v.z > max.z) max.z = v.z;
|
|
170
|
+
}
|
|
171
|
+
if (norm(cross(sub(b, a), sub(c, a))) < AREA_EPS) degenerateFaces += 1;
|
|
172
|
+
volume6 += dot(a, cross(b, c));
|
|
173
|
+
const ka = vertexKey(a);
|
|
174
|
+
const kb = vertexKey(b);
|
|
175
|
+
const kc = vertexKey(c);
|
|
176
|
+
for (const [p, q] of [
|
|
177
|
+
[ka, kb],
|
|
178
|
+
[kb, kc],
|
|
179
|
+
[kc, ka]
|
|
180
|
+
]) {
|
|
181
|
+
const key = p < q ? `${p}|${q}` : `${q}|${p}`;
|
|
182
|
+
const seen = edgeFaces.get(key);
|
|
183
|
+
if (seen) seen.push(i);
|
|
184
|
+
else edgeFaces.set(key, [i]);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
let openEdges = 0;
|
|
188
|
+
for (const faces of edgeFaces.values()) {
|
|
189
|
+
if (faces.length !== 2) openEdges += 1;
|
|
190
|
+
const first = findRoot(parent, faces[0]);
|
|
191
|
+
for (let k = 1; k < faces.length; k++) {
|
|
192
|
+
const other = findRoot(parent, faces[k]);
|
|
193
|
+
if (other !== first) parent[other] = first;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const roots = /* @__PURE__ */ new Set();
|
|
197
|
+
for (let i = 0; i < tris.length; i++) roots.add(findRoot(parent, i));
|
|
198
|
+
return {
|
|
199
|
+
triangles: tris.length,
|
|
200
|
+
openEdges,
|
|
201
|
+
watertight: openEdges === 0,
|
|
202
|
+
degenerateFaces,
|
|
203
|
+
volume: Math.abs(volume6) / 6,
|
|
204
|
+
solids: roots.size,
|
|
205
|
+
bbox: {
|
|
206
|
+
min: { ...min },
|
|
207
|
+
max: { ...max },
|
|
208
|
+
size: {
|
|
209
|
+
x: max.x - min.x,
|
|
210
|
+
y: max.y - min.y,
|
|
211
|
+
z: max.z - min.z
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Three fixed, mutually linearly-independent directions built from irrational
|
|
218
|
+
* constants (sqrt(3)-1, sqrt(2)-1, 1/sqrt(5), 1/sqrt(3) and the plastic-number
|
|
219
|
+
* pair 0.7548777 / 0.5698403). Their 3x3 determinant is ~1.77, so no two are
|
|
220
|
+
* near-parallel and no plane contains all three.
|
|
221
|
+
*/
|
|
222
|
+
const RAY_DIRECTIONS = [
|
|
223
|
+
normalize({
|
|
224
|
+
x: 1,
|
|
225
|
+
y: .7548777,
|
|
226
|
+
z: .5698403
|
|
227
|
+
}),
|
|
228
|
+
normalize({
|
|
229
|
+
x: -.7320508,
|
|
230
|
+
y: 1,
|
|
231
|
+
z: .236068
|
|
232
|
+
}),
|
|
233
|
+
normalize({
|
|
234
|
+
x: .4472136,
|
|
235
|
+
y: -.5773503,
|
|
236
|
+
z: 1
|
|
237
|
+
})
|
|
238
|
+
];
|
|
239
|
+
/** Barycentric / parametric tolerances for calling a crossing "on the boundary". */
|
|
240
|
+
const BARY_EPS = 1e-9;
|
|
241
|
+
const DET_EPS = 1e-12;
|
|
242
|
+
const T_EPS = 1e-9;
|
|
243
|
+
const MAX_JITTERS = 8;
|
|
244
|
+
/** Deterministic 32-bit PRNG (mulberry32) — seeded, so probes never use Math.random. */
|
|
245
|
+
function mulberry32(seed) {
|
|
246
|
+
let a = seed >>> 0;
|
|
247
|
+
return () => {
|
|
248
|
+
a = a + 1831565813 >>> 0;
|
|
249
|
+
let t = a;
|
|
250
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
251
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
252
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
/** Stable integer hash of a probe point so its jitter sequence is reproducible. */
|
|
256
|
+
function seedForPoint(p, rayIndex) {
|
|
257
|
+
let h = 2166136261 ^ rayIndex;
|
|
258
|
+
for (const n of [
|
|
259
|
+
p.x,
|
|
260
|
+
p.y,
|
|
261
|
+
p.z
|
|
262
|
+
]) {
|
|
263
|
+
const bits = Math.round(n * 1e6) | 0;
|
|
264
|
+
h = Math.imul(h ^ bits & 65535, 16777619);
|
|
265
|
+
h = Math.imul(h ^ bits >>> 16 & 65535, 16777619);
|
|
266
|
+
}
|
|
267
|
+
return h >>> 0;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Moller-Trumbore crossing count along one ray. Returns `degenerate` when the ray
|
|
271
|
+
* touches a triangle edge/vertex, lies in a triangle's plane, or starts on a face
|
|
272
|
+
* — the three cases where a parity count is not trustworthy.
|
|
273
|
+
*/
|
|
274
|
+
function castRay(tris, origin, dir) {
|
|
275
|
+
let crossings = 0;
|
|
276
|
+
for (const [v0, v1, v2] of tris) {
|
|
277
|
+
const e1 = sub(v1, v0);
|
|
278
|
+
const e2 = sub(v2, v0);
|
|
279
|
+
const h = cross(dir, e2);
|
|
280
|
+
const det = dot(e1, h);
|
|
281
|
+
const scale = norm(e1) * norm(e2);
|
|
282
|
+
if (Math.abs(det) <= DET_EPS * Math.max(scale, 1)) {
|
|
283
|
+
const n = cross(e1, e2);
|
|
284
|
+
const nl = norm(n);
|
|
285
|
+
if (nl > AREA_EPS && Math.abs(dot(sub(origin, v0), n)) / nl <= T_EPS) return { degenerate: true };
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
const f = 1 / det;
|
|
289
|
+
const s = sub(origin, v0);
|
|
290
|
+
const u = f * dot(s, h);
|
|
291
|
+
const q = cross(s, e1);
|
|
292
|
+
const v = f * dot(dir, q);
|
|
293
|
+
const w = 1 - u - v;
|
|
294
|
+
if (u < -1e-9 || v < -1e-9 || w < -1e-9) continue;
|
|
295
|
+
const t = f * dot(e2, q);
|
|
296
|
+
if (t < -1e-9) continue;
|
|
297
|
+
if (Math.abs(t) <= T_EPS) return { degenerate: true };
|
|
298
|
+
if (u <= BARY_EPS || v <= BARY_EPS || w <= BARY_EPS) return { degenerate: true };
|
|
299
|
+
crossings += 1;
|
|
300
|
+
}
|
|
301
|
+
return { crossings };
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Point-in-solid membership by ray parity, made robust three ways.
|
|
305
|
+
*
|
|
306
|
+
* ROBUSTNESS ARGUMENT. A parity test is exact except on a measure-zero set: rays
|
|
307
|
+
* that graze a triangle edge or vertex (the crossing is counted twice or zero
|
|
308
|
+
* times), rays coplanar with a face, and origins lying on the surface. Those cases
|
|
309
|
+
* are not merely rare here — they are SYSTEMATIC, because OpenSCAD emits
|
|
310
|
+
* axis-aligned meshes whose vertices land on the same round millimetre lattice the
|
|
311
|
+
* spec's probe coordinates come from, so an axis-aligned ray from a probe point
|
|
312
|
+
* hits shared edges constantly. Three defences, in order:
|
|
313
|
+
* 1. DIRECTIONS. The three fixed directions are irrational combinations, so a ray
|
|
314
|
+
* from a lattice point cannot stay in an axis-aligned face plane and cannot
|
|
315
|
+
* run along a lattice edge.
|
|
316
|
+
* 2. DETECTION + DETERMINISTIC RE-JITTER. Grazing is DETECTED (a barycentric
|
|
317
|
+
* coordinate within BARY_EPS of 0, |det| below DET_EPS with the origin in the
|
|
318
|
+
* plane, or |t| within T_EPS) rather than hoped away. A detected ray is re-cast
|
|
319
|
+
* with a small direction perturbation drawn from a mulberry32 PRNG seeded by
|
|
320
|
+
* the probe coordinates and ray index, so the whole judge stays deterministic:
|
|
321
|
+
* the same mesh and the same point always take the same sequence of re-casts.
|
|
322
|
+
* 3. MAJORITY VOTE. The verdict is the majority of three independent directions,
|
|
323
|
+
* so even an undetected miscount on one ray cannot flip the answer.
|
|
324
|
+
* A point sitting exactly ON the surface has no correct answer; every direction
|
|
325
|
+
* degenerates there and the vote falls back to whatever the jittered casts say.
|
|
326
|
+
* The specs therefore place probes with >=1 mm clearance from any surface.
|
|
327
|
+
*/
|
|
328
|
+
function pointInSolid(tris, point) {
|
|
329
|
+
let inside = 0;
|
|
330
|
+
let votes = 0;
|
|
331
|
+
for (let i = 0; i < RAY_DIRECTIONS.length; i++) {
|
|
332
|
+
const base = RAY_DIRECTIONS[i];
|
|
333
|
+
const rand = mulberry32(seedForPoint(point, i));
|
|
334
|
+
let dir = base;
|
|
335
|
+
for (let attempt = 0; attempt <= MAX_JITTERS; attempt++) {
|
|
336
|
+
const r = castRay(tris, point, dir);
|
|
337
|
+
if (!("degenerate" in r)) {
|
|
338
|
+
votes += 1;
|
|
339
|
+
if (r.crossings % 2 === 1) inside += 1;
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
dir = normalize({
|
|
343
|
+
x: base.x + (rand() - .5) * .001,
|
|
344
|
+
y: base.y + (rand() - .5) * .001,
|
|
345
|
+
z: base.z + (rand() - .5) * .001
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (votes === 0) return false;
|
|
350
|
+
return inside * 2 > votes;
|
|
351
|
+
}
|
|
352
|
+
function inBand(v, [lo, hi]) {
|
|
353
|
+
return v >= lo && v <= hi;
|
|
354
|
+
}
|
|
355
|
+
function fmt(n) {
|
|
356
|
+
return String(Math.round(n * 1e3) / 1e3);
|
|
357
|
+
}
|
|
358
|
+
function pt(p) {
|
|
359
|
+
return `(${p[0]}, ${p[1]}, ${p[2]})`;
|
|
360
|
+
}
|
|
361
|
+
/** Score a measured mesh against the task's spec — one named check per assertion. */
|
|
362
|
+
function scoreAgainstSpec(geo, tris, spec) {
|
|
363
|
+
const checks = [];
|
|
364
|
+
const add = (name, ok, measured, expected) => checks.push({
|
|
365
|
+
name,
|
|
366
|
+
ok,
|
|
367
|
+
measured,
|
|
368
|
+
expected
|
|
369
|
+
});
|
|
370
|
+
for (const axis of [
|
|
371
|
+
"x",
|
|
372
|
+
"y",
|
|
373
|
+
"z"
|
|
374
|
+
]) {
|
|
375
|
+
const band = spec.bbox?.[axis];
|
|
376
|
+
if (!band) continue;
|
|
377
|
+
const got = geo.bbox.size[axis];
|
|
378
|
+
add(`bbox${axis.toUpperCase()}`, inBand(got, band), fmt(got), `[${band[0]}, ${band[1]}]`);
|
|
379
|
+
}
|
|
380
|
+
if (spec.volume) add("volume", inBand(geo.volume, spec.volume), fmt(geo.volume), `[${spec.volume[0]}, ${spec.volume[1]}]`);
|
|
381
|
+
if (spec.solids) add("solids", inBand(geo.solids, spec.solids), String(geo.solids), `[${spec.solids[0]}, ${spec.solids[1]}]`);
|
|
382
|
+
if (spec.minTriangles != null) add("minTriangles", geo.triangles >= spec.minTriangles, String(geo.triangles), `>= ${spec.minTriangles}`);
|
|
383
|
+
for (const [i, p] of (spec.probesInsideSolid ?? []).entries()) {
|
|
384
|
+
const got = pointInSolid(tris, {
|
|
385
|
+
x: p[0],
|
|
386
|
+
y: p[1],
|
|
387
|
+
z: p[2]
|
|
388
|
+
});
|
|
389
|
+
add(`probeInside[${i}] ${pt(p)}`, got, got ? "inside" : "outside", "inside");
|
|
390
|
+
}
|
|
391
|
+
for (const [i, p] of (spec.probesOutsideSolid ?? []).entries()) {
|
|
392
|
+
const got = pointInSolid(tris, {
|
|
393
|
+
x: p[0],
|
|
394
|
+
y: p[1],
|
|
395
|
+
z: p[2]
|
|
396
|
+
});
|
|
397
|
+
add(`probeOutside[${i}] ${pt(p)}`, !got, got ? "inside" : "outside", "outside");
|
|
398
|
+
}
|
|
399
|
+
const failed = checks.filter((c) => !c.ok);
|
|
400
|
+
return {
|
|
401
|
+
checks,
|
|
402
|
+
failed,
|
|
403
|
+
score: checks.length ? (checks.length - failed.length) / checks.length : 0,
|
|
404
|
+
resolved: checks.length > 0 && failed.length === 0
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
/** Run openscad under xvfb (it wants a GL context even headless). */
|
|
408
|
+
async function openscad(args, cwd) {
|
|
409
|
+
return execFileAsync("xvfb-run", [
|
|
410
|
+
"-a",
|
|
411
|
+
"openscad",
|
|
412
|
+
...args
|
|
413
|
+
], {
|
|
414
|
+
cwd,
|
|
415
|
+
maxBuffer: 1024 * 1024 * 64,
|
|
416
|
+
timeout: 6e5
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Strip a markdown fence if the model wrapped its answer in one. The prompt asks
|
|
421
|
+
* for bare source; a fenced answer is a formatting slip, not a geometry failure,
|
|
422
|
+
* and the geometric gate downstream is unchanged either way.
|
|
423
|
+
*/
|
|
424
|
+
function stripCodeFence(artifact) {
|
|
425
|
+
const trimmed = artifact.trim();
|
|
426
|
+
const fenced = /^```[a-zA-Z]*\n([\s\S]*?)\n?```$/.exec(trimmed);
|
|
427
|
+
return (fenced ? fenced[1] : trimmed).trim();
|
|
428
|
+
}
|
|
429
|
+
/** Compile OpenSCAD source and measure the resulting mesh. Judge-time only. */
|
|
430
|
+
async function compileAndMeasure(src) {
|
|
431
|
+
const dir = await mkdtemp(join(tmpdir(), "mcad-"));
|
|
432
|
+
const scadPath = join(dir, "model.scad");
|
|
433
|
+
const stlPath = join(dir, "model.stl");
|
|
434
|
+
await writeFile(scadPath, `${src}\n`);
|
|
435
|
+
try {
|
|
436
|
+
await openscad([
|
|
437
|
+
"--export-format=asciistl",
|
|
438
|
+
"-o",
|
|
439
|
+
stlPath,
|
|
440
|
+
scadPath
|
|
441
|
+
], dir);
|
|
442
|
+
} catch (err) {
|
|
443
|
+
return {
|
|
444
|
+
ok: false,
|
|
445
|
+
detail: `compile failed: ${(err instanceof Error ? err.message : String(err)).slice(0, 400)}`
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
let stl;
|
|
449
|
+
try {
|
|
450
|
+
stl = await readFile(stlPath, "utf8");
|
|
451
|
+
} catch {
|
|
452
|
+
return {
|
|
453
|
+
ok: false,
|
|
454
|
+
detail: "compiled but produced no STL (empty geometry)"
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
const tris = parseAsciiStl(stl);
|
|
458
|
+
if (tris.length === 0) return {
|
|
459
|
+
ok: false,
|
|
460
|
+
detail: "STL parsed to zero triangles (empty geometry)"
|
|
461
|
+
};
|
|
462
|
+
return {
|
|
463
|
+
ok: true,
|
|
464
|
+
geo: measureMesh(tris),
|
|
465
|
+
tris,
|
|
466
|
+
stlPath
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
function createMcadBenchAdapter() {
|
|
470
|
+
return {
|
|
471
|
+
name: "mcad",
|
|
472
|
+
async preflight() {
|
|
473
|
+
try {
|
|
474
|
+
await execFileAsync("xvfb-run", [
|
|
475
|
+
"-a",
|
|
476
|
+
"openscad",
|
|
477
|
+
"--version"
|
|
478
|
+
], { timeout: 3e4 });
|
|
479
|
+
} catch (err) {
|
|
480
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
481
|
+
throw new Error(`mcad preflight failed: ${msg}\nFix: install OpenSCAD + Xvfb (Debian/Ubuntu: sudo apt-get install -y openscad xvfb). The judge runs \`xvfb-run -a openscad --export-format=asciistl -o out.stl model.scad\` — both must be on PATH.`);
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
async loadTasks(opts = {}) {
|
|
485
|
+
let tasks = MCAD_TASKS;
|
|
486
|
+
if (opts.ids) tasks = tasks.filter((t) => opts.ids?.includes(t.id));
|
|
487
|
+
if (opts.limit != null) tasks = tasks.slice(0, opts.limit);
|
|
488
|
+
return tasks.map((t) => ({
|
|
489
|
+
id: t.id,
|
|
490
|
+
prompt: t.prompt,
|
|
491
|
+
metadata: {
|
|
492
|
+
spec: t.spec,
|
|
493
|
+
source: t.source,
|
|
494
|
+
calibrated: t.calibrated
|
|
495
|
+
}
|
|
496
|
+
}));
|
|
497
|
+
},
|
|
498
|
+
async goldArtifact(task) {
|
|
499
|
+
return MCAD_GOLDS[task.id];
|
|
500
|
+
},
|
|
501
|
+
async judge(task, artifact) {
|
|
502
|
+
const spec = task.metadata?.spec;
|
|
503
|
+
if (!spec) return {
|
|
504
|
+
resolved: false,
|
|
505
|
+
score: 0,
|
|
506
|
+
detail: `task ${task.id} carries no mcad spec`
|
|
507
|
+
};
|
|
508
|
+
const src = stripCodeFence(artifact);
|
|
509
|
+
if (!src) return {
|
|
510
|
+
resolved: false,
|
|
511
|
+
score: 0,
|
|
512
|
+
detail: "empty artifact"
|
|
513
|
+
};
|
|
514
|
+
const built = await compileAndMeasure(src);
|
|
515
|
+
if (!built.ok) return {
|
|
516
|
+
resolved: false,
|
|
517
|
+
score: 0,
|
|
518
|
+
detail: built.detail
|
|
519
|
+
};
|
|
520
|
+
const { geo, tris, stlPath } = built;
|
|
521
|
+
if (!geo.watertight) return {
|
|
522
|
+
resolved: false,
|
|
523
|
+
score: 0,
|
|
524
|
+
detail: JSON.stringify({
|
|
525
|
+
gate: "watertight",
|
|
526
|
+
failed: [{
|
|
527
|
+
name: "watertight",
|
|
528
|
+
measured: `${geo.openEdges} edge(s) not shared by exactly two faces`,
|
|
529
|
+
expected: "0 open edges (closed 2-manifold solid)"
|
|
530
|
+
}],
|
|
531
|
+
geo: {
|
|
532
|
+
triangles: geo.triangles,
|
|
533
|
+
degenerateFaces: geo.degenerateFaces
|
|
534
|
+
},
|
|
535
|
+
stlPath
|
|
536
|
+
})
|
|
537
|
+
};
|
|
538
|
+
const scored = scoreAgainstSpec(geo, tris, spec);
|
|
539
|
+
return {
|
|
540
|
+
resolved: scored.resolved,
|
|
541
|
+
score: scored.score,
|
|
542
|
+
detail: JSON.stringify({
|
|
543
|
+
failed: scored.failed.map((c) => ({
|
|
544
|
+
name: c.name,
|
|
545
|
+
measured: c.measured,
|
|
546
|
+
expected: c.expected
|
|
547
|
+
})),
|
|
548
|
+
checks: Object.fromEntries(scored.checks.map((c) => [c.name, c.ok])),
|
|
549
|
+
geo: {
|
|
550
|
+
triangles: geo.triangles,
|
|
551
|
+
solids: geo.solids,
|
|
552
|
+
volume: +geo.volume.toFixed(2),
|
|
553
|
+
degenerateFaces: geo.degenerateFaces,
|
|
554
|
+
bbox: {
|
|
555
|
+
x: +geo.bbox.size.x.toFixed(3),
|
|
556
|
+
y: +geo.bbox.size.y.toFixed(3),
|
|
557
|
+
z: +geo.bbox.size.z.toFixed(3)
|
|
558
|
+
}
|
|
559
|
+
},
|
|
560
|
+
stlPath
|
|
561
|
+
})
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
//#endregion
|
|
567
|
+
export { compileAndMeasure, createMcadBenchAdapter, measureMesh, parseAsciiStl, pointInSolid, scoreAgainstSpec, stripCodeFence };
|
|
568
|
+
|
|
569
|
+
//# sourceMappingURL=mcad-bench.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcad-bench.js","names":[],"sources":["../../src/benchmarks/mcad-bench.ts"],"sourcesContent":["/**\n * MCAD adapter. Worker artifact = OpenSCAD source for a dimensioned mechanical\n * part; judge = the REAL OpenSCAD kernel (compile + ASCII-STL export) followed by\n * pure-TS mesh measurement against the task's `McadSpec`. No LLM, no self-report:\n * the part either has the stated extents, volume, body count and holes, or it does\n * not.\n *\n * Pipeline, in order, and every stage is a hard fact about the produced mesh:\n * 1. compile : `xvfb-run -a openscad --export-format=asciistl -o out.stl` exits 0\n * 2. watertight : every undirected edge shared by exactly two faces (2-manifold)\n * 3. measure : bbox extents, enclosed volume (divergence theorem), connected\n * -component count (\"solids\"), triangle count\n * 4. probe : ray-parity point-in-solid at the spec's pinned hole/material\n * coordinates — this is what makes a missing or misplaced bore\n * fail, which no aggregate measure can catch\n *\n * Scoring: compile failure or a non-watertight mesh is 0 / unresolved — a mesh that\n * is not a closed solid has no well-defined volume or interior, so every downstream\n * number would be fiction. Otherwise the score is the fraction of ASSERTED spec\n * checks passed (each bbox axis, the volume band, the solids band, the triangle\n * floor, and EACH probe count as one check), and `resolved` requires all of them.\n *\n * Requires `openscad` + `xvfb-run` on PATH at JUDGE time only; `loadTasks` and the\n * geometry engine below are dependency-free and run anywhere.\n *\n * Geometry checks 1-3 are ported from supervisor-lab's `bench/cad-stl-gate.ts`\n * (edge-key manifold parity + signed-tetrahedron volume). Ported, not imported:\n * these packages do not share a dependency edge.\n */\n\nimport { execFile } from 'node:child_process'\nimport { mkdtemp, readFile, writeFile } from 'node:fs/promises'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\nimport { promisify } from 'node:util'\nimport { MCAD_GOLDS } from './mcad-golds'\nimport { MCAD_TASKS, type McadSpec, type McadTask } from './mcad-tasks'\nimport type { BenchmarkAdapter, BenchScore, BenchTask, LoadOptions } from './types'\n\nconst execFileAsync = promisify(execFile)\n\nexport interface Vec3 {\n x: number\n y: number\n z: number\n}\n\n/** One triangle as three vertices, in the STL's stated winding order. */\nexport type Tri = readonly [Vec3, Vec3, Vec3]\n\nexport interface McadGeometry {\n triangles: number\n /** Edges NOT shared by exactly two faces. 0 iff the surface is closed + 2-manifold. */\n openEdges: number\n watertight: boolean\n /** Zero-area faces — bad geometry even when the topology closes. */\n degenerateFaces: number\n /** Enclosed volume (absolute) via the signed-tetrahedron sum. */\n volume: number\n /** Connected components of the triangle-adjacency graph = disconnected bodies. */\n solids: number\n bbox: { min: Vec3; max: Vec3; size: Vec3 }\n}\n\n/** Coincident-vertex quantisation, in model units (mm). */\nconst VERTEX_QUANTUM = 1e-6\nconst AREA_EPS = 1e-9\nconst VOLUME_EPS = 1e-9\n\n// ---------------------------------------------------------------------------\n// Mesh parsing + measurement (ported from supervisor-lab bench/cad-stl-gate.ts)\n// ---------------------------------------------------------------------------\n\n/** Parse an ASCII STL into triangles by reading `vertex x y z` lines in groups of three. */\nexport function parseAsciiStl(stl: string): Tri[] {\n const verts: Vec3[] = []\n for (const line of stl.split('\\n')) {\n const m = line.trim().match(/^vertex\\s+(\\S+)\\s+(\\S+)\\s+(\\S+)/i)\n if (!m) continue\n const x = Number(m[1])\n const y = Number(m[2])\n const z = Number(m[3])\n if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) continue\n verts.push({ x, y, z })\n }\n const tris: Tri[] = []\n for (let i = 0; i + 2 < verts.length; i += 3) tris.push([verts[i]!, verts[i + 1]!, verts[i + 2]!])\n return tris\n}\n\n/** Quantise a vertex so shared edges match despite float authoring noise. */\nfunction vertexKey(v: Vec3): string {\n const r = (n: number) => (Math.round(n / VERTEX_QUANTUM) * VERTEX_QUANTUM).toFixed(6)\n return `${r(v.x)},${r(v.y)},${r(v.z)}`\n}\n\nfunction sub(a: Vec3, b: Vec3): Vec3 {\n return { x: a.x - b.x, y: a.y - b.y, z: a.z - b.z }\n}\nfunction cross(a: Vec3, b: Vec3): Vec3 {\n return { x: a.y * b.z - a.z * b.y, y: a.z * b.x - a.x * b.z, z: a.x * b.y - a.y * b.x }\n}\nfunction dot(a: Vec3, b: Vec3): number {\n return a.x * b.x + a.y * b.y + a.z * b.z\n}\nfunction norm(a: Vec3): number {\n return Math.sqrt(dot(a, a))\n}\nfunction normalize(a: Vec3): Vec3 {\n const n = norm(a)\n return { x: a.x / n, y: a.y / n, z: a.z / n }\n}\n\n/** Disjoint-set over triangle indices — the \"how many separate bodies\" primitive. */\nfunction findRoot(parent: Int32Array, i: number): number {\n let r = i\n while (parent[r] !== r) r = parent[r]!\n let c = i\n while (parent[c] !== c) {\n const next = parent[c]!\n parent[c] = r\n c = next\n }\n return r\n}\n\n/**\n * Measure the closed-solid properties of a triangle soup. Pure: no I/O, no deps.\n *\n * `solids` is the number of connected components of the graph whose nodes are\n * triangles and whose edges join two triangles that share a quantised mesh edge.\n * On a watertight mesh that count IS the number of disconnected bodies (an\n * enclosed internal cavity is its own shell and counts, which is the behaviour\n * the specs want — a \"hollow\" body that is really two nested shells is not one\n * fused solid).\n */\nexport function measureMesh(tris: Tri[]): McadGeometry {\n if (tris.length === 0) {\n const zero = { x: 0, y: 0, z: 0 }\n return {\n triangles: 0,\n openEdges: 0,\n watertight: false,\n degenerateFaces: 0,\n volume: 0,\n solids: 0,\n bbox: { min: zero, max: zero, size: zero },\n }\n }\n\n const min: Vec3 = { x: Number.POSITIVE_INFINITY, y: Number.POSITIVE_INFINITY, z: Number.POSITIVE_INFINITY }\n const max: Vec3 = { x: Number.NEGATIVE_INFINITY, y: Number.NEGATIVE_INFINITY, z: Number.NEGATIVE_INFINITY }\n const edgeFaces = new Map<string, number[]>()\n const parent = new Int32Array(tris.length)\n for (let i = 0; i < tris.length; i++) parent[i] = i\n let degenerateFaces = 0\n let volume6 = 0\n\n for (let i = 0; i < tris.length; i++) {\n const [a, b, c] = tris[i]!\n for (const v of [a, b, c]) {\n if (v.x < min.x) min.x = v.x\n if (v.y < min.y) min.y = v.y\n if (v.z < min.z) min.z = v.z\n if (v.x > max.x) max.x = v.x\n if (v.y > max.y) max.y = v.y\n if (v.z > max.z) max.z = v.z\n }\n if (norm(cross(sub(b, a), sub(c, a))) < AREA_EPS) degenerateFaces += 1\n volume6 += dot(a, cross(b, c))\n\n const ka = vertexKey(a)\n const kb = vertexKey(b)\n const kc = vertexKey(c)\n for (const [p, q] of [\n [ka, kb],\n [kb, kc],\n [kc, ka],\n ] as const) {\n const key = p < q ? `${p}|${q}` : `${q}|${p}`\n const seen = edgeFaces.get(key)\n if (seen) seen.push(i)\n else edgeFaces.set(key, [i])\n }\n }\n\n let openEdges = 0\n for (const faces of edgeFaces.values()) {\n if (faces.length !== 2) openEdges += 1\n // Union every face pair on this edge (a >2-face edge still connects bodies).\n const first = findRoot(parent, faces[0]!)\n for (let k = 1; k < faces.length; k++) {\n const other = findRoot(parent, faces[k]!)\n if (other !== first) parent[other] = first\n }\n }\n\n const roots = new Set<number>()\n for (let i = 0; i < tris.length; i++) roots.add(findRoot(parent, i))\n\n return {\n triangles: tris.length,\n openEdges,\n watertight: openEdges === 0,\n degenerateFaces,\n volume: Math.abs(volume6) / 6,\n solids: roots.size,\n bbox: {\n min: { ...min },\n max: { ...max },\n size: { x: max.x - min.x, y: max.y - min.y, z: max.z - min.z },\n },\n }\n}\n\n// ---------------------------------------------------------------------------\n// Point-in-solid by ray parity\n// ---------------------------------------------------------------------------\n\n/**\n * Three fixed, mutually linearly-independent directions built from irrational\n * constants (sqrt(3)-1, sqrt(2)-1, 1/sqrt(5), 1/sqrt(3) and the plastic-number\n * pair 0.7548777 / 0.5698403). Their 3x3 determinant is ~1.77, so no two are\n * near-parallel and no plane contains all three.\n */\nconst RAY_DIRECTIONS: readonly Vec3[] = [\n normalize({ x: 1, y: 0.7548777, z: 0.5698403 }),\n normalize({ x: -0.7320508, y: 1, z: 0.236068 }),\n normalize({ x: 0.4472136, y: -0.5773503, z: 1 }),\n]\n\n/** Barycentric / parametric tolerances for calling a crossing \"on the boundary\". */\nconst BARY_EPS = 1e-9\nconst DET_EPS = 1e-12\nconst T_EPS = 1e-9\nconst MAX_JITTERS = 8\n\n/** Deterministic 32-bit PRNG (mulberry32) — seeded, so probes never use Math.random. */\nfunction mulberry32(seed: number): () => number {\n let a = seed >>> 0\n return () => {\n a = (a + 0x6d2b79f5) >>> 0\n let t = a\n t = Math.imul(t ^ (t >>> 15), t | 1)\n t ^= t + Math.imul(t ^ (t >>> 7), t | 61)\n return ((t ^ (t >>> 14)) >>> 0) / 4294967296\n }\n}\n\n/** Stable integer hash of a probe point so its jitter sequence is reproducible. */\nfunction seedForPoint(p: Vec3, rayIndex: number): number {\n let h = 2166136261 ^ rayIndex\n for (const n of [p.x, p.y, p.z]) {\n const bits = Math.round(n * 1e6) | 0\n h = Math.imul(h ^ (bits & 0xffff), 16777619)\n h = Math.imul(h ^ ((bits >>> 16) & 0xffff), 16777619)\n }\n return h >>> 0\n}\n\ntype CastResult = { crossings: number } | { degenerate: true }\n\n/**\n * Moller-Trumbore crossing count along one ray. Returns `degenerate` when the ray\n * touches a triangle edge/vertex, lies in a triangle's plane, or starts on a face\n * — the three cases where a parity count is not trustworthy.\n */\nfunction castRay(tris: Tri[], origin: Vec3, dir: Vec3): CastResult {\n let crossings = 0\n for (const [v0, v1, v2] of tris) {\n const e1 = sub(v1, v0)\n const e2 = sub(v2, v0)\n const h = cross(dir, e2)\n const det = dot(e1, h)\n const scale = norm(e1) * norm(e2)\n if (Math.abs(det) <= DET_EPS * Math.max(scale, 1)) {\n // Ray parallel to (or inside) this triangle's plane. Only a problem when the\n // ray could actually meet the triangle; distinguishing that costs more than a\n // re-cast, so treat it as degenerate whenever the origin is near the plane.\n const n = cross(e1, e2)\n const nl = norm(n)\n if (nl > AREA_EPS && Math.abs(dot(sub(origin, v0), n)) / nl <= T_EPS) return { degenerate: true }\n continue\n }\n const f = 1 / det\n const s = sub(origin, v0)\n const u = f * dot(s, h)\n const q = cross(s, e1)\n const v = f * dot(dir, q)\n const w = 1 - u - v\n if (u < -BARY_EPS || v < -BARY_EPS || w < -BARY_EPS) continue\n const t = f * dot(e2, q)\n if (t < -T_EPS) continue\n // Inside the triangle (or within EPS of its border). Anything within EPS of a\n // border, or of the origin plane, makes the parity ambiguous.\n if (Math.abs(t) <= T_EPS) return { degenerate: true }\n if (u <= BARY_EPS || v <= BARY_EPS || w <= BARY_EPS) return { degenerate: true }\n crossings += 1\n }\n return { crossings }\n}\n\n/**\n * Point-in-solid membership by ray parity, made robust three ways.\n *\n * ROBUSTNESS ARGUMENT. A parity test is exact except on a measure-zero set: rays\n * that graze a triangle edge or vertex (the crossing is counted twice or zero\n * times), rays coplanar with a face, and origins lying on the surface. Those cases\n * are not merely rare here — they are SYSTEMATIC, because OpenSCAD emits\n * axis-aligned meshes whose vertices land on the same round millimetre lattice the\n * spec's probe coordinates come from, so an axis-aligned ray from a probe point\n * hits shared edges constantly. Three defences, in order:\n * 1. DIRECTIONS. The three fixed directions are irrational combinations, so a ray\n * from a lattice point cannot stay in an axis-aligned face plane and cannot\n * run along a lattice edge.\n * 2. DETECTION + DETERMINISTIC RE-JITTER. Grazing is DETECTED (a barycentric\n * coordinate within BARY_EPS of 0, |det| below DET_EPS with the origin in the\n * plane, or |t| within T_EPS) rather than hoped away. A detected ray is re-cast\n * with a small direction perturbation drawn from a mulberry32 PRNG seeded by\n * the probe coordinates and ray index, so the whole judge stays deterministic:\n * the same mesh and the same point always take the same sequence of re-casts.\n * 3. MAJORITY VOTE. The verdict is the majority of three independent directions,\n * so even an undetected miscount on one ray cannot flip the answer.\n * A point sitting exactly ON the surface has no correct answer; every direction\n * degenerates there and the vote falls back to whatever the jittered casts say.\n * The specs therefore place probes with >=1 mm clearance from any surface.\n */\nexport function pointInSolid(tris: Tri[], point: Vec3): boolean {\n let inside = 0\n let votes = 0\n for (let i = 0; i < RAY_DIRECTIONS.length; i++) {\n const base = RAY_DIRECTIONS[i]!\n const rand = mulberry32(seedForPoint(point, i))\n let dir = base\n for (let attempt = 0; attempt <= MAX_JITTERS; attempt++) {\n const r = castRay(tris, point, dir)\n if (!('degenerate' in r)) {\n votes += 1\n if (r.crossings % 2 === 1) inside += 1\n break\n }\n dir = normalize({\n x: base.x + (rand() - 0.5) * 1e-3,\n y: base.y + (rand() - 0.5) * 1e-3,\n z: base.z + (rand() - 0.5) * 1e-3,\n })\n }\n }\n // Every direction degenerating means the point is on the surface; call it outside\n // (fail-closed: a probe that must be inside the material will report a failure).\n if (votes === 0) return false\n return inside * 2 > votes\n}\n\n// ---------------------------------------------------------------------------\n// Spec scoring\n// ---------------------------------------------------------------------------\n\nexport interface McadCheck {\n name: string\n ok: boolean\n measured: string\n expected: string\n}\n\nexport interface McadScoring {\n checks: McadCheck[]\n failed: McadCheck[]\n score: number\n resolved: boolean\n}\n\nfunction inBand(v: number, [lo, hi]: [number, number]): boolean {\n return v >= lo && v <= hi\n}\n\nfunction fmt(n: number): string {\n return String(Math.round(n * 1000) / 1000)\n}\n\nfunction pt(p: readonly [number, number, number]): string {\n return `(${p[0]}, ${p[1]}, ${p[2]})`\n}\n\n/** Score a measured mesh against the task's spec — one named check per assertion. */\nexport function scoreAgainstSpec(geo: McadGeometry, tris: Tri[], spec: McadSpec): McadScoring {\n const checks: McadCheck[] = []\n const add = (name: string, ok: boolean, measured: string, expected: string) =>\n checks.push({ name, ok, measured, expected })\n\n for (const axis of ['x', 'y', 'z'] as const) {\n const band = spec.bbox?.[axis]\n if (!band) continue\n const got = geo.bbox.size[axis]\n add(`bbox${axis.toUpperCase()}`, inBand(got, band), fmt(got), `[${band[0]}, ${band[1]}]`)\n }\n if (spec.volume) add('volume', inBand(geo.volume, spec.volume), fmt(geo.volume), `[${spec.volume[0]}, ${spec.volume[1]}]`)\n if (spec.solids) add('solids', inBand(geo.solids, spec.solids), String(geo.solids), `[${spec.solids[0]}, ${spec.solids[1]}]`)\n if (spec.minTriangles != null)\n add('minTriangles', geo.triangles >= spec.minTriangles, String(geo.triangles), `>= ${spec.minTriangles}`)\n\n for (const [i, p] of (spec.probesInsideSolid ?? []).entries()) {\n const got = pointInSolid(tris, { x: p[0], y: p[1], z: p[2] })\n add(`probeInside[${i}] ${pt(p)}`, got, got ? 'inside' : 'outside', 'inside')\n }\n for (const [i, p] of (spec.probesOutsideSolid ?? []).entries()) {\n const got = pointInSolid(tris, { x: p[0], y: p[1], z: p[2] })\n add(`probeOutside[${i}] ${pt(p)}`, !got, got ? 'inside' : 'outside', 'outside')\n }\n\n const failed = checks.filter((c) => !c.ok)\n const score = checks.length ? (checks.length - failed.length) / checks.length : 0\n return { checks, failed, score, resolved: checks.length > 0 && failed.length === 0 }\n}\n\n// ---------------------------------------------------------------------------\n// Adapter\n// ---------------------------------------------------------------------------\n\n/** Task metadata carried onto every `BenchTask`, so the judge needs no lookup table. */\nexport interface McadTaskMeta extends Record<string, unknown> {\n spec: McadSpec\n source: string\n calibrated: boolean\n}\n\n/** Run openscad under xvfb (it wants a GL context even headless). */\nasync function openscad(args: string[], cwd: string): Promise<{ stdout: string; stderr: string }> {\n return execFileAsync('xvfb-run', ['-a', 'openscad', ...args], {\n cwd,\n maxBuffer: 1024 * 1024 * 64,\n timeout: 600_000,\n })\n}\n\n/**\n * Strip a markdown fence if the model wrapped its answer in one. The prompt asks\n * for bare source; a fenced answer is a formatting slip, not a geometry failure,\n * and the geometric gate downstream is unchanged either way.\n */\nexport function stripCodeFence(artifact: string): string {\n const trimmed = artifact.trim()\n const fenced = /^```[a-zA-Z]*\\n([\\s\\S]*?)\\n?```$/.exec(trimmed)\n return (fenced ? fenced[1]! : trimmed).trim()\n}\n\n/** Compile OpenSCAD source and measure the resulting mesh. Judge-time only. */\nexport async function compileAndMeasure(\n src: string,\n): Promise<{ ok: true; geo: McadGeometry; tris: Tri[]; stlPath: string } | { ok: false; detail: string }> {\n const dir = await mkdtemp(join(tmpdir(), 'mcad-'))\n const scadPath = join(dir, 'model.scad')\n const stlPath = join(dir, 'model.stl')\n await writeFile(scadPath, `${src}\\n`)\n\n try {\n await openscad(['--export-format=asciistl', '-o', stlPath, scadPath], dir)\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err)\n return { ok: false, detail: `compile failed: ${msg.slice(0, 400)}` }\n }\n\n let stl: string\n try {\n stl = await readFile(stlPath, 'utf8')\n } catch {\n return { ok: false, detail: 'compiled but produced no STL (empty geometry)' }\n }\n const tris = parseAsciiStl(stl)\n if (tris.length === 0) return { ok: false, detail: 'STL parsed to zero triangles (empty geometry)' }\n return { ok: true, geo: measureMesh(tris), tris, stlPath }\n}\n\nexport function createMcadBenchAdapter(): BenchmarkAdapter {\n return {\n name: 'mcad',\n\n async preflight() {\n try {\n await execFileAsync('xvfb-run', ['-a', 'openscad', '--version'], { timeout: 30_000 })\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err)\n throw new Error(\n `mcad preflight failed: ${msg}\\n` +\n 'Fix: install OpenSCAD + Xvfb (Debian/Ubuntu: sudo apt-get install -y openscad xvfb). ' +\n 'The judge runs `xvfb-run -a openscad --export-format=asciistl -o out.stl model.scad` — both must be on PATH.',\n )\n }\n },\n\n async loadTasks(opts: LoadOptions = {}) {\n let tasks: McadTask[] = MCAD_TASKS\n if (opts.ids) tasks = tasks.filter((t) => opts.ids?.includes(t.id))\n if (opts.limit != null) tasks = tasks.slice(0, opts.limit)\n return tasks.map(\n (t): BenchTask => ({\n id: t.id,\n prompt: t.prompt,\n metadata: { spec: t.spec, source: t.source, calibrated: t.calibrated } satisfies McadTaskMeta,\n }),\n )\n },\n\n async goldArtifact(task: BenchTask) {\n return MCAD_GOLDS[task.id]\n },\n\n async judge(task: BenchTask, artifact: string): Promise<BenchScore> {\n const spec = (task.metadata as McadTaskMeta | undefined)?.spec\n if (!spec) return { resolved: false, score: 0, detail: `task ${task.id} carries no mcad spec` }\n\n const src = stripCodeFence(artifact)\n if (!src) return { resolved: false, score: 0, detail: 'empty artifact' }\n\n const built = await compileAndMeasure(src)\n if (!built.ok) return { resolved: false, score: 0, detail: built.detail }\n\n const { geo, tris, stlPath } = built\n if (!geo.watertight) {\n return {\n resolved: false,\n score: 0,\n detail: JSON.stringify({\n gate: 'watertight',\n failed: [\n {\n name: 'watertight',\n measured: `${geo.openEdges} edge(s) not shared by exactly two faces`,\n expected: '0 open edges (closed 2-manifold solid)',\n },\n ],\n geo: { triangles: geo.triangles, degenerateFaces: geo.degenerateFaces },\n stlPath,\n }),\n }\n }\n\n const scored = scoreAgainstSpec(geo, tris, spec)\n return {\n resolved: scored.resolved,\n score: scored.score,\n detail: JSON.stringify({\n failed: scored.failed.map((c) => ({ name: c.name, measured: c.measured, expected: c.expected })),\n checks: Object.fromEntries(scored.checks.map((c) => [c.name, c.ok])),\n geo: {\n triangles: geo.triangles,\n solids: geo.solids,\n volume: +geo.volume.toFixed(2),\n degenerateFaces: geo.degenerateFaces,\n bbox: {\n x: +geo.bbox.size.x.toFixed(3),\n y: +geo.bbox.size.y.toFixed(3),\n z: +geo.bbox.size.z.toFixed(3),\n },\n },\n stlPath,\n }),\n }\n },\n }\n}\n"],"mappings":"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"}
|