feat(v0.2.0): tracked face/edge refs across transforms + booleans#53
Merged
feat(v0.2.0): tracked face/edge refs across transforms + booleans#53
Conversation
Per-shape HistoryMap on OcctBackend; direct OCCT access at boolean + edge-feature sites; transforms preserve identity 1:1. Walk-back-to-primitive resolver. 4 new diagnostic codes for ambiguous-split + removed-face. Geometry-fallback (NORTHSTAR v0.3) remains deferred.
15 tasks (Task 0 branch + Task 14 cutover). Each task TDD-style with bite-sized steps. Task 12 integration matrix is the v0.2.0 ship gate. Task 13 R2 bench verifies SimplifyResult-skip doesn't regress consumers. npm publish stays deferred per v0.1.0 precedent.
src/naming/evolutionRecord.ts: FaceHash, EdgeHash, FaceLineage, EdgeLineage, HistoryMap, EdgeHistoryMap. propagateTransformHistory maps input face hashes 1:1 through topology-preserving transforms. Tests skipped pending Task 5 (faceHashes() helper on OcctBackend). Part of v0.2.0 — tracked face/edge refs across transforms + booleans.
…tory capture cutWithHistory / fuseWithHistory / intersectWithHistory bypass Replicad's Shape3D.cut/fuse/intersect (which discard the BRepAlgoAPI_* builder before history can be read). Reads Generated/Modified/IsDeleted via SetToFillHistory. Skips SimplifyResult() to preserve hash stability across the operation. Tests skipped pending Task 5 (faceHashes() helpers on OcctBackend). Part of v0.2.0 — tracked face/edge refs across transforms + booleans.
Plan's code blocks used (shape as any).wrapped — that's wrong. OcctBackend has a private 'shape' field and exposes getReplicadShape(). The correct path is (body.getReplicadShape() as any).wrapped. Tasks 3, 7, 8, 9 reference this pattern; correction note added at top of plan.
Two leak sites found in code review: 1. listToHashes() iterator wrappers (it, end) — now wrapped in try/finally with explicit .delete() calls 2. TopTools_ListOfShape from builder.Modified() in recordFace/recordEdge — now wrapped in try/finally with explicit .delete() Critical for kernelCAD where boolean operations run inside recompute loops; WASM linear memory pressure doesn't trigger V8 GC in the worker context. No behavior change. Tests still 7 skipped, 0 failed (Task 5 unskips them).
…ory capture filletWithHistory / chamferWithHistory / shellWithHistory bypass Replicad's edge-feature wrappers to read BRepFilletAPI_*/BRepOffsetAPI_* history. Returns face+edge maps so the resolver can walk forward through edge feature operations. Uses body.getReplicadShape().wrapped (public accessor) — not (body as any).wrapped. All WASM handles (builders, explorers, iterators, progress ranges, ListOfShape) wrapped in try/finally for guaranteed cleanup. Tests skipped pending Task 5 (faceHashes()/edgeHashes()/findCanonicalFaceHash() helpers on OcctBackend). Part of v0.2.0 — tracked face/edge refs across transforms + booleans.
Resolves canonical FaceRef against an OcctBackend.historyMap.
Counts matches: 1 = success, 0 = face-ref-removed, >1 = ambiguous-after-split.
Emits new diagnostic codes per spec A5. Diagnostic surface is parameterized
('edge-feature' vs 'face-feature') so callers in fillet/chamfer/shell get
the right code path.
Includes unit tests with synthetic HistoryMap stubs (5 tests, all passing).
Real-world resolver tests using OcctBackend.historyMap come in Task 5+.
Also adds readonly historyMap? field declaration to OcctBackend (minimal
surface needed for resolveFaceRef.ts to type-check; full wiring in Task 5).
Part of v0.2.0 — tracked face/edge refs across transforms + booleans.
… 5 scope shrink Task 4 surfaced two corrections: (1) CompilerDiagnostic is in src/diagnostics/diagnostic.ts (not src/intent/types.ts); (2) Task 4 added the historyMap field declaration to OcctBackend itself (needed for type-check), so Task 5's scope is just constructor param + helper methods.
…x WASM list iteration OcctBackend gains three new methods required for topology-aware face tracking: - faceHashes(): enumerate faces via TopExp_Explorer, return OCCT HashCode hex strings - edgeHashes(): same for edges - findCanonicalFaceHash(name): locate a box/cylinder face by centroid + hash it The 3rd constructor param (historyMap?) is also wired in. Two pre-existing bugs in Tasks 2 and 3 are fixed while unskipping tests: 1. historyAwareBooleans/historyAwareEdgeFeatures: listToHashes/listToHashStrings used begin()/end() STL iterators that are not bound in the WASM module. Fixed by copying the TopTools_ListOfShape_3 and iterating via First_1()+RemoveFirst(). 2. historyAwareEdgeFeatures: findEdgeByHash returned a raw TopoDS_Shape from TopExp_Explorer.Current(); BRepFilletAPI::Add_2 requires TopoDS_Edge. Fixed by calling oc.TopoDS.Edge_1(e) before passing to Add_2. Unskips 11 tests across evolutionRecord, historyAwareBooleans, historyAwareEdgeFeatures. All 16 tests in the target suites now pass; full suite: 848 passed, 25 skipped, 1 pre-existing unrelated failure (docs competitor-ref sentinel in a plan file, not introduced here).
…DS downcast Two more pre-existing plan errors found when Task 5 unskipped tests: (1) TopTools_ListOfShape has no begin()/end() in WASM bindings — must use destructive First_1()/RemoveFirst() on a copy; (2) TopExp_Explorer.Current() returns generic TopoDS_Shape — must downcast via oc.TopoDS.Edge_1/Face_1 before passing to typed APIs like BRepFilletAPI.Add_2. Apply in Tasks 2/3/6/8 wherever the plan's code blocks show begin()/end() pattern or pass Current() to a typed API.
…p threading Task 6 of v0.2.0: the boolean case in occtLowerer now calls cutWithHistory/fuseWithHistory/intersectWithHistory instead of the Replicad convenience methods. After each operation, mergeBooleanHistory merges the two input HistoryMaps through the OCCT-reported face lineage into the output OcctBackend. mergeBooleanHistory added to historyAwareBooleans.ts (pure merge logic over BooleanHistoryResult; co-located with the type it consumes to avoid a circular import). replicad.cast() wraps the raw TopoDS_Shape back into the correct Replicad Shape3D subtype before passing it to the OcctBackend constructor. TSC: 0 errors. 848 tests pass; 1 pre-existing competitor-refs doc failure (unrelated to this task).
Wire propagateTransformHistory into the post-hoc transform loop (translate, rotateAxis, scale, reflect) and the mirror feature case in occtLowerer.ts. For each op, capture input faceHashes + historyMap before the transform, then re-wrap the output OcctBackend with the propagated map when face counts match. Mirror is treated as transform-like for source faces; if the mirror union changes face count (plane-touching faces merge), the shape is left without a historyMap so the resolver returns face-ref-not-resolvable.
…-aware ops Task 8: rewires the three edge-feature case blocks in occtLowerer.ts to call filletWithHistory / chamferWithHistory / shellWithHistory and thread the resulting HistoryMap via the new mergeEdgeFeatureHistory helper. Fixes two pre-existing bugs in historyAwareEdgeFeatures.shellWithHistory: - MakeThickSolidByJoin was missing the required 10th arg (theRange) - thickness sign was positive; OCCT expects negative for inward offset
…ive thickness Two more OCCT WASM API mismatches found by TDD: (1) MakeThickSolidByJoin needs Message_ProgressRange as 10th arg (plan showed 9); (2) thickness must be negative for inward offset (Replicad does the same).
…Map in edge/face selection pickEdges and pickFace now call resolveFaceRef for shapes that carry a historyMap (transformed or boolean'd), eliminating the face-ref-not-resolvable failure mode for those shapes. Adds faceByHash and edgesOfFaceByHash helpers that look up replicad Face wrappers by their OCCT hash code with proper try/finally cleanup. Raw un-transformed primitives (no historyMap) continue to use the centroid heuristic.
…it and face-ref-removed diagnostic codes Adds 4 new HINTS entries for the diagnostic codes emitted by resolveFaceRef (face-ref-ambiguous-after-split and face-ref-removed for both edge-feature and face-feature variants). Updates 4 existing entries: face-ref-not-resolvable entries drop the outdated 'apply transforms after fillet' workaround (no longer needed as of v0.2.0), and face-ref-not-supported entries clarify that tracked/ created/propagated are internal-only v0.2 types with future-version exposure planned. The rc.17 hintsCoverage sentinel now passes. SKILL.md update is Task 11.
… through operations Delete the now-stale "Canonical Face Refs — Critical Constraint" section (workaround applied edge features before transforms) — Tasks 1-9 closed the underlying limitation. Replace with "Face refs through operations" describing transparent lineage tracking through transforms and booleans, plus the two split/removed diagnostics that remain meaningful. Add the 4 new diagnostic codes introduced in Task 10 to the Diagnostic Codes table: face-ref-ambiguous-after-split and face-ref-removed for both edge-feature and face-feature groups. Update skill.test.ts sentinel to match the new section heading.
…ms and booleans Primitives (box, cylinder) now have their historyMap seeded at construction time with canonical face lineage (top/bottom/left/right/front/back for box; top/bottom for cylinder). This allows resolveFaceRef to walk the historyMap on any downstream shape — after transforms, booleans, or edge features — without falling back to the centroid heuristic. Key changes: - occtLowerer.ts: seed HistoryMap in case 'box' (6 faces) and case 'cylinder' (2 faces); leave sphere unseeded (no canonical planar names). Add sharp-edge dihedral filter before fillet to skip G1-smooth edges that OCCT cannot fillet. Make the WASM-exception no-op conditional on r.inputs.face !== undefined so radius-too-large failures still surface feature.fillet.failed. - resolveFaceRef.ts: guard on `map === undefined` (not `!map || map.size === 0`) so empty maps (all faces removed by boolean) reach the 0-matches path and emit face-ref-removed. - edgeSelection.ts: guard on `base.historyMap !== undefined` (same fix) and rewrite faceByHash to iterate replicad .faces array by hash rather than by TopExp_Explorer index, avoiding index skew from iterTopo deduplication. Updated unit tests to reflect v0.2 behavioral changes: - reflect.test.ts: canonical face refs now resolve after reflect (not an error). - resolveFaceRef.test.ts: face-feature prefix test uses undefined historyMap. - getEdgesOf.test.ts: boolean results now have historyMap; face_name not in map emits a non-null error (face-ref-removed) rather than "not applicable". - whyDidThisFail.test.ts: upstream-chain test uses radius-too-large fillet (translate+fillet now succeeds with v0.2).
…transforms and booleans 21 tests covering: backward-compat (fillet before transforms), canonical fillet/chamfer/shell after translate/rotate/scale/reflect/mirror, canonical fillet after booleans (subtract-with-hole, union, intersect), combined transform+boolean, ambiguous-split (face-ref-ambiguous-after-split), removed-face (face-ref-removed), iterated edge features, and cylinder top/bottom refs. All 21 tests pass. This file is the v0.2.0 ship gate.
5 representative boolean scripts (box-minus-cylinder, box-minus-divider, two-boxes-fused, cylinder-intersected, nested-booleans). All evaluate successfully with no diagnostics, confirming that skipping SimplifyResult in historyAwareBooleans.ts doesn't cause kernel crashes or topology errors on the common cases. If a future regression appears, the SimplifyResult-after-history-extraction fallback (re-apply SimplifyResult after capturing history maps + remap face hashes against the simplified topology) lives at TODO in historyAwareBooleans.ts.
Closes the SKILL.md 'Critical Constraint' section. Canonical face refs work across every transform and unambiguous boolean. Geometry-fallback for ambiguous splits is planned for a future release; ambiguous and removed-face cases produce clear diagnostics today. This is part of an ongoing prototype effort — the agent layer remains the weakest part of the project and is the priority for upcoming work. Also fixes 5 pre-existing lint errors (no-explicit-any) in occtLowerer.ts that were blocking qc:full lint stage. qc:full: lint clean, typecheck pass, 869 tests pass / 25 skip / 1 fail (pre-existing competitor-refs sentinel, not v0.2.0 scope).
…er-2 section Replaces 'a real CAD library (DeepCAD, Fusion 360 Gallery)' with 'a public CAD model dataset' per the no-competitor-refs-in-repo rule. Lineage of the actual datasets captured in internal memory (kernelcad_design_lineage.md) before the strip.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes NORTHSTAR's v0.2 module.
box(...).translate().fillet({face:'top'})and analogous patterns now work; the SKILL.md "Critical Constraint" section is deleted.HistoryMaponOcctBackend(face hash → lineage to originating primitive's canonical name)BRepAlgoAPI_*::Generated/Modified/IsDeleted); bypasses Replicad's wrappers (which discard the builder before history can be read)TopExp_Explorerwalksface-ref-ambiguous-after-splitandface-ref-removed, on both edge-feature and face-feature surfacesSpec: `docs/superpowers/specs/2026-05-02-v0.2.0-tracked-face-refs-design.md`
Plan: `docs/superpowers/plans/2026-05-02-v0.2.0-tracked-face-refs.md`
Test plan