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 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
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_diff = require("./diff-CZ4mLtrf.cjs");
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;
@@ -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-D7ZBNRJG.js";
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-CZ4mLtrf.cjs");
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-D7ZBNRJG.js";
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-cad-owned advice keyed on `BrepErrorCode` values (see `brepjs`'s public
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, radius, edges)."
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, distance, edges)."
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) return { message: `${prefix}: ${e.message}` };
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.init();
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-cad-owned advice keyed on `BrepErrorCode` values (see `brepjs`'s public
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, radius, edges)."
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, distance, edges)."
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) return { message: `${prefix}: ${e.message}` };
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.init();
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-cad-viewer" && d.dynamicRoot === true && typeof d.serverApiVersion === "number" && d.serverApiVersion >= 1;
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-cad-viewer" && d.dynamicRoot === true && typeof d.serverApiVersion === "number" && d.serverApiVersion >= 1;
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 {
@@ -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-cad-viewer",
48
+ app: "brepjs-verify-viewer",
49
49
  port,
50
50
  dynamicRoot: true,
51
51
  serverApiVersion: 1
@@ -8,7 +8,7 @@ export interface StaticServer {
8
8
  close(): Promise<void>;
9
9
  }
10
10
  export interface ServerDescriptor {
11
- app: 'brepjs-cad-viewer';
11
+ app: 'brepjs-verify-viewer';
12
12
  port: number;
13
13
  dynamicRoot: true;
14
14
  serverApiVersion: number;
@@ -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-cad-viewer",
47
+ app: "brepjs-verify-viewer",
48
48
  port,
49
49
  dynamicRoot: true,
50
50
  serverApiVersion: 1
@@ -2,3 +2,4 @@ import type * as Brep from 'brepjs';
2
2
  export type BrepNs = typeof Brep;
3
3
  export declare function toolDir(): string;
4
4
  export declare function loadBrep(): Promise<BrepNs>;
5
+ export declare function initOcctWasm(brep: BrepNs): Promise<void>;
@@ -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.2.1",
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
  }