partforge 0.80.0 → 0.82.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.
Files changed (32) hide show
  1. package/bin/cli.js +50 -4
  2. package/docs/AUTHORING-PARTS.md +99 -161
  3. package/docs/ERROR-PATTERNS.md +6 -0
  4. package/package.json +1 -1
  5. package/src/framework/geometry/probe.js +21 -8
  6. package/src/framework/jobs.js +35 -12
  7. package/src/framework/lint/rules-build.js +12 -2
  8. package/src/framework/lint/rules-shape.js +19 -0
  9. package/src/framework/oracle/measure.js +87 -0
  10. package/src/framework/oracle/verify.js +4 -1
  11. package/src/framework/worker.js +6 -3
  12. package/src/oracle.js +18 -14
  13. package/src/parts/import-demo.js +22 -1
  14. package/types/oracle.d.ts +6 -11
  15. package/types/testing.d.ts +15 -177
  16. package/types/worker.d.ts +10 -2
  17. package/src/framework/oracle/describe/accept.js +0 -188
  18. package/src/framework/oracle/describe/features/dressups.js +0 -173
  19. package/src/framework/oracle/describe/features/holes.js +0 -129
  20. package/src/framework/oracle/describe/features/prismatic.js +0 -454
  21. package/src/framework/oracle/describe/features/sweeps.js +0 -233
  22. package/src/framework/oracle/describe/fit.js +0 -535
  23. package/src/framework/oracle/describe/hints.js +0 -91
  24. package/src/framework/oracle/describe/limits.js +0 -19
  25. package/src/framework/oracle/describe/patterns.js +0 -494
  26. package/src/framework/oracle/describe/ransac.js +0 -391
  27. package/src/framework/oracle/describe/report.js +0 -217
  28. package/src/framework/oracle/describe/segment.js +0 -498
  29. package/src/framework/oracle/describe/snap.js +0 -83
  30. package/src/framework/oracle/describe/surface-graph.js +0 -396
  31. package/src/framework/oracle/describe/topology.js +0 -121
  32. package/src/framework/oracle/describe.js +0 -643
@@ -1,6 +1,7 @@
1
1
  import { buildView } from "./build.js";
2
2
  import { cachedBVH } from "./bvh.js";
3
3
  import { assemblyOverlaps } from "../assembly.js";
4
+ import { resolveParams } from "../part-model.js";
4
5
  import { meshGaps, pairKey, CONTACT_EPS, GAP_THRESHOLD } from "./gaps.js";
5
6
  import { bounds, meshArea, meshCentroid } from "./mesh.js";
6
7
  import { minWall, DIAGNOSTIC_SAMPLES } from "./min-wall.js";
@@ -12,6 +13,80 @@ const unionBounds = (list) => list.reduce(
12
13
  { min: [Infinity, Infinity, Infinity], max: [-Infinity, -Infinity, -Infinity] },
13
14
  );
14
15
 
16
+ // ── probes ──────────────────────────────────────────────────────────────────
17
+ // Part-declared measurements: `probes: { name: (k, p, d) => Solid | JSON }`,
18
+ // pure functions with build's exact contract but whose result lands in the
19
+ // REPORT instead of the scene. The instrument a rebuild-against-reference
20
+ // workflow needs — before this, getting a cross-section's numbers out of the
21
+ // pipeline meant authoring throwaway `exportable: false` sub-parts and fishing
22
+ // their facts out of the sub-part list (the "Probes" feedback report).
23
+ // A Solid anywhere in the return value (duck-typed on volume+toMesh, the two
24
+ // queries the facts need) is replaced by a fact object; scalars/arrays/objects
25
+ // pass through; a throw becomes `{ error }` — probes are instrumentation, so
26
+ // they never crash the measurement and never gate `ok`.
27
+
28
+ const isSolid = (v) => v !== null && typeof v === "object"
29
+ && typeof v.volume === "function" && typeof v.toMesh === "function";
30
+
31
+ function solidProbeFacts(solid) {
32
+ const mesh = solid.toMesh();
33
+ // Empty = the probe's boolean found nothing (a slab that misses the part).
34
+ // A first-class answer, not degenerate infinite bounds: "the reference has no
35
+ // material here" is exactly what a localizing probe is asked.
36
+ const empty = typeof solid.isEmpty === "function" ? solid.isEmpty() : mesh.triangles === 0;
37
+ if (empty) {
38
+ return { empty: true, bbox: null, bounds: null, centerOfMass: null,
39
+ volume: 0, surfaceArea: 0, triangleCount: 0, watertight: null, holes: null };
40
+ }
41
+ const b = bounds(mesh.positions);
42
+ return {
43
+ empty: false,
44
+ bbox: size(b),
45
+ bounds: { min: b.min, max: b.max },
46
+ centerOfMass: meshCentroid(mesh.positions, mesh.indices),
47
+ volume: solid.volume(),
48
+ surfaceArea: meshArea(mesh.positions, mesh.indices),
49
+ triangleCount: mesh.triangles,
50
+ // Mirrors the sub-part fact: answered by isEmpty where the backend has it
51
+ // (and this branch already means it said false), null where it can't say.
52
+ watertight: typeof solid.isEmpty === "function" ? true : null,
53
+ holes: typeof solid.genus === "function" ? solid.genus() : null,
54
+ };
55
+ }
56
+
57
+ // Bounded so a self-referential or absurdly deep return value can't hang the
58
+ // report; past the cap the value is summarized rather than walked.
59
+ const MAX_PROBE_VALUE_DEPTH = 4;
60
+ function resolveProbeValue(v, depth = 0) {
61
+ if (isSolid(v)) return solidProbeFacts(v);
62
+ if (v === null || typeof v !== "object") {
63
+ return typeof v === "function" ? { error: "probe returned a function — return a Solid or plain JSON" } : v;
64
+ }
65
+ if (depth >= MAX_PROBE_VALUE_DEPTH) return { error: `probe value deeper than ${MAX_PROBE_VALUE_DEPTH} levels` };
66
+ if (Array.isArray(v)) return v.map((x) => resolveProbeValue(x, depth + 1));
67
+ return Object.fromEntries(Object.entries(v).map(([key, x]) => [key, resolveProbeValue(x, depth + 1)]));
68
+ }
69
+
70
+ // Evaluate every declared probe with resolved (p, d). Reads all solid facts
71
+ // eagerly, so the caller may free the kernel's objects afterwards. Never
72
+ // throws: each probe's failure is its own `{ error }` entry.
73
+ function evaluateProbes(kernel, part, params) {
74
+ const { p, d } = resolveParams(part, params);
75
+ // Oracle-owned cache round, same reasoning as buildView's: probe geometry must
76
+ // not evict what the viewer is showing, and the next round evicts this one.
77
+ kernel.beginSubPart?.("oracle:probes");
78
+ try {
79
+ return Object.fromEntries(Object.entries(part.probes).map(([name, fn]) => {
80
+ try {
81
+ if (typeof fn !== "function") throw new Error("probe must be a function (k, p, d)");
82
+ return [name, resolveProbeValue(fn(kernel, p, d))];
83
+ } catch (e) {
84
+ return [name, { error: e?.message || String(e) }];
85
+ }
86
+ }));
87
+ } finally { kernel.endSubPart?.(); }
88
+ }
89
+
15
90
  // Headless geometric report for one view of a part (Manifold-only). Reads exact
16
91
  // solid facts (volume/genus/emptiness) and mesh facts (bbox/area/triangles), plus
17
92
  // the assembly overlap check plus pair gap distances (near misses are reported,
@@ -100,6 +175,15 @@ export function measure(kernel, part, view = Object.keys(part.views)[0], params
100
175
  };
101
176
  });
102
177
 
178
+ // Declared probes, evaluated regardless of view (they are part-level facts —
179
+ // per-view probes were exactly the annoyance this replaces) and before
180
+ // assemblyOverlaps/cleanup below frees the kernel's objects. `opts.probes:
181
+ // false` skips them: verify's per-case re-measures pass it because no gate
182
+ // reads probe values, so re-running their booleans per case buys nothing.
183
+ const probes = opts.probes !== false && part.probes && Object.keys(part.probes).length
184
+ ? evaluateProbes(kernel, part, params)
185
+ : undefined;
186
+
103
187
  // Pair surface distances from the meshes already built — no kernel dependency,
104
188
  // so this reads on OCCT too. nearMisses = the issue-#29 signal: pairs that
105
189
  // *almost* touch; overlapping pairs are excluded by name (a fully-contained
@@ -155,6 +239,9 @@ export function measure(kernel, part, view = Object.keys(part.views)[0], params
155
239
  overlaps,
156
240
  gaps,
157
241
  nearMisses,
242
+ // Present only when the part declares probes AND this run evaluated them —
243
+ // a probe error stays inside its own entry and never reaches `ok` below.
244
+ ...(probes ? { probes } : {}),
158
245
  ok: subparts.every((s) => s.watertight !== false) && overlaps.length === 0,
159
246
  };
160
247
  }
@@ -266,7 +266,10 @@ export function verify(kernel, part, { process, view, measureFn = defaultMeasure
266
266
  const key = signature(params);
267
267
  if (memo.has(key)) return memo.get(key);
268
268
  if (quick) return null; // a case the seed does not cover — reported, never built
269
- memo.set(key, measureFn(kernel, part, view, params, { minWall: needMinWall }));
269
+ // `probes: false` no gate reads probe values, so re-running their booleans
270
+ // for every case buys nothing. (A seed measured WITH probes is a superset in
271
+ // the same way a min-wall seed is: the extra key is simply never read here.)
272
+ memo.set(key, measureFn(kernel, part, view, params, { minWall: needMinWall, probes: false }));
270
273
  return memo.get(key);
271
274
  };
272
275
 
@@ -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";
@@ -1,5 +1,7 @@
1
1
  // Reference part for docs/AUTHORING-PARTS.md's "Importing geometry" section —
2
- // the worked example for BOTH import uses in one part:
2
+ // and for its "Probes" section (the `probes` block below measures the import
3
+ // live into the measure report) — the worked example for BOTH import uses in
4
+ // one part:
3
5
  // • reference — `ref` is a translucent ghost of the imported scan (never
4
6
  // exported); `body` is a parametric rebuild of the same block, bound to
5
7
  // the scan via `reference: "scan"` and held to it by the three ref*
@@ -108,6 +110,25 @@ export default {
108
110
  // socket never touches `body` (see `mountOffsetX`). "reference" is the
109
111
  // ghost-overlay view, browsed by hand or with an explicit view argument.
110
112
  views: { assembly: { label: "Assembly" }, reference: { label: "Reference overlay" } },
113
+ // Probes — measurements that land in the `measure` report instead of the
114
+ // scene (docs/AUTHORING-PARTS.md "Probes"). Pure (k, p, d) functions like
115
+ // build; never rendered, never exported, reported for every view.
116
+ probes: {
117
+ // Paired 1 mm cross-sections of the rebuild and the scan at the same X
118
+ // station — the localizing instrument for the deviation gate above: when
119
+ // refXorVolume creeps up, slide the slab along X to find WHERE the two
120
+ // solids disagree instead of guessing from one whole-part number.
121
+ midSlab: (k, p) => {
122
+ const slab = () => k.box({ min: [9.5, -50, -50], max: [10.5, 50, 50] });
123
+ return {
124
+ body: k.box({ min: [0, 0, 0], max: [p.scanW, p.scanD, p.scanH] }).intersect(slab()),
125
+ scan: k.import("scan").intersect(slab()),
126
+ };
127
+ },
128
+ // A live reading straight off the import — the numbers `defaults` were
129
+ // measured from. Plain JSON passes through the report verbatim.
130
+ scanBounds: (k) => k.import("scan").boundingBox(),
131
+ },
111
132
  verify: {
112
133
  process: "fdm-pla",
113
134
  expect: {
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";
@@ -186,6 +186,21 @@ export function meshVolume(positions: ArrayLike<number>, indices?: ArrayLike<num
186
186
  /** `[dx, dy, dz]` extent of a flat position array. */
187
187
  export function bboxSize(positions: ArrayLike<number>): [number, number, number];
188
188
 
189
+ /** World-frame axis-aligned bounds of a flat position array. */
190
+ export function bounds(positions: ArrayLike<number>): { min: [number, number, number]; max: [number, number, number] };
191
+
192
+ /** Total surface area of an indexed (or soup, when `indices` is omitted) triangle mesh, mm². */
193
+ export function meshArea(positions: ArrayLike<number>, indices?: ArrayLike<number>): number;
194
+
195
+ /** Triangles as `[v0, v1, v2]` coordinate triples, from an indexed mesh or a soup. */
196
+ export function meshTriangles(mesh: Mesh): [number, number, number][][];
197
+
198
+ /** Parse a binary or ASCII STL into a welded, indexed mesh. */
199
+ export function parseStl(bytes: Uint8Array | ArrayBuffer): { positions: Float32Array; indices: Uint32Array };
200
+
201
+ /** Parse a 3MF archive's first mesh object into a welded, indexed mesh. */
202
+ export function parse3MF(bytes: Uint8Array | ArrayBuffer): { positions: Float32Array; indices: Uint32Array };
203
+
189
204
  // --- the BVH ----------------------------------------------------------------
190
205
 
191
206
  /** A triangle BVH over one mesh — nearest ray hit, nearest point, exact mesh distance. */
@@ -454,183 +469,6 @@ export function matchViews(
454
469
  opts?: { scaleAware?: boolean },
455
470
  ): { best: ({ view: string } & MatchScores) | null; views: Record<string, number> };
456
471
 
457
- // --- describe (the semantic mesh oracle) ------------------------------------
458
-
459
- /** A raw measurement snapped to intent — never destroys the measurement. */
460
- export interface Snapped {
461
- raw: number;
462
- to: number;
463
- note: string | null;
464
- }
465
-
466
- /** One fitted surface patch. `fit` is the raw per-type fit record (fit.js). */
467
- export interface DescribeSurface {
468
- id: string;
469
- type: string;
470
- area: number;
471
- triangles: number;
472
- rms: number;
473
- maxDev: number;
474
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- shape varies by surface type (plane/cylinder/cone/torus/sphere)
475
- fit: Record<string, any>;
476
- }
477
-
478
- /** One fitted edge between two surfaces. */
479
- export interface DescribeArc {
480
- between: [string, string];
481
- convexity: "convex" | "concave" | "flat";
482
- kind: string;
483
- radius: number | null;
484
- axis: unknown;
485
- length: number;
486
- }
487
-
488
- /**
489
- * One recognised feature. Shape varies by `type` (a hole carries `diameter`/
490
- * `axis`, a fillet carries `radius`/`between`, …) — the fields every family
491
- * shares are pulled out here; the rest is read by `type`. Every field here
492
- * must be something a rebuilding agent would want (round 4 review) —
493
- * `surfaces`/`evidence` are; nothing per-triangle belongs here. describe.js
494
- * strips any such internal-plumbing field (`faceScope`, a prismatic
495
- * candidate builder's own per-triangle index map) before a feature reaches
496
- * this shape, so this catch-all is not a substitute for that discipline.
497
- */
498
- export interface DescribeFeature {
499
- id: string;
500
- key: string;
501
- type: string;
502
- /** The marginal xor-volume reduction that admitted this feature, normalised
503
- * to the source volume — the fraction of the PART'S VOLUME this feature
504
- * accounts for, not a certainty rating (see `DescribeScore`'s own note): a
505
- * small-but-certain feature legitimately reports a small share. `null` for
506
- * a type acceptCandidates never proposes (fillet, chamfer, revolve, shell). */
507
- volumeShare: number | null;
508
- /** Snapped values for whichever of diameter/depth/radius/width/thickness this feature carries. */
509
- snapped: Record<string, Snapped>;
510
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- per-type facts (axis, profile, evidence, …)
511
- [key: string]: any;
512
- }
513
-
514
- /** A repetition (grid/linear/circular) or a detected mirror plane over the feature list. */
515
- export interface DescribePattern {
516
- id: string;
517
- type: "grid" | "linear" | "circular";
518
- members: string[];
519
- axis: number[] | null;
520
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- per-type spacing/count facts
521
- [key: string]: any;
522
- }
523
-
524
- /** One connected island of mesh area no surface fit claimed. */
525
- export interface DescribeResidualRegion {
526
- triangles: number;
527
- centroid: number[];
528
- bounds: { min: number[]; max: number[] };
529
- }
530
-
531
- /** One proposed rebuild step, in the order acceptCandidates actually admitted it. */
532
- export interface DescribeSuggestionStep {
533
- op: string;
534
- explains: string[];
535
- pattern: string | null;
536
- score: number;
537
- args: Record<string, unknown>;
538
- }
539
-
540
- /** A proposed reconstruction — an interpretation, not a measurement (see `disclaimer`). */
541
- export interface DescribeSuggestion {
542
- disclaimer: string;
543
- params: Array<{ name: string; value: number; from: string }>;
544
- steps: DescribeSuggestionStep[];
545
- }
546
-
547
- /** Which of the report's capped arrays actually hit their ceiling (`DESCRIBE_LIMITS`). */
548
- export interface DescribeTruncated {
549
- surfaces: boolean;
550
- edges: boolean;
551
- features: boolean;
552
- patterns: boolean;
553
- residualRegions: boolean;
554
- suggestionSteps: boolean;
555
- }
556
-
557
- export interface DescribeScore {
558
- /** Surface coverage from segmentation: fraction of the mesh's area fitted to some surface type. */
559
- explainedArea: number;
560
- /** Shape coverage from reconstruction: fraction of the part's volume the accepted features rebuild. */
561
- explainedVolumeFraction: number;
562
- xorFraction: number;
563
- xorVolume: number;
564
- note: string;
565
- }
566
-
567
- /** A full `describe()` report — everything measured, meant for archival/`--json`. */
568
- export interface DescribeReport {
569
- source: { name: string | null; digest: string | null; triangles: number; watertight: boolean | null; units: "mm" };
570
- frame: { up: "+Z"; note: string };
571
- bounds: { min: number[]; max: number[]; size: number[] };
572
- counts: { surfaces: number; edges: number };
573
- surfaces: DescribeSurface[];
574
- edges: DescribeArc[];
575
- features: DescribeFeature[];
576
- patterns: DescribePattern[];
577
- symmetry: unknown[];
578
- residual: { areaFraction: number; regions: DescribeResidualRegion[] };
579
- score: DescribeScore;
580
- suggestion: DescribeSuggestion | null;
581
- truncated: DescribeTruncated;
582
- /** Present only when the acceptance loop hit its boolean budget before converging. */
583
- warning?: "budget-exceeded";
584
- }
585
-
586
- /** The model-facing view: capped arrays elided to counts, a coverage banner first when low. */
587
- export type DescribeCompactReport = Omit<DescribeReport, "surfaces" | "edges"> & {
588
- /** Present, and serialized FIRST, only when coverage is below `LOW_COVERAGE`. */
589
- warning?: string;
590
- };
591
-
592
- /** A closed-set failure, returned rather than thrown (spec §5's diagnostic triple). */
593
- export interface DescribeFailure {
594
- error: "not-manifold" | "too-large" | "empty" | "budget-exceeded" | "unreadable";
595
- detail: string;
596
- diagnostic: { cause: string; location: string; correctiveAction: string };
597
- source: { name: string | null; digest: string | null; [key: string]: unknown };
598
- }
599
-
600
- /** The closed set of error codes `describe()` can return. */
601
- export const DESCRIBE_ERRORS: readonly string[];
602
-
603
- /** Report array ceilings (`MAX_SURFACES`, `MAX_FEATURES`, …) — a plain-data module, no imports. */
604
- export const DESCRIBE_LIMITS: {
605
- MAX_SURFACES: number;
606
- MAX_EDGES: number;
607
- MAX_FEATURES: number;
608
- MAX_PATTERNS: number;
609
- MAX_RESIDUAL_REGIONS: number;
610
- MAX_SUGGESTION_STEPS: number;
611
- };
612
-
613
- /** Below this fraction (the worse of `explainedArea`/`explainedVolumeFraction`) `compactDescribe` prepends a warning. */
614
- export const LOW_COVERAGE: number;
615
-
616
- /** A fresh, caller-owned digest memo — scope one per worker, or per test. */
617
- export function describeMemo(): Map<string, DescribeReport>;
618
-
619
- /**
620
- * Mesh in, semantic feature report out. `solid` is a LIVE kernel `Solid` — the
621
- * kernel has no public mesh->solid constructor, so a caller reads one back via
622
- * `kernel.import(name)`. Keyed by `opts.digest` in `opts.memo` (when both are
623
- * given): the report depends on nothing but the mesh bytes, so an edit to the
624
- * part that produced the mesh can never invalidate it.
625
- */
626
- export function describe(
627
- kernel: GeometryKernel,
628
- solid: Solid,
629
- opts?: { name?: string; digest?: string; budget?: number; memo?: Map<string, DescribeReport> },
630
- ): DescribeReport | DescribeFailure;
631
-
632
- /** The full report, reduced to what a model should read: capped arrays elided to counts, low-coverage banner first. */
633
- export function compactDescribe(full: DescribeReport): DescribeCompactReport;
634
472
 
635
473
  // --- rendering --------------------------------------------------------------
636
474
 
package/types/worker.d.ts CHANGED
@@ -17,5 +17,13 @@ export interface WorkerHandle {
17
17
  setPart(newPart: PartDefinition): void;
18
18
  }
19
19
 
20
- /** Run the worker job loop for `part`. Call once, at worker module top level. */
21
- export function runWorker(part: PartDefinition): WorkerHandle;
20
+ /**
21
+ * Run the worker job loop for `part`. Call once, at worker module top level.
22
+ * `opts.loadOracle` injects the closed semantic-mesh-oracle package (a thunk
23
+ * resolving its barrel: `describe`, `describeMemo`, `compactDescribe`); omitted,
24
+ * describe jobs answer with a structured `oracle-unavailable` report.
25
+ */
26
+ export function runWorker(
27
+ part: PartDefinition,
28
+ opts?: { loadOracle?: () => Promise<{ describe: Function; describeMemo: () => Map<string, unknown>; compactDescribe: Function }> },
29
+ ): WorkerHandle;