gabriel / muse public
Open #66 Enhancement
filed by gabriel human · 19 days ago

CAD Domain — implementation plan (TDD-first, comprehensive)

0 Anchors
Blast radius
Churn 30d
0 Proposals

CAD Domain — Implementation Plan

Background

Muse treats "domain" as a first-class primitive (muse/domain.py::MuseDomainPlugin). A domain plugin implements six methods — snapshot, diff, merge, drift, apply, schema — and gets the full VCS engine for free: the commit DAG, branching, checkout, lineage walking, log graph, and merge-base finder. The reference implementation is muse.plugins.midi, which proved the model for a domain with structured symbolic data (notes, CC events, track structure) plus binary payload concerns. muse.plugins.code proved it for AST-level source. muse.plugins.social and muse.plugins.identity proved it for graph-shaped state with CRDT and quorum semantics respectively.

CAD is the next proof point, and a harder one: geometry is continuous (not discrete symbols), feature histories are causally ordered (a later feature can depend on an earlier one by reference), constraints form solvable systems that can become over/under-constrained after a merge, and payloads are large binary B-Rep or mesh data that cannot be diffed as text. If Muse can version, diff, merge, and query CAD state with the same semantic awareness it has for code and MIDI, it establishes the domain model as general-purpose for any structured engineering artifact — not just software and music. That is the thesis this plan exists to test.

This issue is the plan only. No implementation code lands against this issue. Each numbered phase below is expected to be split into its own staging issue once this plan is reviewed and accepted (see §10).

Goal

Ship a muse/plugins/cad/ domain plugin, structured like muse/plugins/midi/ (a plugin.py entrypoint plus focused private modules for diff/merge/query/invariants), that:

  • Registers in muse domains --json alongside code, midi, identity, mist, social.
  • Passes muse code test --json for every module it touches.
  • Can snapshot, diff, and merge a CAD assembly with feature-level and geometry-level semantic awareness — not file-level opaque blobs.
  • Supports a CAD query DSL (muse cad query "...", mirroring muse code query) capable of answering the concrete query examples in §6.
  • Detects the drift and validation classes listed in §7 via muse status / muse check.
  • Has test coverage across every tier listed in §8, with test IDs (CAD_01, CAD_02, …) referenced from both this plan and the test files that satisfy them.

"Done" for the plan itself (this issue) means: reviewed, decomposed into the issue breakdown in §10, and each phase has explicit acceptance criteria a different agent could implement against without re-deriving decisions made here.


1. Domain Scope

Supported initial CAD targets

  • Parametric solid modeling, feature-history based (sketch → constrain → extrude/ revolve/sweep/loft → boolean → pattern), the same mental model as FreeCAD, Fusion 360, SolidWorks, and Onshape.
  • v0 targets single parts and simple multi-part assemblies (tens of parts, not thousands). Large assemblies (vehicle/aircraft scale) are explicitly deferred — see out-of-scope.

Two-layer strategy, mirroring how the MIDI domain separates the symbolic model (notes/CC/track structure) from the container format (.mid):

  1. Canonical semantic layer (Muse-owned): a JSON-serializable CAD IR — the "Canonical Model" in §2 — that Muse snapshots, diffs, merges, and queries directly. This is the domain's actual state, analogous to SnapshotManifest for MIDI notes.
  2. Interchange/import-export boundary: STEP (ISO 10303-21, AP242) as the authoritative geometry interchange format for B-Rep solids, because it is the only CAD format that is (a) an open ISO standard, (b) not tied to a single vendor's proprietary kernel, and (c) already the de facto neutral format every major CAD tool can export/import losslessly for solid geometry. FreeCAD's .FCStd (a zipped XML + OCCT .brep/.bin bundle) is the recommended reference authoring tool for v0 development and fixtures, because it is open source, Python-scriptable end-to-end, and built on OpenCASCADE (OCCT) — the same kernel family STEP import/ export already has to interoperate with.

Concretely: Muse's CAD plugin parses STEP (via OCCT/python-occ or FreeCAD's bundled OCCT bindings) and FreeCAD's native document XML into the canonical IR on snapshot(), and can re-serialize the IR back to both formats on apply()/checkout. Geometry itself (B-Rep shape data) is stored as content-addressed binary blobs (see §3), never as re-derived/re-tessellated data — byte-identical round-trip is a hard requirement (§8, round-trip tests).

What is in scope for v0

  • Single-body and multi-body parts with a linear (non-branching-within-file) feature history.
  • Assemblies composed of parts + mates/constraints between them (rigid, revolute, slider — the common subset every tool supports).
  • Sketches with 2D geometric + dimensional constraints (the constraint types in §2).
  • Materials as named property sets (density, stated tolerances) attached to parts.
  • Metadata: units, author, coordinate system, external reference manifest.
  • Import/export round-trip through STEP AP242 and FreeCAD .FCStd.
  • Semantic diff, three-way merge, drift detection, and query DSL for all of the above.

What is explicitly out of scope for v0

  • Surface/freeform (NURBS-heavy) modeling as a first-class semantic target — v0 treats freeform surfaces as opaque geometry blobs diffed by content hash, not by control- point structure.
  • Sheet metal, weldments, and mold-specific feature types.
  • FEA/simulation results, GD&T callouts beyond basic tolerance values, and drawing/ annotation (2D drawing sheet) data.
  • Kinematic simulation (motion studies) — only the static mate/constraint graph.
  • Cloud-native formats with no open export path (e.g. Onshape's proprietary internal representation) as authoring targets — they may still be added as import-only adapters later if a documented export path exists.
  • Multi-thousand-part assemblies and any performance work aimed at that scale.
  • Real-time collaborative (CRDT) constraint solving — v0 merge is offline/three-way only, same as code and midi (merge_mode: three_way), not social's CRDT mode.

2. Canonical Model

All elements are content-addressed (sha256:<hex>) and referenced by ID, never by positional index, so that reordering never produces a spurious diff — the same principle code's AST symbol identity and midi's note identity already rely on.

Element Fields Notes
Part id, name, bodies: [Body.id], features: [Feature.id] (ordered), material: Material.id \| null, metadata One feature history per part.
Assembly id, name, parts: [{part_ref, instance_id, transform, coordinate_system}], mates: [Constraint.id], metadata An assembly can nest other assemblies (sub-assembly instances) — must remain acyclic (see Invariants, §7).
Body id, kind: solid \| surface \| wireframe, geometry_ref: content_id (points at the B-Rep blob store, §3), bounding_box, topology_summary (face/edge/vertex counts, for cheap diff pre-filtering) Geometry bytes live in the object store; the Body element is metadata + a pointer.
Sketch id, plane_ref (CoordinateSystem.id or Feature.id it's sketched on), entities: [{id, kind: line\|arc\|circle\|spline\|point, params}], constraints: [Constraint.id] 2D only in v0.
Constraint id, kind (coincident, parallel, perpendicular, tangent, equal, symmetric, distance, angle, radius, horizontal, vertical, mate kinds: rigid, revolute, slider, cylindrical, planar), refs: [entity_id, ...], value: float \| null, unit, driving: bool (driving vs. reference dimension) Distinct from generic "attribute" — constraints are solved, not just descriptive.
Feature id, kind (extrude, revolve, sweep, loft, boolean_union, boolean_subtract, boolean_intersect, fillet, chamfer, pattern_linear, pattern_circular, mirror, datum_plane, datum_axis), params, input_refs: [Feature.id \| Sketch.id \| Body.id], output_body: Body.id, suppressed: bool input_refs makes the feature dependency DAG explicit — this is what makes feature-level diff/merge tractable (§4, §5).
Material id, name, density_kg_m3, properties: {key: value} (e.g. yield_strength_mpa), source (library name or "custom") Named, reusable, content-addressed by (name + properties) so two parts with identical material converge to one object.
Measurement id, kind (length, angle, area, volume, mass), value, unit, derived_from: [Feature.id \| Body.id], stale: bool Derived/cached values — recomputed on checkout, flagged stale if inputs changed and recompute hasn't run (ties into drift, §7).
CoordinateSystem id, origin, x_axis, y_axis, z_axis, parent_ref: CoordinateSystem.id \| null Nested/relative frames for assemblies.
Metadata units (length/mass/angle unit system — e.g. mm/kg/deg), author, created_at, source_format (step | fcstd), source_checksum, schema_version units is load-bearing: cross-unit comparisons are a first-class validation check (§7).
ExternalReference id, kind (linked_part, library_material, imported_step), uri_or_path, content_id_at_link_time, resolved: bool Explicit modeling of "this assembly links a part that lives outside this snapshot" — required for drift detection (§7, missing references).

Design invariant carried over from code/midi: identity is content- and reference-based, never positional. A feature moved earlier or later in an unrelated part's history is not itself a diff; only a change in input_refs or params is.


3. Snapshot Design

How CAD state is normalized

snapshot() walks the live state (a .FCStd bundle or STEP file under state/, exactly as MIDI's snapshot() walks a .mid file) and produces the canonical IR described in §2 as the SnapshotManifest. Normalization steps, applied in a fixed order so the same input always produces the same output:

  1. Resolve all names to stable IDs (content hash of (kind, defining_params) for elements without a natural stable ID — e.g. two independently authored fillets with identical inputs get the same ID; this is what makes rename/move detection in §4 possible instead of guesswork).
  2. Canonicalize numeric values: round to a fixed epsilon-safe precision (default 1e-9 in SI base units after unit conversion) so that floating-point serialization jitter from different CAD kernels never produces a diff. The epsilon is a domain-schema constant, not user-configurable in v0 (avoids non-determinism from config drift).
  3. Canonicalize ordering: feature lists stay in causal (dependency) order as recorded by the source tool; sketch entities, materials, and constraint lists are sorted by ID for serialization (never by insertion order) so a no-op re-save never diffs.
  4. Compute topology_summary and bounding_box for every Body at snapshot time — cheap invariants used to short-circuit expensive geometry diffs (§4).

How geometry is content-addressed

Each Body's B-Rep (or, for surfaces/out-of-scope-freeform, mesh) payload is extracted as its own binary blob and hashed independently: sha256:<hex> over the canonical binary form (OCCT's own .brep serialization, not the STEP text wrapper, to avoid STEP-header non-determinism — see below). The Body element in the IR stores only geometry_ref pointing at that blob's content ID. Two bodies with byte-identical geometry — e.g. two identical bolts in an assembly — collapse to one stored blob automatically, the same content-addressing win MIDI gets for duplicate note buffers and mist gets for duplicate artifacts.

How large binary payloads are stored

Binary geometry blobs go into .muse/objects/ exactly like any other Muse object — no separate large-file store in v0. This is a deliberate scope-limiting decision: Muse already has generic object storage; a CAD-specific LFS-style side channel is treated as a future optimization (tracked as an open question in §10) contingent on measuring real repository sizes from v0 usage, not designed speculatively now. Geometry blobs are written once (content-addressed, so no rewrites) and are candidates for the existing muse gc / muse prune unreachable-object sweep exactly like other domains' blobs.

How deterministic serialization is guaranteed

  • STEP files embed a header with a timestamp and implementation-defined ordering that differs run-to-run even for identical geometry — Muse never hashes the STEP file bytes directly. It always re-serializes through the canonical IR → OCCT native .brep binary path described above.
  • FreeCAD .FCStd documents are zip containers with a manifest whose XML attribute order and zip entry timestamps are non-deterministic across FreeCAD versions — same rule: never hash the container, always canonicalize through the IR.
  • A golden-snapshot test suite (§8) locks this down: re-snapshotting the same fixture twice, or snapshotting the same design authored in FreeCAD vs. round-tripped through STEP, must produce byte-identical canonical JSON and identical geometry blob hashes.

4. Semantic Diff

diff(base, target, *, repo_root=None) returns a StructuredDelta of typed ops, same contract as MIDI/code. CAD-specific op granularity:

  • Feature-level diffs: InsertOp/DeleteOp/ReplaceOp keyed on Feature.id. A ReplaceOp on a feature carries a content_summary naming which params changed (e.g. "extrude depth 10mm -> 12mm"), and — critically — does not recurse into re-diffing every downstream feature unless that feature's own params or input_refs changed. Downstream features that merely recompute to new geometry because an upstream input changed are represented as a single derived_geometry_changed annotation on the changed feature, not N separate feature diffs. This is the single most important design decision for diff readability — feature histories are causal chains, and a diff must show the cause, not every effect.
  • Geometry diffs: only computed when a Body.geometry_ref content ID actually changes. First check topology_summary/bounding_box (cheap) — if both are unchanged, geometry is reported unchanged without loading the B-Rep blob at all. If they differ, report at the coarsest useful level (face/edge/vertex count deltas, volume delta, bounding-box delta) — v0 does not attempt face-correspondence matching between two arbitrary B-Reps (that is a research-grade geometric problem); it reports "geometry changed, here is what changed about it structurally" rather than a face-by-face diff.
  • Constraint diffs: InsertOp/DeleteOp/ReplaceOp keyed on Constraint.id, same rename/move-detection rule as features (content ID stable across reorders). A changed constraint value (e.g. a dimension 10mm → 12mm) is reported distinctly from a changed constraint kind or refs (which is a structural change, not a value change) — different severity for merge purposes (§5).
  • Material/property diffs: diffed at the Material.id level; since materials are content-addressed by (name, properties), a part switching from one named material to another is a single ReplaceOp on Part.material, never a deep diff of the material object itself (materials are treated as atomic/immutable, like MIDI's note pitch values).
  • Assembly relationship diffs: mates/parts list diffed by instance_id, not position — moving a part instance in an assembly's on-disk list order is not a diff; changing its transform, swapping which part_ref an instance points to, or changing a mate's refs/kind is.
  • Rename/move detection: since every element ID is content-derived (§3, step 1), a "rename" is detected as same content ID, different name field — reported as a RenameOp (name-only change), not a delete+insert. A "move" (e.g. a feature moved to a different part, or a part moved to a different assembly) is detected as same Body/Part content ID appearing under a different parent — reported as a MoveOp, not delete+insert, mirroring how code's symbol-identity diff already handles moved functions.
  • Tolerance-aware comparisons: every numeric comparison in diff (dimensions, transforms, geometry bounding boxes) uses the same fixed epsilon from snapshot normalization (§3). A value changing by less than epsilon is not a diff. This is what prevents kernel-recompute jitter (e.g. re-running a fillet in a newer OCCT point release) from generating spurious no-op diffs across a round trip.

5. Merge Strategy

merge(base, left, right, *, repo_root=None) — three-way, merge_mode: three_way in the domain schema (§9), consistent with code and midi. CAD-specific resolution rules, in order of how they're evaluated:

  • Non-conflicting edits (the common case): edits to disjoint elements — one branch adds a fillet to Part A, the other renames Part B — merge automatically via the engine's default address-keyed map merge (no domain-specific code needed beyond correctly declaring dimensions in schema(), §9).
  • Same-feature conflicts: both branches modify the same Feature.id's params or input_refs to different values. Not auto-resolvable in v0 — always a reported conflict. (Auto-merging "both changed the extrude depth" by e.g. averaging is explicitly rejected: silently picking a value neither author chose is worse than asking a human, consistent with the workspace-wide rule against blind --ours/ --theirs.)
  • Constraint conflicts: both branches modify the same Constraint.id's value — reported conflict, same as same-feature conflicts. Additionally: if the union of both branches' non-conflicting constraint changes would leave the sketch over-constrained or under-constrained (checked via the invariant solver hook in §7), that is reported as a derived merge conflict even if no single constraint edit collided — this is CAD-specific and has no code/MIDI analog, because constraints interact globally in a way symbols and notes don't.
  • Assembly conflicts: both branches change the same mate's refs or kind, or both branches move the same part instance to conflicting positions in the assembly tree (e.g. left nests it under sub-assembly X, right deletes sub-assembly X) — reported conflict; the second case (edit vs. delete-of-ancestor) is escalated as structural, meaning the conflict report must show the full ancestor chain, not just the leaf.
  • Geometry tolerance conflicts: both branches produce different geometry_ref content IDs for the same Body.id where the causing feature edits did not conflict (e.g. both branches independently added an unrelated feature elsewhere in the tree, but the recomputed geometry still differs by more than the tolerance epsilon at the affected body) — this can happen when feature interactions are nonlinear. Reported as a conflict requiring the merge to trigger a recompute step (re-run the combined feature history through the CAD kernel) rather than a pure data merge — this is the one place CAD merge needs kernel access, not just IR manipulation, and must be called out explicitly in the plugin's merge() implementation and its tests (§8).
  • Human-readable conflict reports: every conflict in the returned MergeResult carries: which element(s), which two branches' values, the ancestor base value, and a plain-language cause string (e.g. "Both branches changed Feature 'boss_extrude' depth: left=12mm, right=15mm, base=10mm"). This mirrors the existing three-way conflict marker convention (<<<<<<< ours / ||||||| base / ======= theirs) used workspace-wide, rendered for CAD elements instead of text lines.

6. Query Support

A CAD query DSL mirroring muse code query's predicate syntax (muse cad query "<predicate>" --json), plus a set of purpose-built subcommands for the most common questions (mirroring how muse code hotspots/gravity/entangle sit alongside the generic muse code query).

Example CAD query DSL

# Generic predicate query — same style as `muse code query "symbol == 'X'"`
muse cad query "part.material == 'Aluminum-6061'" --json
muse cad query "constraint.kind == 'distance' and constraint.value > 50" --json
muse cad query "feature.modified_since == 'v1.2.0'" --json

Find parts by material

muse cad query "part.material == 'Aluminum-6061'" --json
muse cad find-by-material "Aluminum-6061" --json     # convenience alias

Find dimensions above/below thresholds

muse cad query "constraint.kind == 'distance' and constraint.value > 100 and constraint.unit == 'mm'" --json
muse cad dimensions --above 100mm --json
muse cad dimensions --below 5mm --kind radius --json

Find modified features

muse cad query "feature.modified_since == HEAD~5" --json
muse cad modified-features --since v1.2.0 --json     # mirrors `muse code hotspots` framing

Find broken constraints

muse cad query "constraint.solved == false" --json
muse cad broken-constraints --json                   # runs the invariant solver hook, §7

Find duplicated geometry

muse cad query "body.geometry_ref in duplicates()" --json
muse cad duplicate-geometry --json     # groups Body elements by shared geometry_ref
                                        # — this is nearly free given content-addressing (§3)

Find assembly dependencies

muse cad query "assembly.contains(part == 'bracket-v2')" --json
muse cad deps <assembly-id> --json                    # mirrors `muse code deps`, walks
                                                       # the assembly/part/feature ref graph
muse cad impact <part-id> --json                      # mirrors `muse code impact`:
                                                       # which assemblies instantiate this part?

7. Validation and Drift Detection

Surfaces through muse status (via drift()) and muse check/muse code invariants (via a CAD-specific invariant checker registered the same way MIDI's _invariants.py plugs into the engine).

  • Invalid geometry: self-intersecting bodies, zero-volume solids, non-manifold topology (an edge shared by more than two faces) — detected via OCCT's own BRepCheck (or equivalent) validity pass run at snapshot time; flagged as a schema- level invariant violation, not silently accepted into a commit.
  • Missing references: any ExternalReference with resolved: false, or any input_refs/refs pointing at an element ID not present in the current snapshot (e.g. a feature referencing a deleted sketch) — flagged as broken reference, blocks commit by default (escapable only the same way other domains allow force- committing dirty state, not via a CAD-specific bypass).
  • Broken constraints: sketches/assemblies that are over-constrained (more constraints than degrees of freedom, or contradictory constraints) or under- constrained (fewer constraints than DOF, meaning the design is not fully determined) — detected by running the constraint set through a DOF-counting pass (not a full numerical solver in v0 — full solve is a stretch goal, DOF counting is the v0 gate).
  • Unit inconsistencies: any dimension/constraint whose stated unit doesn't match the document's declared Metadata.units system without an explicit, recorded conversion — flagged at snapshot time before it ever reaches a diff/merge, because unit bugs are exactly the kind of silent, catastrophic error (see: Mars Climate Orbiter) that semantic versioning should catch and text-diff cannot.
  • Non-deterministic imports: if re-importing the same source STEP/FCStd file twice produces different canonical IR (should be impossible per §3, but the checker exists as a regression gate) — flagged as a snapshot determinism failure, which is a hard error, not a warning, because it would silently break content-addressing and the entire diff/merge model built on top of it.
  • Schema migration risks: Metadata.schema_version mismatches between the domain plugin's current schema() output and a snapshot's recorded version — flagged before attempting a diff/merge across the boundary, surfaced as a muse code migrate-style warning (never auto-migrated silently, consistent with the workspace-wide rule that muse code migrate requires explicit permission).

8. TDD Plan

Test IDs use the CAD_NN prefix, referenced from both this plan and the eventual test files, per the workspace convention (e.g. IC_01, EX_03).

Tier Coverage Example test IDs
Unit Canonical model (de)serialization for every element type in §2; epsilon-based numeric comparison helper; content-ID derivation for elements without natural IDs. CAD_01 element round-trip, CAD_02 epsilon comparator boundary values
Golden snapshot A fixed set of authored fixtures (simple bracket, bolted assembly, filleted block, over-constrained sketch) snapshotted once and checked byte-for-byte against a committed golden JSON + geometry blob hash on every run. CAD_10CAD_14, one per fixture
Round-trip FCStd → IR → FCStd and STEP → IR → STEP must reproduce input geometry within tolerance and identical canonical IR; cross-format (FCStd → IR → STEP → IR) must produce identical IR (proves the IR, not the format, is canonical). CAD_20 FCStd round-trip, CAD_21 STEP round-trip, CAD_22 cross-format equivalence
Diff One test per diff category in §4 (feature, geometry, constraint, material, assembly, rename, move, tolerance-suppression-of-noise). CAD_30CAD_37
Merge One test per non-conflicting case and each conflict category in §5, including the "derived merge conflict" (constraint union becomes over-constrained) and the geometry-tolerance-triggers-recompute case. CAD_40CAD_47
Conflict Conflict report shape/content correctness — every conflict includes both branch values, base value, and a plain-language cause string; muse resolve correctly clears a CAD conflict from muse conflicts --json. CAD_50CAD_53
Property-based Randomized feature-history generation (bounded depth/branching) fed through snapshot→diff→apply and asserting apply(diff(A,B), A) == B; randomized constraint sets fed through the DOF counter asserting it never crashes and is monotonic under constraint addition. CAD_60, CAD_61 (hypothesis-style)
Validation/drift One test per bullet in §7 (invalid geometry, missing reference, broken constraint, unit mismatch, non-deterministic import, schema mismatch). CAD_70CAD_75
Query One test per query example in §6 against a fixture with known expected results. CAD_80CAD_86

Fixture strategy

  • Fixtures are authored once, by hand, in FreeCAD (open, scriptable, checked into the test suite as both .FCStd and its STEP export) — never generated programmatically for the golden/round-trip tiers, so they represent realistic tool output including whatever non-determinism real tools introduce (which is exactly what §3's normalization must absorb).
  • A small fixture library covers: a single-body part with 3+ dependent features; a multi-body part; a 2-part bolted assembly; a sketch that is deliberately over- constrained; a sketch that is deliberately under-constrained; an assembly with a missing external reference; a part with mismatched units.
  • Property-based tests use synthetic generators (not the hand-authored fixtures) since they need to explore structural variation the fixture library doesn't cover.

9. Backend Integration

Domain plugin interface implementation

muse/plugins/cad/ following the muse/plugins/midi/ module layout:

muse/plugins/cad/
  __init__.py
  plugin.py          # MuseDomainPlugin implementation: snapshot/diff/merge/drift/apply/schema
  entity.py           # Canonical model dataclasses (§2)
  manifest.py         # SnapshotManifest (de)serialization, normalization (§3)
  cad_diff.py          # Diff algorithms (§4)
  cad_merge.py          # Merge algorithms (§5)
  _query.py            # Query DSL implementation (§6)
  _invariants.py       # Validation/drift checks (§7)
  _import_step.py      # STEP <-> IR adapter
  _import_fcstd.py     # FreeCAD document <-> IR adapter

schema() declares (mirroring the domains --json shape shown by code/midi/ identity):

{
  "schema_version": "<muse_version>",
  "merge_mode": "three_way",
  "description": "Semantic version control for parametric CAD models...",
  "dimensions": [
    {"name": "parts", "description": "Parts and their feature histories."},
    {"name": "assemblies", "description": "Assembly instances, mates, and coordinate frames."},
    {"name": "geometry", "description": "Content-addressed B-Rep/mesh bodies."},
    {"name": "constraints", "description": "Sketch and mate constraint graphs."},
    {"name": "materials", "description": "Named, content-addressed material property sets."},
    {"name": "metadata", "description": "Units, authorship, source format, schema version."}
  ]
}

Storage implications

No new storage subsystem — geometry blobs live in .muse/objects/ like any other domain's binary payloads (§3). muse count-objects, muse gc, muse prune need no CAD-specific changes; they already operate on content-addressed objects generically. Open question (§10): repository size growth from large B-Rep blobs, and whether that motivates a future LFS-style side channel — explicitly deferred, not designed here.

API endpoints needed (MuseHub)

  • GET /:owner/:repo/cad/:ref/parts — list parts at a ref (mirrors an eventual code-domain symbol listing endpoint if one exists, else new).
  • GET /:owner/:repo/cad/:ref/diff?base=&target= — structured CAD diff for PR/proposal review rendering.
  • GET /:owner/:repo/cad/:ref/geometry/:body_id — stream a body's geometry blob (for viewer hydration, see UI below) — content-addressed, cacheable indefinitely.
  • Reuses existing generic muse hub proposal/issue endpoints unchanged — CAD needs no new proposal/review primitives, only new content rendering for the existing ones.

CLI implications

New muse cad subcommand group, structured like muse code: muse cad query, muse cad find-by-material, muse cad dimensions, muse cad modified-features, muse cad broken-constraints, muse cad duplicate-geometry, muse cad deps, muse cad impact. All accept --json. No changes needed to domain-agnostic commands (status, diff, merge, log, etc.) — they already dispatch through the registered plugin.

MuseHub UI implications

  • Proposal/diff view needs a CAD diff renderer: feature-history tree with changed features highlighted, a 3D viewer (e.g. three.js + a glTF or OCCT-JS conversion of the changed bodies) for geometry-level changes, and a constraint-conflict panel for merge conflict review. This is the largest net-new UI surface in this plan and should be its own issue (§10) — not attempted until the plugin's diff/merge output shape is stable.
  • Repo browse view needs a part/assembly tree view (mirrors a file tree, but keyed on the Part/Assembly hierarchy from §2 rather than the filesystem).

MCP/tooling implications

Expose cad_query, cad_diff, cad_impact as MCP tools (mirroring however code domain tools are already exposed, if they are) so agentception workers can reason about CAD state the same way they reason about code state today — e.g. "does this assembly still satisfy its mate constraints after this branch's changes" as an agent-callable check before a merge is attempted.


10. Staging Deliverables

Proposed issue breakdown

  1. CAD-1 — Canonical model + snapshot (§2, §3): entity dataclasses, normalization, content-addressing, STEP + FCStd import adapters, golden-snapshot tests (CAD_01CAD_22). Load-bearing — nothing else can start until this lands.
  2. CAD-2 — Semantic diff (§4): depends on CAD-1. CAD_30CAD_37.
  3. CAD-3 — Merge strategy (§5): depends on CAD-1, CAD-2. CAD_40CAD_53.
  4. CAD-4 — Validation & drift (§7): depends on CAD-1; can proceed in parallel with CAD-2/CAD-3 once CAD-1 lands. CAD_70CAD_75.
  5. CAD-5 — Query DSL + CLI (§6, CLI portion of §9): depends on CAD-1, CAD-2. CAD_80CAD_86.
  6. CAD-6 — Property-based test suite (§8): depends on CAD-1, CAD-2, CAD-3. CAD_60CAD_61.
  7. CAD-7 — MuseHub API endpoints (§9): depends on CAD-1, CAD-2.
  8. CAD-8 — MuseHub UI: CAD diff/viewer (§9): depends on CAD-7. Largest, most speculative issue — should not be scoped in detail until CAD-2/CAD-3 output shapes are proven stable against real fixtures.
  9. CAD-9 — MCP tooling exposure (§9): depends on CAD-5.

Milestones

  • M1 (Foundation): CAD-1 merged, muse domains --json lists cad as active, golden-snapshot suite green.
  • M2 (Semantic core): CAD-2 + CAD-3 merged; a hand-authored two-branch conflict fixture produces a correct, human-readable conflict report end-to-end via muse merge --dry-run.
  • M3 (Usable): CAD-4 + CAD-5 merged; muse cad query/broken-constraints/ duplicate-geometry all answer correctly against the fixture library.
  • M4 (Ecosystem): CAD-7, CAD-8, CAD-9 merged; a CAD proposal on staging MuseHub renders a real feature-level diff and geometry viewer.

Acceptance criteria (whole-plan level)

  • muse domains --json shows cad with active: true and the schema in §9.
  • Every fixture in §8's fixture library round-trips (§8, round-trip tier) with zero diff against itself.
  • The over-constrained and under-constrained fixtures are correctly flagged by muse check/muse status (§7) with no false positives on the well-constrained fixtures.
  • A merge of two branches that independently edit disjoint features/parts/mates auto-resolves with zero reported conflicts.
  • A merge of two branches that edit the same constraint value is reported as a conflict with a correct plain-language cause string (§5).
  • muse cad query correctly answers all seven example queries in §6 against the fixture library.
  • No test in the suite depends on network access, a specific FreeCAD version's exact binary output, or wall-clock time (determinism requirement, tying back to §3/§7).

Risks

  • Geometry kernel dependency risk: OCCT (via python-occ or FreeCAD's bundled bindings) is a large, non-trivial dependency to add to Muse's Python environment. Mitigation: isolate all kernel calls behind the _import_step.py/_import_fcstd.py adapters so the rest of the plugin (diff/merge/query/IR) has zero kernel dependency and can be tested/developed without OCCT installed, using pre-extracted fixture JSON.
  • Geometry-diff granularity risk: face-correspondence diffing (a true "what specifically changed about this shape's topology" diff) is a genuinely hard geometric problem; v0's coarse approach (§4) may prove too coarse for real review workflows. Mitigation: ship the coarse version first, gather real usage feedback via CAD-8's UI before investing in face-correspondence matching.
  • Constraint solver risk: full numerical constraint solving (not just DOF counting) is needed for some over/under-constraint edge cases to be conclusively judged; v0's DOF-counting approach can have false negatives (DOF count matches but the system is still degenerate). Mitigation: explicitly scope this as a known v0 limitation in §7, not a silent gap — flagged in the acceptance criteria as "no false positives," not "no false negatives."
  • Repository size risk: B-Rep geometry blobs can be large; without real usage data it's unknown whether .muse/objects/ scales acceptably for CAD-heavy repos. Tracked as an open question below, not designed against speculatively.

Open questions

  • Does geometry storage eventually need an LFS-style side channel separate from .muse/objects/, and at what repo-size threshold does that become necessary?
  • Should v0 support a full numerical constraint solver (not just DOF counting), or is that deferred to a v1 CAD-2 follow-up once real usage shows DOF counting's false- negative rate?
  • Is FreeCAD's .FCStd the right sole v0 authoring-tool target, or should a second open pipeline (e.g. Blender's CAD-Sketcher, or a bare OCCT script) be a fixture source too, to stress-test that the canonical IR is genuinely tool-agnostic rather than accidentally FreeCAD-shaped?
  • Should the MuseHub 3D viewer (CAD-8) target glTF conversion of changed bodies, or a WASM-compiled OCCT viewer for full-fidelity B-Rep rendering — this is primarily a frontend performance/fidelity tradeoff that should be spiked separately before CAD-8 is scoped in detail.

Implementation order

CAD-1 → CAD-2 → (CAD-3 and CAD-4 in parallel) → CAD-5 → CAD-6 → CAD-7 → CAD-8 and CAD-9 in parallel. This mirrors §1's dependency structure: nothing can diff, merge, validate, or query state that hasn't been normalized into the canonical model yet, and nothing in MuseHub/MCP can expose behavior the plugin doesn't yet have.


This is a plan-only issue. No implementation lands against it. Each numbered deliverable in §10 should be filed as its own staging issue once this plan is reviewed, referencing the test IDs and acceptance criteria defined above so implementation can proceed test-first without re-deriving these decisions.

Activity
gabriel opened this issue 19 days ago
No activity yet. Use the CLI to comment.