Files
koios/docs/tools/neo4j/proposals/cad-design-addendum.md

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:

  1. the overall design job and its refined spec,
  2. a per-part manifest (intent, target dimensions, build order, status), and
  3. 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: specifyingbuildingvalidatingcomplete (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:

  1. target_dims as 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.
  2. History granularity. Dated ValidationResult ids preserve every pass. Do we also want dated Part revisions, or is status churn + updated_at enough? (Leaning: status churn is enough; FreeCAD's own undo/redo + .FCStd saves are the real revision history.)
  3. Part vs sub-feature. FEATURE_OF lets a pocket/fillet be its own Part. Is that useful manifest granularity, or noise? May collapse to "a Part is a top-level solid only."
  4. .FCStd location & ownership. fcstd_path assumes a stable CAD working dir on caliban. Decide owner/permissions (the GUI bridge runs as robert).
  5. Abandoned/rework cleanup. When a Design is abandoned, do dependent Part/ValidationResult nodes stay (history) or get pruned? Leaning: stay.
  6. Multiple Designs per Prototype. prototype_id is 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.md under 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).