brepjs-verify 0.3.0 → 0.8.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,83 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.8.0](https://github.com/andymai/brepjs/compare/brepjs-verify-v0.7.1...brepjs-verify-v0.8.0) (2026-06-13)
4
+
5
+
6
+ ### Features
7
+
8
+ * **brepjs-verify:** auto-open browser on --serve + document the MCP server ([#1308](https://github.com/andymai/brepjs/issues/1308)) ([de4272c](https://github.com/andymai/brepjs/commit/de4272c9196f70baec4ff762a33636fccef4c012))
9
+ * **brepjs-verify:** burn bbox dimensions into agent snapshots ([#1280](https://github.com/andymai/brepjs/issues/1280)) ([25b6b8d](https://github.com/andymai/brepjs/commit/25b6b8df68909a41eaef5307519a29a8a05ccc00))
10
+ * **verify:** add center of mass to the verify report ([#1288](https://github.com/andymai/brepjs/issues/1288)) ([5738600](https://github.com/andymai/brepjs/commit/5738600e31f56c00d928d62e32ab9d5e8220b377))
11
+ * **verify:** add export_part MCP tool and sandbox export ([#1316](https://github.com/andymai/brepjs/issues/1316)) ([8a52f90](https://github.com/andymai/brepjs/commit/8a52f90c5b3d76c8bf6ef50a36c6977833af59bb))
12
+ * **verify:** add JSONL run-record provenance for sandbox runs ([#1309](https://github.com/andymai/brepjs/issues/1309)) ([6bda9b6](https://github.com/andymai/brepjs/commit/6bda9b630b8e4edf6eb860002c58b3e36cf1bfd4))
13
+ * **verify:** add manifold flag to the topology channel ([#1291](https://github.com/andymai/brepjs/issues/1291)) ([5ea5bb4](https://github.com/andymai/brepjs/commit/5ea5bb4db5bc8539f2773752e1c42770c91b5e0d))
14
+ * **verify:** add MCP server with run_program tool (stdio) ([#1300](https://github.com/andymai/brepjs/issues/1300)) ([e3c2c9e](https://github.com/andymai/brepjs/commit/e3c2c9e678dd719608cea8b6ee38101de9775e5d))
15
+ * **verify:** add topology counts to the verify report ([#1285](https://github.com/andymai/brepjs/issues/1285)) ([17a0eed](https://github.com/andymai/brepjs/commit/17a0eede727ac29007591cb0249274a35896facb))
16
+ * **verify:** sandbox executor — run agent code in an isolated child process ([#1295](https://github.com/andymai/brepjs/issues/1295)) ([8b72aa2](https://github.com/andymai/brepjs/commit/8b72aa2e272a58aa6d3886b8304eeaefb1a09b2e))
17
+ * **verify:** validate each body of multi-solid assemblies ([#1293](https://github.com/andymai/brepjs/issues/1293)) ([deb682f](https://github.com/andymai/brepjs/commit/deb682f1104179f261f232be7d94ceb154985328))
18
+ * **viewer:** click-to-inspect face picking in verify --serve ([#1278](https://github.com/andymai/brepjs/issues/1278)) ([735dc04](https://github.com/andymai/brepjs/commit/735dc0401143ff47046a79e6fb7bac53cf00a91e))
19
+ * **viewer:** measurements info panel in verify --serve ([#1277](https://github.com/andymai/brepjs/issues/1277)) ([c1ccf1d](https://github.com/andymai/brepjs/commit/c1ccf1d7c50ab43dc0444468f92fcf9365fda9da))
20
+ * **viewer:** orthographic/perspective projection toggle ([#1281](https://github.com/andymai/brepjs/issues/1281)) ([96673e4](https://github.com/andymai/brepjs/commit/96673e45e1ee316f9d26e52c995b4daba691e8b0))
21
+ * **viewer:** section/clipping plane in verify --serve ([#1279](https://github.com/andymai/brepjs/issues/1279)) ([cc0d00b](https://github.com/andymai/brepjs/commit/cc0d00b7a6296cc698fce1ae42b7503e6d47c032))
22
+ * **viewer:** shared ViewerControls toolbar; interactive verify --serve ([#1275](https://github.com/andymai/brepjs/issues/1275)) ([139ae15](https://github.com/andymai/brepjs/commit/139ae15a29d8a7ad5e520ba21d6dc9788242c089))
23
+
24
+ ## [0.7.1](https://github.com/andymai/brepjs/compare/brepjs-verify-v0.7.0...brepjs-verify-v0.7.1) (2026-06-13)
25
+
26
+
27
+ ### Bug Fixes
28
+
29
+ * **brepjs-verify:** repair preview viewer + GLB Y-up/materials fidelity ([#1271](https://github.com/andymai/brepjs/issues/1271)) ([2823d21](https://github.com/andymai/brepjs/commit/2823d212e2fc5f79e785911ec2b9f3320bdfdbbf))
30
+
31
+ ## [0.7.0](https://github.com/andymai/brepjs/compare/brepjs-verify-v0.6.0...brepjs-verify-v0.7.0) (2026-06-10)
32
+
33
+
34
+ ### Features
35
+
36
+ * **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))
37
+ * **brepjs-verify:** live text-to-cad eval flywheel ([#1215](https://github.com/andymai/brepjs/issues/1215)) ([4e81fc4](https://github.com/andymai/brepjs/commit/4e81fc4053491ce3e08182d57d76bd649252ea3c))
38
+ * **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))
39
+
40
+
41
+ ### Bug Fixes
42
+
43
+ * **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))
44
+ * **brepjs-verify:** point skills entry at ./skill directory, not SKILL.md ([#1270](https://github.com/andymai/brepjs/issues/1270)) ([9413a57](https://github.com/andymai/brepjs/commit/9413a57d8c2cac943371e75bcbaf11b3fdd9a657))
45
+
46
+ ## [0.6.0](https://github.com/andymai/brepjs/compare/brepjs-verify-v0.5.1...brepjs-verify-v0.6.0) (2026-06-09)
47
+
48
+
49
+ ### Features
50
+
51
+ * **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))
52
+ * **brepjs-verify:** live text-to-cad eval flywheel ([#1215](https://github.com/andymai/brepjs/issues/1215)) ([4e81fc4](https://github.com/andymai/brepjs/commit/4e81fc4053491ce3e08182d57d76bd649252ea3c))
53
+ * **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))
54
+
55
+
56
+ ### Bug Fixes
57
+
58
+ * **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))
59
+
60
+ ## [0.5.0](https://github.com/andymai/brepjs/compare/brepjs-verify-v0.4.1...brepjs-verify-v0.5.0) (2026-06-08)
61
+
62
+
63
+ ### Features
64
+
65
+ * **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))
66
+ * **brepjs-verify:** live text-to-cad eval flywheel ([#1215](https://github.com/andymai/brepjs/issues/1215)) ([4e81fc4](https://github.com/andymai/brepjs/commit/4e81fc4053491ce3e08182d57d76bd649252ea3c))
67
+ * **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))
68
+
69
+
70
+ ### Bug Fixes
71
+
72
+ * **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))
73
+
74
+ ## [0.4.0](https://github.com/andymai/brepjs/compare/brepjs-verify-v0.3.0...brepjs-verify-v0.4.0) (2026-06-04)
75
+
76
+
77
+ ### Features
78
+
79
+ * **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))
80
+
3
81
  ## [0.3.0](https://github.com/andymai/brepjs/compare/brepjs-verify-v0.2.1...brepjs-verify-v0.3.0) (2026-06-04)
4
82
 
5
83
 
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>`):
@@ -36,28 +45,58 @@ export default () => box(40, 20, 10, { centered: true });
36
45
  ```
37
46
  npx -y brepjs-verify part.brep.ts --step part.step --json report.json # primary STEP + deterministic report
38
47
  npx -y brepjs-verify part.brep.ts --snapshot shots/ # iso/front/top/right PNGs
39
- npx -y brepjs-verify part.brep.ts --serve # clickable preview link (renders the real STEP)
48
+ npx -y brepjs-verify part.brep.ts --serve # preview server + opens the viewer in your browser
49
+ npx -y brepjs-verify part.brep.ts --serve --no-open # preview server; just print the URL (no browser)
40
50
  ```
41
51
 
42
- `--snapshot`/`--serve` use the bundled viewer (shipped under `viewer/dist`, including the OCCT WASM). The viewer is read-only display + screenshot in v1.
52
+ `--snapshot`/`--serve` use the bundled viewer (shipped under `viewer/dist`, including the OCCT WASM). The `--serve` link is interactive: a toolbar offers view presets + fit, solid/wireframe/x-ray modes, edge/grid toggles, a turntable, click-to-inspect face picking, a section/clipping plane, a measurements panel, and an in-browser PNG screenshot. `--snapshot` loads the same page with `ui=0` to suppress the toolbar, and burns the bounding-box size into each PNG (`dims=1`) so the agent can read scale from the image.
53
+
54
+ `--serve` prints the viewer URL and, in an interactive terminal, opens it in your default browser. Auto-open is skipped when it would be unwanted — when the server is reused (a tab is already open), under CI, when output is piped (non-TTY, e.g. agent runs), or on Linux with no display server. Pass `--no-open` to always suppress it.
43
55
 
44
56
  ## CLI reference
45
57
 
46
58
  The `brepjs-verify` bin is a multi-command CLI. `verify` is the default command, so `brepjs-verify part.brep.ts` runs it directly.
47
59
 
48
- | Command | What it does |
49
- | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
50
- | `brepjs-verify verify <file>` | Default command. Loads the part, runs deterministic checks, prints the JSON report. Flags: `--json <out>`, `--step <out>`, `--glb <out>`, `--snapshot <dir>`, `--serve`. Exits non-zero when the report is not `ok` (unless `--serve`). |
51
- | `brepjs-verify init <name>` | Scaffolds a parameterized `<name>.brep.ts` + `tsconfig.json` + `README.md` into `./<name>` (or `--out <dir>`). Never overwrites existing files. |
52
- | `brepjs-verify watch <file>` | Re-verifies on every save until Ctrl-C (debounced; watches the parent dir to survive editor rename-on-save). |
53
- | `brepjs-verify export <file>` | Batch artifacts behind a validity gate: `--step`, `--glb`, `--stl`, or `--all`; `--out <dir>` (default `.`). Exits non-zero on failure. |
54
- | `brepjs-verify measure <a> [b]` | Measurements for one part; with a second module, the distance between the two parts. |
55
- | `brepjs-verify diff <a> <b>` | Compares the measurements of a baseline and a comparison module. |
56
- | `brepjs-verify snapshot` | Multi-view PNG capture — surfaced via `verify --snapshot <dir>`. Requires the optional `puppeteer`/Chrome dependency; degrades with a clear message when absent. |
57
- | `brepjs-verify serve` | Preview server with a `?dir=&file=` deep link — surfaced via `verify --serve`. |
60
+ | Command | What it does |
61
+ | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
62
+ | `brepjs-verify verify <file>` | Default command. Loads the part, runs deterministic checks, prints the JSON report. Flags: `--json <out>`, `--step <out>`, `--glb <out>`, `--snapshot <dir>`, `--serve`, `--no-open` (with `--serve`, don't auto-open the browser). Exits non-zero when the report is not `ok` (unless `--serve`). |
63
+ | `brepjs-verify init <name>` | Scaffolds a parameterized `<name>.brep.ts` + `tsconfig.json` + `README.md` into `./<name>` (or `--out <dir>`). Never overwrites existing files. |
64
+ | `brepjs-verify watch <file>` | Re-verifies on every save until Ctrl-C (debounced; watches the parent dir to survive editor rename-on-save). |
65
+ | `brepjs-verify export <file>` | Batch artifacts behind a validity gate: `--step`, `--glb`, `--stl`, or `--all`; `--out <dir>` (default `.`). Exits non-zero on failure. |
66
+ | `brepjs-verify measure <a> [b]` | Measurements for one part; with a second module, the distance between the two parts. |
67
+ | `brepjs-verify diff <a> <b>` | Compares the measurements of a baseline and a comparison module. |
68
+ | `brepjs-verify snapshot` | Multi-view PNG capture — surfaced via `verify --snapshot <dir>`. Requires the optional `puppeteer`/Chrome dependency; degrades with a clear message when absent. |
69
+ | `brepjs-verify serve` | Preview server with a `?dir=&file=` deep link — surfaced via `verify --serve`. Auto-opens the browser in an interactive terminal (suppressed under CI / non-TTY / no display, or with `--no-open`). |
58
70
 
59
71
  Every command writes a single machine-readable JSON document to stdout; diagnostics (paths, kernel chatter, watch notices) go to stderr.
60
72
 
73
+ ## MCP server
74
+
75
+ `brepjs-verify-mcp` is a stdio [MCP](https://modelcontextprotocol.io) server that exposes the verify substrate to MCP-capable agents (Claude Code, Claude Desktop, any MCP client). It currently provides one tool:
76
+
77
+ | Tool | Input | Returns |
78
+ | ------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
79
+ | `run_program` | `{ code: string, timeoutMs?: number }` | Executes the brepjs `.brep.ts` source in an isolated, timeout/OOM-bounded sandbox and returns the verification report (validity, measurements, topology) as JSON. `isError` is set when the part fails checks, times out, or crashes. |
80
+
81
+ This is the closed _build → verify_ loop as a single call: the agent sends part source, gets back the deterministic report. The program runs in a separate process with a wall-clock timeout and a memory cap, so a runaway part can't hang the agent.
82
+
83
+ ### Connect (local build)
84
+
85
+ Build the package, then register the server by absolute path. Run both commands from the package root (`packages/brepjs-verify`), where `dist/` is emitted — `$(pwd)` is resolved by your shell at that location:
86
+
87
+ ```bash
88
+ npm run build # emits dist/mcp/server.js
89
+ claude mcp add brepjs-verify -- node "$(pwd)/dist/mcp/server.js"
90
+ ```
91
+
92
+ Once the package is published to npm, the same server is available without a local build:
93
+
94
+ ```bash
95
+ claude mcp add brepjs-verify -- npx -y --package brepjs-verify brepjs-verify-mcp
96
+ ```
97
+
98
+ The server runs locally as a child process of your agent (stdio) — geometry never leaves your machine.
99
+
61
100
  ## Examples gallery
62
101
 
63
102
  Few-shot examples live under `skill/examples/<name>.brep.ts`, each with a `<name>.expected.json` baseline. Grouped by category:
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_diff = require("./diff-mxAOOl_m.cjs");
2
+ const require_diff = require("./diff-DDEu6YJM.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-Covv1XuJ.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-BCECMYSQ.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,11 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const require_diff = require("../diff-mxAOOl_m.cjs");
3
+ const require_diff = require("../diff-DDEu6YJM.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");
7
7
  let commander = require("commander");
8
8
  let node_os = require("node:os");
9
+ let node_child_process = require("node:child_process");
9
10
  //#region src/cli/scaffold.ts
10
11
  function partTemplate(name) {
11
12
  return `import { box, cut, unwrap } from 'brepjs';
@@ -177,6 +178,43 @@ async function exportPart(modulePath, formats, outDir) {
177
178
  };
178
179
  }
179
180
  //#endregion
181
+ //#region src/cli/openBrowser.ts
182
+ /**
183
+ * Whether `--serve` should auto-open the browser for the current environment.
184
+ *
185
+ * Opens only for an interactive session — suppressed under CI, when stderr is
186
+ * not a TTY (agent/piped runs), or on Linux without a display server — so
187
+ * automation never spawns a browser unexpectedly. An explicit `--no-open` is a
188
+ * separate, always-on override handled by the caller.
189
+ */
190
+ function shouldAutoOpen({ env = process.env, platform = process.platform, isTTY = Boolean(process.stderr.isTTY) } = {}) {
191
+ if (env["CI"]) return false;
192
+ if (!isTTY) return false;
193
+ if (platform === "linux" && !env["DISPLAY"] && !env["WAYLAND_DISPLAY"]) return false;
194
+ return true;
195
+ }
196
+ /** The platform-specific command that opens `url` in the default browser. */
197
+ function browserCommand(url, platform) {
198
+ if (platform === "darwin") return ["open", [url]];
199
+ if (platform === "win32") return ["rundll32", ["url.dll,FileProtocolHandler", url]];
200
+ return ["xdg-open", [url]];
201
+ }
202
+ /**
203
+ * Best-effort open of `url` in the default browser. Never throws and never
204
+ * blocks — a missing opener just leaves the printed URL as the fallback.
205
+ */
206
+ function openBrowser(url, platform = process.platform) {
207
+ try {
208
+ const [cmd, args] = browserCommand(url, platform);
209
+ const child = (0, node_child_process.spawn)(cmd, args, {
210
+ stdio: "ignore",
211
+ detached: true
212
+ });
213
+ child.on("error", () => {});
214
+ child.unref();
215
+ } catch {}
216
+ }
217
+ //#endregion
180
218
  //#region src/cli/main.ts
181
219
  console.log = (...args) => {
182
220
  process.stderr.write(args.map(String).join(" ") + "\n");
@@ -192,7 +230,7 @@ async function loadSnapshotShoot() {
192
230
  }
193
231
  var program = new commander.Command();
194
232
  program.name("brepjs-verify");
195
- program.command("verify", { isDefault: true }).argument("<file>", "path to a .brep.ts module with a default-exported part function").option("--step <out>", "write the primary STEP artifact to this path").option("--glb <out>", "write a derived GLB preview to this path").option("--json <out>", "write the JSON report to this path").option("--check", "type-check the part (against brepjs types) before running; skip execution on type errors").option("--snapshot <dir>", "render iso/front/top/right PNGs to this dir (requires built viewer)").option("--serve", "after verifying, start a preview server and print a ?dir=&file= deep link (stays running)").action(async (file, opts) => {
233
+ program.command("verify", { isDefault: true }).argument("<file>", "path to a .brep.ts module with a default-exported part function").option("--step <out>", "write the primary STEP artifact to this path").option("--glb <out>", "write a derived GLB preview to this path").option("--json <out>", "write the JSON report to this path").option("--check", "type-check the part (against brepjs types) before running; skip execution on type errors").option("--snapshot <dir>", "render iso/front/top/right PNGs to this dir (requires built viewer)").option("--serve", "after verifying, start a preview server and print a ?dir=&file= deep link (stays running)").option("--no-open", "with --serve, do not auto-open the browser (just print the viewer URL)").action(async (file, opts) => {
196
234
  const wantStep = Boolean(opts.step) || Boolean(opts.snapshot) || Boolean(opts.serve);
197
235
  const { report, step, glb, shape } = await require_diff.runPart((0, node_path.resolve)(file), {
198
236
  step: wantStep,
@@ -227,8 +265,9 @@ program.command("verify", { isDefault: true }).argument("<file>", "path to a .br
227
265
  if (!require_diff.reportOk(report)) process.exitCode = 1;
228
266
  if (Boolean(opts.serve) && stepPath !== void 0 && require_diff.reportOk(report) && stepPath) {
229
267
  const { serve } = await Promise.resolve().then(() => require("../snapshot/serve.cjs"));
230
- const { url } = await serve({ file: stepPath });
268
+ const { url, reused } = await serve({ file: stepPath });
231
269
  process.stderr.write(`viewer: ${url}\n`);
270
+ if (!reused && opts.open && shouldAutoOpen()) openBrowser(url);
232
271
  }
233
272
  });
234
273
  program.command("measure").argument("<a>", "path to a .brep.ts module").argument("[b]", "optional second module; if given, measures distance between the two parts").action(async (a, b) => {
package/dist/cli/main.js CHANGED
@@ -1,10 +1,11 @@
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-Covv1XuJ.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-BCECMYSQ.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";
6
6
  import { Command } from "commander";
7
7
  import { tmpdir } from "node:os";
8
+ import { spawn } from "node:child_process";
8
9
  //#region src/cli/scaffold.ts
9
10
  function partTemplate(name) {
10
11
  return `import { box, cut, unwrap } from 'brepjs';
@@ -176,6 +177,43 @@ async function exportPart(modulePath, formats, outDir) {
176
177
  };
177
178
  }
178
179
  //#endregion
180
+ //#region src/cli/openBrowser.ts
181
+ /**
182
+ * Whether `--serve` should auto-open the browser for the current environment.
183
+ *
184
+ * Opens only for an interactive session — suppressed under CI, when stderr is
185
+ * not a TTY (agent/piped runs), or on Linux without a display server — so
186
+ * automation never spawns a browser unexpectedly. An explicit `--no-open` is a
187
+ * separate, always-on override handled by the caller.
188
+ */
189
+ function shouldAutoOpen({ env = process.env, platform = process.platform, isTTY = Boolean(process.stderr.isTTY) } = {}) {
190
+ if (env["CI"]) return false;
191
+ if (!isTTY) return false;
192
+ if (platform === "linux" && !env["DISPLAY"] && !env["WAYLAND_DISPLAY"]) return false;
193
+ return true;
194
+ }
195
+ /** The platform-specific command that opens `url` in the default browser. */
196
+ function browserCommand(url, platform) {
197
+ if (platform === "darwin") return ["open", [url]];
198
+ if (platform === "win32") return ["rundll32", ["url.dll,FileProtocolHandler", url]];
199
+ return ["xdg-open", [url]];
200
+ }
201
+ /**
202
+ * Best-effort open of `url` in the default browser. Never throws and never
203
+ * blocks — a missing opener just leaves the printed URL as the fallback.
204
+ */
205
+ function openBrowser(url, platform = process.platform) {
206
+ try {
207
+ const [cmd, args] = browserCommand(url, platform);
208
+ const child = spawn(cmd, args, {
209
+ stdio: "ignore",
210
+ detached: true
211
+ });
212
+ child.on("error", () => {});
213
+ child.unref();
214
+ } catch {}
215
+ }
216
+ //#endregion
179
217
  //#region src/cli/main.ts
180
218
  console.log = (...args) => {
181
219
  process.stderr.write(args.map(String).join(" ") + "\n");
@@ -191,7 +229,7 @@ async function loadSnapshotShoot() {
191
229
  }
192
230
  var program = new Command();
193
231
  program.name("brepjs-verify");
194
- program.command("verify", { isDefault: true }).argument("<file>", "path to a .brep.ts module with a default-exported part function").option("--step <out>", "write the primary STEP artifact to this path").option("--glb <out>", "write a derived GLB preview to this path").option("--json <out>", "write the JSON report to this path").option("--check", "type-check the part (against brepjs types) before running; skip execution on type errors").option("--snapshot <dir>", "render iso/front/top/right PNGs to this dir (requires built viewer)").option("--serve", "after verifying, start a preview server and print a ?dir=&file= deep link (stays running)").action(async (file, opts) => {
232
+ program.command("verify", { isDefault: true }).argument("<file>", "path to a .brep.ts module with a default-exported part function").option("--step <out>", "write the primary STEP artifact to this path").option("--glb <out>", "write a derived GLB preview to this path").option("--json <out>", "write the JSON report to this path").option("--check", "type-check the part (against brepjs types) before running; skip execution on type errors").option("--snapshot <dir>", "render iso/front/top/right PNGs to this dir (requires built viewer)").option("--serve", "after verifying, start a preview server and print a ?dir=&file= deep link (stays running)").option("--no-open", "with --serve, do not auto-open the browser (just print the viewer URL)").action(async (file, opts) => {
195
233
  const wantStep = Boolean(opts.step) || Boolean(opts.snapshot) || Boolean(opts.serve);
196
234
  const { report, step, glb, shape } = await runPart(resolve(file), {
197
235
  step: wantStep,
@@ -226,8 +264,9 @@ program.command("verify", { isDefault: true }).argument("<file>", "path to a .br
226
264
  if (!reportOk(report)) process.exitCode = 1;
227
265
  if (Boolean(opts.serve) && stepPath !== void 0 && reportOk(report) && stepPath) {
228
266
  const { serve } = await import("../snapshot/serve.js");
229
- const { url } = await serve({ file: stepPath });
267
+ const { url, reused } = await serve({ file: stepPath });
230
268
  process.stderr.write(`viewer: ${url}\n`);
269
+ if (!reused && opts.open && shouldAutoOpen()) openBrowser(url);
231
270
  }
232
271
  });
233
272
  program.command("measure").argument("<a>", "path to a .brep.ts module").argument("[b]", "optional second module; if given, measures distance between the two parts").action(async (a, b) => {
@@ -0,0 +1,23 @@
1
+ /** Inputs that decide whether auto-opening a browser is appropriate. */
2
+ export interface AutoOpenEnv {
3
+ env?: NodeJS.ProcessEnv;
4
+ platform?: NodeJS.Platform;
5
+ /** Whether stderr is an interactive terminal. */
6
+ isTTY?: boolean;
7
+ }
8
+ /**
9
+ * Whether `--serve` should auto-open the browser for the current environment.
10
+ *
11
+ * Opens only for an interactive session — suppressed under CI, when stderr is
12
+ * not a TTY (agent/piped runs), or on Linux without a display server — so
13
+ * automation never spawns a browser unexpectedly. An explicit `--no-open` is a
14
+ * separate, always-on override handled by the caller.
15
+ */
16
+ export declare function shouldAutoOpen({ env, platform, isTTY, }?: AutoOpenEnv): boolean;
17
+ /** The platform-specific command that opens `url` in the default browser. */
18
+ export declare function browserCommand(url: string, platform: NodeJS.Platform): [string, string[]];
19
+ /**
20
+ * Best-effort open of `url` in the default browser. Never throws and never
21
+ * blocks — a missing opener just leaves the printed URL as the fallback.
22
+ */
23
+ export declare function openBrowser(url: string, platform?: NodeJS.Platform): void;
@@ -162,6 +162,14 @@ var HINT_TABLE = {
162
162
  fix: "The boolean subtraction failed — often a tool that does not actually intersect the base, or tolerance issues.",
163
163
  nextStep: "Confirm the tool overlaps the base, optionally heal the inputs, then re-cut."
164
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
+ },
165
173
  BOOLEAN_HAS_ERRORS: {
166
174
  fix: "The boolean ran but the kernel reported errors (often coincident faces or near-tangent contact).",
167
175
  nextStep: "Perturb one operand slightly so contact is a clean overlap, or heal the inputs, then retry."
@@ -197,6 +205,10 @@ var HINT_TABLE = {
197
205
  TYPECHECK: {
198
206
  fix: "Fix the TypeScript type error before running the part — the API call or value does not match brepjs’s types.",
199
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."
200
212
  }
201
213
  };
202
214
  /** Synthetic code attached to validity-check failures (validSolid returns a plain string error). */
@@ -247,7 +259,7 @@ function shapeTypeOf(brep, s) {
247
259
  return "Unknown";
248
260
  }
249
261
  function runChecks(brep, shape) {
250
- const { isSolid, isShape3D, isFace, measureVolume, measureArea, getBounds, validSolid, isOk } = brep;
262
+ const { isSolid, isShape3D, isFace, measureVolumeProps, measureArea, getBounds, getFaces, getEdges, getWires, getVertices, getSolids, getShells, isManifoldShell, validSolid, isOk } = brep;
251
263
  const r = emptyReport();
252
264
  r.shapeType = shapeTypeOf(brep, shape);
253
265
  if (isSolid(shape)) {
@@ -264,19 +276,41 @@ function runChecks(brep, shape) {
264
276
  });
265
277
  }
266
278
  r.checks.push(validCheck);
279
+ } else {
280
+ const solids = getSolids(shape);
281
+ if (solids.length > 0) {
282
+ const failures = [];
283
+ solids.forEach((s, i) => {
284
+ const v = validSolid(s);
285
+ if (!isOk(v)) failures.push(`body ${i}: ${v.error}`);
286
+ });
287
+ const bodiesCheck = {
288
+ name: "allBodiesValid",
289
+ passed: failures.length === 0
290
+ };
291
+ if (failures.length > 0) {
292
+ bodiesCheck.detail = `${failures.length}/${solids.length} bodies invalid — ${failures.join("; ")}`;
293
+ r.errorInfos.push({
294
+ message: `allBodiesValid: ${bodiesCheck.detail}`,
295
+ code: VALIDITY_FAILURE_CODE
296
+ });
297
+ }
298
+ r.checks.push(bodiesCheck);
299
+ }
267
300
  }
268
301
  if (isShape3D(shape)) {
269
- const vol = measureVolume(shape);
270
- if (isOk(vol)) {
271
- r.measurements.volume = vol.value;
302
+ const volProps = measureVolumeProps(shape);
303
+ if (isOk(volProps)) {
304
+ r.measurements.volume = volProps.value.volume;
305
+ r.measurements.centerOfMass = volProps.value.centerOfMass;
272
306
  r.checks.push({
273
307
  name: "positiveVolume",
274
- passed: vol.value > 0
308
+ passed: volProps.value.volume > 0
275
309
  });
276
310
  } else pushError(r, {
277
- message: `measureVolume: ${vol.error.message}`,
278
- code: vol.error.code,
279
- suggestion: vol.error.suggestion
311
+ message: `measureVolume: ${volProps.error.message}`,
312
+ code: volProps.error.code,
313
+ suggestion: volProps.error.suggestion
280
314
  });
281
315
  }
282
316
  if (isFace(shape) || isShape3D(shape)) {
@@ -288,6 +322,18 @@ function runChecks(brep, shape) {
288
322
  } catch (e) {
289
323
  pushError(r, { message: `getBounds: ${e.message}` });
290
324
  }
325
+ try {
326
+ r.topology = {
327
+ faceCount: getFaces(shape).length,
328
+ edgeCount: getEdges(shape).length,
329
+ wireCount: getWires(shape).length,
330
+ vertexCount: getVertices(shape).length
331
+ };
332
+ } catch {}
333
+ if (r.topology) try {
334
+ const shells = getShells(shape);
335
+ if (shells.length > 0) r.topology.manifold = shells.every((s) => isManifoldShell(s));
336
+ } catch {}
291
337
  r.hints = buildHints(r);
292
338
  return r;
293
339
  }
@@ -300,8 +346,38 @@ function pctDelta(actual, expected) {
300
346
  return Math.abs(actual - expected) / Math.abs(expected) * 100;
301
347
  }
302
348
  function withinTolerance(actual, expected, tolerancePct) {
349
+ if (Math.abs(actual - expected) <= 1e-6) return true;
303
350
  return pctDelta(actual, expected) <= tolerancePct;
304
351
  }
352
+ var TOP_LEVEL_KEYS = new Set([
353
+ "volume",
354
+ "area",
355
+ "bounds",
356
+ "tolerancePct"
357
+ ]);
358
+ var BOUND_KEYS = new Set([
359
+ "xMin",
360
+ "xMax",
361
+ "yMin",
362
+ "yMax",
363
+ "zMin",
364
+ "zMax"
365
+ ]);
366
+ /**
367
+ * Keys in an `expected` block that the CLI does not understand and would silently ignore — a
368
+ * `{ min: [...], max: [...] }` or `{ x: [...] }` bounds shape, or a misspelled top-level field.
369
+ * Surfaced as an error (not dropped) so a wrong `expected` shape fails loud instead of passing
370
+ * vacuously with the intended assertion never run.
371
+ */
372
+ function unknownExpectedKeys(expected) {
373
+ const bad = [];
374
+ for (const k of Object.keys(expected)) if (!TOP_LEVEL_KEYS.has(k)) bad.push(k);
375
+ const bounds = expected.bounds;
376
+ if (bounds && typeof bounds === "object") {
377
+ for (const k of Object.keys(bounds)) if (!BOUND_KEYS.has(k)) bad.push(`bounds.${k}`);
378
+ }
379
+ return bad;
380
+ }
305
381
  function isExpectedDims(v) {
306
382
  if (typeof v !== "object" || v === null) return false;
307
383
  const r = v;
@@ -425,6 +501,25 @@ var COMPILER_OPTIONS = {
425
501
  skipLibCheck: true,
426
502
  allowImportingTsExtensions: true
427
503
  };
504
+ /**
505
+ * Locate the `@types/node` declarations so a part may import Node built-ins (`node:fs` to load a
506
+ * font, `node:fs/promises` to read a STEP file, etc.) without `--check` failing on the import.
507
+ * Returns the `@types` directory to use as a `typeRoots` entry. Probes the tool's own install
508
+ * first (where `@types/node` ships as a dependency), then the part's directory.
509
+ */
510
+ function nodeTypesRoot(partPath, toolDir) {
511
+ const froms = [
512
+ import.meta.url,
513
+ toolDir ? pathToFileURL(resolve(toolDir, "package.json")).href : void 0,
514
+ pathToFileURL(partPath).href
515
+ ];
516
+ for (const from of froms) {
517
+ if (!from) continue;
518
+ try {
519
+ return dirname(dirname(createRequire(from).resolve("@types/node/package.json")));
520
+ } catch {}
521
+ }
522
+ }
428
523
  function diagnosticToErrorInfo(d) {
429
524
  const text = ts.flattenDiagnosticMessageText(d.messageText, "\n");
430
525
  let where = "";
@@ -449,6 +544,11 @@ function typecheckPart(partPath, toolDir) {
449
544
  const dts = resolveBrepjsTypes(partPath, toolDir);
450
545
  const options = { ...COMPILER_OPTIONS };
451
546
  if (dts) options.paths = { brepjs: [dts] };
547
+ const typesRoot = nodeTypesRoot(partPath, toolDir);
548
+ if (typesRoot) {
549
+ options.typeRoots = [typesRoot];
550
+ options.types = ["node"];
551
+ }
452
552
  const program = ts.createProgram([partPath], options);
453
553
  const errors = [
454
554
  ...program.getSemanticDiagnostics(),
@@ -462,6 +562,39 @@ function typecheckPart(partPath, toolDir) {
462
562
  }
463
563
  //#endregion
464
564
  //#region src/verify/runPart.ts
565
+ /** Centroid of a face group's vertices, in part (Z-up, mm) coordinates. */
566
+ function faceCentroid(m, start, count) {
567
+ let x = 0;
568
+ let y = 0;
569
+ let z = 0;
570
+ for (let i = start; i < start + count; i++) {
571
+ const vi = (m.triangles[i] ?? 0) * 3;
572
+ x += m.vertices[vi] ?? 0;
573
+ y += m.vertices[vi + 1] ?? 0;
574
+ z += m.vertices[vi + 2] ?? 0;
575
+ }
576
+ const n = count || 1;
577
+ return [
578
+ x / n,
579
+ y / n,
580
+ z / n
581
+ ];
582
+ }
583
+ function buildMaterialMap(m, spec) {
584
+ if (spec === void 0 || spec === null) return {};
585
+ if (typeof spec !== "function" && (typeof spec !== "object" || Array.isArray(spec))) return { warning: "export const materials must be a function or a material object — ignored" };
586
+ const sel = spec;
587
+ const select = typeof sel === "function" ? sel : () => sel;
588
+ const map = /* @__PURE__ */ new Map();
589
+ for (const fg of m.faceGroups) {
590
+ const mat = select({
591
+ faceId: fg.faceId,
592
+ center: faceCentroid(m, fg.start, fg.count)
593
+ });
594
+ if (mat) map.set(fg.faceId, mat);
595
+ }
596
+ return map.size > 0 ? { map } : {};
597
+ }
465
598
  async function loadPart(modulePath) {
466
599
  try {
467
600
  return await import(pathToFileURL(modulePath).href);
@@ -485,7 +618,13 @@ function toErrorInfo(prefix, e) {
485
618
  code: e.code,
486
619
  suggestion: e.suggestion
487
620
  };
488
- if (e instanceof Error) return { message: `${prefix}: ${e.message}` };
621
+ if (e instanceof Error) {
622
+ const code = e.message.match(/\[[A-Z][A-Z0-9_]*\]\s+([A-Z][A-Z0-9_]+):/)?.[1];
623
+ return code ? {
624
+ message: `${prefix}: ${e.message}`,
625
+ code
626
+ } : { message: `${prefix}: ${e.message}` };
627
+ }
489
628
  return { message: `${prefix}: ${String(e)}` };
490
629
  }
491
630
  function finalize(result) {
@@ -565,11 +704,22 @@ async function runPart(modulePath, opts = {}) {
565
704
  if (isExpectedDims(mod.expected)) {
566
705
  const expected = mod.expected;
567
706
  result.assertions = evaluateExpected(expected, result.measurements);
707
+ const unknown = unknownExpectedKeys(expected);
708
+ if (unknown.length > 0) pushError(result, {
709
+ message: `expected has unrecognized keys (ignored): ${unknown.join(", ")}. Valid keys: volume, area, tolerancePct, bounds.{xMin,xMax,yMin,yMax,zMin,zMax}.`,
710
+ code: "EXPECTED_UNKNOWN_KEY"
711
+ });
568
712
  }
569
713
  let glb;
570
714
  let step;
571
715
  if (opts.glb) try {
572
- glb = exportGlb(mesh(shape));
716
+ const shapeMesh = mesh(shape);
717
+ const { map, warning } = buildMaterialMap(shapeMesh, mod.materials);
718
+ if (warning) pushError(result, {
719
+ message: `materials: ${warning}`,
720
+ code: "MATERIALS_IGNORED"
721
+ });
722
+ glb = map ? exportGlb(shapeMesh, { materials: map }) : exportGlb(shapeMesh);
573
723
  } catch (e) {
574
724
  pushError(result, toErrorInfo("exportGlb", e));
575
725
  }