Skip to content

feat(v0.2.0): tracked face/edge refs across transforms + booleans#53

Merged
w1ne merged 26 commits intodevelopfrom
feat/v0.2.0-tracked-face-refs
May 2, 2026
Merged

feat(v0.2.0): tracked face/edge refs across transforms + booleans#53
w1ne merged 26 commits intodevelopfrom
feat/v0.2.0-tracked-face-refs

Conversation

@w1ne
Copy link
Copy Markdown
Owner

@w1ne w1ne commented May 2, 2026

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.

  • Per-shape HistoryMap on OcctBackend (face hash → lineage to originating primitive's canonical name)
  • Direct OCCT WASM access at boolean + edge-feature sites (BRepAlgoAPI_*::Generated/Modified/IsDeleted); bypasses Replicad's wrappers (which discard the builder before history can be read)
  • Transforms preserve identity 1:1 via parallel TopExp_Explorer walks
  • Walk-back-to-primitive + walk-forward-through-history resolver
  • 4 new diagnostic codes: face-ref-ambiguous-after-split and face-ref-removed, on both edge-feature and face-feature surfaces
  • Geometry-fallback for ambiguous splits stays deferred to v0.3.0 per NORTHSTAR

Spec: `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

  • All existing tests still pass
  • New unit tests pass (resolver, history capture helpers, transform propagation)
  • Integration matrix passes (every transform × every boolean × every edge feature × every input shape category)
  • `feature.edge-feature.face-ref-not-resolvable` no longer fires for transformed primitives or boolean results
  • 4 new HINTS entries pass the rc.17 hints-coverage sentinel
  • No volume / face-count / edge-count regression on the R2 bench corpus

w1ne added 26 commits May 2, 2026 22:47
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.
@w1ne w1ne marked this pull request as ready for review May 2, 2026 23:34
@w1ne w1ne merged commit 8d28561 into develop May 2, 2026
2 checks passed
@w1ne w1ne deleted the feat/v0.2.0-tracked-face-refs branch May 2, 2026 23:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant