brepjs-verify 0.2.1 → 0.4.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.
- package/CHANGELOG.md +20 -0
- package/README.md +26 -0
- package/dist/brepjs-verify.cjs +1 -1
- package/dist/brepjs-verify.js +1 -1
- package/dist/cli/main.cjs +1 -1
- package/dist/cli/main.js +1 -1
- package/dist/{diff-CZ4mLtrf.cjs → diff-BNmCp_8I.cjs} +90 -5
- package/dist/{diff-D7ZBNRJG.js → diff-D5U3Ie2F.js} +90 -5
- package/dist/snapshot/registry.cjs +1 -1
- package/dist/snapshot/registry.js +1 -1
- package/dist/snapshot/static.cjs +1 -1
- package/dist/snapshot/static.d.ts +1 -1
- package/dist/snapshot/static.js +1 -1
- package/dist/verify/brepjsRuntime.d.ts +1 -0
- package/dist/verify/expected.d.ts +15 -0
- package/package.json +8 -3
- package/reference/llms-full.txt +2052 -0
- package/reference/llms.txt +1810 -0
- package/viewer/dist/assets/{brepjs-CDZqKweN.js → brepjs-CI5VXw8W.js} +19 -19
- package/viewer/dist/assets/{index-B8QUQDqM.js → index-CiN0lKoi.js} +1 -1
- package/viewer/dist/assets/{kernelWorker-C6s5i9JH.js → kernelWorker-BtcMpY8t.js} +1 -1
- package/viewer/dist/index.html +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.0](https://github.com/andymai/brepjs/compare/brepjs-verify-v0.3.0...brepjs-verify-v0.4.0) (2026-06-04)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **brepjs-verify:** eval-driven skill, hint, and reference improvements ([#1219](https://github.com/andymai/brepjs/issues/1219)) ([1a9b80f](https://github.com/andymai/brepjs/commit/1a9b80f3d3dbb44d7a8ae2f601ff305b70534efe))
|
|
9
|
+
|
|
10
|
+
## [0.3.0](https://github.com/andymai/brepjs/compare/brepjs-verify-v0.2.1...brepjs-verify-v0.3.0) (2026-06-04)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* **brepjs-verify:** live text-to-cad eval flywheel ([#1215](https://github.com/andymai/brepjs/issues/1215)) ([4e81fc4](https://github.com/andymai/brepjs/commit/4e81fc4053491ce3e08182d57d76bd649252ea3c))
|
|
16
|
+
* **brepjs-verify:** standalone bundled CLI + rename from brepjs-cad ([#1211](https://github.com/andymai/brepjs/issues/1211)) ([05b3799](https://github.com/andymai/brepjs/commit/05b3799a0e9ee4968d4cac92f3a2ea236e39cd35))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
* **brepjs-verify:** correct fillet/chamfer arg order in no-edges hints ([#1218](https://github.com/andymai/brepjs/issues/1218)) ([835f13a](https://github.com/andymai/brepjs/commit/835f13ac966b4264ba56a5cfc371bbbbbd1a0f01))
|
|
22
|
+
|
|
3
23
|
## [0.2.1](https://github.com/andymai/brepjs/compare/brepjs-cad-v0.2.0...brepjs-cad-v0.2.1) (2026-06-04)
|
|
4
24
|
|
|
5
25
|
|
package/README.md
CHANGED
|
@@ -21,6 +21,15 @@ The CLI the skill invokes. Install it in **your** project, where `brepjs` + the
|
|
|
21
21
|
npm i -D brepjs-verify brepjs occt-wasm
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
+
## API reference
|
|
25
|
+
|
|
26
|
+
The package bundles brepjs's full API reference for offline/agent use — the complete export surface with signatures and examples:
|
|
27
|
+
|
|
28
|
+
- `reference/llms-full.txt` — every export, full signatures (the deep reference)
|
|
29
|
+
- `reference/llms.txt` — the same content as a quicker index
|
|
30
|
+
|
|
31
|
+
Point your agent at `node_modules/brepjs-verify/reference/llms-full.txt` for anything the skill's curated references don't cover.
|
|
32
|
+
|
|
24
33
|
## The `.brep.ts` contract
|
|
25
34
|
|
|
26
35
|
A model is a module whose default export is a zero-arg function returning a shape (or a `Result<shape>`):
|
|
@@ -71,6 +80,23 @@ Few-shot examples live under `skill/examples/<name>.brep.ts`, each with a `<name
|
|
|
71
80
|
|
|
72
81
|
`npm run eval` (`bench/run.ts`) replays every `skill/examples/*.brep.ts` with a sibling `*.expected.json` through the public `runPart` runtime, compares measured volume/area/validity/shape-type against the recorded baseline within each file's tolerance (default 0.5%), prints a PASS/FAIL scorecard, and exits non-zero on any regression. It is deterministic — no LLM or API key — so it runs in CI as the package's regression net. Refresh a baseline by re-recording the example's `*.expected.json` after an intentional geometry change.
|
|
73
82
|
|
|
83
|
+
### Live eval (`npm run eval:live`)
|
|
84
|
+
|
|
85
|
+
The measurement flywheel: sends ~18 natural-language part prompts (`bench/prompts.ts`) to a real model — using the **deployed `SKILL.md` as the system prompt**, so it measures the actual skill — then verifies each generated part two ways:
|
|
86
|
+
|
|
87
|
+
- **Auto (objective):** `runPart --check` → valid solid + any pinned dims within tolerance.
|
|
88
|
+
- **Judge (intent):** a multimodal Claude call looks at the rendered iso/front/top/right snapshots and decides whether the part matches the request + rubric.
|
|
89
|
+
|
|
90
|
+
The scorecard reports per-category `valid` / `judge` / `both` rates and stamps the model + **resolved brepjs version** + date (so trend lines don't mix kernel versions).
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
ANTHROPIC_API_KEY=sk-... npm run eval:live -w brepjs-verify # opus by default
|
|
94
|
+
ANTHROPIC_API_KEY=sk-... npm run eval:live -w brepjs-verify -- --model claude-sonnet-4-6
|
|
95
|
+
# --only <id|category> run a subset --keep keep the generated parts
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Opt-in and **billed** (real API calls), so it does _not_ run in CI — the deterministic replay above is the CI gate. Snapshots (hence the judge) need `puppeteer`/Chrome; without them the run scores on auto-verify alone and notes the skipped judge.
|
|
99
|
+
|
|
74
100
|
## Programmatic API
|
|
75
101
|
|
|
76
102
|
```ts
|
package/dist/brepjs-verify.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_diff = require("./diff-
|
|
2
|
+
const require_diff = require("./diff-BNmCp_8I.cjs");
|
|
3
3
|
exports.DEFAULT_TOLERANCE_PCT = require_diff.DEFAULT_TOLERANCE_PCT;
|
|
4
4
|
exports.TYPECHECK_CODE = require_diff.TYPECHECK_CODE;
|
|
5
5
|
exports.emptyReport = require_diff.emptyReport;
|
package/dist/brepjs-verify.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as typecheckPart, c as isExpectedDims, d as emptyReport, i as TYPECHECK_CODE, l as pctDelta, m as serializeReport, n as runMeasure, o as DEFAULT_TOLERANCE_PCT, r as runPart, s as evaluateExpected, t as runDiff, u as runChecks } from "./diff-
|
|
1
|
+
import { a as typecheckPart, c as isExpectedDims, d as emptyReport, i as TYPECHECK_CODE, l as pctDelta, m as serializeReport, n as runMeasure, o as DEFAULT_TOLERANCE_PCT, r as runPart, s as evaluateExpected, t as runDiff, u as runChecks } from "./diff-D5U3Ie2F.js";
|
|
2
2
|
export { DEFAULT_TOLERANCE_PCT, TYPECHECK_CODE, emptyReport, evaluateExpected, isExpectedDims, pctDelta, runChecks, runDiff, runMeasure, runPart, serializeReport, typecheckPart };
|
package/dist/cli/main.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
-
const require_diff = require("../diff-
|
|
3
|
+
const require_diff = require("../diff-BNmCp_8I.cjs");
|
|
4
4
|
let node_url = require("node:url");
|
|
5
5
|
let node_fs = require("node:fs");
|
|
6
6
|
let node_path = require("node:path");
|
package/dist/cli/main.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { f as pushError, h as loadBrep, m as serializeReport, n as runMeasure, p as reportOk, r as runPart, t as runDiff } from "../diff-
|
|
2
|
+
import { f as pushError, h as loadBrep, m as serializeReport, n as runMeasure, p as reportOk, r as runPart, t as runDiff } from "../diff-D5U3Ie2F.js";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { existsSync, mkdirSync, realpathSync, watch, writeFileSync } from "node:fs";
|
|
5
5
|
import { basename, dirname, join, resolve } from "node:path";
|
|
@@ -41,6 +41,14 @@ function loadBrep() {
|
|
|
41
41
|
}
|
|
42
42
|
return cached;
|
|
43
43
|
}
|
|
44
|
+
var kernelReady;
|
|
45
|
+
function initOcctWasm(brep) {
|
|
46
|
+
if (!kernelReady) kernelReady = (async () => {
|
|
47
|
+
const kernel = await (await import("occt-wasm")).OcctKernel.init();
|
|
48
|
+
brep.registerKernel("occt-wasm", brep.OcctWasmAdapter.fromKernel(kernel));
|
|
49
|
+
})();
|
|
50
|
+
return kernelReady;
|
|
51
|
+
}
|
|
44
52
|
//#endregion
|
|
45
53
|
//#region src/verify/report.ts
|
|
46
54
|
function emptyReport() {
|
|
@@ -62,7 +70,7 @@ function reportOk(r) {
|
|
|
62
70
|
return r.errors.length === 0 && r.checks.every((c) => c.passed) && r.assertions.every((a) => a.passed);
|
|
63
71
|
}
|
|
64
72
|
/**
|
|
65
|
-
* Local, brepjs-
|
|
73
|
+
* Local, brepjs-verify-owned advice keyed on `BrepErrorCode` values (see `brepjs`'s public
|
|
66
74
|
* `BrepErrorCode`). Intentionally not importing the library's internal `getSuggestionForCode`:
|
|
67
75
|
* this table is the agent loop's own actionable `fix` + `nextStep` guidance, and the library's
|
|
68
76
|
* public `BrepError.suggestion` is still surfaced alongside it on each hint.
|
|
@@ -70,11 +78,11 @@ function reportOk(r) {
|
|
|
70
78
|
var HINT_TABLE = {
|
|
71
79
|
FILLET_NO_EDGES: {
|
|
72
80
|
fix: "Select real edges before filleting — pass an edge query (e.g. find edges by direction/position) or a non-empty edge list, not the whole solid.",
|
|
73
|
-
nextStep: "List the solid’s edges, pick the ones to round, then call fillet(solid,
|
|
81
|
+
nextStep: "List the solid’s edges, pick the ones to round, then call fillet(solid, edges, radius)."
|
|
74
82
|
},
|
|
75
83
|
CHAMFER_NO_EDGES: {
|
|
76
84
|
fix: "Select real edges before chamfering — pass a non-empty edge query/list rather than relying on a default that matched nothing.",
|
|
77
|
-
nextStep: "Enumerate the solid’s edges, choose the target edges, then call chamfer(solid,
|
|
85
|
+
nextStep: "Enumerate the solid’s edges, choose the target edges, then call chamfer(solid, edges, distance)."
|
|
78
86
|
},
|
|
79
87
|
INVALID_FILLET_RADIUS: {
|
|
80
88
|
fix: "Use a fillet radius that is > 0 and small enough to fit the adjacent faces (well under half the thinnest wall).",
|
|
@@ -156,6 +164,14 @@ var HINT_TABLE = {
|
|
|
156
164
|
fix: "The boolean subtraction failed — often a tool that does not actually intersect the base, or tolerance issues.",
|
|
157
165
|
nextStep: "Confirm the tool overlaps the base, optionally heal the inputs, then re-cut."
|
|
158
166
|
},
|
|
167
|
+
FILLET_FAILED: {
|
|
168
|
+
fix: "The fillet could not be built — usually the radius is too large for an edge it touched (it cannot exceed the adjacent face/wall), or you filleted EVERY edge (the no-edge-list form) including ones too thin to round.",
|
|
169
|
+
nextStep: "Select only the edges you mean to round — e.g. edgeFinder().inDirection(\"Z\").findAll(solid) — and/or reduce the radius below the thinnest adjacent wall, then re-verify."
|
|
170
|
+
},
|
|
171
|
+
CHAMFER_FAILED: {
|
|
172
|
+
fix: "The chamfer could not be built — usually the distance is too large for an edge it touched, or you chamfered EVERY edge (the no-edge-list form) including ones too thin.",
|
|
173
|
+
nextStep: "Select only the target edges with an edge finder and/or reduce the distance below the shortest adjacent edge, then re-verify."
|
|
174
|
+
},
|
|
159
175
|
BOOLEAN_HAS_ERRORS: {
|
|
160
176
|
fix: "The boolean ran but the kernel reported errors (often coincident faces or near-tangent contact).",
|
|
161
177
|
nextStep: "Perturb one operand slightly so contact is a clean overlap, or heal the inputs, then retry."
|
|
@@ -191,6 +207,10 @@ var HINT_TABLE = {
|
|
|
191
207
|
TYPECHECK: {
|
|
192
208
|
fix: "Fix the TypeScript type error before running the part — the API call or value does not match brepjs’s types.",
|
|
193
209
|
nextStep: "Correct the flagged type (e.g. argument/return type or import), then re-verify."
|
|
210
|
+
},
|
|
211
|
+
EXPECTED_UNKNOWN_KEY: {
|
|
212
|
+
fix: "Your `expected` block has keys the CLI does not assert (so the intended check never ran). Bounds must be `{ xMin, xMax, yMin, yMax, zMin, zMax }` — not `{ min, max }` or `{ x, y, z }`.",
|
|
213
|
+
nextStep: "Rewrite `expected` using only volume, area, tolerancePct, and bounds.{xMin..zMax}, then re-verify."
|
|
194
214
|
}
|
|
195
215
|
};
|
|
196
216
|
/** Synthetic code attached to validity-check failures (validSolid returns a plain string error). */
|
|
@@ -294,8 +314,38 @@ function pctDelta(actual, expected) {
|
|
|
294
314
|
return Math.abs(actual - expected) / Math.abs(expected) * 100;
|
|
295
315
|
}
|
|
296
316
|
function withinTolerance(actual, expected, tolerancePct) {
|
|
317
|
+
if (Math.abs(actual - expected) <= 1e-6) return true;
|
|
297
318
|
return pctDelta(actual, expected) <= tolerancePct;
|
|
298
319
|
}
|
|
320
|
+
var TOP_LEVEL_KEYS = new Set([
|
|
321
|
+
"volume",
|
|
322
|
+
"area",
|
|
323
|
+
"bounds",
|
|
324
|
+
"tolerancePct"
|
|
325
|
+
]);
|
|
326
|
+
var BOUND_KEYS = new Set([
|
|
327
|
+
"xMin",
|
|
328
|
+
"xMax",
|
|
329
|
+
"yMin",
|
|
330
|
+
"yMax",
|
|
331
|
+
"zMin",
|
|
332
|
+
"zMax"
|
|
333
|
+
]);
|
|
334
|
+
/**
|
|
335
|
+
* Keys in an `expected` block that the CLI does not understand and would silently ignore — a
|
|
336
|
+
* `{ min: [...], max: [...] }` or `{ x: [...] }` bounds shape, or a misspelled top-level field.
|
|
337
|
+
* Surfaced as an error (not dropped) so a wrong `expected` shape fails loud instead of passing
|
|
338
|
+
* vacuously with the intended assertion never run.
|
|
339
|
+
*/
|
|
340
|
+
function unknownExpectedKeys(expected) {
|
|
341
|
+
const bad = [];
|
|
342
|
+
for (const k of Object.keys(expected)) if (!TOP_LEVEL_KEYS.has(k)) bad.push(k);
|
|
343
|
+
const bounds = expected.bounds;
|
|
344
|
+
if (bounds && typeof bounds === "object") {
|
|
345
|
+
for (const k of Object.keys(bounds)) if (!BOUND_KEYS.has(k)) bad.push(`bounds.${k}`);
|
|
346
|
+
}
|
|
347
|
+
return bad;
|
|
348
|
+
}
|
|
299
349
|
function isExpectedDims(v) {
|
|
300
350
|
if (typeof v !== "object" || v === null) return false;
|
|
301
351
|
const r = v;
|
|
@@ -419,6 +469,25 @@ var COMPILER_OPTIONS = {
|
|
|
419
469
|
skipLibCheck: true,
|
|
420
470
|
allowImportingTsExtensions: true
|
|
421
471
|
};
|
|
472
|
+
/**
|
|
473
|
+
* Locate the `@types/node` declarations so a part may import Node built-ins (`node:fs` to load a
|
|
474
|
+
* font, `node:fs/promises` to read a STEP file, etc.) without `--check` failing on the import.
|
|
475
|
+
* Returns the `@types` directory to use as a `typeRoots` entry. Probes the tool's own install
|
|
476
|
+
* first (where `@types/node` ships as a dependency), then the part's directory.
|
|
477
|
+
*/
|
|
478
|
+
function nodeTypesRoot(partPath, toolDir) {
|
|
479
|
+
const froms = [
|
|
480
|
+
{}.url,
|
|
481
|
+
toolDir ? (0, node_url.pathToFileURL)((0, node_path.resolve)(toolDir, "package.json")).href : void 0,
|
|
482
|
+
(0, node_url.pathToFileURL)(partPath).href
|
|
483
|
+
];
|
|
484
|
+
for (const from of froms) {
|
|
485
|
+
if (!from) continue;
|
|
486
|
+
try {
|
|
487
|
+
return (0, node_path.dirname)((0, node_path.dirname)((0, node_module.createRequire)(from).resolve("@types/node/package.json")));
|
|
488
|
+
} catch {}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
422
491
|
function diagnosticToErrorInfo(d) {
|
|
423
492
|
const text = typescript.default.flattenDiagnosticMessageText(d.messageText, "\n");
|
|
424
493
|
let where = "";
|
|
@@ -443,6 +512,11 @@ function typecheckPart(partPath, toolDir) {
|
|
|
443
512
|
const dts = resolveBrepjsTypes(partPath, toolDir);
|
|
444
513
|
const options = { ...COMPILER_OPTIONS };
|
|
445
514
|
if (dts) options.paths = { brepjs: [dts] };
|
|
515
|
+
const typesRoot = nodeTypesRoot(partPath, toolDir);
|
|
516
|
+
if (typesRoot) {
|
|
517
|
+
options.typeRoots = [typesRoot];
|
|
518
|
+
options.types = ["node"];
|
|
519
|
+
}
|
|
446
520
|
const program = typescript.default.createProgram([partPath], options);
|
|
447
521
|
const errors = [
|
|
448
522
|
...program.getSemanticDiagnostics(),
|
|
@@ -479,7 +553,13 @@ function toErrorInfo(prefix, e) {
|
|
|
479
553
|
code: e.code,
|
|
480
554
|
suggestion: e.suggestion
|
|
481
555
|
};
|
|
482
|
-
if (e instanceof Error)
|
|
556
|
+
if (e instanceof Error) {
|
|
557
|
+
const code = e.message.match(/\[[A-Z][A-Z0-9_]*\]\s+([A-Z][A-Z0-9_]+):/)?.[1];
|
|
558
|
+
return code ? {
|
|
559
|
+
message: `${prefix}: ${e.message}`,
|
|
560
|
+
code
|
|
561
|
+
} : { message: `${prefix}: ${e.message}` };
|
|
562
|
+
}
|
|
483
563
|
return { message: `${prefix}: ${String(e)}` };
|
|
484
564
|
}
|
|
485
565
|
function finalize(result) {
|
|
@@ -501,7 +581,7 @@ async function runPart(modulePath, opts = {}) {
|
|
|
501
581
|
let brep;
|
|
502
582
|
try {
|
|
503
583
|
brep = await loadBrep();
|
|
504
|
-
await brep
|
|
584
|
+
await initOcctWasm(brep);
|
|
505
585
|
} catch (e) {
|
|
506
586
|
pushError(report, toErrorInfo("kernel init failed", e));
|
|
507
587
|
return finalize({
|
|
@@ -559,6 +639,11 @@ async function runPart(modulePath, opts = {}) {
|
|
|
559
639
|
if (isExpectedDims(mod.expected)) {
|
|
560
640
|
const expected = mod.expected;
|
|
561
641
|
result.assertions = evaluateExpected(expected, result.measurements);
|
|
642
|
+
const unknown = unknownExpectedKeys(expected);
|
|
643
|
+
if (unknown.length > 0) pushError(result, {
|
|
644
|
+
message: `expected has unrecognized keys (ignored): ${unknown.join(", ")}. Valid keys: volume, area, tolerancePct, bounds.{xMin,xMax,yMin,yMax,zMin,zMax}.`,
|
|
645
|
+
code: "EXPECTED_UNKNOWN_KEY"
|
|
646
|
+
});
|
|
562
647
|
}
|
|
563
648
|
let glb;
|
|
564
649
|
let step;
|
|
@@ -39,6 +39,14 @@ function loadBrep() {
|
|
|
39
39
|
}
|
|
40
40
|
return cached;
|
|
41
41
|
}
|
|
42
|
+
var kernelReady;
|
|
43
|
+
function initOcctWasm(brep) {
|
|
44
|
+
if (!kernelReady) kernelReady = (async () => {
|
|
45
|
+
const kernel = await (await import("occt-wasm")).OcctKernel.init();
|
|
46
|
+
brep.registerKernel("occt-wasm", brep.OcctWasmAdapter.fromKernel(kernel));
|
|
47
|
+
})();
|
|
48
|
+
return kernelReady;
|
|
49
|
+
}
|
|
42
50
|
//#endregion
|
|
43
51
|
//#region src/verify/report.ts
|
|
44
52
|
function emptyReport() {
|
|
@@ -60,7 +68,7 @@ function reportOk(r) {
|
|
|
60
68
|
return r.errors.length === 0 && r.checks.every((c) => c.passed) && r.assertions.every((a) => a.passed);
|
|
61
69
|
}
|
|
62
70
|
/**
|
|
63
|
-
* Local, brepjs-
|
|
71
|
+
* Local, brepjs-verify-owned advice keyed on `BrepErrorCode` values (see `brepjs`'s public
|
|
64
72
|
* `BrepErrorCode`). Intentionally not importing the library's internal `getSuggestionForCode`:
|
|
65
73
|
* this table is the agent loop's own actionable `fix` + `nextStep` guidance, and the library's
|
|
66
74
|
* public `BrepError.suggestion` is still surfaced alongside it on each hint.
|
|
@@ -68,11 +76,11 @@ function reportOk(r) {
|
|
|
68
76
|
var HINT_TABLE = {
|
|
69
77
|
FILLET_NO_EDGES: {
|
|
70
78
|
fix: "Select real edges before filleting — pass an edge query (e.g. find edges by direction/position) or a non-empty edge list, not the whole solid.",
|
|
71
|
-
nextStep: "List the solid’s edges, pick the ones to round, then call fillet(solid,
|
|
79
|
+
nextStep: "List the solid’s edges, pick the ones to round, then call fillet(solid, edges, radius)."
|
|
72
80
|
},
|
|
73
81
|
CHAMFER_NO_EDGES: {
|
|
74
82
|
fix: "Select real edges before chamfering — pass a non-empty edge query/list rather than relying on a default that matched nothing.",
|
|
75
|
-
nextStep: "Enumerate the solid’s edges, choose the target edges, then call chamfer(solid,
|
|
83
|
+
nextStep: "Enumerate the solid’s edges, choose the target edges, then call chamfer(solid, edges, distance)."
|
|
76
84
|
},
|
|
77
85
|
INVALID_FILLET_RADIUS: {
|
|
78
86
|
fix: "Use a fillet radius that is > 0 and small enough to fit the adjacent faces (well under half the thinnest wall).",
|
|
@@ -154,6 +162,14 @@ var HINT_TABLE = {
|
|
|
154
162
|
fix: "The boolean subtraction failed — often a tool that does not actually intersect the base, or tolerance issues.",
|
|
155
163
|
nextStep: "Confirm the tool overlaps the base, optionally heal the inputs, then re-cut."
|
|
156
164
|
},
|
|
165
|
+
FILLET_FAILED: {
|
|
166
|
+
fix: "The fillet could not be built — usually the radius is too large for an edge it touched (it cannot exceed the adjacent face/wall), or you filleted EVERY edge (the no-edge-list form) including ones too thin to round.",
|
|
167
|
+
nextStep: "Select only the edges you mean to round — e.g. edgeFinder().inDirection(\"Z\").findAll(solid) — and/or reduce the radius below the thinnest adjacent wall, then re-verify."
|
|
168
|
+
},
|
|
169
|
+
CHAMFER_FAILED: {
|
|
170
|
+
fix: "The chamfer could not be built — usually the distance is too large for an edge it touched, or you chamfered EVERY edge (the no-edge-list form) including ones too thin.",
|
|
171
|
+
nextStep: "Select only the target edges with an edge finder and/or reduce the distance below the shortest adjacent edge, then re-verify."
|
|
172
|
+
},
|
|
157
173
|
BOOLEAN_HAS_ERRORS: {
|
|
158
174
|
fix: "The boolean ran but the kernel reported errors (often coincident faces or near-tangent contact).",
|
|
159
175
|
nextStep: "Perturb one operand slightly so contact is a clean overlap, or heal the inputs, then retry."
|
|
@@ -189,6 +205,10 @@ var HINT_TABLE = {
|
|
|
189
205
|
TYPECHECK: {
|
|
190
206
|
fix: "Fix the TypeScript type error before running the part — the API call or value does not match brepjs’s types.",
|
|
191
207
|
nextStep: "Correct the flagged type (e.g. argument/return type or import), then re-verify."
|
|
208
|
+
},
|
|
209
|
+
EXPECTED_UNKNOWN_KEY: {
|
|
210
|
+
fix: "Your `expected` block has keys the CLI does not assert (so the intended check never ran). Bounds must be `{ xMin, xMax, yMin, yMax, zMin, zMax }` — not `{ min, max }` or `{ x, y, z }`.",
|
|
211
|
+
nextStep: "Rewrite `expected` using only volume, area, tolerancePct, and bounds.{xMin..zMax}, then re-verify."
|
|
192
212
|
}
|
|
193
213
|
};
|
|
194
214
|
/** Synthetic code attached to validity-check failures (validSolid returns a plain string error). */
|
|
@@ -292,8 +312,38 @@ function pctDelta(actual, expected) {
|
|
|
292
312
|
return Math.abs(actual - expected) / Math.abs(expected) * 100;
|
|
293
313
|
}
|
|
294
314
|
function withinTolerance(actual, expected, tolerancePct) {
|
|
315
|
+
if (Math.abs(actual - expected) <= 1e-6) return true;
|
|
295
316
|
return pctDelta(actual, expected) <= tolerancePct;
|
|
296
317
|
}
|
|
318
|
+
var TOP_LEVEL_KEYS = new Set([
|
|
319
|
+
"volume",
|
|
320
|
+
"area",
|
|
321
|
+
"bounds",
|
|
322
|
+
"tolerancePct"
|
|
323
|
+
]);
|
|
324
|
+
var BOUND_KEYS = new Set([
|
|
325
|
+
"xMin",
|
|
326
|
+
"xMax",
|
|
327
|
+
"yMin",
|
|
328
|
+
"yMax",
|
|
329
|
+
"zMin",
|
|
330
|
+
"zMax"
|
|
331
|
+
]);
|
|
332
|
+
/**
|
|
333
|
+
* Keys in an `expected` block that the CLI does not understand and would silently ignore — a
|
|
334
|
+
* `{ min: [...], max: [...] }` or `{ x: [...] }` bounds shape, or a misspelled top-level field.
|
|
335
|
+
* Surfaced as an error (not dropped) so a wrong `expected` shape fails loud instead of passing
|
|
336
|
+
* vacuously with the intended assertion never run.
|
|
337
|
+
*/
|
|
338
|
+
function unknownExpectedKeys(expected) {
|
|
339
|
+
const bad = [];
|
|
340
|
+
for (const k of Object.keys(expected)) if (!TOP_LEVEL_KEYS.has(k)) bad.push(k);
|
|
341
|
+
const bounds = expected.bounds;
|
|
342
|
+
if (bounds && typeof bounds === "object") {
|
|
343
|
+
for (const k of Object.keys(bounds)) if (!BOUND_KEYS.has(k)) bad.push(`bounds.${k}`);
|
|
344
|
+
}
|
|
345
|
+
return bad;
|
|
346
|
+
}
|
|
297
347
|
function isExpectedDims(v) {
|
|
298
348
|
if (typeof v !== "object" || v === null) return false;
|
|
299
349
|
const r = v;
|
|
@@ -417,6 +467,25 @@ var COMPILER_OPTIONS = {
|
|
|
417
467
|
skipLibCheck: true,
|
|
418
468
|
allowImportingTsExtensions: true
|
|
419
469
|
};
|
|
470
|
+
/**
|
|
471
|
+
* Locate the `@types/node` declarations so a part may import Node built-ins (`node:fs` to load a
|
|
472
|
+
* font, `node:fs/promises` to read a STEP file, etc.) without `--check` failing on the import.
|
|
473
|
+
* Returns the `@types` directory to use as a `typeRoots` entry. Probes the tool's own install
|
|
474
|
+
* first (where `@types/node` ships as a dependency), then the part's directory.
|
|
475
|
+
*/
|
|
476
|
+
function nodeTypesRoot(partPath, toolDir) {
|
|
477
|
+
const froms = [
|
|
478
|
+
import.meta.url,
|
|
479
|
+
toolDir ? pathToFileURL(resolve(toolDir, "package.json")).href : void 0,
|
|
480
|
+
pathToFileURL(partPath).href
|
|
481
|
+
];
|
|
482
|
+
for (const from of froms) {
|
|
483
|
+
if (!from) continue;
|
|
484
|
+
try {
|
|
485
|
+
return dirname(dirname(createRequire(from).resolve("@types/node/package.json")));
|
|
486
|
+
} catch {}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
420
489
|
function diagnosticToErrorInfo(d) {
|
|
421
490
|
const text = ts.flattenDiagnosticMessageText(d.messageText, "\n");
|
|
422
491
|
let where = "";
|
|
@@ -441,6 +510,11 @@ function typecheckPart(partPath, toolDir) {
|
|
|
441
510
|
const dts = resolveBrepjsTypes(partPath, toolDir);
|
|
442
511
|
const options = { ...COMPILER_OPTIONS };
|
|
443
512
|
if (dts) options.paths = { brepjs: [dts] };
|
|
513
|
+
const typesRoot = nodeTypesRoot(partPath, toolDir);
|
|
514
|
+
if (typesRoot) {
|
|
515
|
+
options.typeRoots = [typesRoot];
|
|
516
|
+
options.types = ["node"];
|
|
517
|
+
}
|
|
444
518
|
const program = ts.createProgram([partPath], options);
|
|
445
519
|
const errors = [
|
|
446
520
|
...program.getSemanticDiagnostics(),
|
|
@@ -477,7 +551,13 @@ function toErrorInfo(prefix, e) {
|
|
|
477
551
|
code: e.code,
|
|
478
552
|
suggestion: e.suggestion
|
|
479
553
|
};
|
|
480
|
-
if (e instanceof Error)
|
|
554
|
+
if (e instanceof Error) {
|
|
555
|
+
const code = e.message.match(/\[[A-Z][A-Z0-9_]*\]\s+([A-Z][A-Z0-9_]+):/)?.[1];
|
|
556
|
+
return code ? {
|
|
557
|
+
message: `${prefix}: ${e.message}`,
|
|
558
|
+
code
|
|
559
|
+
} : { message: `${prefix}: ${e.message}` };
|
|
560
|
+
}
|
|
481
561
|
return { message: `${prefix}: ${String(e)}` };
|
|
482
562
|
}
|
|
483
563
|
function finalize(result) {
|
|
@@ -499,7 +579,7 @@ async function runPart(modulePath, opts = {}) {
|
|
|
499
579
|
let brep;
|
|
500
580
|
try {
|
|
501
581
|
brep = await loadBrep();
|
|
502
|
-
await brep
|
|
582
|
+
await initOcctWasm(brep);
|
|
503
583
|
} catch (e) {
|
|
504
584
|
pushError(report, toErrorInfo("kernel init failed", e));
|
|
505
585
|
return finalize({
|
|
@@ -557,6 +637,11 @@ async function runPart(modulePath, opts = {}) {
|
|
|
557
637
|
if (isExpectedDims(mod.expected)) {
|
|
558
638
|
const expected = mod.expected;
|
|
559
639
|
result.assertions = evaluateExpected(expected, result.measurements);
|
|
640
|
+
const unknown = unknownExpectedKeys(expected);
|
|
641
|
+
if (unknown.length > 0) pushError(result, {
|
|
642
|
+
message: `expected has unrecognized keys (ignored): ${unknown.join(", ")}. Valid keys: volume, area, tolerancePct, bounds.{xMin,xMax,yMin,yMax,zMin,zMax}.`,
|
|
643
|
+
code: "EXPECTED_UNKNOWN_KEY"
|
|
644
|
+
});
|
|
560
645
|
}
|
|
561
646
|
let glb;
|
|
562
647
|
let step;
|
|
@@ -11,7 +11,7 @@ async function probe(port) {
|
|
|
11
11
|
const res = await fetch(`http://127.0.0.1:${port}/__cad/server`, { signal: ctrl.signal });
|
|
12
12
|
if (!res.ok) return false;
|
|
13
13
|
const d = await res.json();
|
|
14
|
-
return d.app === "brepjs-
|
|
14
|
+
return d.app === "brepjs-verify-viewer" && d.dynamicRoot === true && typeof d.serverApiVersion === "number" && d.serverApiVersion >= 1;
|
|
15
15
|
} catch {
|
|
16
16
|
return false;
|
|
17
17
|
} finally {
|
|
@@ -10,7 +10,7 @@ async function probe(port) {
|
|
|
10
10
|
const res = await fetch(`http://127.0.0.1:${port}/__cad/server`, { signal: ctrl.signal });
|
|
11
11
|
if (!res.ok) return false;
|
|
12
12
|
const d = await res.json();
|
|
13
|
-
return d.app === "brepjs-
|
|
13
|
+
return d.app === "brepjs-verify-viewer" && d.dynamicRoot === true && typeof d.serverApiVersion === "number" && d.serverApiVersion >= 1;
|
|
14
14
|
} catch {
|
|
15
15
|
return false;
|
|
16
16
|
} finally {
|
package/dist/snapshot/static.cjs
CHANGED
|
@@ -45,7 +45,7 @@ async function handle(req, res, port) {
|
|
|
45
45
|
const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
|
|
46
46
|
if (url.pathname === "/__cad/server") {
|
|
47
47
|
const d = {
|
|
48
|
-
app: "brepjs-
|
|
48
|
+
app: "brepjs-verify-viewer",
|
|
49
49
|
port,
|
|
50
50
|
dynamicRoot: true,
|
|
51
51
|
serverApiVersion: 1
|
package/dist/snapshot/static.js
CHANGED
|
@@ -44,7 +44,7 @@ async function handle(req, res, port) {
|
|
|
44
44
|
const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
|
|
45
45
|
if (url.pathname === "/__cad/server") {
|
|
46
46
|
const d = {
|
|
47
|
-
app: "brepjs-
|
|
47
|
+
app: "brepjs-verify-viewer",
|
|
48
48
|
port,
|
|
49
49
|
dynamicRoot: true,
|
|
50
50
|
serverApiVersion: 1
|
|
@@ -16,8 +16,23 @@ export interface ExpectedDims {
|
|
|
16
16
|
tolerancePct?: number;
|
|
17
17
|
}
|
|
18
18
|
export declare const DEFAULT_TOLERANCE_PCT = 0.5;
|
|
19
|
+
/**
|
|
20
|
+
* Absolute slack (mm / mm² / mm³) below which a deviation always passes, independent of
|
|
21
|
+
* percent tolerance. Kernel coordinates carry sub-nanometer float noise (e.g. a loft base
|
|
22
|
+
* lands at z = -1e-7, not 0), so a percent comparison against an expected `0` — where any
|
|
23
|
+
* nonzero deviation is infinite percent — would make a zero-valued bound or measurement
|
|
24
|
+
* impossible to assert. 1e-6 is far above that noise and far below any real feature size.
|
|
25
|
+
*/
|
|
26
|
+
export declare const ABS_EPSILON = 0.000001;
|
|
19
27
|
/** Percent deviation of `actual` from `expected`; 0 expected matches only 0 actual. */
|
|
20
28
|
export declare function pctDelta(actual: number, expected: number): number;
|
|
29
|
+
/**
|
|
30
|
+
* Keys in an `expected` block that the CLI does not understand and would silently ignore — a
|
|
31
|
+
* `{ min: [...], max: [...] }` or `{ x: [...] }` bounds shape, or a misspelled top-level field.
|
|
32
|
+
* Surfaced as an error (not dropped) so a wrong `expected` shape fails loud instead of passing
|
|
33
|
+
* vacuously with the intended assertion never run.
|
|
34
|
+
*/
|
|
35
|
+
export declare function unknownExpectedKeys(expected: object): string[];
|
|
21
36
|
export declare function isExpectedDims(v: unknown): v is ExpectedDims;
|
|
22
37
|
/**
|
|
23
38
|
* Compare measured dimensions against a part's `expected` export. Each declared field becomes one
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brepjs-verify",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Agent skill + verify/preview tooling for authoring parametric brepjs CAD code",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -34,21 +34,24 @@
|
|
|
34
34
|
"files": [
|
|
35
35
|
"dist",
|
|
36
36
|
"viewer/dist",
|
|
37
|
+
"reference",
|
|
37
38
|
"CHANGELOG.md",
|
|
38
39
|
"LICENSE",
|
|
39
40
|
"README.md"
|
|
40
41
|
],
|
|
41
42
|
"scripts": {
|
|
42
|
-
"build": "vite build && vite build --config viewer/vite.config.ts",
|
|
43
|
+
"build": "node scripts/copyReference.mjs && vite build && vite build --config viewer/vite.config.ts",
|
|
43
44
|
"typecheck": "tsc --noEmit && tsc -p viewer/tsconfig.json --noEmit && tsc -p bench/tsconfig.json",
|
|
44
45
|
"lint": "eslint src tests viewer bench",
|
|
45
46
|
"test": "vitest run",
|
|
46
47
|
"eval": "tsx bench/run.ts",
|
|
48
|
+
"eval:live": "tsx bench/live.ts",
|
|
47
49
|
"smoke": "node dist/cli/main.js verify tests/fixtures/validBox.brep.ts | node -e \"const r=JSON.parse(require('fs').readFileSync(0,'utf8'));if(r.ok!==true||!(r.measurements.volume>0))process.exit(1)\"",
|
|
48
50
|
"smoke:standalone": "node scripts/smokeStandalone.mjs",
|
|
49
51
|
"prepack": "npm run build"
|
|
50
52
|
},
|
|
51
53
|
"dependencies": {
|
|
54
|
+
"@types/node": "^25.9.1",
|
|
52
55
|
"brepjs": "^18.0.0",
|
|
53
56
|
"commander": "^13.0.0",
|
|
54
57
|
"occt-wasm": "^3.0.0",
|
|
@@ -58,6 +61,7 @@
|
|
|
58
61
|
"puppeteer": "^25.0.4"
|
|
59
62
|
},
|
|
60
63
|
"devDependencies": {
|
|
64
|
+
"@anthropic-ai/sdk": "0.100.1",
|
|
61
65
|
"@react-three/drei": "^10.7.7",
|
|
62
66
|
"@react-three/fiber": "^9.6.1",
|
|
63
67
|
"@types/node": "^25.9.1",
|
|
@@ -73,6 +77,7 @@
|
|
|
73
77
|
"tsx": "^4.22.3",
|
|
74
78
|
"vite": "^8.0.0",
|
|
75
79
|
"vite-plugin-dts": "^5.0.1",
|
|
76
|
-
"vitest": "^4.0.0"
|
|
80
|
+
"vitest": "^4.0.0",
|
|
81
|
+
"zod": "4.4.3"
|
|
77
82
|
}
|
|
78
83
|
}
|