@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
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { readFile } from 'node:fs/promises'
|
|
15
15
|
import type { BenchScore, BenchTask, BenchmarkAdapter, LoadOptions } from './types'
|
|
16
16
|
import { renderBpy } from '../worker-blender'
|
|
17
|
+
import { runBenchRouterTurn } from '../router-turn'
|
|
17
18
|
|
|
18
19
|
interface CadBenchMeta {
|
|
19
20
|
name: string
|
|
@@ -48,14 +49,26 @@ async function judgeCriteria(
|
|
|
48
49
|
`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
50
|
const content: unknown[] = [{ type: 'text', text }]
|
|
50
51
|
for (const url of renders) content.push({ type: 'image_url', image_url: { url } })
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
52
|
+
const turn = await runBenchRouterTurn(
|
|
53
|
+
{
|
|
54
|
+
routerBaseUrl: base,
|
|
55
|
+
routerKey: key,
|
|
56
|
+
profile: {
|
|
57
|
+
name: 'cadbench-vision-judge',
|
|
58
|
+
harness: 'cli-base',
|
|
59
|
+
model: {
|
|
60
|
+
provider: 'tangle-router',
|
|
61
|
+
default: model,
|
|
62
|
+
metadata: {
|
|
63
|
+
temperature: 0,
|
|
64
|
+
maxTokens: Number(process.env.JUDGE_MAX_TOKENS ?? 1500),
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{ messages: [{ role: 'user', content }] },
|
|
70
|
+
)
|
|
71
|
+
const raw = turn.finalText
|
|
59
72
|
const m = /\[\s*(?:true|false)[\s\S]*?\]/i.exec(raw)
|
|
60
73
|
if (!m) return { passed: criteria.map(() => false), note: `judge returned no parseable verdict: ${raw.slice(0, 80)}` }
|
|
61
74
|
let arr: unknown
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { readFile, stat } from 'node:fs/promises'
|
|
11
11
|
import { join } from 'node:path'
|
|
12
|
+
import { runBenchRouterTurn } from '../router-turn'
|
|
12
13
|
import { benchRoot } from './_harness'
|
|
13
14
|
import type { BenchmarkAdapter, BenchScore, BenchTask, LoadOptions } from './types'
|
|
14
15
|
|
|
@@ -180,22 +181,25 @@ function parseJudgeScore(content: string): { score: number; raw: unknown } {
|
|
|
180
181
|
async function runOfficialJudge(meta: FinResearchMeta, response: string): Promise<BenchScore> {
|
|
181
182
|
if (!meta.judgeSystemPrompt) throw new Error(`FinResearchBench task ${meta.id} missing judge_system_prompt`)
|
|
182
183
|
const router = routerConfig()
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
184
|
+
const turn = await runBenchRouterTurn(
|
|
185
|
+
{
|
|
186
|
+
routerBaseUrl: router.baseUrl,
|
|
187
|
+
routerKey: router.key,
|
|
188
|
+
profile: {
|
|
189
|
+
name: 'finresearchbench-judge',
|
|
190
|
+
harness: 'cli-base',
|
|
191
|
+
model: {
|
|
192
|
+
provider: 'tangle-router',
|
|
193
|
+
default: router.model,
|
|
194
|
+
metadata: { temperature: 0 },
|
|
195
|
+
},
|
|
196
|
+
prompt: { systemPrompt: meta.judgeSystemPrompt },
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
fillTemplate(meta, response),
|
|
200
|
+
)
|
|
201
|
+
const content = turn.finalText
|
|
202
|
+
if (!content) throw new Error('FinResearchBench judge returned no message content')
|
|
199
203
|
const { score, raw } = parseJudgeScore(content)
|
|
200
204
|
return {
|
|
201
205
|
resolved: score >= Number(process.env.FINRESEARCHBENCH_PASS_THRESHOLD ?? 0.8),
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
import { readFile } from 'node:fs/promises'
|
|
33
33
|
import { join } from 'node:path'
|
|
34
34
|
import { fileURLToPath } from 'node:url'
|
|
35
|
+
import { runBenchRouterTurn } from '../router-turn'
|
|
35
36
|
import type { BenchmarkAdapter, BenchScore, BenchTask, LoadOptions } from './types'
|
|
36
37
|
|
|
37
38
|
const BENCH_ROOT = fileURLToPath(new URL('../..', import.meta.url))
|
|
@@ -206,26 +207,25 @@ function parseJudgeOutput(content: string): { resolved: boolean; score: number;
|
|
|
206
207
|
|
|
207
208
|
/** Run the record's own judge via the router. Fail loud on transport/parse errors. */
|
|
208
209
|
async function runRecordJudge(meta: FinSearchMeta, response: string, router: JudgeRouter): Promise<BenchScore> {
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
210
|
+
const turn = await runBenchRouterTurn(
|
|
211
|
+
{
|
|
212
|
+
routerBaseUrl: router.baseUrl,
|
|
213
|
+
routerKey: router.key,
|
|
214
|
+
profile: {
|
|
215
|
+
name: 'finsearchcomp-judge',
|
|
216
|
+
harness: 'cli-base',
|
|
217
|
+
model: {
|
|
218
|
+
provider: 'tangle-router',
|
|
219
|
+
default: router.model,
|
|
220
|
+
metadata: { temperature: 0 },
|
|
221
|
+
},
|
|
222
|
+
prompt: { systemPrompt: meta.judgeSystemPrompt },
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
fillJudgePrompt(meta, response),
|
|
226
|
+
)
|
|
227
|
+
const content = turn.finalText
|
|
228
|
+
if (!content) throw new Error('FinSearchComp judge returned no message content')
|
|
229
229
|
const { resolved, score, raw } = parseJudgeOutput(content)
|
|
230
230
|
return {
|
|
231
231
|
resolved,
|
package/src/benchmarks/frames.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { readFile } from 'node:fs/promises'
|
|
|
25
25
|
import { join } from 'node:path'
|
|
26
26
|
import { fileURLToPath } from 'node:url'
|
|
27
27
|
import { promisify } from 'node:util'
|
|
28
|
+
import { runBenchRouterTurn } from '../router-turn'
|
|
28
29
|
import type { BenchmarkAdapter, BenchScore, BenchTask, LoadOptions } from './types'
|
|
29
30
|
|
|
30
31
|
const execFileAsync = promisify(execFile)
|
|
@@ -322,24 +323,24 @@ async function tier2Judge(
|
|
|
322
323
|
candidate: string,
|
|
323
324
|
router: JudgeRouter,
|
|
324
325
|
): Promise<boolean> {
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
326
|
+
const turn = await runBenchRouterTurn(
|
|
327
|
+
{
|
|
328
|
+
routerBaseUrl: router.baseUrl,
|
|
329
|
+
routerKey: router.key,
|
|
330
|
+
profile: {
|
|
331
|
+
name: 'frames-equivalence-judge',
|
|
332
|
+
harness: 'cli-base',
|
|
333
|
+
model: {
|
|
334
|
+
provider: 'tangle-router',
|
|
335
|
+
default: router.model,
|
|
336
|
+
metadata: { temperature: 0, seed: 0 },
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
JUDGE_PROMPT(question, gold, candidate),
|
|
341
|
+
)
|
|
342
|
+
const content = turn.finalText
|
|
343
|
+
if (!content) throw new Error('FRAMES Tier-2 judge returned no message content')
|
|
343
344
|
const fenced = content.match(/```(?:json)?\s*([\s\S]*?)```/)
|
|
344
345
|
const raw = (fenced ? fenced[1] : content)?.trim() ?? ''
|
|
345
346
|
let parsed: { verdict?: unknown }
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCAD adapter tests, in two halves.
|
|
3
|
+
*
|
|
4
|
+
* PURE: hand-written ASCII STL fixtures exercise the geometry engine with no
|
|
5
|
+
* OpenSCAD anywhere — a unit cube, an open box, two disjoint cubes, and a cube
|
|
6
|
+
* with a square through-channel. The fixtures are deliberately lattice-aligned,
|
|
7
|
+
* because that is the case a naive axis-aligned ray-parity test gets wrong.
|
|
8
|
+
*
|
|
9
|
+
* LIVE (gated on `openscad` + `xvfb-run`): the real judge, in BOTH directions.
|
|
10
|
+
* Accept — every `calibrated: true` task scores 1.0 on its own gold. Reject — the
|
|
11
|
+
* task-01 gold with its four holes deleted must fail the bore probes, and the same
|
|
12
|
+
* gold scaled to 90 x 54 x 18 must fail all three bbox axes. A judge that has only
|
|
13
|
+
* been shown passing inputs is not calibrated, so the reject direction is not
|
|
14
|
+
* optional here.
|
|
15
|
+
*
|
|
16
|
+
* npx vitest run src/benchmarks/mcad-bench.test.mts
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { execFileSync } from 'node:child_process'
|
|
20
|
+
import { describe, expect, it } from 'vitest'
|
|
21
|
+
import {
|
|
22
|
+
createMcadBenchAdapter,
|
|
23
|
+
measureMesh,
|
|
24
|
+
parseAsciiStl,
|
|
25
|
+
pointInSolid,
|
|
26
|
+
scoreAgainstSpec,
|
|
27
|
+
stripCodeFence,
|
|
28
|
+
} from './mcad-bench'
|
|
29
|
+
import { MCAD_GOLDS } from './mcad-golds'
|
|
30
|
+
import { MCAD_TASKS } from './mcad-tasks'
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// ASCII STL fixture builders (outward-facing winding, 2-manifold by construction)
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
type P = readonly [number, number, number]
|
|
37
|
+
type T = readonly [P, P, P]
|
|
38
|
+
|
|
39
|
+
/** A quad a-b-c-d as two triangles sharing the a-c diagonal. */
|
|
40
|
+
function quad(a: P, b: P, c: P, d: P): T[] {
|
|
41
|
+
return [
|
|
42
|
+
[a, b, c],
|
|
43
|
+
[a, c, d],
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function boxTris(min: P, max: P): T[] {
|
|
48
|
+
const [x0, y0, z0] = min
|
|
49
|
+
const [x1, y1, z1] = max
|
|
50
|
+
const p000: P = [x0, y0, z0]
|
|
51
|
+
const p100: P = [x1, y0, z0]
|
|
52
|
+
const p110: P = [x1, y1, z0]
|
|
53
|
+
const p010: P = [x0, y1, z0]
|
|
54
|
+
const p001: P = [x0, y0, z1]
|
|
55
|
+
const p101: P = [x1, y0, z1]
|
|
56
|
+
const p111: P = [x1, y1, z1]
|
|
57
|
+
const p011: P = [x0, y1, z1]
|
|
58
|
+
return [
|
|
59
|
+
...quad(p000, p010, p110, p100), // bottom, -Z
|
|
60
|
+
...quad(p001, p101, p111, p011), // top, +Z
|
|
61
|
+
...quad(p000, p100, p101, p001), // front, -Y
|
|
62
|
+
...quad(p010, p011, p111, p110), // back, +Y
|
|
63
|
+
...quad(p000, p001, p011, p010), // left, -X
|
|
64
|
+
...quad(p100, p110, p111, p101), // right, +X
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function toStl(tris: T[]): string {
|
|
69
|
+
const facets = tris
|
|
70
|
+
.map(([a, b, c]) =>
|
|
71
|
+
[
|
|
72
|
+
' facet normal 0 0 0',
|
|
73
|
+
' outer loop',
|
|
74
|
+
` vertex ${a.join(' ')}`,
|
|
75
|
+
` vertex ${b.join(' ')}`,
|
|
76
|
+
` vertex ${c.join(' ')}`,
|
|
77
|
+
' endloop',
|
|
78
|
+
' endfacet',
|
|
79
|
+
].join('\n'),
|
|
80
|
+
)
|
|
81
|
+
.join('\n')
|
|
82
|
+
return `solid fixture\n${facets}\nendsolid fixture\n`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const UNIT_CUBE = toStl(boxTris([0, 0, 0], [1, 1, 1]))
|
|
86
|
+
/** The unit cube with its two +Z triangles deleted — four edges left unpaired. */
|
|
87
|
+
const OPEN_BOX = toStl(boxTris([0, 0, 0], [1, 1, 1]).filter((_, i) => i !== 2 && i !== 3))
|
|
88
|
+
const TWO_CUBES = toStl([...boxTris([0, 0, 0], [1, 1, 1]), ...boxTris([3, 0, 0], [4, 1, 1])])
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* A 6 x 6 x 6 block with a 2 x 2 square channel running through it in Z. Both end
|
|
92
|
+
* faces are annuli split into four trapezoids whose corner diagonals are shared, so
|
|
93
|
+
* the mesh stays 2-manifold. Enclosed volume: 216 - 24 = 192.
|
|
94
|
+
*/
|
|
95
|
+
function channelCubeTris(): T[] {
|
|
96
|
+
const o = 6
|
|
97
|
+
const a = 2
|
|
98
|
+
const b = 4
|
|
99
|
+
const tris: T[] = []
|
|
100
|
+
// outer side walls
|
|
101
|
+
const outer: Array<[P, P]> = [
|
|
102
|
+
[[0, 0, 0], [o, 0, 0]],
|
|
103
|
+
[[o, 0, 0], [o, o, 0]],
|
|
104
|
+
[[o, o, 0], [0, o, 0]],
|
|
105
|
+
[[0, o, 0], [0, 0, 0]],
|
|
106
|
+
]
|
|
107
|
+
for (const [s, e] of outer) {
|
|
108
|
+
tris.push(...quad(s, e, [e[0], e[1], o], [s[0], s[1], o]))
|
|
109
|
+
}
|
|
110
|
+
// end-face annuli: four trapezoids per face, outer edge -> inner edge
|
|
111
|
+
const strips: Array<[P, P, P, P]> = [
|
|
112
|
+
[[0, 0, 0], [o, 0, 0], [b, a, 0], [a, a, 0]],
|
|
113
|
+
[[o, 0, 0], [o, o, 0], [b, b, 0], [b, a, 0]],
|
|
114
|
+
[[o, o, 0], [0, o, 0], [a, b, 0], [b, b, 0]],
|
|
115
|
+
[[0, o, 0], [0, 0, 0], [a, a, 0], [a, b, 0]],
|
|
116
|
+
]
|
|
117
|
+
for (const [p, q, r, s] of strips) {
|
|
118
|
+
const up = (v: P): P => [v[0], v[1], o]
|
|
119
|
+
tris.push(...quad(up(p), up(q), up(r), up(s))) // +Z face
|
|
120
|
+
tris.push(...quad(s, r, q, p)) // -Z face, reversed winding
|
|
121
|
+
}
|
|
122
|
+
// channel walls, outward normals point INTO the void
|
|
123
|
+
tris.push(...quad([a, a, 0], [a, b, 0], [a, b, o], [a, a, o])) // +X
|
|
124
|
+
tris.push(...quad([b, a, 0], [b, a, o], [b, b, o], [b, b, 0])) // -X
|
|
125
|
+
tris.push(...quad([a, a, 0], [a, a, o], [b, a, o], [b, a, 0])) // +Y
|
|
126
|
+
tris.push(...quad([a, b, 0], [b, b, 0], [b, b, o], [a, b, o])) // -Y
|
|
127
|
+
return tris
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const CHANNEL_CUBE = toStl(channelCubeTris())
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// Pure geometry tests
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
describe('mcad geometry engine (pure ASCII STL fixtures)', () => {
|
|
137
|
+
it('measures a unit cube: watertight, one solid, volume 1', () => {
|
|
138
|
+
const g = measureMesh(parseAsciiStl(UNIT_CUBE))
|
|
139
|
+
expect(g.triangles).toBe(12)
|
|
140
|
+
expect(g.watertight).toBe(true)
|
|
141
|
+
expect(g.openEdges).toBe(0)
|
|
142
|
+
expect(g.degenerateFaces).toBe(0)
|
|
143
|
+
expect(g.solids).toBe(1)
|
|
144
|
+
expect(g.volume).toBeCloseTo(1, 9)
|
|
145
|
+
expect(g.bbox.size).toEqual({ x: 1, y: 1, z: 1 })
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('rejects an open box: four unpaired edges, not watertight', () => {
|
|
149
|
+
const g = measureMesh(parseAsciiStl(OPEN_BOX))
|
|
150
|
+
expect(g.triangles).toBe(10)
|
|
151
|
+
expect(g.watertight).toBe(false)
|
|
152
|
+
expect(g.openEdges).toBe(4)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('counts two disjoint cubes as two solids', () => {
|
|
156
|
+
const g = measureMesh(parseAsciiStl(TWO_CUBES))
|
|
157
|
+
expect(g.watertight).toBe(true)
|
|
158
|
+
expect(g.solids).toBe(2)
|
|
159
|
+
expect(g.volume).toBeCloseTo(2, 9)
|
|
160
|
+
expect(g.bbox.size.x).toBe(4)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('measures the through-channel cube: one solid, volume 192', () => {
|
|
164
|
+
const g = measureMesh(parseAsciiStl(CHANNEL_CUBE))
|
|
165
|
+
expect(g.watertight).toBe(true)
|
|
166
|
+
expect(g.openEdges).toBe(0)
|
|
167
|
+
expect(g.solids).toBe(1)
|
|
168
|
+
expect(g.degenerateFaces).toBe(0)
|
|
169
|
+
expect(g.volume).toBeCloseTo(192, 6)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('returns an empty measurement for a triangle-free STL', () => {
|
|
173
|
+
const g = measureMesh(parseAsciiStl('solid empty\nendsolid empty\n'))
|
|
174
|
+
expect(g.triangles).toBe(0)
|
|
175
|
+
expect(g.watertight).toBe(false)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('places probes correctly in the through-channel cube', () => {
|
|
179
|
+
const tris = parseAsciiStl(CHANNEL_CUBE)
|
|
180
|
+
// channel centre is void; the surrounding material is solid
|
|
181
|
+
expect(pointInSolid(tris, { x: 3, y: 3, z: 3 })).toBe(false)
|
|
182
|
+
expect(pointInSolid(tris, { x: 1, y: 1, z: 3 })).toBe(true)
|
|
183
|
+
expect(pointInSolid(tris, { x: 5, y: 5, z: 3 })).toBe(true)
|
|
184
|
+
expect(pointInSolid(tris, { x: 1, y: 5, z: 3 })).toBe(true)
|
|
185
|
+
// outside the block entirely, including straight up the channel
|
|
186
|
+
expect(pointInSolid(tris, { x: 3, y: 3, z: 9 })).toBe(false)
|
|
187
|
+
expect(pointInSolid(tris, { x: -1, y: 3, z: 3 })).toBe(false)
|
|
188
|
+
})
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
describe('ray-parity robustness on lattice-aligned probes', () => {
|
|
192
|
+
const cube = parseAsciiStl(toStl(boxTris([0, 0, 0], [2, 2, 2])))
|
|
193
|
+
const channel = parseAsciiStl(CHANNEL_CUBE)
|
|
194
|
+
|
|
195
|
+
it('answers correctly for points sitting on the cube edge lines', () => {
|
|
196
|
+
// Every one of these shares two coordinates with a cube corner, so an
|
|
197
|
+
// axis-aligned ray from it runs exactly along an edge or inside a face plane.
|
|
198
|
+
expect(pointInSolid(cube, { x: 3, y: 0, z: 0 })).toBe(false)
|
|
199
|
+
expect(pointInSolid(cube, { x: 3, y: 2, z: 2 })).toBe(false)
|
|
200
|
+
expect(pointInSolid(cube, { x: -1, y: 0, z: 2 })).toBe(false)
|
|
201
|
+
expect(pointInSolid(cube, { x: 1, y: 3, z: 0 })).toBe(false)
|
|
202
|
+
expect(pointInSolid(cube, { x: 1, y: 1, z: 1 })).toBe(true)
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('answers correctly for interior points lying in a channel-wall plane', () => {
|
|
206
|
+
// y = 2 and x = 2 are the channel wall planes; these points are in material,
|
|
207
|
+
// and an axis-aligned ray from each would graze the channel wall's edge.
|
|
208
|
+
expect(pointInSolid(channel, { x: 1, y: 2, z: 3 })).toBe(true)
|
|
209
|
+
expect(pointInSolid(channel, { x: 2, y: 1, z: 3 })).toBe(true)
|
|
210
|
+
expect(pointInSolid(channel, { x: 5, y: 2, z: 3 })).toBe(true)
|
|
211
|
+
expect(pointInSolid(channel, { x: 4, y: 5, z: 3 })).toBe(true)
|
|
212
|
+
// and for outside points on the same degenerate lines
|
|
213
|
+
expect(pointInSolid(channel, { x: 7, y: 2, z: 3 })).toBe(false)
|
|
214
|
+
expect(pointInSolid(channel, { x: 3, y: 2, z: 7 })).toBe(false)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('is deterministic: the same point and mesh always give the same answer', () => {
|
|
218
|
+
const pts = [
|
|
219
|
+
{ x: 3, y: 3, z: 3 },
|
|
220
|
+
{ x: 1, y: 2, z: 3 },
|
|
221
|
+
{ x: 2, y: 1, z: 3 },
|
|
222
|
+
]
|
|
223
|
+
for (const p of pts) {
|
|
224
|
+
const first = pointInSolid(channel, p)
|
|
225
|
+
for (let i = 0; i < 5; i++) expect(pointInSolid(channel, p)).toBe(first)
|
|
226
|
+
}
|
|
227
|
+
})
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
describe('spec scoring', () => {
|
|
231
|
+
const tris = parseAsciiStl(CHANNEL_CUBE)
|
|
232
|
+
const geo = measureMesh(tris)
|
|
233
|
+
|
|
234
|
+
it('scores 1.0 and resolves when every asserted check passes', () => {
|
|
235
|
+
const s = scoreAgainstSpec(geo, tris, {
|
|
236
|
+
bbox: { x: [5.9, 6.1], y: [5.9, 6.1], z: [5.9, 6.1] },
|
|
237
|
+
volume: [190, 194],
|
|
238
|
+
solids: [1, 1],
|
|
239
|
+
minTriangles: 20,
|
|
240
|
+
probesInsideSolid: [[1, 1, 3]],
|
|
241
|
+
probesOutsideSolid: [[3, 3, 3]],
|
|
242
|
+
})
|
|
243
|
+
expect(s.score).toBe(1)
|
|
244
|
+
expect(s.resolved).toBe(true)
|
|
245
|
+
expect(s.failed).toEqual([])
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('gives partial credit and names every failed check with measured vs expected', () => {
|
|
249
|
+
const s = scoreAgainstSpec(geo, tris, {
|
|
250
|
+
bbox: { x: [10, 12] },
|
|
251
|
+
volume: [190, 194],
|
|
252
|
+
solids: [1, 1],
|
|
253
|
+
probesOutsideSolid: [[1, 1, 3]],
|
|
254
|
+
})
|
|
255
|
+
expect(s.resolved).toBe(false)
|
|
256
|
+
expect(s.score).toBeCloseTo(2 / 4, 9)
|
|
257
|
+
const names = s.failed.map((c) => c.name)
|
|
258
|
+
expect(names).toContain('bboxX')
|
|
259
|
+
expect(names.some((n) => n.startsWith('probeOutside[0]'))).toBe(true)
|
|
260
|
+
const bbox = s.failed.find((c) => c.name === 'bboxX')
|
|
261
|
+
expect(bbox?.measured).toBe('6')
|
|
262
|
+
expect(bbox?.expected).toBe('[10, 12]')
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('counts each bbox axis, each band and each probe as one check', () => {
|
|
266
|
+
const s = scoreAgainstSpec(geo, tris, {
|
|
267
|
+
bbox: { x: [5, 7], y: [5, 7], z: [5, 7] },
|
|
268
|
+
volume: [190, 194],
|
|
269
|
+
solids: [1, 1],
|
|
270
|
+
minTriangles: 1,
|
|
271
|
+
probesInsideSolid: [
|
|
272
|
+
[1, 1, 3],
|
|
273
|
+
[5, 5, 3],
|
|
274
|
+
],
|
|
275
|
+
probesOutsideSolid: [[3, 3, 3]],
|
|
276
|
+
})
|
|
277
|
+
expect(s.checks).toHaveLength(9)
|
|
278
|
+
})
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
describe('artifact handling', () => {
|
|
282
|
+
it('strips a markdown fence the model may have wrapped its answer in', () => {
|
|
283
|
+
expect(stripCodeFence('```openscad\ncube([1,1,1]);\n```')).toBe('cube([1,1,1]);')
|
|
284
|
+
expect(stripCodeFence('```\ncube([1,1,1]);\n```')).toBe('cube([1,1,1]);')
|
|
285
|
+
expect(stripCodeFence(' cube([1,1,1]); ')).toBe('cube([1,1,1]);')
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it('loads tasks with spec/source/calibrated metadata and no openscad', async () => {
|
|
289
|
+
const a = createMcadBenchAdapter()
|
|
290
|
+
const tasks = await a.loadTasks()
|
|
291
|
+
expect(tasks).toHaveLength(MCAD_TASKS.length)
|
|
292
|
+
for (const t of tasks) {
|
|
293
|
+
const md = t.metadata as { spec: unknown; source: string; calibrated: boolean }
|
|
294
|
+
expect(md.spec).toBeTruthy()
|
|
295
|
+
expect(typeof md.source).toBe('string')
|
|
296
|
+
expect(typeof md.calibrated).toBe('boolean')
|
|
297
|
+
expect(typeof (await a.goldArtifact(t))).toBe('string')
|
|
298
|
+
}
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
it('honours ids and limit', async () => {
|
|
302
|
+
const a = createMcadBenchAdapter()
|
|
303
|
+
expect((await a.loadTasks({ ids: ['l-bracket'] })).map((t) => t.id)).toEqual(['l-bracket'])
|
|
304
|
+
expect(await a.loadTasks({ limit: 3 })).toHaveLength(3)
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
it('task 10 asks for separate bodies, never "one fused solid"', () => {
|
|
308
|
+
const t10 = MCAD_TASKS.find((t) => t.id === 'planetary-gear-stage')
|
|
309
|
+
expect(t10?.prompt).not.toContain('one fused solid')
|
|
310
|
+
expect(t10?.prompt).toContain('separate')
|
|
311
|
+
// every other task keeps the single-solid boilerplate
|
|
312
|
+
for (const t of MCAD_TASKS) {
|
|
313
|
+
if (t.id === 'planetary-gear-stage') continue
|
|
314
|
+
expect(t.prompt).toContain('one fused solid')
|
|
315
|
+
}
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
it('scores an empty artifact 0 without touching openscad', async () => {
|
|
319
|
+
const a = createMcadBenchAdapter()
|
|
320
|
+
const [t] = await a.loadTasks({ ids: ['calibration-block'] })
|
|
321
|
+
const s = await a.judge(t!, ' ')
|
|
322
|
+
expect(s.resolved).toBe(false)
|
|
323
|
+
expect(s.score).toBe(0)
|
|
324
|
+
expect(s.detail).toBe('empty artifact')
|
|
325
|
+
})
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
// ---------------------------------------------------------------------------
|
|
329
|
+
// Live judge (needs openscad + xvfb-run)
|
|
330
|
+
// ---------------------------------------------------------------------------
|
|
331
|
+
|
|
332
|
+
function openscadPresent(): boolean {
|
|
333
|
+
try {
|
|
334
|
+
execFileSync('xvfb-run', ['-a', 'openscad', '--version'], { stdio: 'ignore', timeout: 60_000 })
|
|
335
|
+
return true
|
|
336
|
+
} catch {
|
|
337
|
+
return false
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const OPENSCAD = openscadPresent()
|
|
342
|
+
const JUDGE_TIMEOUT = 300_000
|
|
343
|
+
|
|
344
|
+
/** Task 01's gold with the four through-holes deleted — the geometry is otherwise identical. */
|
|
345
|
+
const BLOCK_WITHOUT_HOLES = `
|
|
346
|
+
$fn=96;
|
|
347
|
+
module chamfered_block(l, w, h, c) {
|
|
348
|
+
hull() {
|
|
349
|
+
translate([-l/2, -w/2, 0]) cube([l, w, h - c]);
|
|
350
|
+
translate([-l/2 + c, -w/2 + c, h - c]) cube([l - 2*c, w - 2*c, c]);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
chamfered_block(100, 60, 20, 2);
|
|
354
|
+
`
|
|
355
|
+
|
|
356
|
+
/** Task 01's gold at 0.9 scale: 90 x 54 x 18, every feature otherwise correct.
|
|
357
|
+
* Wrapped in a module rather than a bare `scale(){...}` block because OpenSCAD
|
|
358
|
+
* rejects a module definition inside a transform's child block but accepts one
|
|
359
|
+
* inside a module body — this keeps the fixture derived from the gold itself. */
|
|
360
|
+
const BLOCK_SCALED_90 = `module gold() {\n${MCAD_GOLDS['calibration-block']}\n}\nscale([0.9, 0.9, 0.9]) gold();`
|
|
361
|
+
|
|
362
|
+
describe.skipIf(!OPENSCAD)('mcad judge, live openscad', () => {
|
|
363
|
+
it('preflight passes when the toolchain is present', async () => {
|
|
364
|
+
await expect(createMcadBenchAdapter().preflight()).resolves.toBeUndefined()
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
for (const task of MCAD_TASKS.filter((t) => t.calibrated)) {
|
|
368
|
+
it(
|
|
369
|
+
`gold for ${task.id} resolves at score 1.0`,
|
|
370
|
+
async () => {
|
|
371
|
+
const a = createMcadBenchAdapter()
|
|
372
|
+
const [t] = await a.loadTasks({ ids: [task.id] })
|
|
373
|
+
const gold = await a.goldArtifact(t!)
|
|
374
|
+
expect(typeof gold).toBe('string')
|
|
375
|
+
const s = await a.judge(t!, gold as string)
|
|
376
|
+
const failed = JSON.parse(s.detail ?? '{}').failed ?? []
|
|
377
|
+
expect(failed, `failed checks: ${JSON.stringify(failed)}`).toEqual([])
|
|
378
|
+
expect(s.score).toBe(1)
|
|
379
|
+
expect(s.resolved).toBe(true)
|
|
380
|
+
},
|
|
381
|
+
JUDGE_TIMEOUT,
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
it(
|
|
386
|
+
'MUST REJECT: the calibration block with its four holes removed fails the bore probes',
|
|
387
|
+
async () => {
|
|
388
|
+
const a = createMcadBenchAdapter()
|
|
389
|
+
const [t] = await a.loadTasks({ ids: ['calibration-block'] })
|
|
390
|
+
const s = await a.judge(t!, BLOCK_WITHOUT_HOLES)
|
|
391
|
+
expect(s.resolved).toBe(false)
|
|
392
|
+
expect(s.score).toBeLessThan(1)
|
|
393
|
+
const failed = (JSON.parse(s.detail as string).failed as Array<{ name: string; measured: string }>).map(
|
|
394
|
+
(f) => f.name,
|
|
395
|
+
)
|
|
396
|
+
// all five "must be outside" probes now sit in solid material
|
|
397
|
+
const outside = failed.filter((n) => n.startsWith('probeOutside['))
|
|
398
|
+
const spec = MCAD_TASKS.find((x) => x.id === 'calibration-block')?.spec
|
|
399
|
+
expect(outside).toHaveLength(spec?.probesOutsideSolid?.length ?? 0)
|
|
400
|
+
expect(failed).toContain('volume')
|
|
401
|
+
// the block itself is still the right size, so bbox must NOT be blamed
|
|
402
|
+
expect(failed).not.toContain('bboxX')
|
|
403
|
+
},
|
|
404
|
+
JUDGE_TIMEOUT,
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
it(
|
|
408
|
+
'MUST REJECT: the calibration block scaled to 90 x 54 x 18 fails every bbox axis',
|
|
409
|
+
async () => {
|
|
410
|
+
const a = createMcadBenchAdapter()
|
|
411
|
+
const [t] = await a.loadTasks({ ids: ['calibration-block'] })
|
|
412
|
+
const s = await a.judge(t!, BLOCK_SCALED_90)
|
|
413
|
+
expect(s.resolved).toBe(false)
|
|
414
|
+
const detail = JSON.parse(s.detail as string) as {
|
|
415
|
+
failed: Array<{ name: string; measured: string; expected: string }>
|
|
416
|
+
geo: { bbox: { x: number; y: number; z: number } }
|
|
417
|
+
}
|
|
418
|
+
expect(detail.geo.bbox).toEqual({ x: 90, y: 54, z: 18 })
|
|
419
|
+
const failed = detail.failed.map((f) => f.name)
|
|
420
|
+
expect(failed).toContain('bboxX')
|
|
421
|
+
expect(failed).toContain('bboxY')
|
|
422
|
+
expect(failed).toContain('bboxZ')
|
|
423
|
+
const x = detail.failed.find((f) => f.name === 'bboxX')
|
|
424
|
+
expect(x?.measured).toBe('90')
|
|
425
|
+
expect(x?.expected).toBe('[99, 101]')
|
|
426
|
+
},
|
|
427
|
+
JUDGE_TIMEOUT,
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
it(
|
|
431
|
+
'MUST REJECT: a non-watertight surface is scored 0 by the hard gate',
|
|
432
|
+
async () => {
|
|
433
|
+
const a = createMcadBenchAdapter()
|
|
434
|
+
const [t] = await a.loadTasks({ ids: ['calibration-block'] })
|
|
435
|
+
const s = await a.judge(t!, 'polyhedron(points=[[0,0,0],[10,0,0],[0,10,0]], faces=[[0,1,2]]);')
|
|
436
|
+
expect(s.resolved).toBe(false)
|
|
437
|
+
expect(s.score).toBe(0)
|
|
438
|
+
expect(JSON.parse(s.detail as string).gate).toBe('watertight')
|
|
439
|
+
},
|
|
440
|
+
JUDGE_TIMEOUT,
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
it(
|
|
444
|
+
'MUST REJECT: source that does not compile is scored 0',
|
|
445
|
+
async () => {
|
|
446
|
+
const a = createMcadBenchAdapter()
|
|
447
|
+
const [t] = await a.loadTasks({ ids: ['calibration-block'] })
|
|
448
|
+
const s = await a.judge(t!, 'this is not openscad {{{')
|
|
449
|
+
expect(s.resolved).toBe(false)
|
|
450
|
+
expect(s.score).toBe(0)
|
|
451
|
+
expect(s.detail).toMatch(/compile failed/)
|
|
452
|
+
},
|
|
453
|
+
JUDGE_TIMEOUT,
|
|
454
|
+
)
|
|
455
|
+
})
|