Problem
DFM's autofix loop (dfm_apply_fix MCP tool) needs every emitted DfmIssue to carry an origin_op: NodeId pointing at the IR node responsible for the offending geometry. Without it, a SetParam { node, path, value } patch has nowhere to land and the agent can only return Manual descriptions.
The data model exists (crates/vcad-kernel-dfm/src/geom/provenance.rs::ProvenanceMap) but the v1 implementation is a coarse fallback: ProvenanceMap::single_root(brep, root_id) attributes every face in a part to that part's root NodeId. It works for primitives (cube.runDfm() → all 6 faces blame the cube node) but is wrong for compositions:
- A boolean root has dozens of faces, only one of which corresponds to the small-radius hole the agent should mutate. The autofix patch ends up targeting the boolean op instead of the cylinder.
- Sweep / loft / fillet results have no source-op attribution at all.
- The richer per-edge / per-loop provenance the BVH samplers want is unavailable.
Why now
Per-feature lineage is the difference between "DFM suggests a manual fix" and "DFM mutates the source op so the agent can iterate." The agent loop is the moat (see #TBD), and this is the data dependency.
Proposed approach
Thread an Option<&mut ProvenanceMap> through the BRep construction path so primitives, features, and booleans tag faces as they create them.
- Add a
tracing: Option<&mut ProvenanceMap> parameter to make_cube / make_cylinder / make_sphere / make_cone in crates/vcad-kernel-primitives/src/lib.rs. Each face emitted by the constructor calls tracing.tag(face_idx, current_node_id).
- Same for the feature crates (fillet, sweep, loft, shell, sketch).
- The boolean pipeline inherits tags from operands: a face that was wholly in
left keeps its tag; a new boundary face tags both contributing operands (use a small enum FaceOrigin { Single(NodeId), Composite(NodeId, NodeId) } instead of a flat NodeId).
- The IR evaluator (
crates/vcad-kernel-wasm/src/lib.rs evaluateDocument path and packages/engine/src/evaluate.ts TS fallback) builds the ProvenanceMap once per part and stashes it on the evaluated scene so Solid.runDfm can consume it.
Downstream cleanup:
- Drop
ProvenanceMap::single_root once the proper pass lands (or keep as a fallback for parts evaluated through code paths that haven't been threaded through yet).
- Update DFM rules to emit op-specific
SetParam paths (radius, size.x, etc.) keyed off the originating op type — currently the CNC rule guesses "radius", which only works on cylinders.
Acceptance criteria
make_cube / make_cylinder / etc. accept and populate a ProvenanceMap.
- A boolean of
cube.difference(cylinder) produces a map where the cylinder's barrel face attributes to the cylinder NodeId, not the boolean root.
dfm_check on a chamfered cube with a too-small chamfer flags the chamfer feature, and dfm_apply_fix raises the chamfer's distance parameter.
crates/vcad-kernel-dfm/tests/ includes a golden test for the boolean attribution case.
References
crates/vcad-kernel-dfm/src/geom/provenance.rs — current ProvenanceMap shape
crates/vcad-kernel-primitives/src/lib.rs — primitive constructors that need tagging
crates/vcad-kernel-booleans/ — boolean pipeline that needs lineage propagation
crates/vcad-kernel-wasm/src/lib.rs Solid.runDfm — current consumer site (currently passes single_root)
packages/engine/src/dfm.ts — TS-side evaluator wrapper that passes per-part root IDs
Problem
DFM's autofix loop (
dfm_apply_fixMCP tool) needs every emittedDfmIssueto carry anorigin_op: NodeIdpointing at the IR node responsible for the offending geometry. Without it, aSetParam { node, path, value }patch has nowhere to land and the agent can only returnManualdescriptions.The data model exists (
crates/vcad-kernel-dfm/src/geom/provenance.rs::ProvenanceMap) but the v1 implementation is a coarse fallback:ProvenanceMap::single_root(brep, root_id)attributes every face in a part to that part's root NodeId. It works for primitives (cube.runDfm()→ all 6 faces blame the cube node) but is wrong for compositions:Why now
Per-feature lineage is the difference between "DFM suggests a manual fix" and "DFM mutates the source op so the agent can iterate." The agent loop is the moat (see #TBD), and this is the data dependency.
Proposed approach
Thread an
Option<&mut ProvenanceMap>through the BRep construction path so primitives, features, and booleans tag faces as they create them.tracing: Option<&mut ProvenanceMap>parameter tomake_cube/make_cylinder/make_sphere/make_coneincrates/vcad-kernel-primitives/src/lib.rs. Each face emitted by the constructor callstracing.tag(face_idx, current_node_id).leftkeeps its tag; a new boundary face tags both contributing operands (use a smallenum FaceOrigin { Single(NodeId), Composite(NodeId, NodeId) }instead of a flatNodeId).crates/vcad-kernel-wasm/src/lib.rsevaluateDocument path andpackages/engine/src/evaluate.tsTS fallback) builds theProvenanceMaponce per part and stashes it on the evaluated scene soSolid.runDfmcan consume it.Downstream cleanup:
ProvenanceMap::single_rootonce the proper pass lands (or keep as a fallback for parts evaluated through code paths that haven't been threaded through yet).SetParampaths (radius,size.x, etc.) keyed off the originating op type — currently the CNC rule guesses"radius", which only works on cylinders.Acceptance criteria
make_cube/make_cylinder/ etc. accept and populate aProvenanceMap.cube.difference(cylinder)produces a map where the cylinder's barrel face attributes to the cylinder NodeId, not the boolean root.dfm_checkon a chamfered cube with a too-small chamfer flags the chamfer feature, anddfm_apply_fixraises the chamfer's distance parameter.crates/vcad-kernel-dfm/tests/includes a golden test for the boolean attribution case.References
crates/vcad-kernel-dfm/src/geom/provenance.rs— currentProvenanceMapshapecrates/vcad-kernel-primitives/src/lib.rs— primitive constructors that need taggingcrates/vcad-kernel-booleans/— boolean pipeline that needs lineage propagationcrates/vcad-kernel-wasm/src/lib.rsSolid.runDfm— current consumer site (currently passessingle_root)packages/engine/src/dfm.ts— TS-side evaluator wrapper that passes per-part root IDs