docs(neo4j): propose CAD design schema addendum for Architect workflow
This commit is contained in:
281
docs/tools/neo4j/proposals/cad-design-addendum.md
Normal file
281
docs/tools/neo4j/proposals/cad-design-addendum.md
Normal file
@@ -0,0 +1,281 @@
|
||||
# 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.
|
||||
|
||||
```cypher
|
||||
(: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.
|
||||
|
||||
```cypher
|
||||
(: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).
|
||||
|
||||
```cypher
|
||||
(: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
|
||||
|
||||
```cypher
|
||||
(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.
|
||||
|
||||
```cypher
|
||||
// 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)
|
||||
|
||||
```cypher
|
||||
// 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).
|
||||
Reference in New Issue
Block a user