Skip to content

Build FaceId→NodeId provenance during evaluation, replacing the v1 single-root heuristic #187

@ecto

Description

@ecto

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.

  1. 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).
  2. Same for the feature crates (fillet, sweep, loft, shell, sketch).
  3. 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).
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    dfmenhancementNew feature or requestkernelRust kernel crates

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions