CAD Domain — implementation plan (TDD-first, comprehensive)
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 --jsonalongsidecode,midi,identity,mist,social. - Passes
muse code test --jsonfor 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 "...", mirroringmuse 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.
Recommended MVP file format strategy
Two-layer strategy, mirroring how the MIDI domain separates the symbolic model (notes/CC/track structure) from the container format (.mid):
- 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
SnapshotManifestfor MIDI notes. - 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/.binbundle) 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
codeandmidi(merge_mode: three_way), notsocial'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:
- 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). - 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).
- 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.
- Compute
topology_summaryandbounding_boxfor everyBodyat 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
.brepbinary path described above. - FreeCAD
.FCStddocuments 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/ReplaceOpkeyed onFeature.id. AReplaceOpon a feature carries acontent_summarynaming whichparamschanged (e.g."extrude depth 10mm -> 12mm"), and — critically — does not recurse into re-diffing every downstream feature unless that feature's ownparamsorinput_refschanged. Downstream features that merely recompute to new geometry because an upstream input changed are represented as a singlederived_geometry_changedannotation 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_refcontent ID actually changes. First checktopology_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/ReplaceOpkeyed onConstraint.id, same rename/move-detection rule as features (content ID stable across reorders). A changed constraintvalue(e.g. a dimension 10mm → 12mm) is reported distinctly from a changed constraintkindorrefs(which is a structural change, not a value change) — different severity for merge purposes (§5). - Material/property diffs: diffed at the
Material.idlevel; since materials are content-addressed by (name, properties), a part switching from one named material to another is a singleReplaceOponPart.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/partslist diffed byinstance_id, not position — moving a part instance in an assembly's on-disk list order is not a diff; changing itstransform, swapping whichpart_refan instance points to, or changing a mate'srefs/kindis. - Rename/move detection: since every element ID is content-derived (§3, step 1),
a "rename" is detected as same content ID, different
namefield — reported as aRenameOp(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 sameBody/Partcontent ID appearing under a different parent — reported as aMoveOp, not delete+insert, mirroring howcode'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
dimensionsinschema(), §9). - Same-feature conflicts: both branches modify the same
Feature.id'sparamsorinput_refsto 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'svalue— 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
refsorkind, 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_refcontent IDs for the sameBody.idwhere 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'smerge()implementation and its tests (§8). - Human-readable conflict reports: every conflict in the returned
MergeResultcarries: which element(s), which two branches' values, the ancestorbasevalue, 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
ExternalReferencewithresolved: false, or anyinput_refs/refspointing 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
unitdoesn't match the document's declaredMetadata.unitssystem 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_versionmismatches between the domain plugin's currentschema()output and a snapshot's recorded version — flagged before attempting a diff/merge across the boundary, surfaced as amuse code migrate-style warning (never auto-migrated silently, consistent with the workspace-wide rule thatmuse code migraterequires 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_10–CAD_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_30–CAD_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_40–CAD_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_50–CAD_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_70–CAD_75 |
| Query | One test per query example in §6 against a fixture with known expected results. | CAD_80–CAD_86 |
Fixture strategy
- Fixtures are authored once, by hand, in FreeCAD (open, scriptable, checked into
the test suite as both
.FCStdand 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 eventualcode-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/issueendpoints 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
- CAD-1 — Canonical model + snapshot (§2, §3): entity dataclasses, normalization,
content-addressing, STEP + FCStd import adapters, golden-snapshot tests
(
CAD_01–CAD_22). Load-bearing — nothing else can start until this lands. - CAD-2 — Semantic diff (§4): depends on CAD-1.
CAD_30–CAD_37. - CAD-3 — Merge strategy (§5): depends on CAD-1, CAD-2.
CAD_40–CAD_53. - CAD-4 — Validation & drift (§7): depends on CAD-1; can proceed in parallel with
CAD-2/CAD-3 once CAD-1 lands.
CAD_70–CAD_75. - CAD-5 — Query DSL + CLI (§6, CLI portion of §9): depends on CAD-1, CAD-2.
CAD_80–CAD_86. - CAD-6 — Property-based test suite (§8): depends on CAD-1, CAD-2, CAD-3.
CAD_60–CAD_61. - CAD-7 — MuseHub API endpoints (§9): depends on CAD-1, CAD-2.
- 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.
- CAD-9 — MCP tooling exposure (§9): depends on CAD-5.
Milestones
- M1 (Foundation): CAD-1 merged,
muse domains --jsonlistscadas 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-geometryall 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 --jsonshowscadwithactive: trueand 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 querycorrectly 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-occor 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.pyadapters 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
.FCStdthe 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.