12 KiB
PROPOSAL: CAD Design Schema Addendum
Status: PROPOSED / EXPERIMENTAL — not yet part of the canonical schema. This addendum proposes new Engineering-team node types for the FreeCAD MCP CAD-design workflow (Architect / Part Builder / Validator sub-team under Harper). It is a working draft meant to be exercised against the live graph before promotion. Do not treat these shapes as final — fields are expected to change once we build against them.
proposed_version: 2.4.0-draft targets: docs/tools/neo4j/unified-schema.md, docs/tools/neo4j/engineering.md owner_assistant: Architect (new, Engineering team) depends_on: 2.3.0 author: design exploration with Robert date: 2026-05-28
Why this exists
Harper frames a CAD concept (a Prototype), then hands it to a new Architect
agent that orchestrates a small CAD team (Part Builder, Validator) driving the
FreeCAD Robust MCP server on caliban. That workflow needs to persist three
things the current schema (v2.3.0, 79 nodes) has no home for:
- the overall design job and its refined spec,
- a per-part manifest (intent, target dimensions, build order, status), and
- validation results (issues found, never auto-fixed).
This addendum adds Design, Part, and ValidationResult under the
Engineering team, owned by the Architect. Harper's existing Prototype node is
the upstream concept; the new Design realizes it.
Layering principle (the core design constraint)
There are two authoritative stores, each owning a different layer:
| Layer | Source of truth | Node / artifact |
|---|---|---|
| Intent — what we mean to build, dims, status | the graph | Design, Part |
| Geometry — the actual solid model | the .FCStd file in FreeCAD |
Design.fcstd_path |
The graph does not duplicate geometry. Part.freecad_object is the join key
between the intent record and the real object in the FreeCAD document. The
Validator's job is precisely to reconcile the two and record discrepancies as
a ValidationResult. This is why there is no "geometry" or "shape data" field
anywhere below — that lives in FreeCAD, queried live via the MCP tools.
Proposed Node Types
Design
The overall CAD job. One Design ↔ one .FCStd document ↔ (usually) one
upstream Prototype. Owned/written by the Architect.
(:Design {
id: String!, // "design_wall_bracket"
name: String!,
status: String!, // specifying, building, validating, complete, abandoned
prototype_id: String, // FK → Prototype.id (the Harper handoff link)
spec: String, // refined requirements: dims, tolerances, material, loads, mounting
fcstd_path: String, // ground-truth model file on caliban (e.g. /home/robert/cad/wall_bracket.FCStd)
export_paths: [String], // STEP/STL/3MF produced at completion
screenshot_refs: [String],// paths/URIs of GUI screenshots captured by the Architect
units: String, // "mm" (default), "in" — explicit to avoid silent scale bugs
notes: String,
created_at: DateTime,
updated_at: DateTime
})
Status lifecycle: specifying → building → validating → complete
(or abandoned). The Architect advances it; Harper reads it to know the job is
done (complete) without parsing geometry.
Part
One modelled part = an intent record, not geometry. Gives the Architect a
queryable manifest and a build order. Written by the Architect; status may be
advanced by the Part Builder as it works.
(:Part {
id: String!, // "part_wall_bracket_baseplate"
name: String!,
design_id: String!, // FK → Design.id
status: String!, // planned, building, built, validated, rework
freecad_object: String, // name of the object in the .FCStd — the RECONCILIATION KEY
target_dims: Map, // {length: 80, width: 40, thickness: 4} (in Design.units)
feature_of: String, // optional Part.id — for sub-features (a pocket of a baseplate)
depends_on: [String], // Part.ids that must be built first → build order
notes: String,
created_at: DateTime,
updated_at: DateTime
})
On target_dims: Map — Neo4j map properties are flat (no nested maps,
primitive values only). That is sufficient for dimensions but is one of the
things to validate in experimentation (see Open Questions). If it proves
awkward, the fallback is parallel dim_* scalar fields or a JSON string.
ValidationResult
The Validator writes these and never repairs anything. One result per
validation pass of a part (or the whole design if part_id is null).
(:ValidationResult {
id: String!, // "valresult_2026-05-28_baseplate" (date-stamped, allows history)
design_id: String!, // FK → Design.id
part_id: String, // FK → Part.id, or null for a whole-document check
valid: Boolean!,
issues: [String], // structured strings: "Baseplate: invalid shape after pocket"
freecad_state: String, // recompute/error snapshot from validate_document
checked_by: String, // "Validator" — provenance, since multiple agents write the graph
created_at: DateTime,
updated_at: DateTime
})
Keeping a dated id per pass means validation history is preserved (a part can fail, get reworked, then pass) rather than overwritten — useful for the rework loop and for Harper's final report.
Proposed Relationships
(Prototype)-[:REALIZED_BY]->(Design) // Harper's concept → the CAD job
(Design)-[:HAS_PART]->(Part)
(Part)-[:DEPENDS_ON]->(Part) // build ordering
(Part)-[:FEATURE_OF]->(Part) // sub-feature composition (optional)
(ValidationResult)-[:VALIDATES]->(Part)
(ValidationResult)-[:VALIDATES]->(Design) // whole-document pass
(Design)-[:PRODUCED]->(Note) // completion message back to Harper's inbox
All follow the existing edge-naming style (REALIZED_BY, HAS_PART mirror the
work team's WON_FROM, HAS_* conventions).
Coordination: the existing Note-based inbox
No new mechanism. Handoffs are Note nodes (already the schema's generic message
carrier) with addressing tags and action_required. The wake trigger is
out-of-band — an agent checks its inbox when invoked or resumed.
// Harper → Architect (hand off a job)
MERGE (n:Note {id: "note_2026-05-28_freecad_bracket_req"})
ON CREATE SET n.created_at = datetime()
SET n.type = "idea",
n.action_required = true,
n.tags = ["for_architect", "cad_job"],
n.related_to = ["proto_wall_bracket"],
n.content = "Wall-mount bracket: holds 5kg, fits 35mm DIN rail, 2x M4 mounts...",
n.updated_at = datetime();
// Architect inbox query (run first thing on invoke/resume)
MATCH (n:Note)
WHERE "for_architect" IN n.tags AND n.action_required = true
RETURN n ORDER BY n.created_at;
// Architect → Harper (job complete) — mirror, tags:["for_harper"]; link to Design
MATCH (d:Design {id: "design_wall_bracket"})
MERGE (n:Note {id: "note_2026-05-28_bracket_done"})
ON CREATE SET n.created_at = datetime()
SET n.type = "insight", n.action_required = true,
n.tags = ["for_harper", "cad_result"],
n.related_to = ["design_wall_bracket", "proto_wall_bracket"],
n.content = "Bracket complete. 3 parts validated. STEP+STL exported. See Design.export_paths.",
n.updated_at = datetime()
MERGE (d)-[:PRODUCED]->(n);
A note is "consumed" by setting action_required = false once read/handled
(keeps the inbox query clean without deleting history).
End-to-end example (one bracket, sequential)
// 1. Harper frames the concept
MERGE (p:Prototype {id: "proto_wall_bracket"})
ON CREATE SET p.created_at = datetime()
SET p.name = "Wall bracket", p.status = "building",
p.tech_stack = ["freecad", "freecad-mcp"], p.updated_at = datetime();
// 2. Architect creates the Design + part manifest (after spec refinement)
MERGE (d:Design {id: "design_wall_bracket"})
ON CREATE SET d.created_at = datetime()
SET d.name = "Wall bracket", d.status = "building",
d.prototype_id = "proto_wall_bracket", d.units = "mm",
d.fcstd_path = "/home/robert/cad/wall_bracket.FCStd",
d.spec = "Holds 5kg; 35mm rail; 2x M4 c'bore mounts; ABS",
d.updated_at = datetime();
MATCH (p:Prototype {id: "proto_wall_bracket"}), (d:Design {id: "design_wall_bracket"})
MERGE (p)-[:REALIZED_BY]->(d);
MERGE (pt:Part {id: "part_wall_bracket_baseplate"})
ON CREATE SET pt.created_at = datetime()
SET pt.name = "Baseplate", pt.design_id = "design_wall_bracket",
pt.status = "planned", pt.freecad_object = "Baseplate",
pt.target_dims = {length: 80, width: 40, thickness: 4},
pt.updated_at = datetime();
MATCH (d:Design {id: "design_wall_bracket"}), (pt:Part {id: "part_wall_bracket_baseplate"})
MERGE (d)-[:HAS_PART]->(pt);
// 3. Part Builder builds in FreeCAD (transactioned), then marks built
MATCH (pt:Part {id: "part_wall_bracket_baseplate"})
SET pt.status = "built", pt.updated_at = datetime();
// 4. Validator validates, writes result, NEVER fixes
MERGE (v:ValidationResult {id: "valresult_2026-05-28_baseplate"})
ON CREATE SET v.created_at = datetime()
SET v.design_id = "design_wall_bracket", v.part_id = "part_wall_bracket_baseplate",
v.valid = true, v.issues = [], v.checked_by = "Validator",
v.freecad_state = "recomputed, no errors", v.updated_at = datetime();
MATCH (v:ValidationResult {id: "valresult_2026-05-28_baseplate"}),
(pt:Part {id: "part_wall_bracket_baseplate"})
MERGE (v)-[:VALIDATES]->(pt);
MATCH (pt:Part {id: "part_wall_bracket_baseplate"})
SET pt.status = "validated", pt.updated_at = datetime();
// 5. all parts validated → Architect completes + notifies Harper (see inbox section)
MATCH (d:Design {id: "design_wall_bracket"})
SET d.status = "complete",
d.export_paths = ["/home/robert/cad/wall_bracket.step", "/home/robert/cad/wall_bracket.stl"],
d.screenshot_refs = ["/home/robert/cad/wall_bracket_iso.png"],
d.updated_at = datetime();
Open questions to resolve during experimentation
These are deliberately unsettled — the reason this is an addendum, not a schema edit:
target_dimsas Map vs scalars vs JSON string. Neo4j maps are flat and can't be indexed; if we never query by a dimension, a Map is fine. If we do, reconsider. Exercise both before promoting.- History granularity. Dated
ValidationResultids preserve every pass. Do we also want datedPartrevisions, or isstatuschurn +updated_atenough? (Leaning: status churn is enough; FreeCAD's own undo/redo +.FCStdsaves are the real revision history.) - Part vs sub-feature.
FEATURE_OFlets a pocket/fillet be its ownPart. Is that useful manifest granularity, or noise? May collapse to "a Part is a top-level solid only." .FCStdlocation & ownership.fcstd_pathassumes a stable CAD working dir on caliban. Decide owner/permissions (the GUI bridge runs asrobert).- Abandoned/rework cleanup. When a Design is
abandoned, do dependentPart/ValidationResultnodes stay (history) or get pruned? Leaning: stay. - Multiple Designs per Prototype.
prototype_idis scalar (1 Design per concept). If a prototype spawns variants, this becomes a[:REALIZED_BY]fan-out — already supported by the relationship; just don't over-rely on the scalar FK.
Promotion checklist (when experimentation settles)
Only after building against the live graph and resolving the open questions:
- Fold the three node definitions into
unified-schema.mdunder a new "Architect's Domain (CAD Design)" subsection (Engineering team). - Add the relationships to the Engineering cross-team relationships block.
- Add 3 rows to the Node Type Summary table (→ 82 nodes) and add the Architect to the Teams & Assistants table.
- Add an "Architect's Nodes" table + handoff note to
engineering.md. - Bump version to 2.4.0 with a Version History line; update the assistant count.
- Delete this proposal file (it has served its purpose).