partforge 0.79.0 → 0.81.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/bin/cli.js CHANGED
@@ -15,8 +15,6 @@ import { bootOcctKernel } from "../src/testing/occt.js";
15
15
  import { bootManifoldKernel } from "../src/testing/manifold.js";
16
16
  import { measure } from "../src/framework/oracle/measure.js";
17
17
  import { verify } from "../src/framework/oracle/verify.js";
18
- import { describe as describeMesh } from "../src/framework/oracle/describe.js";
19
- import { compactDescribe } from "../src/framework/oracle/describe/report.js";
20
18
  import { renderViews } from "../src/testing/render.js";
21
19
  import {
22
20
  createPickServer, requestPicks, formatPickResult,
@@ -27,6 +25,25 @@ import { matchPattern } from "../src/testing/error-patterns.js";
27
25
  import { lintPart } from "../src/lint.js";
28
26
 
29
27
  const die = (msg) => { console.error(msg); process.exit(1); };
28
+
29
+ // The semantic mesh oracle (`describe`) lives in a separate, closed package — this
30
+ // CLI names it only here, and resolves it at CALL time so every other verb works
31
+ // without it installed. PARTFORGE_ORACLE overrides the specifier (a path or module
32
+ // name) — how the oracle package's own repo points this CLI at its working tree.
33
+ const ORACLE_PACKAGE = "@pixiteapps/partforge-oracle";
34
+ async function loadOracle() {
35
+ const spec = process.env.PARTFORGE_ORACLE ?? ORACLE_PACKAGE;
36
+ const target = spec.startsWith(".") || spec.startsWith("/")
37
+ ? pathToFileURL(resolve(process.cwd(), spec)).href
38
+ : spec;
39
+ try {
40
+ return await import(target);
41
+ } catch (e) {
42
+ die(`describe needs the mesh-oracle package (${ORACLE_PACKAGE}), which is not installed.\n` +
43
+ `Install it from the private registry (or set PARTFORGE_ORACLE to a local path) and retry.\n` +
44
+ `(import of ${spec} failed: ${e?.message ?? e})`);
45
+ }
46
+ }
30
47
  const USAGE = "usage: partforge <lint|measure|render|describe|pick-serve|pick> …";
31
48
 
32
49
  // Crash contract (issue #27): with --json, a thrown error becomes structured
@@ -210,6 +227,7 @@ const commands = {
210
227
  die(`describe: "${importName}" is not a declared import of ${partPath} ` +
211
228
  `(have: ${Object.keys(part.imports ?? {}).join(", ") || "none"})`);
212
229
  }
230
+ const { describe: describeMesh, compactDescribe } = await loadOracle();
213
231
  const kernel = await bootKernel(part);
214
232
  const solid = kernel.import(importName);
215
233
  const report = describeMesh(kernel, solid, {
@@ -222,7 +240,7 @@ const commands = {
222
240
  writeFileSync(flags.out, JSON.stringify(report, null, 2));
223
241
  }
224
242
  if (flags.json) console.log(JSON.stringify(report, null, 2));
225
- else printDescribe(report, { surfaces: !!flags.surfaces });
243
+ else printDescribe(report, { surfaces: !!flags.surfaces }, compactDescribe);
226
244
  if (flags.out) console.log(`\nwrote ${flags.out}`);
227
245
  // A closed-set error exits non-zero; LOW COVERAGE does not. Coverage is a finding
228
246
  // the caller must be able to read, and an exit code that conflated the two would
@@ -448,7 +466,7 @@ function printVerify(v) {
448
466
  // opts back in; `--json` always has everything. Reads the SAME compactDescribe() output
449
467
  // a model reads (not a hand-rolled subset), so what a human sees here and what an agent
450
468
  // sees over `--json` cannot drift apart.
451
- function printDescribe(report, { surfaces }) {
469
+ function printDescribe(report, { surfaces }, compactDescribe) {
452
470
  if (report.error) {
453
471
  console.error(`describe: ${report.error}${report.detail ? ` — ${report.detail}` : ""}`);
454
472
  return;
@@ -1496,165 +1496,29 @@ build: (k, p, d) => {
1496
1496
 
1497
1497
  ## Describing an imported mesh
1498
1498
 
1499
- Before you write the parametric rebuild, ask the mesh itself what's in it: `partforge
1500
- describe <part-module>#<importName>` reads an already-declared import (`k.import`'s own
1501
- name, not a bare file path the `imports` field is how the file gets to a kernel at
1502
- all, so it's how `describe` finds it too) and emits a semantic feature report — holes,
1503
- bosses, pockets, extrusions, patterns, symmetryrather than a triangle soup. On
1504
- `import-demo.js`, whose `scan` import is a plain 20×14×8mm block:
1499
+ `npx partforge describe <part-module>#<importName>` reads an already-declared import
1500
+ and emits a semantic feature report holes, bosses, pockets, extrusions, patterns,
1501
+ symmetry rather than a triangle soup, so an agent can rebuild an STL parametrically.
1502
+
1503
+ The engine behind it the semantic mesh oracle is **not part of this package**: it
1504
+ ships as a separate, closed package (`@pixiteapps/partforge-oracle`) whose docs carry
1505
+ the full report contract (the two report shapes, feature vocabulary, `volumeShare`
1506
+ semantics, coverage scores, budget behavior, and the closed error set). This repo
1507
+ keeps only the seams:
1508
+
1509
+ - **CLI** — `partforge describe` resolves the oracle package at call time and prints
1510
+ an install pointer when it is absent (`PARTFORGE_ORACLE` overrides the module
1511
+ specifier, which is how the oracle's own repo points this CLI at its working tree).
1512
+ - **Worker** — the `describe` job runs whatever `runWorker(part, { loadOracle })`
1513
+ injected; without a loader it answers a structured
1514
+ `{error: "oracle-unavailable"}` report (see
1515
+ [ERROR-PATTERNS.md#describe-oracle-unavailable](ERROR-PATTERNS.md#describe-oracle-unavailable)),
1516
+ never a stall. The loader resolves the oracle barrel: `describe`, `describeMemo`,
1517
+ `compactDescribe`.
1518
+ - **Helpers** — the oracle package peer-depends on this one and consumes
1519
+ `partforge/oracle`'s mesh/BVH helpers and file parsers (`bounds`, `meshArea`,
1520
+ `meshTriangles`, `parseStl`, `parse3MF`); those exports are part of its contract.
1505
1521
 
1506
- ```
1507
- $ npx partforge describe src/parts/import-demo.js#scan
1508
-
1509
- scan — 12 triangles, 20.00 x 14.00 x 8.00 mm, +Z up
1510
-
1511
- Features (1): share = fraction of part volume this feature accounts for — a size measure, not certainty
1512
- f0 extrusion 8.000 share 100.0%
1513
-
1514
- Score: 100.0% surface area explained, 100.0% volume reconstructed (residual xor 0.00% of volume)
1515
- Residual: 0.00% of area in 0 region(s)
1516
- ```
1517
-
1518
- **Two report shapes, and which one is authoritative.** `describe()` (`src/framework/
1519
- oracle/describe.js`) returns the FULL report — every surface, every fitted edge, every
1520
- feature at full precision — and that is the archive: `--json` prints it whole, and
1521
- `--out <file>` writes it whole. `compactDescribe()` (`src/framework/oracle/describe/
1522
- report.js`) derives a second, smaller shape from it — features, patterns, symmetry,
1523
- score, residual, and the suggested rebuild, with `surfaces`/`edges` reduced to counts —
1524
- and that is what a model reads, and what the terminal printer above reads too: **the
1525
- same `compactDescribe()` output**, not a hand-rolled subset, so what a human sees in the
1526
- default summary and what an agent gets from a cloud turn cannot drift apart. The full
1527
- report is authoritative for the facts (a fitted surface's rms, an edge's exact radius);
1528
- the compact report is authoritative for what's worth paying attention to.
1529
-
1530
- **Computed once, reused for the session.** `describe` depends on nothing but the mesh's
1531
- own bytes — no part params, no derived state — so unlike `measure`/`verify` it never
1532
- needs to re-run on every parameter edit. The worker keys its memo on the import's
1533
- content digest (`kernel._importDigest(name)`), so re-describing the same file, or asking
1534
- again mid-session while you iterate on the parametric rebuild, is free after the first
1535
- call. The CLI doesn't carry a memo across process invocations — each `partforge
1536
- describe` run is its own process — but within one browser session or one long-running
1537
- tool loop, the mesh is segmented once.
1538
-
1539
- **`--surfaces` and `--json`, and why the default elides.** A hand-modelled block
1540
- segments to six surfaces; a real 24k-triangle CAD export segments to hundreds. Printing
1541
- every one of them by default would bury the two or three that actually matter under
1542
- exactly the noise this oracle exists to remove — so the plain-text summary shows
1543
- features, patterns, symmetry, score, and residual only. Pass `--surfaces` to add the
1544
- surface table back (`s0 plane area … rms …`, one line per fitted patch); pass `--json`
1545
- to get everything, always, structured — the mode an agent should default to over
1546
- scraping the text summary. `--out <file>` writes the full report to disk independently
1547
- of either flag, so you can capture it once and read it multiple ways.
1548
-
1549
- **Low coverage exits zero — it's a finding, not a failure.** A poor segmentation (a
1550
- dome, a free-form scan, anything the four feature detectors can't reconstruct) is
1551
- exactly the situation an agent most needs to see, not one that should make itself
1552
- harder to see. `describe` treats "the shape isn't well explained" and "the command
1553
- failed" as different questions: a `LOW COVERAGE` banner at the top of the summary (and
1554
- `compactDescribe()`'s own `warning` field in `--json` output) is how a poor result
1555
- announces itself, and the process still exits 0. Only a genuine closed-set error — the
1556
- mesh couldn't be read or segmented at all — exits 1. Coverage is gated on the WORSE of
1557
- two numbers, both always printed rather than blended into one: `explainedArea` (how much
1558
- of the mesh's *surface* a fitted primitive accounts for) and `explainedVolumeFraction`
1559
- (how much of the part's actual *shape* the accepted features reconstruct). They can
1560
- diverge totally — a dome segments to ~100% surface area (it fits a sphere cleanly) and
1561
- 0% volume (a sphere isn't a candidate-eligible feature type for any detector) — so
1562
- printing only one of them, or an average, would hide precisely the gap the banner exists
1563
- to catch. Each feature's own score is named `volumeShare`, never "confidence": it is the
1564
- fraction of the part's volume that feature accounts for — a measure of *size* — so a
1565
- small-but-certain feature (a precisely-fitted 3mm hole in a large plate) legitimately
1566
- reports a small share. Read a feature's fitted surface `rms`/`maxDev` (under `--surfaces`
1567
- or `--json`) for an actual certainty signal. The one-line hint at the `Features (N):`
1568
- header states the size-not-certainty point at its point of use; the longer explanation
1569
- (`score.note` — both this and the `explainedArea`/`explainedVolumeFraction` distinction
1570
- above, spelled out for a reader who never gets this far) stays `--json`-only, since
1571
- printing it in full under `Score:` used to be the single largest visual element in every
1572
- plain-text run — measured at 36-43% of a typical report's line count, bigger than the
1573
- feature list, the banners, and the score line combined.
1574
-
1575
- **Why a null `volumeShare` isn't always the same story.** A feature can carry
1576
- `volumeShare: null` for three different reasons, distinguished by its sibling field
1577
- `volumeShareReason` (`--surfaces`/`--json`; the text view renders it as `share n/a
1578
- (<reason>)`):
1579
-
1580
- - `"not-proposed"` — this feature TYPE is never turned into an acceptance candidate at
1581
- all. Fillets, chamfers, revolves and shells fall here today; they're still reported as
1582
- features, just not yet reconstructable through this pipeline.
1583
- - `"budget"` — a candidate WAS proposed, but the search ran out of `--budget` before
1584
- reaching it. Raise `--budget` and re-describe; the feature may resolve to a real share
1585
- once given the chance.
1586
- - `"rejected"` — a candidate was proposed, built, and evaluated, but never won a round
1587
- (`accept.js`'s `MIN_GAIN_FRACTION` gate, or simply never the best candidate available).
1588
- Raising `--budget` will not change this outcome — the feature genuinely doesn't fit
1589
- well enough to explain a meaningful share of the part.
1590
-
1591
- `"budget"` and `"rejected"` are the two that matter most to get right, and the
1592
- distinction is real, not cosmetic: "try a bigger budget" and "this doesn't fit" are
1593
- different next actions for whoever is rebuilding the part.
1594
-
1595
- **What `describe` reconstructs today, and what it doesn't.** The FACTS layer — surfaces,
1596
- arcs, holes, dress-ups, sweeps, patterns, symmetry — covers the tool's full detection
1597
- vocabulary; a feature can be *reported* there regardless of shape. The RECONSTRUCTION
1598
- score (`explainedVolumeFraction`, and every accepted feature's own `volumeShare`) is
1599
- narrower: `toCandidate` only proposes box and cylinder footprints as acceptance
1600
- candidates today, so it currently reconstructs prismatic parts built from roughly fewer
1601
- than eight box/cylinder features well, and does not yet reconstruct round bosses,
1602
- revolves, fillets, chamfers, or shells — those are detected and reported (with
1603
- `volumeShareReason: "not-proposed"`) but never turned into a candidate that could win
1604
- volume back. Measured directly on this repo's own reference parts: `demo.js`
1605
- reconstructs 65.7% of its volume, `filleted-box.js` 36.2%, `bracket.js` 23.6%, a plain
1606
- tube 0.0%, and a hollow box 1.9%. The low-coverage banner fires on every one of these, so
1607
- nothing here is misreported — but a low `explainedVolumeFraction` on a turned or
1608
- feature-dense part means **"not yet reconstructable by this tool"**, not "not
1609
- understood" or "broken." Read the FACTS (features, surfaces, patterns) as the ground
1610
- truth regardless of the volume score; read the volume score as a measure of how much of
1611
- that ground truth also comes with a working, boolean-verified rebuild recipe.
1612
-
1613
- **Closed error set.** Anything short of a programming mistake comes back as
1614
- `{error: "<code>", detail, diagnostic}` rather than a thrown exception — a CLI or an
1615
- agent can act on a code far better than on a stack trace — and every code has an
1616
- ERROR-PATTERNS.md entry:
1617
-
1618
- - `not-manifold` — the mesh still has open edges after repair; see
1619
- [ERROR-PATTERNS.md#describe-not-manifold](ERROR-PATTERNS.md#describe-not-manifold).
1620
- - `too-large` — over the 400,000-triangle segmentation ceiling; see
1621
- [ERROR-PATTERNS.md#describe-too-large](ERROR-PATTERNS.md#describe-too-large).
1622
- - `empty` — the mesh has zero triangles; see
1623
- [ERROR-PATTERNS.md#describe-empty](ERROR-PATTERNS.md#describe-empty).
1624
- - `unreadable` — `solid.toMesh()` itself threw; see
1625
- [ERROR-PATTERNS.md#describe-unreadable](ERROR-PATTERNS.md#describe-unreadable).
1626
-
1627
- `not-manifold` and `empty` describe real states `describe()` itself checks for and can
1628
- return through any caller that hands it a `Solid` directly, but through the CLI they are
1629
- largely theoretical: `kernel.import()`'s own registration validation rejects an empty or
1630
- a wide-open (non-manifold) mesh *before* a `Solid` ever exists to describe, so
1631
- `partforge describe` on such a file fails earlier, as a generic import error (the
1632
- `crash()` path every other verb shares — `describe: import "x": mesh is not a solid
1633
- after repair…`) rather than as a structured `{error: "not-manifold"}` / `{error:
1634
- "empty"}` report. See
1635
- [ERROR-PATTERNS.md#import-mesh-not-solid](ERROR-PATTERNS.md#import-mesh-not-solid) for
1636
- that failure. `describe-unreadable`'s own ERROR-PATTERNS entry already draws this same
1637
- line for its own code (a `k.import` parse failure is a *different*, separately-thrown
1638
- error, not `describe-unreadable`); this is the general version of that point.
1639
-
1640
- A `budget-exceeded` report (the acceptance loop ran out of boolean attempts before the
1641
- residual converged) is not one of these — it's a `warning` on an otherwise-valid report,
1642
- same tier as low coverage, not a reason to exit non-zero; raise `--budget` and re-run.
1643
- `compactDescribe()` surfaces it exactly where the low-coverage banner lives, and shows
1644
- both together if a report happens to earn both — an exhausted budget is exactly the kind
1645
- of run that also leaves coverage low, and neither warning is allowed to mask the other.
1646
- See [ERROR-PATTERNS.md#describe-budget-exceeded](ERROR-PATTERNS.md#describe-budget-exceeded).
1647
-
1648
- **The loop this completes.** `describe` exists to feed a specific workflow, the one
1649
- "Importing geometry" above sets up and this closes: `describe` the import to get its
1650
- feature list and a proposed reconstruction (`suggestion` in the full report); write a
1651
- parametric part from that proposal; bind the rebuild's sub-part to the import with
1652
- `reference: "scan"`; and let `verify`'s `ref*` gates (`refXorVolume`, `refVolumeDeltaPct`,
1653
- `refBboxDelta` — "The `reference` field and the deviation gate", above) hold the rebuild
1654
- to the scan on every future `measure`/`verify` run, long after the one-time `describe`
1655
- call that suggested it. `describe` proposes; `verify` keeps you honest.
1656
-
1657
- ---
1658
1522
 
1659
1523
  ## Wiring a part into a runnable app
1660
1524
 
@@ -601,6 +601,12 @@ between the Manifold preview and the OCCT STEP export.
601
601
 
602
602
  The same channel carries every other degrade in a build: an `extrude` rim bevel reduced or skipped (`extrude bevel <b> …`), a `roundedBox` rim radius clamped to `round.side`, and the `Shape2D` corner-op clamps in the two entries above. A build result's `warnings` is the complete list of what the part asked for and did not get.
603
603
 
604
+ ## describe-oracle-unavailable
605
+
606
+ - **Symptom:** A `describe` job answers `{"error": "oracle-unavailable"}`, or `partforge describe` exits with "describe needs the mesh-oracle package".
607
+ - **Cause:** The semantic mesh oracle is a separate, closed package (`@pixiteapps/partforge-oracle`), and this app or shell doesn't have it: the worker was started without `runWorker(part, { loadOracle })`, or the CLI could not import the package.
608
+ - **Fix:** Install the oracle package from the private registry and inject it — `runWorker(part, { loadOracle: () => import("@pixiteapps/partforge-oracle") })` in the app's worker file, or plain `npm install` for the CLI. In the oracle package's own repo, set `PARTFORGE_ORACLE` to a local path instead. Every other job (generate, export, inspect) works without it.
609
+
604
610
  ## describe-not-manifold
605
611
 
606
612
  - **Symptom:** `describe` returns `{"error": "not-manifold"}`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "partforge",
3
- "version": "0.79.0",
3
+ "version": "0.81.0",
4
4
  "description": "Turn a declarative part definition into a parametric-CAD web app (three.js + Manifold/Replicad). Requires a Vite-based consumer.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -12,23 +12,29 @@ import { safeName } from "./safe-name.js";
12
12
  import { exportSubParts, resolveParams, buildPosed } from "./part-model.js";
13
13
 
14
14
  // The oracle loads LAZILY, per job family, never at worker boot. It is the largest
15
- // JS payload in the worker's graph (measure/verify/build, silhouette/match, and the
16
- // describe stack), and only the `inspect` and `describe` jobs run any of it — the
17
- // generate/export hot path touches none. Each family below is a literal dynamic
18
- // import(), which Vite splits into its own chunk under `worker.format: "es"` (this
19
- // repo's config and partforge-cloud's both), so a user who never runs an oracle job
20
- // never downloads or parses one. The module loader caches the namespace after the
21
- // first await, so repeat jobs pay a resolved-promise tick, not a re-fetch.
15
+ // JS payload in the worker's graph (measure/verify/build and silhouette/match), and
16
+ // only the `inspect` job runs any of it — the generate/export hot path touches none.
17
+ // Each family below is a literal dynamic import(), which Vite splits into its own
18
+ // chunk under `worker.format: "es"` (this repo's config and partforge-cloud's both),
19
+ // so a user who never runs an oracle job never downloads or parses one. The module
20
+ // loader caches the namespace after the first await, so repeat jobs pay a
21
+ // resolved-promise tick, not a re-fetch.
22
22
  // test/worker-layering.test.js's eager-closure guard holds this in place.
23
23
  const loadInspect = () => Promise.all([
24
24
  import("./oracle/build.js"),
25
25
  import("./oracle/measure.js"),
26
26
  import("./oracle/verify.js"),
27
27
  ]);
28
- const loadDescribe = () => Promise.all([
29
- import("./oracle/describe.js"),
30
- import("./oracle/describe/report.js"),
31
- ]);
28
+
29
+ // The DESCRIBE stack is not part of this package at all: the semantic mesh oracle
30
+ // lives in a separate, closed package, and this open framework never names it. A
31
+ // host that has it INJECTS a loader — `runWorker(part, { loadOracle })`, threaded
32
+ // here as `opts.loadOracle`, resolving to the oracle package's barrel (describe,
33
+ // describeMemo, compactDescribe). Injection rather than a bare import specifier is
34
+ // deliberate: a literal `import("@scope/pkg")` in open source would fail every
35
+ // downstream Vite build where the package isn't installed, while an injected thunk
36
+ // is simply absent — and an absent oracle answers the job with the structured
37
+ // `oracle-unavailable` report below instead of stalling or throwing.
32
38
 
33
39
  // One describe memo for the life of this worker, created alongside the stack's first
34
40
  // load. Deliberately NOT swept on setPart the way solid-cache is: describe is pure in
@@ -417,7 +423,24 @@ export async function handle(kernel, part, msg, post, opts = {}) {
417
423
  // Manifold only, and not by choice on this path: mesh imports on OCCT are never
418
424
  // attempted, so a describe job posted to an OCCT worker is a routing bug, not a
419
425
  // fallback opportunity. It surfaces as an ordinary error rather than a reroute.
420
- const [{ describe: describeMesh, describeMemo }, { compactDescribe }] = await loadDescribe();
426
+ if (!opts.loadOracle) {
427
+ // Same closed-set, returned-not-thrown error contract describe itself keeps
428
+ // (its errors are findings, not crashes) — shaped as the structured triple the
429
+ // whole repo emits, so a caller can act on the code. ERROR-PATTERNS.md#describe-
430
+ // oracle-unavailable carries the fix.
431
+ post({ type: "describe-report", report: {
432
+ error: "oracle-unavailable",
433
+ detail: "this app was built without the mesh oracle package",
434
+ diagnostic: {
435
+ cause: "the describe job needs the closed oracle package, and no loadOracle loader was injected into runWorker",
436
+ location: `describe "${msg.importName}"`,
437
+ correctiveAction: "install the oracle package and pass runWorker(part, { loadOracle: () => import(...) }); see ERROR-PATTERNS.md#describe-oracle-unavailable",
438
+ },
439
+ source: { name: msg.importName, digest: null },
440
+ } });
441
+ return;
442
+ }
443
+ const { describe: describeMesh, describeMemo, compactDescribe } = await opts.loadOracle();
421
444
  const solid = kernel.import(msg.importName); // throws on an unknown name
422
445
  // `_importDigest` is the backend's existing underscore side-channel (KERNEL-CONTRACT
423
446
  // "Conformance classes") — the same digest already folded into every import cache key.
@@ -36,7 +36,10 @@ async function occtKernel() {
36
36
  return createOcctKernel(replicad);
37
37
  }
38
38
 
39
- export function runWorker(part) {
39
+ // `opts.loadOracle` — the injection seam for the closed mesh-oracle package (see
40
+ // jobs.js's describe branch): a thunk resolving to the oracle barrel. Apps without
41
+ // the package simply omit it and describe jobs answer `oracle-unavailable`.
42
+ export function runWorker(part, opts = {}) {
40
43
  const backend = self.name === "occt" ? "occt" : "manifold";
41
44
  let manifold = null; // { preview, print }
42
45
  let occt = null;
@@ -99,7 +102,7 @@ export function runWorker(part) {
99
102
  const kernel = await kernelFor(job.data);
100
103
  // handle() declares each message's transferables (the big binary buffers).
101
104
  const post = (m, transfer = []) => postMessage(m, transfer);
102
- if (job.epoch === null) { await handle(kernel, job.part, job.data, post, { importMeshes }); continue; }
105
+ if (job.epoch === null) { await handle(kernel, job.part, job.data, post, { importMeshes, loadOracle: opts.loadOracle }); continue; }
103
106
  const isStale = () => job.epoch !== epoch;
104
107
  // Post gate. The boundary check cannot catch a generate that goes stale during
105
108
  // its FINAL sub-part — there is no boundary after it — nor a single-sub-part
@@ -108,7 +111,7 @@ export function runWorker(part) {
108
111
  // contract simple: a `meshes` post is current as of the moment it is posted.
109
112
  const gated = (m, transfer = []) =>
110
113
  (m.type === "meshes" && isStale() ? post({ type: "superseded" }) : post(m, transfer));
111
- await handle(kernel, job.part, job.data, gated, { isStale, importMeshes });
114
+ await handle(kernel, job.part, job.data, gated, { isStale, importMeshes, loadOracle: opts.loadOracle });
112
115
  } catch (err) {
113
116
  // Same shape jobs.js posts for a failed build, so hosts need no new branch.
114
117
  // Carry the job's jobId when it has one (capture/export are correlated by it):
package/src/oracle.js CHANGED
@@ -2,26 +2,30 @@
2
2
  //
3
3
  // This is the SEAM between the oracle and everything that consumes it. The same
4
4
  // modules serve three callers: the geometry worker lazy-loads them per job family
5
- // (see jobs.js — an `inspect` pulls measure/verify/build, a `describe` pulls the
6
- // describe stack, and the generate/export hot path pulls none), the CLI and Node
7
- // harnesses import them here directly, and partforge/testing re-exports this whole
8
- // surface so an existing downstream import keeps working. Everything below is
9
- // DOM-free, three-free and node:-free — test/oracle-entry.test.js walks the closure
10
- // and holds that, so the entry stays importable from a worker, a browser, or Node
11
- // alike. If the oracle ever moves to its own package, this file is the boundary
12
- // consumers are already importing through.
5
+ // (see jobs.js — an `inspect` pulls measure/verify/build, and the generate/export
6
+ // hot path pulls none), the CLI and Node harnesses import them here directly, and
7
+ // partforge/testing re-exports this whole surface so an existing downstream import
8
+ // keeps working. Everything below is DOM-free, three-free and node:-free —
9
+ // test/oracle-entry.test.js walks the closure and holds that, so the entry stays
10
+ // importable from a worker, a browser, or Node alike.
11
+ //
12
+ // The SEMANTIC MESH ORACLE (`describe`) is NOT here: it lives in its own closed
13
+ // package, which peer-depends on this one and consumes exactly this entry — the
14
+ // mesh/BVH helpers and file parsers below are exported for it. The framework
15
+ // reaches it only through injection (`runWorker(part, { loadOracle })`, jobs.js)
16
+ // and the CLI resolves it at call time; neither ever bundles it.
13
17
  export { assemblyGaps, meshGaps } from "./framework/oracle/gaps.js";
14
- export { meshVolume, bboxSize } from "./framework/oracle/mesh.js";
18
+ export { meshVolume, bboxSize, bounds, meshArea } from "./framework/oracle/mesh.js";
15
19
  export { buildView } from "./framework/oracle/build.js";
16
20
  export { measure } from "./framework/oracle/measure.js";
17
21
  export { verify } from "./framework/oracle/verify.js";
18
- export { buildBVH } from "./framework/oracle/bvh.js";
22
+ export { buildBVH, meshTriangles } from "./framework/oracle/bvh.js";
19
23
  export { minWall } from "./framework/oracle/min-wall.js";
24
+ // Mesh file parsers — the import pipeline's own readers, browser-safe pure
25
+ // functions; the oracle package's corpus tests read real files through them.
26
+ export { parseStl } from "./framework/geometry/stl-parse.js";
27
+ export { parse3MF } from "./framework/geometry/threemf-parse.js";
20
28
  // Silhouette match scoring — the `inspect` job scores `matchTargets` with exactly
21
29
  // these, re-exported so a downstream harness can reproduce a score outside the job loop.
22
30
  export { MATCH_VIEWS, rasterizeMeshMask, rasterizeRingsMask } from "./framework/oracle/silhouette.js";
23
31
  export { matchMasks, matchViews } from "./framework/oracle/match.js";
24
- // The semantic mesh oracle — what the `describe` job runs.
25
- export { describe, describeMemo, DESCRIBE_ERRORS } from "./framework/oracle/describe.js";
26
- export { compactDescribe, LOW_COVERAGE } from "./framework/oracle/describe/report.js";
27
- export { DESCRIBE_LIMITS } from "./framework/oracle/describe/limits.js";
package/types/oracle.d.ts CHANGED
@@ -2,9 +2,9 @@
2
2
  //
3
3
  // The declarations themselves live in testing.d.ts, where this surface was first
4
4
  // published; this file re-exports exactly the names src/oracle.js exports, plus the
5
- // report/mask/gap types a caller needs to annotate results. If the oracle ever moves
6
- // to its own package, the declarations migrate here and testing.d.ts re-exports
7
- // instead — the direction flips, the names don't.
5
+ // report/mask/gap types a caller needs to annotate results. The semantic mesh
6
+ // oracle (`describe`) is its own closed package now its types ship with it, and
7
+ // the direction of this seam is what lets that package consume these helpers.
8
8
  export type { GeometryKernel, Mesh, PartDefinition, ResolvedParams, Solid } from "./testing.js";
9
9
  export {
10
10
  // measurement + verification
@@ -12,16 +12,11 @@ export {
12
12
  type MeasureReport, type SubPartFacts, type AggregateFacts, type BuiltSubPart,
13
13
  type VerifyReport, type VerifyCaseResult, type VerifyCheck, type CheckStatus,
14
14
  // mesh facts, gaps, BVH, min wall
15
- meshVolume, bboxSize, assemblyGaps, meshGaps, buildBVH, minWall,
15
+ meshVolume, bboxSize, bounds, meshArea, assemblyGaps, meshGaps, buildBVH, meshTriangles, minWall,
16
16
  type Gap, type BVH,
17
+ // mesh file parsers (the import pipeline's own readers)
18
+ parseStl, parse3MF,
17
19
  // silhouette match scoring
18
20
  MATCH_VIEWS, rasterizeMeshMask, rasterizeRingsMask, matchMasks, matchViews,
19
21
  type SilhouetteMask, type MatchScores, type MatchDelta,
20
- // the semantic mesh oracle
21
- describe, describeMemo, compactDescribe,
22
- DESCRIBE_ERRORS, DESCRIBE_LIMITS, LOW_COVERAGE,
23
- type DescribeReport, type DescribeCompactReport, type DescribeFailure,
24
- type DescribeSurface, type DescribeArc, type DescribeFeature, type DescribePattern,
25
- type DescribeResidualRegion, type DescribeSuggestion, type DescribeSuggestionStep,
26
- type DescribeScore, type DescribeTruncated, type Snapped,
27
22
  } from "./testing.js";