# cad-agent: full text > Your one stop shop for agentic CAD. Mapping the best free resources, demonstrating their capabilities, and filling the gaps. Made for AI and people. --- Source: https://cad-agent.valpatel.com/CONTRIBUTING.md # Contributing Several projects build on cad-agent at the same time (the J501 enclosure, Gyropup, the AGX Thor CAN boards, and others), often with AI agents working in parallel. A change to cad-agent can break all of them, so every change goes through a branch, a pull request, CI and a review. ## Branches | Branch | What it is | Who writes to it | |---|---|---| | `main` | Released toolkit, tagged `vX.Y.Z`. | Only by merging a release PR from `dev`. | | `dev` | Integration, and the repository's default branch. Everything here passed CI and review. | Only by merging a reviewed PR. | | `feat/`, `fix/`, `ci/` | One piece of work. | Its author, in its own worktree. | `main` and `dev` are protected: no direct pushes (admins included), no force pushes, both CI jobs must pass and every review conversation must be resolved. ## Workflow Agents: install the workflow kit (`cad-agent agents install`, [docs/agent-workflow.md](https://cad-agent.valpatel.com/docs/agent-workflow.md)) and follow `cad-work-tracking`: an issue per change, an early draft PR with `Closes #`, and pushes as you go. 0. **Issue.** Find or open the GitHub issue the change belongs to, with a checklist of done / next / how to resume. 1. **Worktree from `dev`.** Never edit a checkout another session may be using: ```bash git fetch origin git worktree add --no-track ../cad-agent- -b feat/ origin/dev cd ../cad-agent- && git push -u origin feat/ # the branch tracks itself, not dev ``` 2. **Commit and push** small, focused changes with tests (`tests/`) as you go, not in one batch at the end. Keep the package generic: anything specific to one product stays in that product's repository. 3. **Open a PR into `dev`** early, as a draft if it is unfinished (`gh pr create --draft --base dev`), with `Closes #`; fill in the template, including what projects using cad-agent must change. 4. **CI** (`.github/workflows/ci.yml`) runs `core` on Python 3.10 and 3.12 (required dependencies only; optional features must skip cleanly) and `full` (CAD, FEA, robot and site extras, imported loudly before the tests). 5. **Review.** Every PR gets a review from a person or from a separate review agent that did not write the change, posted as PR comments (the `cad-pr-reviewer` subagent, or a separate Codex session with that skill). GitHub cannot enforce this here (one owner account cannot approve its own PRs), so the merger checks that a review exists. The author answers or fixes each comment and resolves it. 6. **Merge** into `dev` (squash or merge commit). Then, before removing the worktree, repoint anything installed from it: `pip show -f cad-agent` (or the `direct_url.json` it lists) in each project venv tells you where `cadagent` comes from. Reinstall those from the `dev` checkout below, then `git worktree remove ../cad-agent-`. ## Using cad-agent from a project - **Released:** a checkout of `main` (or a tag), installed editable: `pip install -e "[cad,fea]"`. - **Latest reviewed:** a dedicated worktree that only tracks `origin/dev` (`git worktree add ../cad-agent-dev origin/dev`, updated with `git -C ../cad-agent-dev pull --ff-only`). Never edit in it. - **A feature under test:** that feature's worktree, while its PR is open. Say so in the PR. A project that needs a new toolkit feature opens a cad-agent PR for it; it does not patch its local copy. ## Releases Releases are cut when the user asks for one. 1. On a `release/vX.Y.Z` branch from `dev`, bump `version` in `pyproject.toml`; PR it into `dev` and merge. 2. Open a PR from `dev` into `main` titled `Release vX.Y.Z`, listing the merged PRs and what projects must change. Merge it with a **merge commit** (not squash or rebase), so `main` and `dev` keep a common history. 3. Tag the merge commit on `main` and push the tag: `git tag -a vX.Y.Z -m "..." origin/main && git push origin vX.Y.Z`. --- Source: https://cad-agent.valpatel.com/README.md # cad-agent [![CI](https://github.com/Valpatel/cad-agent/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/Valpatel/cad-agent/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Valpatel/cad-agent/blob/dev/LICENSE) **Hardware design with AI agents, checked like engineering.** cad-agent turns a folder of CAD scripts into a repeatable pipeline: build the parts, check the fit, simulate strength and airflow, and publish the evidence as print files, reports and a linked review site. It works with Claude Code first, plus Codex, Cursor and Gemini.

Gyropup, a self-balancing wheel-legged robot designed with cad-agent

3D viewer
3D viewer and review site. Every part in place, with explode, section and one top bar linking every report.
FEA of an enclosure lid
Strength. FEA of a ribbed lid under a 300 N press, coloured by stress as a fraction of the material's strength.
Topology optimisation of an enclosure floor
Less material. Topology optimisation keeps only the floor that carries load.
## How it works A project describes itself in one `cad-agent.yaml`. cad-agent runs it and collects the proof. ```mermaid flowchart LR P["📐 Your project
cad-agent.yaml
CAD scripts · parameters"] B["🔧 build
CAD · checks · simulations
run as ordered steps"] M["🖨️ export-3mf
bed-centred 3MF
read back and verified"] R["✅ check + report
every result, pass or fail
with its evidence"] Z["📦 package
review zip
sha256 manifest"] W["🌐 site --serve
linked review site
3D viewer · live reload"] P --> B --> M --> Z B --> R --> Z R --> W classDef src fill:#1f2933,stroke:#e8892b,color:#f5f7fa classDef step fill:#0e3a43,stroke:#00b8c8,color:#f5f7fa classDef out fill:#2a2440,stroke:#9d7bff,color:#f5f7fa class P src class B,M,R step class Z,W out ``` `cad-agent all` runs build, export-3mf, check, report and package in one go. It never claims a physical fit. A check either has evidence or it says what is assumed. **PCB and enclosure workbenches:** [PCB workflow](https://cad-agent.valpatel.com/docs/pcb.md) covers embedded schematics, copper layers, assembly views and housing renders in the status page; native KiCad ERC/DRC/parity evidence; source freshness; and JLC BOM/CPL checks. Use `cad-agent pcb-check` to bind reports to source files. Qualification gates stay separate from CAD checks. ## What's in the box ```mermaid flowchart LR D["✏️ Design
connectors · mcmaster
robot · geometry · explore"] V["🧪 Verify
fea · cfd · heatsink
topopt · slicer"] S["🖼️ Show
render · simviz · reports
site · 3D viewer"] P["📦 Ship
STL · STEP · 3MF
hashed review zip"] D --> V --> S --> P classDef g fill:#14232b,stroke:#3a8fa3,color:#e8edf5 class D,V,S,P g ``` | | Module | What it gives you | |---|---|---| | ✏️ | `connectors` | Port windows derived from the modelled mating plug, then a sweep check that the plug really goes in | | ✏️ | `mcmaster` | The manufacturer's STEP and specs for a McMaster-Carr part number, normalised for assembly | | ✏️ | `robot` | Mass properties from CAD, links and joints, URDF and MuJoCo export, meshes kept under budget | | ✏️ | `dossier` | The design dossier: requirements, sections, part spec cards, interfaces and decisions with the alternatives rejected, in YAML with `file:line` errors; build-script declarations of parts, cavities, joints, fasteners and assembly steps, exported for the gates; a lint and a per-section status (draft to signed off, voided when the geometry changes) from `cad-agent dossier` ([guide](https://cad-agent.valpatel.com/docs/dossier.md)) | | ✏️ | `gates` | Integrity gates on the assembly: floating parts and unrealised interfaces, items outside their cavity, fastener seat, engagement, tip and tool reach, motion-range collisions, assembly order and access, minimum walls. `cad-agent gates` runs them in parallel processes, writes verdicts bound to the geometry they checked, and `check` fails on a failing or stale one ([guide](https://cad-agent.valpatel.com/docs/gates.md)) | | ✏️ | `explore` | Part catalogues with prices and sources, designs that pick a part per slot, the project's own evaluator, Pareto ranking of the trade-offs, one-part swaps and a search over parts and parameters ([guide](https://cad-agent.valpatel.com/docs/explore.md)) | | 🧪 | `fea` | gmsh + scikit-fem linear elasticity with printed-material allowables, checked against textbook cases | | 🧪 | `compliant` | Snap-fit sizing and checks (cantilever, torsion, annular) by the Covestro/Bayer method, tested against the guide's worked examples; allowable strain for moulded resins and, by print direction, for 13 filaments from their datasheets ([guide](https://cad-agent.valpatel.com/docs/compliant.md)) | | 🧪 | `cfd` | Conjugate heat transfer cases for OpenFOAM written from CAD: fans, fin blocks, heat sources | | 🧪 | `heatsink` | Plate-fin correlations, fan operating point and porous-block coefficients for CFD | | 🧪 | `topopt` | pyMOTO topology optimisation (SIMP, MMA, overhang filter) to cut filament where it does no work | | 🧪 | `slicer` | Bambu Studio's command line for real filament grams and print time | | 🖼️ | `render`, `simviz` | Off-screen renders, stress and flow pictures, MP4 animations; a review packet per dossier section (solid, x-ray, cuts through cavities, joints and screws, exploded along the assembly steps, joint range ends, gate findings at their witness points) with the measurements first and the render checklist, from `cad-agent review-packet` ([guide](https://cad-agent.valpatel.com/docs/review-packet.md)) | | 🖼️ | `report`, `simreport` | Results JSON and standalone HTML reports that name their limits | | 🖼️ | `site`, `view`, webkit | A local review site with progress, decisions, docs and a 3D viewer, all under one top bar | | 📦 | `mesh3mf`, `package` | 3MF writing with an independent read-back, and a hashed review zip | The full guide, the `cad-agent.yaml` format and every command are in [`docs/toolkit.md`](https://cad-agent.valpatel.com/docs/toolkit.md). ## Quick start ```bash git clone https://github.com/Valpatel/cad-agent && cd cad-agent pip install -e ".[cad]" # add fea, sim, robot, site as needed cad-agent all --project path/to/your-project # build, 3MF, check + report, package cad-agent site --serve --project path/to/your-project ``` | Extra | Adds | |---|---| | `cad` | CadQuery, trimesh, VTK: building, checking and rendering parts | | `fea` | gmsh, scikit-fem, PyPardiso: stiffness and strength | | `sim` | PyVista, pyMOTO, manifold3d: airflow cases, topology optimisation, pictures and videos (OpenFOAM and Bambu Studio run as separate programs) | | `robot` | MuJoCo, for robot export | | `site` | Markdown, for the review site | | `explore` | optuna, for the NSGA-II and TPE searches of `cad-agent optimise` (random and grid need nothing) | ## Built with cad-agent | Project | What it is | |---|---| | [J501 AGX enclosure](https://github.com/mvalancy/j501-agx-orin) | A printed enclosure for the Seeed reServer J501 with a Jetson AGX Orin: port windows from mating plugs, FEA, airflow and topology-optimised floor vents | | [Gyropup](https://github.com/Valpatel/gyropup) | A self-balancing wheel-legged robot with a foam-ball turret: five-bar legs, McMaster hardware, strength and fall studies | | AGX Thor CAN boards | PCB and enclosure reviews | Anything specific to one product stays in that product's repository. cad-agent only holds what every project can use. ## How cad-agent grows ```mermaid flowchart LR A["🔎 Adopt
the best public tools,
pinned and security-reviewed"] L["🧠 Learn
rules mined from real
design sessions"] E["📏 Evaluate
tasks with machine-checked
acceptance criteria"] G["🧩 Fill the gaps
only what nobody
else provides"] A --> E L --> E E --> G G -.->|upstream PRs| A ``` - **Adopt.** Six upstreams are vendored under `vendor/`, pinned by commit, scanned and updated weekly through a reviewed process ([`SECURITY.md`](https://github.com/Valpatel/cad-agent/blob/dev/SECURITY.md), [`WEEKLY-UPDATE.md`](https://github.com/Valpatel/cad-agent/blob/dev/WEEKLY-UPDATE.md)): text-to-cad, kicad-happy and kicad-mcp-pro (MIT), and build123d-mcp, agentcad and bd_warehouse (Apache-2.0). OpenFOAM (GPL) and Bambu Studio (AGPL) run only as separate programs. gmsh (GPL) is imported, but only by the optional `fea` extra. Licences are in [`THIRD_PARTY_NOTICES.md`](https://cad-agent.valpatel.com/THIRD_PARTY_NOTICES.md). - **Learn.** [`lessons/`](https://github.com/Valpatel/cad-agent/tree/dev/lessons/) holds eleven sets of rules mined from real design sessions, each citing its source. They cover verifying from source, connectors, fasteners, evidence, simulation and material. - **Evaluate, then fill the gaps.** Each tool is scored on the same tasks. What wins gets adopted, the gaps go upstream, and only what nobody else provides lives here. ## Contributing Several projects and AI agents use cad-agent at once, so every change goes through a pull request. ```mermaid gitGraph commit id: "v0.1" branch dev checkout dev commit id: "integration" branch feature checkout feature commit id: "change + tests" commit id: "review fixes" checkout dev merge feature id: "PR: CI + review" checkout main merge dev id: "release PR" tag: "vX.Y.Z" ``` 1. Work in your own worktree on `feat/`, branched from `dev`. 2. Open a PR into `dev`. CI runs `core` on Python 3.10 and 3.12, and `full` with every extra. 3. A person or a separate review agent reviews it. Every comment is answered and resolved before the merge. 4. Releases go from `dev` to `main` by PR. Both branches are protected. Details are in [`CONTRIBUTING.md`](https://cad-agent.valpatel.com/CONTRIBUTING.md). Rules for AI agents are in [`AGENTS.md`](https://github.com/Valpatel/cad-agent/blob/dev/AGENTS.md). ## Layout | Folder | What is in it | |---|---| | [`src/cadagent/`](https://github.com/Valpatel/cad-agent/tree/dev/src/cadagent/) | The toolkit, with its web kit in `webkit/` | | [`tests/`](https://github.com/Valpatel/cad-agent/tree/dev/tests/) | The test suite, which also runs in CI | | [`docs/`](https://github.com/Valpatel/cad-agent/tree/dev/docs/) | [`toolkit.md`](https://cad-agent.valpatel.com/docs/toolkit.md), the guide to the toolkit, [`explore.md`](https://cad-agent.valpatel.com/docs/explore.md), design exploration, and [`dossier.md`](https://cad-agent.valpatel.com/docs/dossier.md), the design dossier | | [`examples/`](https://github.com/Valpatel/cad-agent/tree/dev/examples/) | Worked examples: [`explore-two-wheel`](https://github.com/Valpatel/cad-agent/tree/dev/examples/explore-two-wheel/) compares robot configurations | | [`skills/`](https://github.com/Valpatel/cad-agent/tree/dev/skills/), [`agents/`](https://github.com/Valpatel/cad-agent/tree/dev/agents/) | Agent skills and subagents for Claude Code and Codex: work tracking, PR review, remote runs, the section-by-section design process ([`design-process`](https://cad-agent.valpatel.com/skills/design-process/SKILL.md)) and McMaster parts ([agent workflow](https://cad-agent.valpatel.com/docs/agent-workflow.md); `cad-agent agents install`) | | [`lessons/`](https://github.com/Valpatel/cad-agent/tree/dev/lessons/) | Rules mined from design sessions, with their sources | | [`research/`](https://github.com/Valpatel/cad-agent/tree/dev/research/) | Landscape surveys, including the simulation tools | | [`upstreams/`](https://github.com/Valpatel/cad-agent/tree/dev/upstreams/), [`vendor/`](https://github.com/Valpatel/cad-agent/tree/dev/vendor/) | The tool registry and the vendored, pinned upstreams | | [`tools/`](https://github.com/Valpatel/cad-agent/tree/dev/tools/), [`mining/`](https://github.com/Valpatel/cad-agent/tree/dev/mining/) | Vendor sync, security scan and the local session miner | ## License MIT. See [`LICENSE`](https://github.com/Valpatel/cad-agent/blob/dev/LICENSE). Vendored code keeps its own licence, listed in [`THIRD_PARTY_NOTICES.md`](https://cad-agent.valpatel.com/THIRD_PARTY_NOTICES.md). --- Source: https://cad-agent.valpatel.com/THIRD_PARTY_NOTICES.md # Third-party notices Code under `vendor/` is copied from the projects below at the pinned commit. Each directory keeps its upstream LICENSE/NOTICE files; those terms apply to that code. Our changes are listed as patches in `vendor/patches/`. | Project | Licence | Upstream | Pinned commit | Imported | |---|---|---|---|---| | text-to-cad | MIT | https://github.com/earthtojake/text-to-cad | `2f793659fb7b` | 2026-10-24 | | kicad-happy | MIT | https://github.com/aklofas/kicad-happy | `2fd9aa0fe2d6` | 2026-10-24 | | kicad-mcp-pro | MIT | https://github.com/oaslananka/kicad-mcp-pro | `f03b478f597d` | 2026-10-24 | | build123d-mcp | Apache-2.0 | https://github.com/pzfreo/build123d-mcp | `59b256121886` | 2026-10-24 | | agentcad | Apache-2.0 | https://github.com/jdilla1277/agentcad | `52af472ddade` | 2026-10-24 | | bd_warehouse | Apache-2.0 | https://github.com/gumyr/bd_warehouse | `e6ae6e7bae74` | 2026-10-24 | ## Runtime dependencies and external programs (not vendored) The simulation modules use these at run time. None of their code is copied into this repository. | Project | Licence | How cad-agent uses it | |---|---|---| | OpenFOAM v2512 (OpenCFD) | GPL-3.0 | External program (`cadagent.cfd` writes a case and runs it natively or in the `opencfd/openfoam-default` Docker image) | | Bambu Studio | AGPL-3.0 | External program (`cadagent.slicer` runs its command line; profiles are read, flattened and passed back as files) | | gmsh | GPL-2.0-or-later | Imported by the optional `fea` extra. Our source stays MIT; distributing a bundle that includes gmsh falls under the GPL | | scikit-fem | BSD-3-Clause | Imported (`fea`) | | pyMOTO | MIT | Imported (`sim`, topology optimisation) | | PyVista, VTK | MIT, BSD-3-Clause | Imported (`sim`, pictures and animations) | | trimesh, manifold3d, Rtree | MIT, Apache-2.0, MIT | Imported (`sim`, mesh booleans and point-in-solid tests) | | imageio, imageio-ffmpeg (FFmpeg) | BSD-2-Clause (FFmpeg LGPL/GPL binary, run as a process) | Imported (`sim`, MP4 writing) | ## Web kit (copied into project sites and published on cad-agent.valpatel.com) `src/cadagent/webkit/vendor/` holds these files unchanged unless noted; each keeps its licence text beside it, and `cad-agent site` copies the licence files with the code. | Project | Licence | Version | Licence file in the kit | |---|---|---|---| | three.js (core and the controls, geometry, loader and utility add-ons) | MIT | r160 | header of `three/three.module.min.js`; `file-parsers/THREE-LICENSE.txt` | | three.js STL and 3MF loaders | MIT | r160, import paths changed to the local copies | `file-parsers/THREE-LICENSE.txt` | | fflate | MIT | as bundled with three.js r160 | `file-parsers/FFLATE-LICENSE.txt` | | Mermaid | MIT | 11.4.1 | `mermaid/LICENSE` | | linkedom | ISC | 0.18.12 | `linkedom/LICENSE` | | occt-import-js (Open CASCADE Technology compiled to WebAssembly) | LGPL-2.1 with the OCCT exception | 0.0.23 | `occt-import-js/` (four licence files; sources in `occt-import-js/source`) | | Inter | SIL Open Font License 1.1 | variable, Latin subset | `fonts/OFL-Inter.txt` | | JetBrains Mono | SIL Open Font License 1.1 | variable, Latin subset | `fonts/OFL-JetBrainsMono.txt` | --- Source: https://cad-agent.valpatel.com/agents/cad-pr-reviewer.md You review a pull request you did not write. Follow the `cad-pr-review` skill (reviewer part). In short: 1. Read the PR description, the linked issue and the diff (`gh pr view `, `gh pr diff `, or `git diff origin/...HEAD` in the checkout you were given). Read the repo's AGENTS.md for its rules. 2. Do not edit, commit or push anything in the checkout, and do not touch any other worktree. Scratch scripts go in a temporary directory outside the repo. 3. Verify before you post: run the tests that cover the change, reproduce each suspected bug with a small script, check generated evidence (STEP/STL bounds and volumes, report numbers, convergence of simulations) against what the PR claims. Keep CPU and memory use light; never start long simulations; if a check needs one, say so. 4. For CAD and simulation work, check the physical assumptions as hard as the code: directions (fan flow, loads, gravity), units, orientation and datum of photo or drawing fits, which numbers are measured and which assumed, and that nothing claims more than CAD or simulation can show. 5. For any change to part geometry, walk cad-agent's common-issues index ([docs/common-issues.md](https://github.com/Valpatel/cad-agent/blob/dev/docs/common-issues.md)), every row: - Every automatic check row must be in the committed report (`exports/cad-agent-results.json`) and pass, bound to the current files. If "Holes clear" is missing or stale, run it without touching the checkout: `cad-agent holes --out /tmp//holes.json` (about 10 s for a large part). A missing, stale or failing row is a blocking finding. Also look at its listed ledges and the blind holes in a section view: they are the check's known misses. - For each review row, look at the geometry (sections, renders, the STEP) and say in the summary what you checked. - When you find a mistake the index does not list, say so: it belongs in the index. 6. Post inline comments on exact lines with severity (blocking / minor / nit) via `gh api repos///pulls//comments` (`commit_id` = head SHA, `path`, `line`, `side=RIGHT`), then one summary review with `gh pr review --comment` that states plainly whether anything blocks the merge. Do not approve or merge. 7. Final message: each finding as `file:line, severity, one sentence`, plus what you checked and found fine, and the links to the review and comments. --- Source: https://cad-agent.valpatel.com/agents/cad-resume.md You pick up work that another session started. Follow the "Resuming" part of the `cad-work-tracking` skill. 1. For each repo you were given (default: the current directory): - `git fetch --all --prune`, `git status -sb`, `git worktree list`, and for each local branch the commits not on its upstream (`git log --oneline @{u}..HEAD` or `git log --oneline --branches --not --remotes`). - Open issues and PRs (`gh issue list --state open`, `gh pr list --state open`), the tracking issue named in the brief or found by the branch's PR (`Closes #N`), its checklist and its last few comments. - CI state of the open PRs (`gh pr checks `) and unresolved review threads. - Work claims on those issues (comments starting ` **Claimed** by agent `a-7kq2m4xw` until 2026-11-01T19:00Z UTC. Scope: … Branch: … ``` Actions are `claim`, `checkpoint` (renews the lease), `release` and `done`. A lease lasts at most 24 h; a claim not refreshed in time has expired and the issue is open for anyone. Of the active claims, the one whose unbroken run of claims and checkpoints started first holds. Tags are pseudonymous (`a-` + 8 of `[a-z2-7]`; one per tool and machine, so sibling sessions share it): no host or user names in public repos. Full rules in [cad-work-tracking](https://cad-agent.valpatel.com/skills/cad-work-tracking/SKILL.md#work-claims-sharing-issues-between-agents). Things that must not live only locally: decisions and reasons, the user's measurements and observations, run commands and results, lessons. Licensed or large inputs stay out of git, but the repo says where they come from and how to fetch them, so a reformatted machine can rebuild. ## In a project repo Point the project's AGENTS.md at this workflow (one line: "Follow the cad-agent workflow kit: cad-work-tracking, cad-pr-review, cad-remote-runs; install with `cad-agent agents install`") and add the project's own rules next to it: its integration branch, who may merge, its compute hosts (in private notes if the repo is public). --- Source: https://cad-agent.valpatel.com/docs/common-issues.md # Common issues in CAD design reviews These mistakes recur in printed parts and assemblies. The list comes from real projects, and each entry links the lesson that recorded it. Every review walks this list, whoever wrote the design and however small the change: 1. Run `cad-agent all`. Every row in the report must pass. A row that fails, is stale or did not run is a finding, never a footnote. 2. Then go through the **Review** rows below. No automatic check covers them yet, so look at the model, the renders and the sources yourself. Say in the review which ones you checked and how. When a review finds a new kind of mistake, add it here, in the same PR or in an issue, with the check that would catch it. Once a check exists, move the entry from "review" to the check. ## Geometry and modelling | Issue | How it shows up | Caught by | Lesson | |---|---|---|---| | **A feature added after the holes refills them**: a rib, boss, skirt or gusset unioned after the screw holes, counterbores, countersinks or insert bores were cut | The part is a valid watertight solid and every fit, strength and slice check passes, but a screw head will not seat and an insert will not go in. Found on a printed lid: stiffening ribs ran into the four socket-head counterbores | `cad-agent holes` (the "Holes clear" rows, run by `all`): every cylindrical hole and countersink cone in each part's STEP is probed for material inside. Fix the build order so every hole is cut last, after all the added features. Known misses, so look at these in a section view: a hole capped at one end reads as blind, and a counterbore blocked right across with a hole cut through the block reads as a ledge (listed in the result) | [03](https://cad-agent.valpatel.com/lessons/03-mechanical-design-rules.md) | | A rib or boss ends on air: it was placed over a vent slot or an opening cut later | It looks attached in a render, but FEA puts the peak stress at the joint. A rib that is fully detached prints as a loose piece | FEA, plus a section view at each rib end. The "Printed parts" rows (one solid) catch a rib that is fully detached | [03](https://cad-agent.valpatel.com/lessons/03-mechanical-design-rules.md) | | Coplanar or flush unions | Faces that only touch leave seams: two solids, a non-manifold edge, or an open STL | The "Printed parts" rows (single solid, watertight). Overlap joined features by 1–2 mm | — | | Parts collide, or a clearance was checked against a bounding box | It fits in the CAD assembly only because something was approximated or left out | Gates relation table; the project's clearance step. Investigate every hit; classify false positives explicitly | [07](https://cad-agent.valpatel.com/lessons/07-verification-and-evidence.md) | | Walls or ribs too thin to print or carry load | Weak parts; gaps in the slicer preview | The wall gate (`gates`); FEA for loaded parts | [10](https://cad-agent.valpatel.com/lessons/10-simulation-and-material.md) | | Overhangs and unsupported bridges in the print orientation | Sagging or failed prints | `cadagent.geometry.downward_area` overhang screen; check in the slicer preview | [03](https://cad-agent.valpatel.com/lessons/03-mechanical-design-rules.md) | | Engraved or embossed text not fused before cutting | Floating islands; an open mesh | "Printed parts" rows (single solid, watertight) | [06](https://cad-agent.valpatel.com/lessons/06-enclosure-features.md) | | Slender columns or insert bosses not tied into a wall | They crack or wobble when the screw is driven | Review: every boss and column has a load path; FEA | [03](https://cad-agent.valpatel.com/lessons/03-mechanical-design-rules.md) | | One weak feature fixed, its siblings left as they were | The same failure on the other three corners | Review: survey all similar features when one is found | [03](https://cad-agent.valpatel.com/lessons/03-mechanical-design-rules.md) | ## Bought parts, sources and assumptions | Issue | How it shows up | Caught by | Lesson | |---|---|---|---| | **The wrong bought part**: a similar model, a guessed size, another revision | A printed lid 15 mm too short for the cooler that was actually fitted | Review: identify the exact part (vendor, part number, datasheet, photos) before sizing anything around it; record it | [10](https://cad-agent.valpatel.com/lessons/10-simulation-and-material.md) | | A photo read in the wrong orientation, or a flow direction assumed | Features on the wrong side; a CFD with the fan blowing the wrong way | Review: record the user's observations (measurements, "the fan blows up") with the date, and check assumptions against them | [01](https://cad-agent.valpatel.com/lessons/01-verify-from-source.md) | | Connector windows sized from a bounding box or the board connector, not the mating plug | The cable's overmould does not fit | `cadagent.connectors`: windows from the modelled mating plug, plus a sweep check | [03](https://cad-agent.valpatel.com/lessons/03-mechanical-design-rules.md), [05](https://cad-agent.valpatel.com/lessons/05-cables-connectors-and-routing.md) | | A vendor model used as if it were measured data | Hidden errors in mounting holes or heights | Review: say which bodies are proxies | [01](https://cad-agent.valpatel.com/lessons/01-verify-from-source.md) | | Vendor files committed or redistributed | A licence problem; a leak that cannot be undone | Review the package and release include lists; fetch references with a script | [agent workflow](https://cad-agent.valpatel.com/docs/agent-workflow.md) | ## Fasteners and assembly | Issue | How it shows up | Caught by | Lesson | |---|---|---|---| | Screw length from the wrong datum, or too little thread engagement | Strips, or bottoms out | The fastener gate; compute the stack plus the engagement; print a coupon | [04](https://cad-agent.valpatel.com/lessons/04-fasteners-and-assembly-access.md) | | No tool access at the step where a fastener goes in | It cannot be assembled in the documented order | The assembly-access gate; the assembly-step renders | [04](https://cad-agent.valpatel.com/lessons/04-fasteners-and-assembly-access.md) | | Inserts, countersinks or D-holes sized without a test print | Loose inserts; bulkheads that turn | Coupons printed before the big parts | [04](https://cad-agent.valpatel.com/lessons/04-fasteners-and-assembly-access.md) | | Cable routes at an impossible bend radius, or tie anchors in the path | Cables that cannot be routed | Review: route from the real mating direction with a stated bend radius | [05](https://cad-agent.valpatel.com/lessons/05-cables-connectors-and-routing.md) | ## Evidence, simulation and release | Issue | How it shows up | Caught by | Lesson | |---|---|---|---| | Stale evidence: reports, renders or results from before the last change | A pass for geometry that no longer exists | Hash-bound results (slice report, hole check, gates, CI parameter check); rerun `cad-agent all` after every geometry change | [07](https://cad-agent.valpatel.com/lessons/07-verification-and-evidence.md) | | Exports that change bytes on every rebuild | The slice report goes stale with no geometry change | Reproducible 3MF writer; check `git diff` after a rebuild with no change | [07](https://cad-agent.valpatel.com/lessons/07-verification-and-evidence.md) | | A simulation trusted before it converged, balanced and was calibrated | Confident but wrong temperatures or stresses | The project's simulation checks: steady, energy balance, calibrated, and inputs that match the design (fan axis and position) | [10](https://cad-agent.valpatel.com/lessons/10-simulation-and-material.md) | | Material savings judged on CAD volume | The slicer disagrees by tens of percent | `cad-agent slice` | [10](https://cad-agent.valpatel.com/lessons/10-simulation-and-material.md) | | A render or report published without being looked at | Wrong colours, clipped views, a stale picture | Review: open every image before it is shown | [07](https://cad-agent.valpatel.com/lessons/07-verification-and-evidence.md) | | Claims stronger than the evidence | "No re-breathing", "fits", "strong enough" from a model only | Review: every claim names its evidence and its limits; nothing physical is claimed without a print | [07](https://cad-agent.valpatel.com/lessons/07-verification-and-evidence.md) | --- Source: https://cad-agent.valpatel.com/docs/compliant.md # Snap fits and compliant features `cadagent.compliant` sizes and checks snap-fit joints so an agent does not re-derive the equations, guess a permissible strain or forget layer lines. It is the calculator half of the compliant-mechanism work (#94); the method, sources and open questions are in [research/compliant-mechanisms.md](https://cad-agent.valpatel.com/research/compliant-mechanisms.md). **Why it is built, not adopted.** The survey of 2026-10-31 ([research/agent-toolkits-2026-10.md](https://cad-agent.valpatel.com/research/agent-toolkits-2026-10.md)) found agent skills for snap fits (static74/3dp-compliant-mechanisms, the AgentCAD snap-fits skill) but they are prompts only: no calculator and no tests against the design guides' worked examples, and static74's printed strain limits are about twice those the filament datasheets support. This module is tested against the guide's own examples, and every material value names its source. ## What it does - **Cantilever, torsion and annular snaps** by the closed-form method of the Covestro (formerly Bayer) guide *Snap-Fit Joints for Plastics – A Design Guide*: permissible deflection from strain, thickness for a target deflection, deflection force from the secant modulus, mating and separation force from friction and the lead and return angles, the torsion-bar and annular-joint relations. Rectangular and ring-segment sections (the guide's Example II section is computed exactly). - **Deterministic pass/fail rows** for a declared snap: root strain against the allowable, root radius (at least 0.38 mm; R/h below 0.6 concentrates strain), mating force against a limit, retention force against a minimum. - **Allowable strain with its source**: moulded resins from the Covestro Table 2 and DuPont Table 10.06; 13 printed filaments from their manufacturers' datasheets **by print direction** ([`allowables.py`](https://github.com/Valpatel/cad-agent/blob/dev/src/cadagent/compliant/allowables.py)). - **Figures**: `tools/snapfit_figures.py` draws the cantilever with its dimensions (`docs/img/compliant/`). ## Declaring snaps ```yaml # snaps.yaml: cad-agent snapfit snaps.yaml --output exports/snap-checks.json snaps: - name: lid-latch material: copa-polymaker-dry # a printed filament: orientation decides the allowable orientation: along # bending stress along the layers (arm lies in XY); 'across' = arm built along Z repeated: true # opened and closed often: 60 % of the single-joining strain length_mm: 14 undercut_mm: 0.5 design: 1 # 1 constant section, 2 thickness tapered to h/2, 3 width tapered to b/4 section: {rect: {width_mm: 6, thickness_mm: 1.6}} root_radius_mm: 0.8 lead_deg: 30 return_deg: 45 max_mating_n: 30 - name: example-I # the guide's Example I, moulded polycarbonate material: pc process: moulding length_mm: 19 undercut_mm: 2.4 design: 2 section: {rect: {width_mm: 9.5, thickness_mm: 3.28}} lead_deg: 30 return_deg: 90 root_radius_mm: 1.0 ``` `cad-agent snapfit FILE` prints one PASS/FAIL line per snap with the root strain, the allowable and the forces, and any warnings. It exits 0 if every check passes, 1 if any fails and 2 for an unreadable or out-of-range file (the message names the snap and the field). `--output` writes every row, the material derivation and the limits as JSON. Fields: - **Required:** `name`, `material` (a name below, or `{name, strain, source, secant_mpa, friction}` with `strain` a **fraction**: 0.03 = 3 %), `length_mm`, `undercut_mm`, `section` (`rect: {width_mm, thickness_mm}` or `ring: {r_inner_mm, r_outer_mm, arc_deg, tension}`), `root_radius_mm`, and `process` (`moulding`, `fdm`, `sla`, `sls` or `mjf`; a printed filament implies `fdm`). - **Optional:** `design` (default 1), `repeated`, `share` (0.5 when both parts flex equally), `secant_mpa`, `friction`, `same_material`, `lead_deg`, `return_deg`, `max_mating_n`, `min_retention_n`. - **Layered processes:** a printed filament needs `orientation` (`along` or `across` the layers). Any other material in a layered process needs `orientation`, `process_factor` (0–1: a derating) and `process_source`. **Nothing passes unchecked.** A missing process, orientation, derating or root radius is a failing row. A declared `max_mating_n` or `min_retention_n` that cannot be computed fails and names what is missing: no secant modulus (only `pc` has guide points; give `secant_mpa` otherwise), no friction (PLA and PETG are not in the guide's table) or no angle. A self-locking hook (μ·tan α ≥ 1, e.g. a 90° return) cannot be pulled apart, so its retention is the hook's own strength, which this method does not compute: a declared retention minimum fails there. Where the guide gives secant-modulus points, the modulus is interpolated at the arm's actual root strain. ## Materials **Moulded** (permissible strain for one joining; frequent use in brackets): Covestro Table 2 — `pc` 4 % (2.4), `pc-high-heat` 4 %, `pc-abs` 2.5 % (1.5), `pc-blend` 3.5 %, `pc-gf10` 2.2 %, `pc-gf20` 2.0 %. DuPont Table 10.06 — `pom-delrin-100` 8 % (2), `pom-delrin-500` 6 % (2), `pa66-dry` 4 % (2), `pa66-50rh` 6 % (3), `pa66-gf-dry` 0.8 % (0.5), `pa66-gf-50rh` 1.5 % (1.0), `pet-gf` 1 % (0.5), `pbt-gf` 1.2 % (0.6), `tpc-hytrel` 20 % (10). Where DuPont gives a range the lower end is used. **Printed**, single joining, along / across the layers (derived from the datasheet; ×0.6 for frequent use): | Name | Along | Across | Name | Along | Across | |---|---|---|---|---|---| | `pla-polymaker` | 1.1 % | 0.9 % | `asa-polymaker` | 1.3 % | 0.8 % | | `pla-bambu-basic` | 1.0 % | 1.0 % | `asa-bambu` | 1.5 % | 1.2 % | | `pla-bambu-matte` | 1.1 % | 1.0 % | `copa-polymaker-dry` | 2.0 % | 0.65 % | | `petg-polymaker` | 1.7 % | 1.6 % | `copa-polymaker-wet` | 3.3 % | 0.85 % | | `petg-bambu-basic` | 2.3 % | 2.3 % | `pa6-cf20-polymaker-dry` | 0.8 % | 0.8 % | | `petg-bambu-hf` | 1.3 % | 1.0 % | `pa6-cf20-polymaker-wet` | 1.5 % | 1.5 % | | `petg-cf-bambu` | 1.7 % | 1.7 % | | | | The rule, per direction: `min(0.7 × UTS / E, 0.5 × elongation at break)` — the guide's 70 %-of-yield rule for amorphous resin with UTS/E as a lower bound on the yield strain, and its half-the-elongation rule for materials without a yield point. Across the layers the smaller of the Z and XY values is used. Nylon is capped at the moulded value of its family: CoPA at PA66 (the cap never binds), carbon-filled PA6 at DuPont's glass-filled PA66 (0.8 % dry, 1.5 % at 50 % RH; both bind, from derived 0.89 % and 1.53 %). The "wet" rows are water-saturated datasheet specimens. **The direction matters more than strength suggests**: across the layers the PLA, PETG, ASA and CoPA datasheets keep 40–90 % of the in-plane strength but only 10–61 % of the elongation at break, and snap fits are limited by strain. (Carbon-filled PA6 keeps 90–96 % of its elongation: the fibre already limits it in-plane.) What this costs in allowable strain depends on which limit governs. Across the layers the allowable is 80–100 % of the in-plane value for PLA and PETG, 78 % for Bambu ASA, 64 % for Polymaker ASA and 26–32 % for CoPA, where half the elongation at break governs: 2.0 % with the arm lying in the bed plane against 0.65 % built upright. Layer lines also act as notches, which no tensile test shows, so lay flexing arms in the bed plane whatever the numbers say. ## Limits - **A pass has no extra margin.** It means the root strain is within the allowable; the guide's own examples design to half the permissible strain. - Closed-form, small-deflection beam theory with one part rigid: no large deflection, contact, creep, fatigue or wall deflection at the root. A warning is given when the deflection exceeds a tenth of the arm length (the guide's Example I is at 0.13). Large-deflection force-travel curves need FEA (CalculiX is the planned external solver). - Printed allowables are **hypotheses until a coupon is measured**: the datasheets are for 100 % infill on one printer and profile. Forces for printed snaps use the datasheet tensile modulus, which is higher than the secant modulus at the design strain, so mating forces are conservative and retention forces optimistic; the force rows say so. - Hook-face geometry and mould release are not checked. CAD and calculation do not prove a joint: print it and measure. ## Tests that keep it honest `tests/test_snapfit.py` reproduces the guide's worked examples — Example I (h 3.28 mm, P 32.5 N, W 58.5 N), Example II (ring-segment hook, y 2.11 mm) and the annular lamp cover (P 748 N, W 1346 N) — checks that every printed row matches the derived table in the research note, that direction changes the verdict for the same geometry, that uncomputable limits, out-of-range inputs and unstated processes fail, the ring section against numerical integration (arcs up to 360°), the torsion relations from first principles, and that this page's YAML example and printed table match the code. The stated elongation and strength ranges are checked against the datasheet rows too. --- Source: https://cad-agent.valpatel.com/docs/design-process-plan.md # Design process plan: dossier, integrity gates and section-by-section review Status: plan, 2026-10-27. Research behind it: [`research/design-workflows.md`](https://cad-agent.valpatel.com/research/design-workflows.md) (cited below as DW §n). Nothing here is implemented yet; tasks T1–T20 at the end are sized for one implementer each. ## 1. Why A turret assembly in a robot project built with cad-agent rendered its ammunition outside the hopper that should hold it, with subassemblies that read as boxes stuck on the side. Every per-part check passed: the parts were valid solids, watertight, within the bed, and no two printed parts overlapped. Nothing asked whether the balls were *inside* the hopper, whether each subassembly was *attached* to its neighbour, or whether anyone had *looked* at the render with a checklist. The toolkit checks parts; it does not check the design. What is missing is a process, and the tools that make the process cheap: | Missing | Today | After this plan | |---|---|---| | Decomposition | one `build.py` per product, interfaces in the author's head or a loose JSON | a **dossier**: requirements → sections → parts and interfaces, declared beside the CAD | | Justification | `decisions.yaml` at project level, written after the fact | a **spec card** per part and section: function, interfaces, loads, decisions with alternatives rejected, written *with* the geometry | | Assembly integrity | pairwise overlap volume only | **gates**: connectivity, containment, floating parts, fastener and stack consistency, motion-range interference, assembly access | | Visual review | renders are produced and linked | renders are **critiqued against a checklist** by a reviewer that did not build the part, with a sign-off record | | Evidence | per-check JSON and a report | every dossier item carries its status and links its evidence; the site shows a section-by-section review page | The practices come from DW §2–§5: the verified gains are from deterministic checks before visual ones (DW §2.5), containment and interpenetration as first-class relations (build123d-mcp `clearance`, DW §4.2), structure expectations declared by the author (agentcad `expect_solids`/`floating`, DW §2.2, §4.6), spec-driven conformance with evidence tiers (build123d-mcp `verify_spec`, DW §2.2), and the engineering artefacts that have had the same shape for decades: interface control documents, requirement traces, decision records, DFMEA rows and assembly sequences (DW §3). ## 2. Principles 1. **Declare, then derive.** The author declares intent (what touches what, what sits inside what, what moves, what fastens what) in a few lines next to the geometry. The gates derive the geometry's actual relations and diff the two. Undeclared relations are findings, not silence. 2. **Deterministic before visual.** Every gate is a kernel query with a number. Visual review comes after, is checklist-driven, and is never the only evidence for a claim (DW §5). 3. **A missing declaration fails.** A part without a spec card, a section without requirements, an interface with one side only: all are failures in `cad-agent check`, the same as a missing check file today. 4. **Explained exclusions, never loosened thresholds** (lesson 07). Intended overlaps (inserts, press fits) and intended floating parts (loose balls, a lid shown exploded) are declared with a reason and reported in their own list. 5. **Generic.** Sections, parts, interfaces, joints and cavities are the vocabulary of any mechanical assembly. Nothing in `cadagent` knows what a turret or an enclosure is. 6. **Minimal boilerplate.** A project that only wants the gates adds one call to its build; a project that wants the whole process adds YAML it would have had to write anyway as a design review. ## 3. The design dossier ### 3.1 Model ``` Project ├─ requirements[] R1..Rn: text, source (user quote | standard | derived), verify_by (analysis|inspection|test|demo), status ├─ sections[] S1..Sn: title, purpose, requirements[], interfaces[], parts[], decisions[], review │ ├─ parts[] the part spec cards (3.2) │ └─ interfaces[] I1..In: between two parts or sections; kind, datum, loads, fastened_by, clearance, owner ├─ decisions[] D1..Dn: as today (site decisions.yaml), plus `scope: project|section|part`, `alternatives[]` with reasons ├─ assembly steps[]: order, what is fitted, from which direction, tool, access check id └─ gates the integrity-gate configuration (4.1) and the per-gate verdicts once run ``` Requirements, sections and decisions are the three lists every design review already needs (DW §3.2–§3.4). An interface is the smallest unit of an interface control document: two parties, a datum, what crosses it, who owns it (DW §3.1). The assembly list is the Boothroyd-Dewhurst/DFA sequence table (DW §3.6, §3.9). ### 3.2 The part spec card ```yaml # dossier/sections/hopper.yaml (one file per section; parts are nested, or `parts: [file.yaml]` to split) id: S3 title: Hopper and feed purpose: Hold 40 foam balls and present one at a time to the feed wheel requirements: [R4, R7, R12] parts: - id: hopper function: Contain the ball store and funnel balls to the feed throat under gravity interfaces: [I7, I8, I9] loads: {static: "40 balls, 0.26 kg, on the floor cone", shock: "1 m drop on a corner (R12)"} material: {name: PETG, process: FDM, print_axis: "+Z", wall_mm: 2.4} fasteners: [{to: pan_base, screws: screw_m3x8, qty: 4, via: I7}] contains: [{what: ball, count: 40, cavity: hopper_cavity, free: true}] # drives the containment gate decisions: - id: D41 decision: Single-wall cone with a 55° floor why: Balls roll at 35°; 55° clears the feed throat without a stirrer alternatives: - {option: Stirrer paddle, rejected: adds a motor and a jam mode (DFMEA F3)} - {option: Vibrating floor, rejected: no evidence it clears 40 mm foam balls} checks: [gate:containment, gate:connectivity, fea:hopper-drop, print:hopper] status: reviewed # draft | built | gated | reviewed | signed_off ``` Fields and where they are checked: | Field | Checked by | |---|---| | `interfaces` | every id exists, has two parties, and this part is one of them; the connectivity gate expects contact or a declared gap along each | | `fasteners` | the fastener gate: a modelled fastener per entry, its stack (seat, grip, thread start, engagement) consistent with the two parts' geometry; tool access at the assembly step that installs it | | `contains` | the containment gate: each instance of `what` inside the named cavity of this part; `free: true` exempts them from the floating-part gate | | `material.wall_mm` | the minimum-wall gate (threshold per part, not per project) | | `decisions[].alternatives` | the dossier lint: a decision with no alternative or no `why` is `incomplete`; a section with an open decision cannot be signed off | | `checks` | each id resolves to a gate verdict or a configured check; the card's status cannot pass `gated` while one fails | | `status` | only the reviewer pass (5.3) may set `reviewed` and `signed_off`; a change of the part's geometry hash voids the review, and the status falls back to what the current evidence supports (`gated` when its gates still pass, else `built`). As built (T2), every status is computed into `exports/dossier-status.json`; a YAML `status:` above the computed one is a finding | ### 3.3 Storage - `dossier/project.yaml`: requirements, sections list (file per section), assembly steps, gate config. - `dossier/sections/.yaml`: the section and its parts (3.2). Parts may be split into `dossier/parts/.yaml` when a section grows; `parts: [path]` entries are loaded in place. - `dossier/decisions.yaml`: the existing site format, extended with `scope` and structured `alternatives`. The site's `site: decisions:` key keeps working; the dossier adds section and part scopes. - `dossier/reviews/
-.json`: reviewer sign-off records (5.3), written by the reviewer pass only. - `exports/dossier-status.json`: the compiled dossier with every item's status and evidence links, written by `cad-agent dossier`; the site and the report read this file, never the YAML. - `exports/gates/.json`: one verdict file per gate, in the existing check shape (`pass`, limits, findings, exclusions, `physical_fit_verified: false`). YAML, not JSON, for the hand-written files: comments and quotes matter in a dossier (requirements are user quotes, lesson 02). `cad-agent.yaml` gains one key: `dossier: dossier/project.yaml`. ### 3.4 Declaration from build scripts with minimal boilerplate Build scripts keep returning shapes. They declare the geometric facts the gates need, in code, because only the code knows which solid is the cavity and where a joint axis is: ```python from cadagent.dossier import part, cavity, joint, fastener, assembly @part("hopper", section="S3") # registers the shape and its dossier card def hopper(p): ... # returns a cq Shape as today @cavity("hopper", "hopper_cavity") # the void the containment gate tests against def hopper_cavity(p): return hopper_shell_inner(p) @joint("I9", kind="revolute", axis=lambda p: Axis(p["flap_pivot"], (0, 1, 0)), range_deg=(0, 70), parts=("hopper_flap", "hopper")) def flap_pivot(p): ... # optional body; the joint is what matters assembly.step(1, fit="pan_base", onto="bench_base", direction="-Z", tool="hex_2.5", fasteners=["screw_m4x12"] * 3) ``` The registry (`cadagent.dossier.REGISTRY`) collects parts, cavities, joints, fasteners and assembly steps at import time; `build()` runs them with `parameters.json` and writes `exports/dossier-geometry.json` (content hashes of every shape as defined in 4.1, bounding boxes, cavity and joint frames, and the `actor` that ran the build, from the `CAD_AGENT_ACTOR` environment variable, see 5.3) next to the STEP files. A project that does not use decorators can instead return a dict from `print_parts()` as today plus `dossier_items()`, so existing builds migrate one part at a time. The YAML cards and the code registry are joined by id; a card without a registered shape, or a shape without a card, is a lint finding. Fasteners come from the existing McMaster catalogue entries (`skills/mcmaster-parts`) or `cadagent.fasteners` envelopes; a `fastener` declaration names the catalogue key, the head seat face and the part it threads into, which is what the stack gate needs (lesson 04). ### 3.5 Rendering on the review site A new page `site/review.html` (and `cad-agent site` section "Design review by section"): - one row per section: title, requirements covered, part count, gate status (pass/fail/unrun), review status (unreviewed / findings open / signed off), the reviewer's date and record link; - expanding a section shows each part card (3.2) with every check as a pill linked to its evidence (gate JSON, FEA page, render), the decisions with their alternatives, and the section's renders with the reviewer's findings overlaid as a list beside each image; - a requirements table at the top: each requirement, the sections and checks that trace to it, and its status (unverified until every linked check passes; `test` requirements stay "not tested" because cad-agent cannot set them); - a gates page `site/gates.html`: the connectivity graph drawn as an SVG (parts as nodes, declared interfaces as edges, undeclared contacts dashed red, floating components highlighted), the containment table, motion sweep results. The existing overview keeps its decisions section; decisions with a section scope also appear in that section's card. As built (T15, [`dossier.md`](https://cad-agent.valpatel.com/docs/dossier.md#on-the-review-site)): the pages live in `site/review/` (`index.html`, one `section-.html` per section instead of expanding rows, `requirements.html` on its own page, `gates.html`); the gates page has a row per gate and each gate's findings with witnesses rather than gate-specific containment and motion tables, and the connectivity graph follows the DOT the gate writes (undeclared contacts orange, unrealised declarations red dashed). ## 4. Integrity gates (`cadagent.gates`) All gates take the same input: `Assembly` = `{part_name: shape in assembly coordinates}` plus the dossier geometry (cavities, joints, fasteners, assembly steps) and the dossier declarations. All write the existing check JSON shape. The CLI `cad-agent gates [--only NAME]` runs them after `build`; `cad-agent check` collects them like any check. ### 4.1 Common machinery (T3) - **Broad phase.** Axis-aligned bounding boxes with a tolerance, all pairs (4,950 for 100 parts) in milliseconds; only overlapping boxes go to the kernel. A measured run on 100 synthetic solids: 100 candidates from 4,950 pairs in 3 ms (DW §4.1). - **Narrow phase.** `BRepExtrema_DistShapeShape` for the minimum gap (about 10 ms per simple pair), `BRepAlgoAPI_Common` volume for exact overlap (about 3 ms per simple pair) on the synthetic benchmark ([`research/bench/gate_timing.py`](https://github.com/Valpatel/cad-agent/blob/dev/research/bench/gate_timing.py)). Real parts cost more: the 10–100× figure in DW §4.1 is an **estimate**, and `DistShapeShape` on freeform or heavily filleted faces, or `Common` on shelled parts, can take seconds per pair. T3's acceptance therefore includes one real, filleted and shelled pair, and the budget in 4.9 is a target to be measured, not a claim. - **Shape identity (content hash).** OCP's `TopoDS_Shape` hash is identity plus location, not content, and is not stable across processes, so it is never used as a key. A shape's id is `cadagent.evidence.shape_content_hash`: the sha256 of its BREP text (`BRepTools.Write_s`, format version 1, without triangulation or normals, so meshing does not change it) with the per-shape flag lines removed (the kernel flips them on its own). The shape's assembly location is part of that text. Shown deterministic across processes in the registry's tests. A shape read back from BREP may hash differently (the reader recomputes frames), so the hash recorded at build time travels with the shape and is never recomputed from a loaded copy. This is the same byte-hash discipline as `cadagent.evidence`, which hashes files with sha256. The same hash is what `dossier-geometry.json` records per part and what sign-off records (5.3) are bound to. - **Pair cache.** Results are cached by the two content hashes (and, for the motion gate, the pose transform of the moving part, 4.5) in `.cad-agent/cache/gates/` under the project root. The directory is created with a `.gitignore` containing `*` so it is never committed; only the verdict JSON goes to `exports/`. A rebuild that changes one part re-queries only its pairs. - **Relations.** For each candidate pair, one of `apart(gap)`, `touching`, `overlapping(volume)`, `contains(a_in_b | b_in_a)`, following build123d-mcp's four-state `clearance` (DW §4.2). `touching` is `gap ≤ touch_tol_mm` with the common volume below `vol_tol_mm3`; `contains` is common volume equal to the smaller shape's volume within `vol_tol_mm3`. Tolerances are absolute, not relative, because boolean results on curved solids carry fuzzy-value and integration error: `vol_tol_mm3 = max(0.01, touch_tol_mm × area_mm2(smaller shape))`, that is, a skin of thickness `touch_tol_mm` over the smaller part. This table is the input to every gate below, computed once. - **Config** in `dossier/project.yaml` → `gates:`: `touch_tol_mm` (default 0.01), `vol_tol_mm3` (override of the default above), `intended_overlaps` (`[{parts, reason}]`, e.g. heat-set inserts), `free_parts` (intended loose), `ignore_pairs` (variants that never coexist). ### 4.2 Connectivity and floating parts (T4) - Build the contact graph: nodes are parts, edges are `touching` or `overlapping` relations, plus declared joints (a revolute joint with a clearance fit is not a contact but is a connection). - Ground: the dossier names one or more `grounded` parts (the base, the bench stand). Every connected component that contains no grounded part is a **floating component**, unless every member is in `free_parts` or is a `contains` item with `free: true`. - Diff against declarations: each declared interface must be realised by an edge (or by a declared gap within its `clearance`); each edge not covered by an interface is an **undeclared contact**. Both are findings with the witness points from the extrema query so the reviewer can look at the right place. - Output: `exports/gates/connectivity.json` with components, floating parts, undeclared contacts, unrealised interfaces, and the graph for the site. Fails on any floating component or unrealised interface; undeclared contacts are warnings until the project sets `gates.strict_interfaces: true`. - Tests: a frame of touching boxes plus a floating box fails; marking it free passes; a declared interface with a 2 mm gap fails and passes once `clearance: 2.5` is declared; a revolute joint links a part that touches nothing. ### 4.3 Containment (T5) - For each `contains` declaration: resolve the cavity solid, which **must be registered** by `@cavity` (there is no derived cavity: OCP has no B-rep convex hull, and a hull-minus-part result would be a mesh, not a solid `Common` can use; a `contains` declaration without a registered cavity is a lint finding). Then for every instance of `what`, `outside_mm3 = instance.Volume() − common(instance, cavity).Volume()`; the instance is inside when `outside_mm3 ≤ vol_tol_mm3` (4.1). Report the volume outside and the witness point from `DistShapeShape`. `BRepClass3d_SolidClassifier` on the instance's vertices and centre is the fast pre-check (24 points in 7 ms). Mesh-only items (an STL with no B-rep) are tested with trimesh `contains` against the tessellated cavity, reported as `sample-only`. - Tangent contact is a first-class test case: a ball resting on the cavity floor (coincident or tangent faces) must report inside, with `outside_mm3` under the tolerance, not a false `interpenetrating`. - Also the inverse: a part declared `inside` another part's cavity must not be `overlapping` with the container's material (the "wall-piercing" case, build123d-mcp `interpenetrating`). - Fails when any instance is partly or wholly outside; `count` mismatch is a finding too. - Tests: 40 spheres in a cone; one moved 5 mm up through the wall fails with the right volume outside; a sphere tangent to the cone wall passes; a `contains` declaration with no registered cavity is a lint finding; a mesh-only item takes the `sample-only` path. ### 4.4 Fastener and stack consistency (T6) Extends `cadagent.fasteners.blind_insert_stack` from numbers to geometry: - For each dossier fastener: locate the screw solid (catalogue STEP placed by the build, or envelope), the head seat face on the clamped part, the thread-start on the mating part (insert top or tapped hole start, from the declaration), and compute seat Z, grip, engagement, tip clearance in the fastener's own axis frame; compare with the stocked length and the required engagement; flag uncommon lengths (lesson 04). - The screw's shank must not overlap the clamped part (the clearance hole must be at least shank + declared clearance, checked as a hole diameter), and its only overlap with the mating part must lie inside that part's declared thread region (an insert or tapped hole); any overlap elsewhere is a finding. - **Tool access**: at the assembly step that installs the fastener, sweep the declared tool envelope (driver diameter × length, or hex key with swing) from the head along the axis; it must not overlap any part present at that step. Reuses the plug-sweep machinery of `cadagent.connectors.check_ports`. - Output `exports/gates/fasteners.json`: a table per fastener, as lesson 04 asks for; fails on engagement below the declared minimum, tip collision, or no tool access. - Tests: the existing `test_fasteners` cases turned into geometry; a screw whose driver is blocked by a gusset. ### 4.5 Motion-range interference (T7) - Input: the declared joints with ranges. For each joint, pose the moving subtree at N samples across the range (default 12, plus the ends), and run the exact pairwise relation table (4.1) between the moving set and everything else, with intended contacts (stops, bearings) declared per joint. The cache key for these pairs includes the pose transform. Exact B-rep queries per pose are affordable at this N (4.9); when a project needs hundreds of poses, the scale-up path is the one DW §4.4 recommends: tessellate once into a trimesh `CollisionManager`, sample densely, and confirm each mesh hit on the exact B-rep at that pose. - Swept volume by sampling, not exact: the sampled approach is what robotics collision checking does in practice (DW §4.4); the gate reports the sample density and the smallest clearance seen, and refuses to claim more. A project can raise N where the clearance is small relative to the motion per step. - Compound motions (two joints) are sampled on a grid only when the dossier declares them `coupled`. - Output `exports/gates/motion.json`: per joint, the minimum clearance over the range, the first colliding pose, the pair, and a render of that pose. Fails on any collision outside the intended contacts. - Tests: a flap on a pivot that clears at 0° and hits a wall at 60°; the stop contact declared intended passes. ### 4.6 Minimum wall and feature size (T8) - Ray-cast thickness on the print mesh (as text-to-cad's `dfam-check`, MIT, DW §4.5): per-face inward rays, the distribution of thickness, and the faces under the part's `material.wall_mm`. Add a negative-offset probe on the B-rep for thin ribs where rays miss (offset by −t/2 and see which solids vanish). - Output per part; fails under the declared wall; the threshold is per part card, not global. ### 4.7 Assembly-sequence access (T9) - For each assembly step: the parts present are the union of earlier steps; the fitted part is swept along its declared direction from outside the present set's bounding box to its final position; any overlap with a present part is a finding (the slide-in path check of lesson 07, generalised). Fastener tool sweeps (4.4) run at the step that installs them, with only the parts present then. - Every part in the dossier must appear in exactly one step; every fastener in the step that installs it. - Output `exports/gates/assembly.json`: steps, swept clearances, blocked steps. ### 4.8 Dossier lint (T2) Not geometric. Loads the YAML, resolves ids, checks the table in 3.2, the requirement trace (every requirement is referenced by a section and at least one check), decisions with alternatives, each section's review status, and the join with the code registry. Writes `exports/gates/dossier-lint.json`. ### 4.9 Performance budget Targets for 100 parts, to be measured in T3 (DW §4.1's real-part figure is an estimate): broad phase < 0.1 s; relation table within a few minutes cold on real parts, seconds warm from the cache; motion gate = samples × moving pairs, so 12 samples × 20 pairs ≈ 240 narrow-phase queries, under a minute. Gates run as `cad-agent.yaml` steps after `build`, so a project can put the slow ones (motion, assembly) in a separate step and run them only when geometry changed (the cache keys make an unchanged assembly free). ## 5. The review workflow for agents ### 5.1 Skill `skills/design-process/SKILL.md` (T10) Built: [`skills/design-process/SKILL.md`](https://cad-agent.valpatel.com/skills/design-process/SKILL.md), the packet and the checklist in [`review-packet.md`](https://cad-agent.valpatel.com/docs/review-packet.md) (`cadagent.render.review_packet`, `cad-agent review-packet`). Tells an agent building or changing a design to work in this order, per section: 1. **Frame the section**: write (or update) its YAML: purpose, requirements it serves (quote the user), interfaces to neighbouring sections with datums, the parts it needs. No geometry yet. Open questions become `open` decisions, and the agent decides them itself where the model can answer (lesson 02), recording the alternatives rejected. 2. **Write the spec card with the geometry**: function, loads, material, fasteners, `contains`, decisions, in the same commit as the build code; declare cavities, joints, fasteners and assembly steps in code. 3. **Build and gate**: `cad-agent build && cad-agent gates`. Fix every finding or declare the exclusion with a reason. Never lower a threshold to pass. 4. **Render for review**: a fixed packet per section (iso, opposite iso, the section cut through each cavity and joint, the assembly step poses, the motion extremes), captioned with the section id and the geometry hash, with the parts coloured by section and declared-free items in a contrast colour. 5. **Self-critique against the checklist** (5.2) and write the answers into the section's `review.self` block, with the render file each answer refers to. 6. **Hand to the reviewer pass** (5.3). Do not set `reviewed` yourself. 7. **Report**: one line per requirement touched, per gate, per open finding; say what is unverified. The skill also carries the rules the lessons already state (one parameter authority; model the mating part, not its bounding box; compute what the model can compute; classify false positives) so a fresh agent gets them in one place. ### 5.2 The render checklist Specific yes/no questions, each tied to a view, which is what works for vision models (DW §5): "In the section cut through `hopper_cavity`, is every ball fully inside the inner wall?", "In the exploded view, does every part have a visible mating feature on its neighbour?", "Does any part appear as a plain box with no features?", "Are the fastener heads visible from the direction the assembly step names?", "Is anything floating in the opposite iso?", "Does the motion-extreme pose show contact other than the declared stop?". Counting and small gaps are not asked of the image; the gates answer those, and the checklist says so. ### 5.3 Reviewer pass and sign-off (T11) Built: `cadagent.dossier.review`, `cad-agent review-section` ([`dossier.md`](https://cad-agent.valpatel.com/docs/dossier.md#the-reviewer-pass)). The separate-reviewer switch is the dossier's `gates: require_separate_reviewer`, and the build actor is recorded per part (`parts..actor`). A separate agent (or person) that did not write the section, given: the section YAML, the gate JSON, the render packet, the checklist, and the diff since the last review. It: - answers the checklist from the renders (and may request extra views by name); - reads each decision and marks it `justified`, `weak` (why) or `missing alternative`; - checks each requirement's trace; - writes `dossier/reviews/
-.json`: findings with severity, the render each refers to, the checklist answers, the decision verdicts, `signed_off: true|false`, the content hashes (4.1) of every part reviewed, and `actor`. **Who is who.** Every build and every review record carries an `actor` string read from the `CAD_AGENT_ACTOR` environment variable (an agent or session id, or a person's handle, set by the harness or the person who starts the session; empty is recorded as `unknown`). `dossier-geometry.json` records the actor of the build that produced each part's hash; the review record records the reviewer's. When `gates: require_separate_reviewer: true` (the default), `cad-agent dossier` refuses a sign-off whose actor equals the build actor for any part in the section, or is `unknown`. This makes the separate-reviewer rule checkable, but it is only as strong as the variable: a harness that sets the same actor for both passes defeats it, and the plan says so rather than pretending otherwise. `cad-agent dossier` sets the section's status from the latest record whose hashes match the current build; any geometry change invalidates sign-off (the status falls back to `gated` when the gates pass on the new geometry, else `built`). The site shows who signed what and when. The reviewer must not change geometry or YAML other than its own record; a reviewer who changes a part becomes that part's builder, so its own sign-off is refused. ### 5.4 How the main agent and implementers use it - The main agent plans sections from the requirements and hands each to an implementer with the skill and the section file; implementers work in parallel on sections whose interfaces are agreed (the interface list is the contract). - Interfaces are owned: the owner section's implementer changes the datum; the other side rebuilds. The lint fails when both sides declare different datums. - The main agent runs the reviewer pass on each section as it comes in, and the full gates on the integrated assembly; nothing is reported as done with an open finding or a missing sign-off. - `cad-agent check` fails while any section is below `gated`, so the pipeline, not the agent's memory, holds the bar. ## 6. Evaluation **Before/after on a real assembly.** Take the assembly from the incident as it was when it happened. Run the gates on that snapshot: the containment gate must report the items outside their cavity, the connectivity gate must report the subassemblies that only touch by coincidence or not at all. Then apply the process: dossier, gates, review. Record the findings count, the time spent, and the review's sign-off. The same on a second, already heavily checked project as a control: the gates should find little and nothing false, and every false positive becomes a test. **Seeded defects.** A fixture assembly in `tests/fixtures/gate-bench/` (20 synthetic parts: a frame, a hinged flap, a box with loose items, four fasteners, one bearing fit) with a script that injects defects one at a time: | Seed | Must be caught by | |---|---| | move one contained item 5 mm through the wall | containment | | delete the interface boss so a bracket floats | connectivity (floating) | | shift a bracket 0.5 mm so it hovers | connectivity (unrealised interface) | | lengthen a screw 4 mm into a cavity | fasteners (tip clearance) | | shorten a screw so engagement < 3 threads | fasteners (engagement) | | add a gusset under a screw head | fasteners (tool access) | | raise the flap stop so the flap hits the wall at 50° | motion | | thin one wall to 0.8 mm | minimum wall | | reorder two assembly steps so the lid goes on before the board | assembly access | | remove a decision's alternatives | dossier lint | CI runs the seeded set; each gate's test asserts the defect is found with the right witness and that the clean fixture passes with zero findings. The target is 10/10 seeds caught and 0 false positives on the clean fixture and the control project. **Visual review.** For the render checklist, keep a small labelled set (the incident renders, the fixed renders, the seeded-defect renders) with the expected checklist answers; run the reviewer pass over them and report agreement. This is the only part of the evaluation whose result is expected to be imperfect (DW §5), which is why the gates come first. ## 7. Tasks Each is one implementer, one PR into `dev`, with tests. Dependencies in brackets; tasks in the same wave run in parallel. | # | Task | Files | Acceptance | Deps | |---|---|---|---|---| | **Wave 1** | | | | | | T1 | Dossier schema and loader: `cadagent.dossier` dataclasses (Requirement, Section, PartCard, Interface, Decision, AssemblyStep, GateConfig), YAML loading with id resolution, `cad-agent.yaml` `dossier:` key, `docs/dossier.md` with the schema | `src/cadagent/dossier/{__init__,model,load}.py`, `tests/test_dossier_load.py`, `docs/dossier.md` | round-trips the example in 3.2; unknown keys reported; bad ids raise with the file and line | none | | T3 | Gate machinery: `Assembly` container, AABB broad phase, pairwise relations (apart/touching/overlapping/contains) with witnesses and absolute tolerances (4.1), content hashing of shapes, pair cache in `.cad-agent/cache/gates/` keyed by content hashes (and pose), verdict JSON writer; `research/bench/gate_timing.py` folded in as a benchmark | `src/cadagent/gates/{__init__,relations,cache,hashing,verdict}.py`, `tests/test_gate_relations.py`, `tests/test_gate_hashing.py` | the synthetic 100-part run under 5 s and one real filleted, shelled pair timed and reported; the content hash is identical across two processes and changes when the shape changes; cache hit skips the kernel; relation states match build123d-mcp's definitions on the four textbook pairs plus tangent contact | none | | T8 | Minimum wall gate on print meshes (ray-cast) plus B-rep negative-offset probe, threshold from the card's `material.wall_mm` | `src/cadagent/gates/wall.py`, `tests/test_gate_wall.py` | a 2.4 mm box passes at 2.4, fails at 2.5; a 0.8 mm rib is found (seed 8) | T1 (the card field; the gate itself can start with a plain threshold argument) | | T13 | Build-script registry: `@part`, `@cavity`, `@joint`, `fastener()`, `assembly.step()`, `exports/dossier-geometry.json` with content hashes and `actor`, and the `dossier_items()` migration path | `src/cadagent/dossier/registry.py`, `tests/test_dossier_registry.py`, `docs/dossier.md` | the example in 3.4 builds; a card without a shape and a shape without a card are lint findings | T1 (same implementer as T1 is the natural split) | | T12 | Gate-bench fixture (20 synthetic parts declaring cavities, joints, fasteners and steps through the T13 registry) and the seeded-defect harness | `tests/fixtures/gate_bench/build.py`, `tests/gate_bench.py` | the fixture builds under the `full` CI job and the test skips cleanly under `core` (no `cad` extra); every seed in 6 is a parametrised case that the matching gate must catch once that gate exists | T13 (until it lands, the fixture declares its items in a plain dict with the same keys) | | **Wave 2** | | | | | | T2 | Dossier lint and `cad-agent dossier` (compile to `exports/dossier-status.json`, status machine, content-hash invalidation, the YAML-to-registry join) | `src/cadagent/dossier/lint.py`, `cli.py`, `tests/test_dossier_lint.py` | the 3.2 table's rules each have a failing and passing test; seed 10 caught; `cad-agent check` picks the lint up | T1, T13, T3 (hashing) | | T4 | Connectivity and floating-parts gate, with the declared-vs-actual diff and the graph export | `src/cadagent/gates/connectivity.py`, `tests/test_gate_connectivity.py` | 4.2's tests; seeds 2 and 3 caught | T1, T3, T13 (joints and `grounded` come from the registry) | | T5 | Containment gate (registered cavity only; mesh `sample-only` fallback for mesh items) | `src/cadagent/gates/containment.py`, `tests/test_gate_containment.py` | 4.3's tests; seed 1 caught with the right outside volume; tangent contact passes | T1, T3, T13 (`@cavity`) | | **Wave 3** | | | | | | T6 | Fastener and stack gate with tool-access sweep | `src/cadagent/gates/fasteners.py`, `tests/test_gate_fasteners.py` | seeds 4–6 caught; the lesson-04 table is the output | T3, T13 | | T7 | Motion-range gate | `src/cadagent/gates/motion.py`, `tests/test_gate_motion.py` | seed 7 caught; sample density reported; intended stop contact passes | T3, T13 | | T9 | Assembly-sequence access gate | `src/cadagent/gates/assembly.py`, `tests/test_gate_assembly.py` | seed 9 caught; every part in exactly one step | T3, T13 | | T14 | `cad-agent gates` command, step integration, report rows, `exports/gates/*.json` collected by `check` | `cli.py`, `report.py`, `tests/test_cli.py` | `cad-agent all` runs every gate after build; a failing gate fails `check` | T4, T5, T6, T7, T8, T9 (the command can land after T4/T5 with the others registered as they merge) | | **Wave 4** | | | | | | T10 | Skill `skills/design-process/SKILL.md` with the section workflow, the render packet spec and the checklist; `cadagent.render.review_packet(section)` helper | `skills/design-process/SKILL.md`, `src/cadagent/render.py`, `tests/test_render.py` | the packet renders the fixture's sections with captions and hashes; the skill passes the repo's skill lint if any | T13, T14 | | T11 | Reviewer pass: `cad-agent review-section ` prompt bundle (YAML, gates, renders, checklist, diff) and the sign-off record schema and validator; `actor` from `CAD_AGENT_ACTOR` in build and review records; status invalidation on hash change | `src/cadagent/dossier/review.py`, `cli.py`, `tests/test_dossier_review.py` | a record with stale hashes does not sign off; a record whose actor equals the build actor, or is `unknown`, is refused while `require_separate_reviewer` is on | T2, T10 | | T15 | Site: section review page, requirements table, gates page with the connectivity SVG | `src/cadagent/site.py`, `webkit/`, `tests/test_site.py` | pages built from the fixture; every link resolves (`html_pages`) | T2, T14 | | **Wave 5** | | | | | | T16 | Evaluation: before/after on the two real projects (in their own repos, results summarised generically in `docs/design-process-eval.md`), labelled render set for the checklist, CI job for the seeded set | `docs/design-process-eval.md`, `.github/workflows/ci.yml` | 10/10 seeds, 0 false positives on the control; the eval doc states what the visual review got wrong | all | | **Wave 6 (stage 2, lower value for the effort; after the gates have run on a real project)** | | | | | | T17 | DFMEA rows in the dossier (`fmea:` per part: function, failure mode, effect, cause, S/O/D, action priority from a declared simplified band table), lint for completeness, and geometric prevention controls that resolve to gate ids | `src/cadagent/dossier/fmea.py`, `docs/dossier.md`, tests | every H-priority row has an action; a control naming a gate fails when the gate fails | T2 | | T18 | DFA worksheet derived from the contact graph and mates: part and fastener counts, insertion directions, symmetry classes from principal inertia, the three necessity criteria with criterion 3 left to a human; a declared simplified time table (DW §3.6: no open Boothroyd tables exist) | `src/cadagent/dossier/dfa.py`, site section, tests | the fixture's worksheet matches a hand count; the table's source is printed on the page | T4, T13 | | T19 | Tolerance stack-up helper: loops along a named direction from the mate graph, nominals from the model, tolerances from the dossier or a general class or the process table (Hubs FDM figures, DW §3.7), worst-case and RSS, flagged when only RSS passes | `src/cadagent/dossier/stackup.py`, tests | the textbook three-plate example reproduces the WC and RSS results | T13 | | T20 | Drawing coverage: title block from the dossier, every critical dimension in the card appears on the drawing, datums declared, revision equals the model revision; drawings by the vendored text-to-cad `engineering-drawing` skill (MIT), never draftwright (AGPL) in-process | `src/cadagent/dossier/drawings.py`, skill update, tests | coverage report for the fixture; a missing critical dimension fails | T2, T13 | **Parallelism.** Wave 1 has two independent roots, T1 and T3; T13 follows T1 on the same branch, T8 needs only the card field from T1, and T12 needs T13's registry (or a plain-dict stand-in until it lands). Wave 2's gates share T3's API and T13's declarations, so both should merge first. Wave 3 is three gates in parallel plus the CLI, which grows as each gate merges. Wave 4 is the process layer. One implementer per task; a single session can carry a wave's tasks sequentially if people are short. **First tasks to hand out:** T3 with T12 and T8 on one branch (gate machinery, fixture, wall gate), and T1 with T13 on another (dossier schema and registry). T4 and T5 are the two gates that would have caught the incident, so they follow as soon as both branches merge. ## 8. Out of scope, risks - No new CAD kernel or viewer; everything is OCP through CadQuery, as today. Projects on build123d use `.wrapped`. - Exact swept volumes are not attempted; sampled poses with a stated density are what the motion gate claims. - The reviewer pass's quality depends on the vision model; the evaluation measures it rather than assuming it. - Dossier YAML is work for the author. The lint makes a missing card a failure, which is the point, but the skill must keep the minimum card small (function, interfaces, one decision) so the process is used rather than skipped. - Licences: ideas and API shapes are taken from MIT and Apache-2.0 projects (text-to-cad, build123d-mcp, agentcad); no code is copied from projects with other licences (DW §7). --- Source: https://cad-agent.valpatel.com/docs/dossier.md # The design dossier The dossier records a design the way a design review needs it: **requirements**, the **sections** the design is broken into, a **spec card** for every part, the **interfaces** between parts and sections, the **decisions** with the alternatives rejected, and the **assembly sequence**. It is written beside the CAD, in YAML, and joined by id to the geometry the build script registers (`cadagent.dossier.registry`, [below](#declaring-geometry-in-the-build-script)). The gates and the review site check it; the plan behind it is [`design-process-plan.md`](https://cad-agent.valpatel.com/docs/design-process-plan.md) §3. What it does not do: a dossier that loads and lints proves the design is *declared*, not that it is right. The gates check the geometry against the declarations; a reviewer checks the declarations against the requirements. Nothing in it proves physical fit or strength. ## Layout ``` cad-agent.yaml cad: dossier: dossier/project.yaml dossier/project.yaml title, requirements, sections, interfaces, decisions, assembly, gates dossier/sections/.yaml one section and its part cards dossier/parts/.yaml a part card split out of its section (optional) dossier/decisions.yaml project decisions (the site's decisions format, extended) dossier/reviews/
-.json reviewer records (written by `cad-agent review-section --submit` only) exports/review-bundle/
/ the reviewer's prompt bundle (written by `cad-agent review-section`) exports/dossier-geometry.json what the build script registered (written by the registry's build()) exports/dossier-status.json every item's computed status and the lint findings (written by `cad-agent dossier`) exports/gates/dossier-lint.json the lint as a gate verdict ``` Paths inside the dossier are relative to the directory of `project.yaml`. `sections:` entries may be globs (`"sections/*.yaml"`, loaded in sorted order) or inline mappings; a section's `parts:` entries may be paths to card files or inline cards. YAML, not JSON: requirements are user quotes and decisions need comments. ```python from cadagent.dossier import load_dossier, load_project_dossier d = load_dossier("dossier/project.yaml") # raises DossierError on any error d = load_project_dossier(load_project(".")) # via cad-agent.yaml; None when it has no dossier: key d = load_dossier(path, strict=False) # errors in d.errors instead of raising (for a lint) d.parts["hopper"].section, d.decisions["D41"].scope, d.findings # findings: unknown keys, interface conflicts ``` ## Ids, errors and findings - Every id starts with a letter and uses letters, digits, `_`, `-` or `.`. Ids share **one namespace** across requirements, sections, parts, interfaces and decisions, so an interface's `between: [a, b]` is never ambiguous. Prefixes (`R4`, `S3`, `I7`, `D41`) are a convention, not a rule. - **Errors** carry the file and line: a malformed id, a duplicate id, a reference to an id that does not exist or is of the wrong kind, an empty file (reported at the line that names it), a gate exemption without a reason ("… names 'R4', which is not an interface in this dossier (it is a requirement)"), a wrong type, a value outside its enumeration, a duplicate YAML key, invalid YAML, a missing file. `load_dossier` raises one `DossierError` listing all of them (`.issues`, and `.file`/`.line` of the first). When a file fails to parse, references are not resolved, so its ids do not cascade into unknown-id errors. - **Findings** are warnings, never fatal: an unknown key (reported with its line and the keys that are known), and an interface declared differently by its two sections. - References the loader does **not** resolve, because they point outside the YAML: a pair exemption's party that is not a dossier part (a fastener name or instance key, checked by the lint's `exemption-unknown-party`), `checks` (check and gate ids, resolved by the lint against `cad-agent.yaml` checks and the gate verdicts), fastener catalogue keys (`screws`, `fastened_by`, step `fasteners`), cavity names (`contains[].cavity`, resolved against the registry), a decision's `confirmation`. The lint (plan T2) and the registry join ([below](#joining-cards-and-geometry)) report those. ## Schema `project.yaml`: ```yaml title: Turntable feeder # optional requirements: - id: R4 text: Holds 40 balls source: {kind: user, quote: "it has to hold forty"} # kind: user | standard | derived | research | agent verify_by: inspection # analysis | inspection | test | demo parent: R1 # optional: the requirement this is derived from rationale: ... # optional checks: [gate:containment] # optional: checks that verify it directly status: waived # optional; computed by the lint, so only `waived` belongs in YAML sections: [sections/base.yaml, "sections/*.yaml"] interfaces: [...] # project-level interfaces (usually between sections) decisions: decisions.yaml # a file (list, or {decisions: [...]}) or an inline list assembly: steps: - order: 1 # unique integer; steps are sorted by it fit: pan_base # the part fitted at this step onto: bench_base # a part id or a list of them direction: -Z # +X..-Z or [x, y, z]: the way the part moves to its seat tool: hex_2.5 fasteners: [screw_m4x12, screw_m4x12, screw_m4x12] access_check: gate:assembly # optional check id note: ... gates: # the integrity-gate configuration (plan §4.1) touch_tol_mm: 0.01 grounded: [bench_base] # parts the connectivity gate treats as ground free_parts: [{part: ball, reason: loose in the hopper}] # a reason is required (a bare id is an error) intended_overlaps: [{parts: [hopper, insert_m3], reason: heat-set insert}] # reasons required here too ignore_pairs: [{parts: [lid_a, lid_b], reason: variants, never fitted together}] # a pair's parties may be parts, instance keys (ball#3) or fasteners by name or instance (screw_0, m3:lid-base#1); # [ball, ball] is the instances of one part among themselves: # intended_overlaps: [{parts: [screw_0, insert_0], reason: thread envelope inside the insert}] strict_interfaces: false # true: an undeclared contact fails instead of warning motion_samples: 12 require_separate_reviewer: true # the reviewer pass: refuse a sign-off by a part's builder or the section's # author (default true; not a gate input, so it never stales a verdict) # any other key is kept in GateConfig.extra (and reported) so a new gate option works before the schema knows it ``` A section (`sections/.yaml`), with its part card exactly as in plan §3.2: ```yaml id: S3 title: Hopper and feed # required purpose: Hold 40 foam balls and present one at a time to the feed wheel requirements: [R4, R7, R12] author: agent-session-12 # optional; a reviewer named here (or on a card or decision) is refused interfaces: - id: I9 between: [hopper_flap, hopper] # two different parts or sections kind: revolute # see "Interface kinds" datum: flap pivot axis, hopper frame loads: {torque: "0.1 N·m at the stop"} fastened_by: pin_3x20 # optional catalogue key clearance: 0.2 # mm: the gap the connectivity gate accepts along this interface owner: S3 # the section (or part) that may change the datum description: ... parts: - id: hopper function: Contain the ball store and funnel balls to the feed throat under gravity interfaces: [I7, I8, I9] loads: {static: "40 balls, 0.26 kg, on the floor cone", shock: "1 m drop on a corner (R12)"} material: {name: PETG, process: FDM, print_axis: "+Z", wall_mm: 2.4} # or a bare name fasteners: [{to: pan_base, screws: screw_m3x8, qty: 4, via: I7}] contains: [{what: ball, count: 40, cavity: hopper_cavity, free: true}] decisions: - id: D41 decision: Single-wall cone with a 55° floor why: Balls roll at 35°; 55° clears the feed throat without a stirrer alternatives: - {option: Stirrer paddle, rejected: adds a motor and a jam mode (DFMEA F3)} - {option: Vibrating floor, rejected: no evidence it clears 40 mm foam balls} checks: [gate:containment, gate:connectivity, fea:hopper-drop, print:hopper] status: reviewed # draft | built | gated | reviewed | signed_off author: ... - parts/hopper_flap.yaml # a card in its own file, loaded in place decisions: [...] # section-scope decisions review: {self: {...}} # the author's self-critique (plan §5.1); free-form here ``` **The minimum card** is three fields: `function`, `interfaces`, and one decision with `why` and one rejected alternative. Everything else is added when it is known (loads when the FEA is set up, `contains` when the part holds something, `fasteners` when the joint is chosen). Keep it that small at first; the lint asks for what is missing. Decisions (`decisions.yaml`, or nested in a section or card): ```yaml - id: D1 title: Print in PETG decision: PETG for every printed part # required why: tougher than PLA status: decided # decided | open | superseded date: 2026-10-27 source: {kind: user, quote: "use PETG", ref: chat 2026-10-27} alternatives: # {option, rejected}; a bare string (the site's older format) loads - {option: PLA, rejected: creeps above 50 °C} # with no reason, and the lint reports it consequences: ... confirmation: print:hopper # MADR's confirmation: the check that shows the decision still holds superseded_by: D7 author: agent-session-12 scope: section # only in decisions.yaml: project (default) | section | part, section: S3 # with the section or part it applies to ``` A nested decision takes its scope from where it is written (`section` in a section, `part` in a card); `to_dict()` leaves the implied scope out so the card round-trips. The site's `site: decisions:` key keeps working: point it at the same file. ### Interface kinds | Kind | Meaning | What the connectivity gate expects | |---|---|---| | `fastened`, `bonded`, `press_fit`, `snap_fit`, `contact` | rigid, 0 DOF | touching or overlapping (an overlap needs `gates.intended_overlaps`) | | `gap` | rigid with a designed gap | apart by at most `clearance` | | `revolute`, `slider`, `cylindrical`, `planar`, `ball` | a mechanism | a registered `@joint` with this interface id; contact is not required | | `electrical`, `thermal`, `fluid`, `other` | what crosses is not (only) mechanical | nothing geometric unless also registered | ## Round trip Every dataclass has `to_dict()`, which writes the YAML shape back with fields at their default left out. A card split into its own file is written back as its path. `dump_yaml(section.to_dict())` gives YAML that loads to the same section. Comments are not preserved: the dossier is edited by hand, `to_dict()` is for tools and tests. ## The `dossier:` key `cad-agent.yaml` gains one key in its `cad:` section, `dossier: dossier/project.yaml`. `CadProject.dossier` is the resolved path (None when absent), and `load_project_dossier(project)` loads it with file names shown relative to the project root. ## Declaring geometry in the build script Only the build code knows which solid is the cavity, where a joint axis is and where each screw sits, so those facts are declared in code, next to the geometry (`cadagent.dossier.registry`). The YAML cards and the code are joined by id. ```python from cadagent.dossier import Axis, part, cavity, joint, fastener, assembly, build @part("bench_base", section="S1", grounded=True) # ground for the connectivity gate def bench_base(p): ... @part("hopper", section="S3") # registers the shape; joined to card "hopper" def hopper(p): ... # returns a shape in assembly coordinates, as today @cavity("hopper", "hopper_cavity") # the void the containment gate tests against def hopper_cavity(p): return hopper_inner(p) # always a declared solid: the gates do not derive one @part("ball", section="S3", free=True, reason="loose in the hopper") # allowed to float; a reason is required def balls(p): return [sphere_at(c) for c in p["ball_centres"]] # a list = one instance per item @joint("I9", kind="revolute", axis=lambda p: Axis(p["flap_pivot"], (0, 1, 0)), range_deg=(0, 70), parts=("hopper_flap", "hopper"), intended_contacts=[("hopper_flap", "hopper_stop")]) def flap_pivot(p): ... # optional body, never called; joint() also works bare fastener("screw_m3x8", clamps="hopper", into="pan_base", via="I7", tool="hex_2.5", at=lambda p: [Axis((x, y, p["flange_z"]), (0, 0, -1)) for x, y in p["flange_holes"]], length_mm=8, thread_start_mm=lambda p: p["flange"], min_engagement_mm=4, shape=lambda p: mcmaster_screw("91292A111")) # optional solid, modelled in the McMaster screw frame assembly.step(1, fit="pan_base", onto="bench_base", direction="-Z", tool="hex_2.5", fasteners=["screw_m4x12"] * 3) if __name__ == "__main__": build() # parameters from cad/parameters.json; writes exports/dossier-geometry.json ``` | Declaration | Arguments | Notes | |---|---|---| | `@part(name, section="", *, place=None, grounded=False, free=False, reason="")` | function `(p)` or `()` returning a CadQuery shape, Workplane, Assembly, build123d object or `TopoDS_Shape`; a list for several instances | `place`: a `cq.Location`, an `(x, y, z)` translation, or a list of them (instances); then the function returns the shape in its own frame | | `@cavity(part, name)` | function returning one solid | in the part's frame: it follows the part's `place` | | `joint(interface, kind="revolute", *, parts=(moving, fixed), axis, range_deg=None, range_mm=None, intended_contacts=(), samples=None)` | `kind`: `fixed`, `revolute` (needs `range_deg`), `slider` (`range_mm`), `cylindrical` (both), `planar`, `ball` | the axis is in assembly coordinates; `axis` may be a function of the parameters | | `fastener(key, *, clamps, into, at, name="", via="", tool="", length_mm, diameter_mm, thread_start_mm, thread_depth_mm, min_engagement_mm, shape=None)` | `at`: one `Axis` per screw, from the head seat in the direction the screw advances | `thread_start_mm`: seat to first engaged thread; numbers may be functions; `shape` is placed at every axis (McMaster frame: origin on the seat, +Z out of the hole) | | `assembly.step(order, fit, onto=None, *, direction="", tool="", fasteners=(), access_check="", note="")` | `direction`: `+X`..`-Z` or `(x, y, z)` | steps may also be written in `project.yaml`; when both exist with the same order they must agree | Declarations are checked when they are made (a free part without a reason, a part both grounded and free, a revolute joint without a range, an unknown kind, a bad id raise `RegistryError`). The same declaration run twice (a re-import) replaces itself; the same name declared at two places raises. Parts, fasteners and cavities are separate kinds: a fastener or cavity named like a part is refused, and fastener names (default `key:clamps-into`) never contain `#`. A cavity belongs to a single-instance part (the containment gate must know which container it is). **Which registry.** The module-level `part`, `cavity`, `joint`, `fastener`, `assembly.step` and `build` write to the *current* registry: the innermost `with use_registry(reg):` block, else `cadagent.dossier.REGISTRY`. `build_module()` builds every script in a fresh registry of its own, so nothing leaks between two builds in one process (a test session, a watch loop, `cad-agent gates`); modules under the project root that the script imports are re-imported, so their declarations land in that build too. A tool or test can also use its own `Registry()` and its methods (`reg.part`, `reg.assembly.step`, ...). `build(params=None, *, out_dir="exports", root=None, write=True)` calls every declaration with the parameters (a mapping, a JSON path, or `cad/parameters.json` under `root`), and returns a `BuildResult`. A declaration that fails raises `RegistryError` naming it and its `file:line`. `build_module("cad/build.py")` executes a build script and builds it, adding `print_parts()`/`dossier_items()` (called again on every build, so new parameters give new shapes). **`root`** defaults to the project root, `project_root(script)`: the nearest directory above the script with `cad-agent.yaml` or `vptest.yaml` (else the parent of a `cad/` directory), never the current directory. `out_dir` must be inside it. Every path in the record is relative to the root; a declaration made in a file outside the root is recorded by its file name only, so the committed record carries no machine paths. One `out_dir` holds one geometry record: a project with several build scripts gives each its own `out_dir` (a second build into the same directory replaces the record and removes the first build's shape files). ### Migrating an existing build: `dossier_items()` A build that keeps `print_parts() -> {name: shape}` adds one function, and moves parts to decorators one at a time (a decorated part wins over a `print_parts()` entry of the same name): ```python def dossier_items(): return { "parts": {"hopper": {"section": "S3"}, "ball": {"section": "S3", "free": True, "reason": "loose"}}, "cavities": {"hopper_cavity": {"part": "hopper", "shape": hopper_inner(P)}}, "joints": [{"interface": "I9", "kind": "revolute", "parts": ("hopper_flap", "hopper"), "axis": Axis(P["flap_pivot"], (0, 1, 0)), "range_deg": (0, 70)}], "fasteners": [{"key": "screw_m3x8", "clamps": "hopper", "into": "pan_base", "at": [...]}], "assembly": [{"order": 1, "fit": "pan_base", "onto": "bench_base", "direction": "-Z"}], } ``` Every `print_parts()` shape is registered; a part not yet given a section here is still a shape, and the join reports it as a shape without a card until its card exists. That list is the migration's to-do list. The same checks apply as for decorators (a free part needs a reason; grounded and free together are refused; a joint needs `interface`). ### `exports/dossier-geometry.json` ```json { "schema": "cad-agent.dossier-geometry/1", "units": "mm", "hash": "cadagent.evidence.shape_content_hash: sha256 of the OCCT BREP text ...", "actor": "agent-session-12", # who ran this build "parameters": {"file": "cad/parameters.json", "sha256": "..."}, "out_dir": "exports", "shapes_dir": "exports/dossier-shapes", "parts": {"ball": {"section": "S3", "grounded": false, "free": true, "free_reason": "loose in the hopper", "sha256": "", "count": 8, "declared_at": "cad/build.py:24", "actor": "agent-session-12", # who built this part's current hash "instances": [{"key": "ball#0", "sha256": "...", "file": "dossier-shapes/.brep", "bbox": {"min": [x, y, z], "max": [x, y, z]}, "volume_mm3": 523.6, "solids": 1, "valid": true}]}}, "cavities": {"hopper_cavity": {"part": "hopper", "sha256": "...", "file": "...", "bbox": {}, "volume_mm3": 0, "solids": 1, "valid": true, "declared_at": "cad/build.py:20"}}, "joints": {"I9": {"kind": "revolute", "moving": "hopper_flap", "fixed": "hopper", "axis": {"origin": [31, 0, 70], "direction": [0, 1, 0]}, "range": [0, 70], "unit": "deg", "intended_contacts": [["hopper_flap", "hopper"]], "samples": null, "declared_at": "..."}}, "fasteners": {"screw_m3x8:hopper-pan_base": {"key": "screw_m3x8", "clamps": "hopper", "into": "pan_base", "via": "I7", "tool": "hex_2.5", "length_mm": 8, "thread_start_mm": 2.4, "min_engagement_mm": 4, "count": 4, "instances": [{"key": "screw_m3x8:hopper-pan_base#0", "origin": [-25, -25, 12.4], "direction": [0, 0, -1], "sha256": "...", "file": "...", "bbox": {}}], "declared_at": "..."}}, "assembly": {"steps": [{"order": 1, "fit": "pan_base", "onto": ["bench_base"], "direction": "-Z", "tool": "hex_2.5", "fasteners": ["screw_m4x12", "screw_m4x12", "screw_m4x12"], "declared_at": "cad/build.py:40"}]}, "physical_fit_verified": false } ``` - **Hashes** are content hashes from one shared function, `cadagent.evidence.shape_content_hash(shape) -> str` (also exported as `cadagent.dossier.shape_content_hash` and `shape_sha256`): sha256 of the shape's BREP text, OCCT format version 1, without triangulation or normals, with the 7-digit per-shape flag lines removed. The registry and the gates use the same function, so a recorded hash is a valid gate cache key. - The same shape built the same way gives the same hash in any process and run (tested in a subprocess); a changed dimension or placement changes it (the location is part of the text, so even `place=(0, 0, 0)` changes it: the safe direction). Queries (`Volume`, `BoundingBox`, `isValid`, `fix`) and tessellation do not. - Two different constructions of the same geometry may hash differently: a cache miss, never a false match. An OCCT upgrade may change every hash, which only invalidates sign-offs. - A shape **read back** from its BREP file may hash differently, because the reader recomputes frame directions and location matrices (last-digit noise, signed zeros). Keep the recorded hash with the shape (`assembly_shapes(with_hashes=True)`, `content_hashes()`); never re-hash a loaded shape to identify it. `evidence.brep_content_hash(file_bytes)` checks a stored file against its record. - **`actor`** (top level) is `CAD_AGENT_ACTOR` from the environment (an agent or session id, or a person), `unknown` when unset: who ran this build. **`parts..actor`** is who built that part's current geometry: the actor of the build that first produced its content hash. A rebuild that gives a part the same hash keeps the earlier actor (read from the record it replaces; a record from before per-part actors gives its top-level `actor`), a changed hash takes this build's actor, and a known actor replaces an earlier `unknown`. The reviewer pass compares the reviewer with these ([below](#the-reviewer-pass)). Neither is a gate input: a rebuild by someone else keeps the verdicts current. - **Shapes** are written once each to `exports/dossier-shapes/.brep` (content-addressed; files no longer referenced are removed). The directory gets its own `.gitignore` (`*`): it is build output, regenerated by every build. The JSON is the record to commit. Any gate cache belongs in a gitignored directory too. - Numbers are rounded to 1e-6 mm so the JSON diffs cleanly; `bbox` is the tight OCCT box. - Instance keys are `name` for a single instance and `name#0`, `name#1`, ... for several. ### Hand-over to the gates The gates (`cadagent.gates`, plan §4) consume the registry through two calls and never import the build script: ```python from cadagent.dossier import build_module, load_geometry result = build_module("cad/build.py") # in the build process, or result = load_geometry("exports/dossier-geometry.json") # in another process (cad-agent gates): reads the BREPs, # checks each file against its recorded hash; the root # comes from the record's out_dir shapes = result.assembly_shapes() # {key: cq.Shape in assembly coordinates}: "hopper", "ball#3", # "screw_m3x8:hopper-pan_base#0" (placed fasteners; fasteners=False omits) pairs = result.assembly_shapes(with_hashes=True) # {key: (cq.Shape, content hash recorded at build time)}: # what the gates' Assembly.from_build() takes, nothing re-hashed result.content_hashes() # {key: hash} for every instance, placed fastener and cavity result.owner("ball#3") # "ball": the part an instance key belongs to result.kind("ball#3") # "part" | "fastener" (None when unknown) result.parts["ball"] # [cq.Shape, ...] instances in order result.cavities["hopper_cavity"] # cq.Shape, in assembly coordinates result.joints["I9"] # {"kind", "moving", "fixed", "axis": Axis, "range", "unit", ...} result.fastener_axes[name] # [Axis(origin=seat, direction=advance), ...] result.geometry # the JSON above: grounded / free flags, hashes for cache keys ``` What each gate reads: connectivity uses `assembly_shapes()`, `grounded` and `free` (union of the code flags, `gates.grounded`/`gates.free_parts` and `contains[].free` in the YAML), the joints and the dossier interfaces; containment uses `cavities` and the instances of `contains[].what`; the fastener gate uses `fasteners` (axes, stack numbers, placed solids) and the steps; the motion gate uses `joints` (axis, range, `intended_contacts`, `samples`, else `gates.motion_samples`); the assembly gate uses the steps. Pair caches key on the recorded hashes. What moves with a joint (settled, final): a joint names one `moving` part, and its body is the transitive closure from it over `fixed` joints and fastener declarations (the fastener, what it clamps and what it threads into), stopped at grounded parts and at any joint's `fixed` part (`cadagent.gates.relations.moving_bodies`, used by `Assembly.from_build`, the connectivity gate and the motion gate). In a chain, a child joint's `fixed` part is therefore its parent's moving part itself; see [`gates.md`](https://cad-agent.valpatel.com/docs/gates.md#motion-range-interference-cadagentgatesmotion). ### Joining cards and geometry `join(dossier, geometry)` (geometry: a `BuildResult` or the JSON mapping, so the lint needs no CAD kernel) returns `Issue`s with `file:line` (the card's YAML line, or the build script's declaration line): | Code | Severity | When | |---|---|---| | `card-without-shape` | error | a part card no `@part` or `print_parts()` registers | | `shape-without-card` | error | a registered part with no card | | `section-mismatch` | error | `@part(section=...)` differs from the section the card is in | | `unknown-cavity`, `cavity-wrong-part` | error | `contains[].cavity` missing, unregistered, or on another part | | `contains-count` | error | `contains[].count` differs from the instances the build made | | `joint-without-interface`, `joint-parts-mismatch`, `joint-kind-mismatch` | error | a joint with no interface of its id, other parties, or another kind (`fixed` matches any rigid kind) | | `interface-without-joint` | error | a mechanism interface (`revolute`, `slider`, ...) with no `@joint` | | `unknown-part`, `unknown-interface` | error | a joint, fastener, cavity or step names something that does not exist | | `fastener-count` | error | a card's `fasteners[].qty` differs from the axes `fastener()` places | | `fastener-not-registered`, `step-fastener-unregistered` | warning | declared in the card or a step, not yet in code | | `step-conflict` | error | a step in `project.yaml` and in code with the same order disagree | The dossier lint runs this with its own checks; every code in this table is an error or warning of the lint under the same name. ## Lint and status `cad-agent dossier` lints the dossier and computes the status of every part card and section. It writes `exports/dossier-status.json` (the site and the report read this, never the YAML) and the lint verdict `exports/gates/dossier-lint.json` (`cadagent.gate/1`, one check per finding plus one passing check per rule). `cad-agent check` runs the same lint as its `cad-dossier` feature, and `cad-agent all` runs it after the build (as the last step of `cad-agent gates`, [gates.md](https://cad-agent.valpatel.com/docs/gates.md#running-the-gates-cad-agent-gates)). It needs no CAD kernel: it reads files only. ```python from cadagent.dossier.lint import lint, lint_project, project_evidence, write_outputs, Evidence, RULES res = lint_project(load_project(".")) # None without a dossier: key res = lint(load_dossier(path, strict=False), Evidence(geometry=..., gates={...}, checks={...}, reviews=[...])) res.findings # Finding(code, message, where=file:line, severity, item, parts): sorted by file and line res.parts["hopper"] # {"status": "gated", "blocked_by": ["no current reviewer record"], "checks": [...], ...} res.sections["S3"], res.requirements["R4"], res.decisions["D41"] res.passed # no error write_outputs(res, "exports") ``` **What it reads.** The loaded YAML (loaded with `strict=False`, so loader errors such as `unknown-id` become findings with their file and line), `exports/dossier-geometry.json`, every verdict in `exports/gates/`, the `cad.checks` of `cad-agent.yaml` (a check passes when every `pass_key` is `true`, as in the report) and the reviewer records in `dossier/reviews/*.json`. **Check ids.** A `checks:` entry (on a card or a requirement, a decision's `confirmation`, a step's `access_check`) is `gate:` (the verdict `exports/gates/.json`; `unrun` until it exists, and an error when `` is neither a known gate nor a written verdict) or the `id` of a `cad.checks` entry. Any other id is `unknown-check`. ### Rules `cadagent.dossier.lint.RULES` is the list; every finding gives its file, line and code. Errors fail `cad-agent check`; warnings are reported. | Code | Severity | What must hold | |---|---|---| | `card-no-function` | error | a part card states its `function` | | `card-no-interfaces` | error | a part card lists its interfaces (a free part, or a one-part dossier, may not) | | `card-no-decision` | warning | the card, or another card or the section it is in, records a decision | | `interface-not-party` | error | a card lists only interfaces whose `between` names the part or its section | | `interface-one-sided` | error | an interface between two parts is listed on both parts' cards | | `interface-datum-conflict` | error | two sections that declare one interface agree on its datum (plan §5.4) | | `interface-conflict` | warning | they agree on its other fields | | `fastener-via-mismatch` | error | a card's fastener runs `via` an interface between this part and `to` | | `exemption-unknown-party` | error | every party of a `gates.intended_overlaps` or `gates.ignore_pairs` entry is a part, one of its instance keys, a fastener or one of a fastener's instance keys in the geometry record and a `[part, part]` pair names a part with at least two instances (unchecked without a record; a name that is another kind of dossier id is the loader's `unknown-id`, and one instance key twice is a loader error) | | `wall-undeclared` | error / warning | a card checked by `gate:wall` (error), or made by an additive process (warning), declares `material.wall_mm` | | `decision-incomplete` | error | a decided decision has a `why` and at least one alternative | | `alternative-no-reason` | error | every alternative says why it was `rejected` | | `decision-open` | warning | an open decision; it also blocks its section's sign-off | | `unknown-check` | error | every check id resolves (above) | | `requirement-untraced` | error | every requirement is served by a section (or derived into one that is) | | `requirement-unchecked` | error | every requirement has a check: its own, or one on a card of a section that serves it | | `waiver-no-rationale` | error | a `waived` requirement says why | | `requirement-status-ignored` | warning | a requirement's status is computed; only `waived` belongs in YAML | | `section-no-requirements` | error | a section names the requirements it serves | | `section-no-parts` | warning | a section has part cards | | `section-below-gated` | error | every section has reached the required status (default `gated`, plan §5.4) | | `status-not-earned` | error | a card says `reviewed` or `signed_off` only when a current reviewer record gives it | | `status-overclaimed` | warning | a card's YAML status is not above the computed one | | `review-invalid` | error | a reviewer record passes `validate_record` ([the reviewer pass](#the-reviewer-pass)): every field, each answer with its evidence, a sign-off consistent with its findings and answers | | `review-unknown-section` | error | a reviewer record names a section of the dossier | | `review-same-actor` | error | the reviewer did not build any part of the section (`parts..actor`) and is not named as an `author` of the section, its cards or decisions; neither the reviewer nor a part's builder is `unknown` (while `gates: require_separate_reviewer`, plan §5.3) | | `review-builder-unknown` | warning | a current review is of a section whose parts were built by a known actor; otherwise nobody can be shown to be someone else, and the review does not count (a fresh checkout built without `CAD_AGENT_ACTOR`: the section stays `gated`, `check` still passes) | | `review-incomplete` | warning | a current review passes `check_record` against the dossier and the packet on disk (for a sign-off: every decision judged, every requirement traced, every question answered; for any record: no answer to a question the packet does not ask, nothing outside the section); otherwise a sign-off does not count, and a change request still counts (it voids an earlier sign-off of the same geometry) | | `review-stale` | warning | the latest review still matches the geometry; otherwise its sign-off no longer counts | | `signoff-open-decision` | error | a section with an open decision is not signed off | | `geometry-missing` | warning | the geometry record exists (without it every card stays `draft`) | | `geometry-invalid`, `verdict-invalid` | error | the geometry record and the gate verdicts are readable | | `verdict-stale` | error | a gate verdict's inputs match the current geometry record ([gates.md](https://cad-agent.valpatel.com/docs/gates.md#verdicts)); a stale or unbound verdict is not a pass | The loader's errors and warnings ([above](#ids-errors-and-findings)) and the join's codes ([Joining cards and geometry](#joining-cards-and-geometry)) are findings under their own codes. ### The status machine Each part card climbs one step at a time, and stops at the first step its evidence does not reach. `blocked_by` says why it is not one step higher. | Status | Reached when | |---|---| | `draft` | the card exists; it stays here while it has a lint error, or the build registered no shape for it | | `built` | lint-clean, and the geometry record has its shape and content hash | | `gated` | every check that applies passes: the card's `checks` plus every gate verdict in `exports/gates/`. A gate counts against the part when a failing finding names it, an instance `name#i`, a fastener that clamps it or threads into it, or a cavity of it, and against every part when a failing finding names nothing known; at least one check must apply | | `reviewed` | a reviewer record for its section, by a separate actor, whose `hashes` match the current hash of every part in the section | | `signed_off` | that record says `signed_off: true` and the section has no open decision | A section's status is the lowest of its parts' (`draft` when it has no cards or a lint error of its own). Open decisions count against the section they are in, or the section of the part they are scoped to (a `part:` decision in `decisions.yaml` included). A lint error on any line inside a card is that card's (with `Evidence.root` set, as `lint_project` does). A requirement's evidence is its **own** `checks` and its derived requirements (`parent:`). The checks on the cards of the sections that serve it satisfy the trace rule and are listed as `covered_by`, but they verify nothing on their own: a wall gate passing says nothing about "the lid stays on". A requirement is - `verified` when it has evidence and all of it passes (every own check passes, every derived requirement is `verified` or `waived`; for `verify_by: inspection`, also every section that serves it is at least `reviewed`); - `failed` when an own check fails or a derived requirement failed; - `not_tested` for `verify_by: test` or `demo` (cad-agent cannot run a test or a demonstration), and for a parent whose only open children are `not_tested`; - `waived`, or else `unverified`. **Geometry changes void sign-off.** A reviewer record binds the content hash of each part it reviewed (the `sha256` of `parts.` in `dossier-geometry.json`, the hash recorded at build time). When a part is rebuilt with different geometry, no record matches any more: the section falls back to `gated` if its gates still pass on the new build (`built` if not), the status file shows the voided record under `invalidated` with the parts that changed, and the lint warns `review-stale`. A card that still says `status: signed_off` in its YAML is then `status-not-earned`: statuses are computed, so leave `status:` out of the cards. The status uses the newest record whose hashes match, so a revert to reviewed geometry restores its sign-off. **Reviewer records** (`dossier/reviews/*.json`, [the reviewer pass](#the-reviewer-pass)) are validated whole; an invalid one is `review-invalid` and does not count. A record by a part's builder or the section's author, or by `unknown`, is refused (`review-same-actor`; a section whose builder is `unknown` gives the warning `review-builder-unknown` and the record does not count either) while the dossier's `gates: require_separate_reviewer` is true (the default; `lint(..., require_separate_reviewer=)` overrides it). Only a record whose hashes match the current build is checked, since only such a record can count: an old record does not become an error because its reviewer later changed a part. A current record read from a file also goes through `check_record` against the dossier and the packet on disk (the checks `--submit` runs, without those the lint cannot make or reports itself: the packet's staleness, open decisions, the gates, and, unless the packet on disk is the one the record names in `packet.sha256`, which views and gates the packet had when the record was written); a sign-off that fails them does not count, and a change request that fails them still counts (`review-incomplete`). With no geometry record nothing is built, so no record is refused or counted. A requirement verified by `inspection` also needs the current record of each section serving it to trace it `yes` and not to request changes. Records are read in time order (`timestamp`, else `date`, then the `-2`, `-3` suffix of a second record the same day); the newest whose hashes match decides. **Stale verdicts.** A gate verdict counts only when it was computed from the current geometry record: its `inputs.geometry.digest` must equal the record's (`cadagent.gates.verdict.geometry_digest`). Otherwise, or when it has no `inputs`, the lint reports `verdict-stale` with the shapes that changed, the gate's checks read `stale`, and the parts stay `built` until `cad-agent gates` runs again. Rebuilding the same geometry (another actor, a declaration moved to another line) keeps verdicts current. For a project (`lint_project`, `cad-agent dossier`) the lint also compares the gate's declarations and options, as `cad-agent check` does ([gates.md](https://cad-agent.valpatel.com/docs/gates.md#verdicts)). **Limits.** A clean lint means the design is declared and agrees with the build. It does not mean the geometry is right (the gates check that) or that it meets its requirements (the reviewer checks that). ## The reviewer pass A section is reviewed by an agent or person that did not build it (plan §5.3, task T11; `cadagent.dossier.review`). The author hands it over (skill [`design-process`](https://cad-agent.valpatel.com/skills/design-process/SKILL.md) §7) once the section is `gated` and its review packet is rendered ([review-packet.md](https://cad-agent.valpatel.com/docs/review-packet.md)). Then: ```bash cad-agent review-section S3 # writes the bundle; exit 1 while something blocks a sign-off cad-agent review-section S3 --bundle-dir /tmp/s3 --since v0.4 # elsewhere; diff against a given revision # the reviewer reads PROMPT.md, copies record-template.json, fills it in, and: CAD_AGENT_ACTOR=review-agent-7 cad-agent review-section S3 --check my-review.json # validate, write nothing CAD_AGENT_ACTOR=review-agent-7 cad-agent review-section S3 --submit my-review.json # validate and write the record cad-agent dossier # the section's status: reviewed or signed_off ``` ```python from cadagent.dossier.review import write_bundle, submit_record, validate_record, check_record manifest = write_bundle(project, "S3", bundle_dir=None, since=None) # -> bundle.json as a dict path, record, problems = submit_record(project, "S3", "my-review.json", reviewer=None, write=True) problems = validate_record(record) # [Problem(code, message)]: the record on its own (what the lint runs) problems = check_record(record, dossier=d, geometry=geo, packet=manifest, packet_stale=None, section_status="gated") ``` ### The bundle `exports/review-bundle/
/` (or `--bundle-dir`; a directory that is not empty and holds no `bundle.json` is refused, never wiped). It needs no CAD kernel: the packet must have been rendered before. | File | What it holds | |---|---| | `PROMPT.md` | the reviewer's instructions: who they are not, what to read first, how to answer, how to submit, what blocks a sign-off now | | `bundle.json` | the manifest (`cad-agent.review-bundle/1`): the section and its computed status, the part hashes and who built each, `require_separate_reviewer`, the authors named in the dossier, the packet's state, open decisions, the diff's base and paths, earlier records, `blockers`, `ready`, and the sha256 of every file | | `section/` | the section's YAML, its split part cards and `project.yaml`, at their project paths | | `gates.json` | per gate: status, stale reason and the checks that name a part of the section (from the packet's `measurements`); the gate findings and the lint findings for the section | | `status.json` | the lint's rows for the section and its parts, each part with `built_by` | | `packet/` | the review packet's `manifest.json` and every image it names | | `checklist.json` | the packet's checklist (questions, `measured_only`, instructions), the decisions to judge and the requirements to trace | | `diff.patch` | the section's sources (the dossier directory without `reviews/`, the build scripts that declared its geometry, the parameters file, `cad-agent.yaml`) in the working tree against the base: the commit the last sign-off names (`commit`), else the commit that added that record, or `--since`; untracked files under those paths are listed in `bundle.json`. No earlier sign-off: no diff, review all of it | | `record-template.json` | the record to fill in: the current hashes, one entry per question, decision and requirement, every answer `null` | `blockers` (and exit 1) name what would refuse a sign-off today: a missing, unrendered or stale packet, a section below `gated`, an open decision, a part whose builder is `unknown`. The bundle is written anyway: a reviewer can still request changes. ### The record (`cad-agent.review/1`) `dossier/reviews/
-.json` (`-2`, `-3`, ... for another record the same day). Written by `--submit`, which fills in `actor` (from `CAD_AGENT_ACTOR`), `date`, `timestamp`, `signed_off` (from `verdict`), `commit`, `build_actors`, `packet` and `toolkit`; the reviewer writes the rest. ```jsonc { "schema": "cad-agent.review/1", "section": "S3", "date": "2026-10-28", "timestamp": "2026-10-28T14:03:12Z", "actor": "review-agent-7", "verdict": "sign_off", // sign_off | changes_requested "signed_off": true, // verdict == sign_off "hashes": {"hopper": "", "hopper_flap": ""}, // every part of the section, as built now "build_actors": {"hopper": "agent-12", "hopper_flap": "agent-12"}, "commit": "", "packet": {"manifest": "exports/review/S3/manifest.json", "sha256": "...", "section_hash": "...", "stale": null}, "checklist": [ {"id": "contained-inside:hopper_cavity:ball", "question": "In the cut through hopper_cavity, ...", "bad_answer": "no", "answer": "yes", "evidence": "every ball is inside the wall in both cuts; containment passes", "views": ["cut-cavity-hopper_cavity", "cut-cavity-hopper_cavity-plan"], "image_only": false, "measurements": ["gate:containment"]}, {"id": "plain-box", "question": "Does any part appear as a plain box ...", "bad_answer": "yes", "answer": "no", "evidence": "the flap has its hinge knuckles and stop", "views": ["solid-iso"], "image_only": true} ], "decisions": [{"id": "D41", "verdict": "justified", "evidence": "both alternatives rejected with a reason"}], "requirements": [{"id": "R4", "traced": "yes", "evidence": "gate:containment checks 40 balls in hopper_cavity"}], "findings": [{"id": "F1", "severity": "minor", "text": "the flap stop has no fillet", "view": "pose-I9-max", "parts": ["hopper_flap"]}], "summary": "...", "toolkit": "0.1.0", "physical_fit_verified": false } ``` | Field | Rule (`validate_record`) | |---|---| | top level | `schema`, `section`, `date` (YYYY-MM-DD), `timestamp` (ISO 8601 with a zone), `actor`, `verdict`, `signed_off`, `hashes` (part → sha256 hex), `checklist`, `decisions`, `requirements`, `findings` are required; `build_actors`, `commit`, `packet`, `bundle`, `summary`, `toolkit`, `physical_fit_verified` (false only) may be given; any other key is refused (a typo such as `signed-off` must not pass silently) | | `checklist[]` | `id`; `answer` `yes`, `no` or `unclear`; `evidence` (non-empty); `views` (view ids); `image_only`: `true` when the answer rests on the images alone (then `views` is not empty and `measurements` is), `false` when a measurement backs it (then `measurements` lists it: `gate:`, a check id or a packet finding id) | | `decisions[]` | `id`; `verdict` `justified`, `weak` or `missing_alternative`; `evidence` | | `requirements[]` | `id`; `traced` `yes`, `no` or `unclear`; `evidence` | | `findings[]` | `id`; `severity` `critical`, `major`, `minor` or `note`; `text`; optional `parts`, `view` (a packet view id: the site shows the finding beside that image), `checklist` (a question id), `decision` | | a sign-off | answers at least one question; no `critical` or `major` finding; no answer `unclear` or equal to its `bad_answer`; no decision `missing_alternative`; every requirement `traced: yes` | | changes requested | at least one finding | `check_record` (run by `--submit` and `--check`) then compares the record with the project now: | Code | Refused when | |---|---| | `stale-hashes` | `hashes` is not exactly the current hash of every part of the section: **stale hashes never sign off** (nor request changes on geometry that is gone); make a new bundle | | `same-actor` | while `gates: require_separate_reviewer`: the reviewer is `unknown`, a part's builder is `unknown`, the reviewer built a part of the section (`parts..actor`) or is the `author` of the section, a card or a decision | | `actor` | the record names an `actor` other than the submitting `CAD_AGENT_ACTOR` | | `unanswered`, `unknown-question`, `question-changed`, `unknown-view`, `unknown-measurement` | the checklist answers a question the packet does not ask, changes a question's text or bad answer, cites a view without an image or a gate the packet does not quote; a sign-off also leaves no question unanswered (`unanswered`), while a change request may stop at the first blocking flaw | | `unjudged`, `unknown-decision`, `untraced`, `unknown-requirement` | a decision or requirement outside the section is judged; a sign-off also judges and traces every one of the section's | | `packet`, `packet-stale` | a sign-off with no rendered packet, or one that is not current (`project_packet_stale`) | | `open-decision` | a sign-off while a decision of the section is `open` | | `below-gated` | a sign-off of a section that is `draft` or `built` | | `section`, `geometry`, `unbuilt` | the section is not in the dossier; nothing is built; a part has no shape | The lint runs `validate_record` on every record, and `check_record` on a record that matches the build, against the dossier and the packet on disk (`review-incomplete`; without a packet on disk the questions cannot be checked), with its own rules on top (`review-same-actor`, `review-builder-unknown`, `review-stale`, `signoff-open-decision`). What it cannot check is whether the packet was current when the record was written: only `--submit` checks that. A record changes the dossier lint that the packet quotes, so after any record the packet reads stale: render it again (`cad-agent review-packet`) before the next sign-off. **On the site.** The review pages ([below](#on-the-review-site)) read the status file's `sections..review` (`record`, `date`, `timestamp`, `actor`, `verdict`, `signed_off`, `findings`: the record's finding count, `blocking`: how many are critical or major) and list the record's `findings` beside the packet image each one's `view` names, with the reviewer's checklist answers. **Limits.** The separate-reviewer rule is only as strong as `CAD_AGENT_ACTOR`: a harness that gives two agents the same value, or one agent two values, defeats it. The lint cannot tell who wrote a record file: a file in `dossier/reviews/` that names another actor counts if it passes the checks (`--submit` at least stamps the actor from the environment; who commits the file is git's record, not the lint's). Someone who only edited the section's YAML (a gate exemption, a card) without building a part and without an `author:` line is not known as its author and is not refused. The per-part builder is carried from the geometry record a build replaces: a build into a directory without that record (a fresh checkout where `exports/` is not committed) records its own actor for every part, so a reviewer who builds there becomes the builder and is refused (the safe direction; commit the record, or review without rebuilding). A sign-off binds part hashes, not the dossier text: editing a card's function or a decision after a sign-off does not void it (the packet goes stale, so the next sign-off needs a new one). The reviewer judged renders and measurements of the model; a signed-off section is not shown to fit, hold or work. ## On the review site `cad-agent site` builds review pages for a project with a `dossier:` or `gates:` key (`cadagent.site_review`), in `site/review/`, linked from the overview's "Design review" block and, when the site configures no `nav`, from three top-bar tabs (Review, Requirements, Gates). A project without either key sees no change. | Page | Shows | |---|---| | `index.html` | one row per section: status, requirements, part count, gate state (pass / fail / stale / unrun over its parts' gate checks), review state (unreviewed / findings open / reviewed / signed off / void), the reviewer record; the lint findings with their file and line | | `section-.html` | the section's requirements (status, verifying checks), its interfaces and those of its parts, section decisions, and each part card: function, material, loads, fasteners, `contains`, decisions with the alternatives rejected and why, checks as pills linked to their evidence, the status ladder draft → built → gated → reviewed → signed_off, what keeps it from the next step, and a **stale** badge when the geometry changed since the last review (`invalidated`). Then every gate finding that names one of its parts (an instance, a fastener that clamps or threads into it, a cavity of it), with the witness, and the lint findings on it; then the review packet | | `requirements.html` | id, text and source quote, status, the sections it traces to, its own checks (and the card checks that only satisfy the trace), derived requirements, waivers and their rationale | | `gates.html` | one row per gate: state (pass, warn, fail, stale, missing, invalid, not applicable, skipped, from `cadagent.gates.run.gate_rows`), counts and links to the verdict's `.md`, `.json` and `.dot`; the connectivity graph as SVG; each gate's findings with witnesses, exclusions, what it did not check, assumptions and limits | **What it reads.** Statuses only from what the pipeline computed: `exports/dossier-status.json`, the verdicts in `exports/gates/` and the reviewer records the status file names. The YAML is loaded (`strict=False`) for the words only (functions, purposes, decisions, interfaces, requirement sources and waivers). When a dossier file is newer than the status file, every page says so ("run `cad-agent dossier`"); with no status file the pages still list the sections from the YAML, marked "not computed". `cad-agent site --serve` rebuilds the pages when any of these files changes, and serves `.md`, `.dot` and `.yaml` files as plain text so the linked verdicts open in the browser. **The connectivity graph** is drawn from `exports/gates/connectivity.dot` by `cadagent.dotsvg`: Graphviz's `dot` when it is installed, else a built-in layered layout (each cluster laid out on its own, layers by distance from the grounded part, left to right). The page says which drew it, and writes `site/review/connectivity.svg` too. The built-in drawing takes its colours from the page's theme (light or dark); Graphviz's keeps its own black on clear. **The review packet** (plan T10, `cad-agent.review-packet/1`) is read from `exports/review/
/manifest.json` when it exists: each of its `views` (`id`, `caption`, `note`, `file` relative to the manifest, null when not rendered) is shown with the checklist questions that name it, and the manifest's `parts.*.sha256` that differ from the current build show a "rendered from older geometry" banner. The reader also takes a plain `images` list (paths or `{file, caption, view, hash}`) and a `hashes: {part: sha256}` map. The current reviewer record's `findings` (`{severity, text|message, render|image|view}`) are listed beside the view they name (by id or file name), the rest below. Without a manifest the page says how to make one. Limits: the pages show what the files say. They do not recompute a status, and a page built before the gates ran again shows the old verdicts (the gates page marks them stale against the current geometry record). --- Source: https://cad-agent.valpatel.com/docs/explore.md # Explore and compare designs (`cadagent.explore`) For any product built from bought and made parts (an enclosure, a robot, a board, a jig): pick parts from a catalogue for each slot of a design, evaluate every design with the project's own model, and rank the trade-offs. cad-agent provides the catalogue checks, the bill of materials, the ranking, the pages and a search loop. The project provides the parts, the designs and, optionally, an evaluator with its physics. ```bash cad-agent catalogue # check the catalogue, and every design against it cad-agent compare --swaps --open # evaluate and rank the designs; try every one-part swap; open the page cad-agent optimise --trials 200 # search parts and parameters for better trade-offs ``` Worked example: [`examples/explore-two-wheel`](https://github.com/Valpatel/cad-agent/tree/dev/examples/explore-two-wheel/) compares small two-wheel-legged robot configurations. The tests use a second, non-robot project: an electronics enclosure choosing a fan, a power supply and screws sold in packs (`tests/explore_fixtures.py`). ## What it does, and what it does not | Piece | Module | What it gives you | |---|---|---| | Catalogue | `cadagent.explore.catalogue` | Part lists (JSON or YAML) with specs, prices with the date checked, sources and confidence flags; loading and per-item checks | | Designs | `cadagent.explore.design` | One catalogue part per slot (or an inline custom part) plus free-form parameters; `extends` for variants; the bill of materials with cost and mass totals | | Evaluator | `cadagent.explore.evaluate` | Runs the project's evaluator (a Python function, or a command in any language that speaks JSON) and turns its output into metrics, checks and extra BOM lines | | Comparison | `cadagent.explore.compare`, `page` | Objectives and constraints, Pareto ranking, one-part swaps, `comparison.json`, a standalone HTML page and design cards for `cad-agent site` | | Search | `cadagent.explore.optimise` | Tries many designs: random and grid sampling built in, NSGA-II and TPE through optuna (`pip install -e ".[explore]"`); leaderboard and resumable records | Not included, on purpose: - **Physics and product rules.** Torque margins, airflow, thermal rise, voltage compatibility: all in the project's evaluator. cad-agent knows prices, masses and quantities, and nothing about what a part does. - **An interactive part picker in the browser** (a guided, choose-your-own-adventure designer). Live numbers in the browser need the evaluator to run there, so it would be a second copy of the project's model. A project that wants one can write its evaluator in JavaScript and use the same file from cad-agent as a command evaluator (`{command: [node, tools/evaluate.mjs]}`), so the page and `cad-agent compare` agree. The static comparison page and `--swaps` cover "what if I picked this part instead" without a second model. - **Fetching prices, specs or product images.** Catalogue numbers are entered from datasheets and shops with their sources and dates; nothing scrapes vendor pages, and vendor images and CAD files are not stored. ## Configuration Everything lives under `cad: explore:` in `cad-agent.yaml`; every key is optional. ```yaml cad: name: my-robot explore: catalogue: ["catalogue/*.json", "catalogue/*.yaml"] categories: [wheel_motor, battery, compute] # when set, any other category is an error currency: usd # prices are read from price_usd (default usd) max_price_age_days: 90 # older price_checked dates are warnings; null: no check slots: wheel_motor: {category: wheel_motor, qty: 2, required: true, group: Drive} battery: {category: battery, required: true, group: Power} compute: {category: compute, choices: [sbc-8gb, edge-ai-module]} # only these parts fit lidar: {category: sensor, where: {specs.type: lidar}} # parts whose fields match designs: ["designs/*.json"] # the default evaluator: tools/evaluate.py:evaluate # or {command: [node, tools/evaluate.mjs], timeout_s: 120} objectives: [{metric: cost_usd, goal: min}, {metric: runtime_min, goal: max}] constraints: [{metric: top_speed_kmh, min: 4}] metrics: {runtime_min: {label: runtime, unit: min, digits: 0}} # display only card_metrics: [cost_usd, mass_kg, runtime_min] # for the site's design cards out: exports/explore # the default search: {...} # see Search ``` | Key | Default | Meaning | |---|---|---| | `catalogue` | none (required) | globs or paths of catalogue files, relative to the project root or absolute (`**` works); a pattern that matches nothing is an error | | `categories` | any | allowed categories | | `currency` | `usd` | three-letter code; prices are read from `price_` | | `max_price_age_days` | `90` | warn about prices checked longer ago; `null` turns it off | | `slots` | none | per slot: `category` (parts must match; swaps and searches draw from it), `choices` (only these ids fit), `where` (field conditions: `{specs.type: lidar}`; a list means one of, `null` means the field is absent), `qty` (default quantity), `required`, `group` (BOM subtotal), `label` | | `designs` | `designs/*.json` | design files | | `evaluator` | none | `file.py:function`, `package.module:function`, or `{command: [...], timeout_s}` | | `objectives` | `cost_` and `mass_kg`, both `min` | what "better" means; Pareto ranking uses all of them | | `constraints` | none | `{metric, min, max}`; a design that misses one, or lacks the metric, is infeasible | | `metrics` | from the key names | display `label`, `unit` and `digits` per metric | | `card_metrics` | the first six columns | metrics on the site's design cards | | `out` | `exports/explore` | where the comparison and the search write | | `search` | none | the optimiser | Units come from the key names, as everywhere in cad-agent: `runtime_min` is in minutes, `top_speed_kmh` in km/h, `catch_lean_deg` in degrees, `cost_usd` in US dollars. ## Catalogue A catalogue file is a list of items, `{"items": [...]}` with optional `"defaults"` merged under every item (a category for the whole file, a shared source), or `{"items": {"": {...}}}` keyed by id. JSON and YAML both work; an unquoted YAML date (`price_checked: 2026-10-26`) is read as the text `"2026-10-26"`, in designs too. ```json { "id": "hub-165-36v", "category": "wheel_motor", "name": "165 mm hub motor, 36 V", "price_usd": 95.0, "price_checked": "2026-10-26", "pack_qty": 1, "mass_g": 1900, "specs": {"tyre_diameter_mm": 165, "peak_torque_nm": 8.0, "voltage_range_v": [24, 48]}, "confidence": "datasheet", "estimates": ["specs.peak_torque_nm"], "sources": ["https://example.com/datasheet.pdf"], "buy": [{"seller": "Example shop", "url": "https://example.com/p/165", "in_stock": true}], "preferred": true, "notes": "Anything that matters when choosing it." } ``` | Field | Rule | |---|---| | `id` | unique across the catalogue; letters, digits, `.` `_` `-` `+` | | `category` | required; a slot takes one category | | `name` | or `vendor` + `model`, or `description`; falls back to the id | | `price_` | one pack before shipping and tax, a number ≥ 0 or `null` (no public price); needs `price_checked` (`YYYY-MM-DD`) | | `pack_qty` | pieces per pack (screws, inserts); a BOM line buys whole packs | | `mass_g` | one piece; `specs.mass_g` is read too (they must agree) | | `specs` | everything else the evaluator reads, units in the key names | | `confidence` | `measured`, `datasheet`, `product_page`, `seller_listing` or `estimate` | | `estimates` | fields that are estimates even when the rest of the item is not (`mass_g`, `specs.`) | | `sources`, `buy` | where the numbers come from; an item with neither is flagged | | `preferred` | listed first in its category | Unknown values are `null`, never guessed. A price in another currency (`price_eur` in a `usd` project) is not converted: convert it and note the rate in `price_note`. Every other field is kept and passed to the evaluator. In YAML, dates (`price_checked: 2026-10-26`) are read as ISO text, quoted or not. `cad-agent catalogue` reports **errors** (a wrong or missing id or category, a price without a date, a date in the future, a negative price, a number that is not finite (NaN or infinity: use `null` for an unknown value), a value JSON cannot carry, disagreeing masses, an unknown confidence, slot `choices` that are not in the catalogue) and **warnings** (no price, no mass, no source, an old price, a source or buy link that is not `http(s)`, which pages show as text rather than a link, a required or narrowed slot that no part fits), then checks every design against the catalogue. It exits 1 when there is an error. A design that uses a part with a non-finite number is an `error`. ## Designs ```json { "name": "Long range", "extends": "standard.json", "parts": { "battery": "pack-13s-10ah", "leg_actuator": {"part": "qdd-17nm", "qty": 4}, "wiring": {"custom": {"name": "Wiring allowance", "category": "hardware", "price_usd": 45, "price_checked": "2026-10-26", "mass_g": 400, "confidence": "estimate"}}, "camera": null }, "params": {"leg": {"length_mm": 160}}, "notes": "Why this variant exists." } ``` - A slot holds a catalogue id, `{"part": id, "qty": n}`, an inline `{"custom": {...}}` part (checked like a catalogue item), or `null`. The quantity defaults to the slot's `qty`, else 1. One part per slot: use more slots for more parts. - `extends` starts from another design (relative to this file): mappings merge key by key, so `{"leg_actuator": {"qty": 2}}` changes only the quantity (a parent's bare id counts as `{"part": id}`); in a slot, `part` and `custom` replace each other and the quantity stays; anything else replaces; `null` empties a slot. `name` is not inherited and defaults to the file name. - `params` is free-form for the evaluator. - An unknown part, a part that does not fit the slot (category, `choices`, `where`), a bad quantity or an empty required slot makes the design an `error`; a slot that is not configured is a warning (a typo shows). - Narrow a slot when its category is broad. In a real catalogue "compute" may hold main computers and microcontrollers alike; without `choices` or `where`, a swap would happily put a CAN transceiver where the main computer goes, and only the evaluator's checks could object. The **bill of materials** has a line per filled slot plus the evaluator's own lines, each with quantity, packs, unit and line price, mass, confidence, the date the price was checked, a source link and stock. The totals become two metrics every design has: `cost_` and `mass_kg`. A line without a price or mass is left out of that total, which is then marked **incomplete**, shown as `≥` (a lower bound: the true value is at least that) and lists what it leaves out in `missing`; a total that includes an estimate is marked **est.**. ## Evaluator The evaluator is the project's model. In Python, `evaluator: tools/evaluate.py:evaluate` (or `package.module:function`): ```python def evaluate(candidate): # cadagent.explore.design.Candidate motor = candidate.part("wheel_motor") # the catalogue item (a dict), or None torque = candidate.spec("wheel_motor", "peak_torque_nm") wheels = candidate.qty("wheel_motor") leg_mm = candidate.param("leg.length_mm", 160) # dotted paths into params mass = candidate.bom["totals"]["mass_kg"] # the catalogue parts (not your own lines) return { "metrics": {"top_speed_kmh": 14.9, "runtime_min": {"value": 413, "estimate": True}}, "checks": [{"id": "volts", "level": "fail", "text": "The battery is above the motor's 48 V."}], "lines": [{"name": "Printed parts", "qty": 1, "price_usd": 32, "mass_g": 1600, "estimate": True}], "summary": "One line for the comparison.", "data": {}, # anything else to keep in comparison.json } ``` Every key is optional; an unknown key is reported, not fatal. A metric is a number (or true/false, text, `null`) or `{"value", "unit", "label", "digits", "estimate", "incomplete"}`; a value that is not finite becomes `null` and makes the design an `error`. Check levels are `ok`, `warn` and `fail`; any other level counts as `fail`. `lines` count in the cost and mass. A metric with the same name as a BOM metric replaces it (`mass_kg` from CAD, say). NumPy scalars are read as plain numbers. The evaluator's folder is appended to `sys.path`, so it can import its neighbours without shadowing a library. Each part the evaluator gets is its own copy; `candidate.catalogue` is shared, so read it only. In any language, `evaluator: {command: [node, tools/evaluate.mjs], timeout_s: 120}`: the request arrives as JSON on stdin (`{id, name, file, design, params, currency, parts: {slot: {item, qty, custom}}, bom}`), the result above is printed as JSON on stdout (log lines before it are fine; the last line is tried too). In the argv `{design}` becomes a temporary JSON file with the resolved design (in a private temporary folder), `{id}` the design's id as a plain file name for a folder of per-design outputs (letters, digits and `. _ + -` only, such as `trial-00007`; an id with other characters, like `designs/a/std`, has them replaced and gets a short hash) and `{python}` the project's python. It runs in the project root with `CAD_AGENT_PROJECT` set, in its own process group: a timeout kills the evaluator and everything it started, anything it leaves running when it exits is killed too, and Ctrl-C stops them. An exception, a non-zero exit, a timeout, output that is not JSON or a number that is not finite (NaN or infinity in `metrics`, `lines` or `data`) makes that one design an `error` with the reason; the other designs are still evaluated, and the BOM's cost and mass are still shown. ## Comparison `cad-agent compare [DESIGN ...] [--swaps] [--workers N] [--json] [--open]` evaluates the configured designs (or the files given, relative to the project root) and writes, under `out`: | File | What | |---|---| | `comparison.json` | every design in leaderboard order with its metrics, checks, BOM, rank, and swaps | | `compare.html` | one standalone page (light and dark, opens from `file://`): a trade-off chart of the first two objectives, the sortable leaderboard, and per design its checks, BOM, parameters and swaps | | `designs.json` | design cards for the project site: set `site: designs: exports/explore/designs.json` | `cad-agent site --serve` adds the comparison page (and the search leaderboard) to the site's top bar when they are in the default `exports/explore`. A design is **feasible** when its verdict is `ok` or `warn`: no failed check, every constraint met, and a number for every objective. Feasible designs are sorted into **Pareto fronts**: rank 1 is the front, where no other feasible design is at least as good on every objective and better on one; rank 2 is the front once rank 1 is removed, and so on. Infeasible designs follow, fewest failures first. **Incomplete totals are lower bounds, never savings.** A cost of `≥ 1,240` (a part without a price) is never counted as better than a value it might exceed: a design does not beat another on that objective unless it certainly does. Two totals that leave out exactly the same lines (the same unpriced camera in both) compare by what they show, since their difference is known. A rank that rests on a lower bound is marked `≥` on the page. A constraint that an incomplete total might break (a maximum), or does not reach yet (a minimum), cannot be confirmed: that is a warning, not a pass. `--swaps` evaluates every one-part swap of every design (each other catalogue part of the slot's category) and shows, per swap, the change in each objective, whether it is still feasible, and whether it beats the design on every objective; a `?` marks a change that cannot be told because one of the totals leaves out a part without a value. With a large catalogue that is many evaluations (one process each for a command evaluator): `--workers N` runs N at a time. Exit codes: 0 when every design was evaluated (whatever its verdict), 1 when a design could not be evaluated or the catalogue failed to load, 2 when the configuration or the evaluator is unusable. ## Search ```yaml search: base: designs/standard.json # every trial starts from this design sampler: nsga2 # random | grid (built in) | nsga2 | tpe (optuna) trials: 200 # per run; grid stops when every combination is done workers: 4 # evaluations at the same time seed: 1 population: 24 # nsga2 space: - {slot: wheel_motor} # every catalogue part of the slot's category - {slot: camera, choices: [depth-camera, null]} # or these; null leaves the slot empty - {param: leg.length_mm, low: 120, high: 200, step: 20} - {param: frame.material, choices: [pla, petg]} ``` `cad-agent optimise [--trials N] [--workers N] [--sampler S] [--seed N] [--fresh]` builds each trial from the base design, writes it to `out/search/trials/trial-00007.json`, evaluates it exactly as `compare` would, and writes `out/search/trials.jsonl` (one record per trial, with its full evaluation) and `leaderboard.json` + `leaderboard.html` (refreshed while it runs, at most a tenth of the time; the JSON keeps metrics, checks and BOM totals). With random, nsga2 and tpe the first trial of a new search is the base design itself, when the space can express it; grid goes through the combinations in order. - **random** and **grid** need nothing installed. Grid tries every combination once (ranges need a `step`). - **nsga2** (multi-objective genetic search) and **tpe** use optuna (3.4 and later). Failed checks, unmet constraints, and minimised totals that leave out more parts than the base design's are passed as optuna constraints, so infeasible trials still guide the search and it cannot drift towards parts without a price. A trial that could not be evaluated is marked failed. The study is kept in `out/search/study.db`; its bookkeeping costs a few tens of milliseconds per trial, which matters only when the evaluator itself takes milliseconds. - A combination that was already evaluated is not evaluated again, including one proposed while it is still running; the summary counts evaluations and repeats separately. - Running again continues: the numbering, the records and the optuna study carry on, and the sampler gets a new seed so it does not replay its first proposals. `out/search/search.json` records what the trials were computed against (objectives, constraints, the space with its choices, slot settings, the base design, the evaluator and its file, the catalogue parts in play); if any of them changed, the run stops and says what, and `--fresh` starts over. - Ctrl-C stops the command evaluators that are running (with whatever they started), writes the leaderboard, and says how many Python evaluations it is still waiting for; a second Ctrl-C stops without them. A Python evaluator cannot be stopped from outside in the middle of a call. - With `workers` above 1, Python evaluators run in threads: they must be thread-safe, and they only run in parallel where they wait on something else (a subprocess, a solver that releases the GIL). For CPU-heavy work use a command evaluator, so each trial is its own process. Exit code 0 when the run finished with at least one feasible trial, 1 otherwise. ## Python API ```python from cadagent.explore import (explore_config, load_configured, check_catalogue, load_design, resolve, bom, load_evaluator, evaluate_design, compare_designs, comparison, write_comparison, swaps) from cadagent.explore.optimise import run_search, search_config ``` ## Limits - A comparison ranks designs on paper. It is as good as the catalogue's numbers (with their sources, dates and confidence) and the evaluator's model. Estimates are flagged and incomplete totals are lower bounds, but nothing here is measured: weigh, test and price the real parts before deciding. - The search finds the best trade-offs of the model, not of the product. Check a winning trial with CAD, FEA and a prototype before building on it. - A Pareto front with many objectives can be large; add constraints, or fewer objectives, to narrow it. --- Source: https://cad-agent.valpatel.com/docs/file-viewer.md # Viewing original CAD files The standalone `webkit/file-viewer.html` opens original STL, STEP/STP and core 3MF files. It parses the requested file itself; it never substitutes a similarly named STL or an assembly manifest. The download link retains the original URL and bytes. ```html View bracket ``` Files and the optional back link must be on the current origin. Deploy the entire webkit directory, including its vendor assets. No CDN, upload service or application server is required. Serve JavaScript modules and WASM with their correct MIME types. The runtime WASM is 7,604,031 bytes (below Cloudflare's 25 MB asset limit). [Dependency notices](https://github.com/Valpatel/cad-agent/blob/dev/src/cadagent/webkit/vendor/FILE-VIEWER-NOTICES.md) include exact source revisions, licenses, hashes, build instructions and library replacement instructions. Keep these notices with copies of the viewer. ## Behavior and limits - Mouse drag orbits; touch uses the standard OrbitControls pan/pinch gestures. Wheel movement follows pixel deltas with easing: a 120px notch changes radius by about 1.57%, rather than jumping through large zoom steps. - Fit preserves the viewing direction. Isometric, Front, Side and Top select a standard view. The model is fitted into the actual canvas, outside the header and controls. All controls have at least 44px touch targets. - STEP uses pinned occt-import-js 0.0.23 in a dedicated worker. Open CASCADE tessellates the requested STEP's surfaces with millimeter output, `bounding_box_ratio` linear deflection 0.001 and angular deflection 0.2 radians. This is a visual tessellation, not a manufacturing or topology validation. - 3MF uses the official Three r160 loader, with declared units explicitly applied. Build/component transforms are baked into geometry, including reflected-instance winding. Core geometry, material colors and vertex colors are supported. Texture images are omitted with a visible limitation; no image load is started in the worker. Required extensions and mixed-unit model documents are rejected with an explanation instead of silently approximated. These limits do not alter downloads. - STL has no unit metadata. Millimeters are the default assumption, shown in file details; the unit selector can reinterpret inches, feet, centimeters, meters or microns. STL's exported orientation can differ from the same design's STEP. - Geometry packets are checked for nonempty finite vertices, valid triangle indices, nondegenerate surfaces, valid normals/colors/material groups and bounded triangle counts. CAD Z-up becomes stage Y-up, with mm converted to meters. The display is centered horizontally and placed on the floor without altering the original file or internal relative placement. - Preview limits: 128 MiB downloaded bytes, 128 MiB expanded ZIP entries, 10,000 ZIP entries, 2 million triangles; ordinary single-volume ZIP only (no ZIP64). Downloads have a 30-second deadline including body reading; parsing has a 120-second deadline. Cancel terminates the worker, including active WASM CPU work. - Status names actual phases rather than fabricated percentages. HTTP, parse, renderer or worker failures expose Retry. An initial entry-module/renderer failure offers a page reload before any usable scene exists. Later file retries keep the previous scene alive and name that retained model while the candidate is loading or failed. Request generations prevent late responses from replacing a newer file. ## Embedding Link `file-viewer.css` and provide the existing Three import map before importing: ```js import {createFileViewer} from './kit/file-viewer.js'; const viewer = createFileViewer(host, { file: '/exports/bracket.step', back: '/build.html', onState({state, message, info}) { /* loading, ready, error, cancelled, empty */ } }); await viewer.load('/exports/other.3mf'); viewer.fit('top'); // fit, iso, front, side, top viewer.cancel(); await viewer.retry(); viewer.dispose(); ``` ## Product themes The default remains dark. Set `theme: 'light'`, `'dark'` or `'auto'` when mounting; the standalone page also accepts `?theme=light` (or `dark` / `auto`). Auto follows the OS preference while mounted. `viewer.setTheme(...)` changes appearance without reloading the file, resetting the camera or changing the original model materials. For a product palette, pass an object: ```js const viewer = createFileViewer(host, { file: '/exports/bracket.step', theme: {mode: 'light', colors: {accent: '#126d58', surface: '#fffaf2'}, fontFamily: 'Inter, system-ui, sans-serif'} }); ``` Color values are six-digit sRGB hex. Supported keys are `background`, `canvas`, `surface`, `text`, `muted`, `border`, `control`, `controlHover`, `controlText`, `focus`, `error`, `accent`, `grid` and `glow`. Supply fonts locally in the consumer; the viewer does not fetch a font or stylesheet from a remote service. Invalid options throw before replacing the active appearance. The same values are CSS custom properties: `--file-background`, `--file-canvas`, `--file-surface`, `--file-text`, `--file-muted`, `--file-border`, `--file-control`, `--file-control-hover`, `--file-control-text`, `--file-focus`, `--file-error`, `--file-accent`, `--file-grid`, `--file-glow` and `--file-font`. Override them after the default stylesheet on `.file-viewer[data-file-theme=light]` (or `dark`). Set matching root tokens and `data-file-theme` on the standalone HTML element to theme its loading/error fallback too. Stage canvas/accent/grid/glow colors are read from these tokens whenever `setTheme` runs; use six-digit hex for those four CSS tokens. Other CSS tokens accept ordinary CSS color values. Consumer palettes must retain readable contrast; colors do not imply a manufacturing-readiness assessment. The reusable stage exposes `setPalette({accent, grid, glow, background})` and the equivalent initial `palette` option. Existing stage callers keep their original palette when neither is supplied. Palette changes preserve camera, controls and model materials. `stage`, `root`, current `model` and `info` are exposed for inspection. `info` includes format, original source URL/name/byte length, triangle count, source bounds in millimeters, unit interpretation and warnings. A canceled/stale load returns false; a usable replacement returns true. `dispose()` stops rendering, aborts work, terminates parsing and releases model/WebGL resources. The standalone page exposes this API as `window.__fileViewer` and sets `body.dataset.ready='1'` after its first usable frame. `?view=iso&ui=0` is available for a clean automated capture. ## Reproducing validation ```sh npm run test:webkit-unit node tests/webkit-file-viewer-browser.mjs PLAYWRIGHT_BROWSER=webkit node tests/webkit-file-viewer-browser.mjs ``` The fixtures use an independently specified 10 × 20 × 30 mm box in all three formats, a declared-inch 3MF, and translated/reflected 3MF instances. Browser checks cover original download identity, malformed bytes/indexes/STEP, unsupported required extensions, texture limitations, retained-scene retry, stale cancellation, bounded worker stalls, import failure recovery, actual projected bounds on 320px portrait and short landscape, small wheel deltas, orbit and standard views. Screenshots and JSON receipts default to `/tmp/cad-agent-file-viewer`. `tests/webkit-file-corpus.mjs` runs the exact shipped worker modules through a small test-only Node adapter, without a GPU. Its input is a JSON object with `files:[{path,sha256}]`; it verifies each original's hash before parsing and records per-file geometry/bounds/triangles, timing, warnings and failures. Run a large CAD corpus detached on an available compute host according to the `cad-remote-runs` skill, and record source revision, manifest hash, log, result path and done marker in the tracking issue. Do not infer that every model renders well from a parser-only pass; retain representative browser and phone screenshot checks separately. Initial development evidence (2026-10-29): actual product barrel originals loaded as 4,744 STL triangles, 2,888 STEP triangles and 4,632 3MF triangles, each from its own bytes. STEP and print orientation differ; no sibling was substituted. The published corpus then passed 495/495 parser checks on an isolated GB10 compute worker (Node 22.23.4), with no failures or warnings: 288 STL, 188 STEP and 19 3MF, 97,454,120 input bytes. The 27 tracked vendor reference STLs outside published roots were excluded. Total parser time was 97.178 seconds; the largest/slowest input was `exports/leg/leg_stand.step` (4,842,223 bytes, 120,166 triangles, 4.067 seconds). The exact [manifest](https://github.com/Valpatel/cad-agent/blob/dev/docs/validation/file-viewer-corpus-manifest.json) and [per-file results](https://github.com/Valpatel/cad-agent/blob/dev/docs/validation/file-viewer-corpus-results.json) retain source hashes, parser revision and limits. The manifest SHA256 is `9fd0962b2e2d2eeb04883de8bdd92390f291ebf98bc3753ad29a220b6eced5ea`. These are parser-only results. Separately, the Chromium and WebKit browser suites exercise rendering, measured phone/landscape framing, keyboard retry, stalled-body timeouts, main-thread responsiveness during busy parsing and native worker cancellation. Chromium also verifies native two-touch pinch. Both browsers render the known-dimension STEP/STL/3MF fixtures and preserve exact original download URLs. An independent reviewer additionally checked outward winding/signed volume for reflected 3MF instances and the STEP fixture, and verified the original download hash. The final [validation summary](https://github.com/Valpatel/cad-agent/blob/dev/docs/validation/file-viewer-validation.json) records 26 Chromium and 25 WebKit browser cases (the additional Chromium case uses native CDP two-touch pinch), 8 unit tests, 47 wheel-file checks and runtime source hashes. --- Source: https://cad-agent.valpatel.com/docs/gates.md # Integrity gates (`cadagent.gates`) Assembly-level checks computed from the CAD model, each written as one verdict file. They answer what per-part checks cannot: is this part touching the one it should be fastened to, is the ball inside the hopper, is this wall thick enough. The design behind them is [`design-process-plan.md`](https://cad-agent.valpatel.com/docs/design-process-plan.md) §4; the declarations they read come from the dossier ([`dossier.md`](https://cad-agent.valpatel.com/docs/dossier.md)). **What a gate does not do.** A gate checks the model against declared intent with stated tolerances. It does not prove that printed parts fit, move or hold: no tolerance stack, no print shrink, no deflection. Every verdict says `physical_fit_verified: false` and lists its assumptions. Status: the shared machinery (T3), the minimum-wall gate (T8), the connectivity gate (T4), the containment gate (T5), the fastener gate (T6), the motion gate (T7), the assembly-access gate (T9), the gate bench (T12), the dossier lint (T2, [`dossier.md`](https://cad-agent.valpatel.com/docs/dossier.md#lint-and-status)), the `cad-agent gates` command with its `check` and report rows (T14, [below](#running-the-gates-cad-agent-gates)) and the site's gates page with the connectivity graph (T15, [`dossier.md`](https://cad-agent.valpatel.com/docs/dossier.md#on-the-review-site)) are built. ## The assembly and the relation table ```python from cadagent.gates import Assembly, Tolerances, PairCache, relation_table, relate_pair, relate asm = Assembly.from_build(build_result) # from a dossier build (live or load_geometry): see below asm = Assembly() # or by hand: asm.add("base", base) # CadQuery / build123d shape or TopoDS_Shape asm.add("arm", arm, location=(40, 0, 12), body="arm", group="S2") asm.add("lid", lid, cq.Location(...), content_hash=recorded) # placement: Location, gp_Trsf, TopLoc, 3x4/4x4 rows asm.add_cavity("hopper_cavity", void, part="hopper") # a declared void: not a part, never in the pair table asm = Assembly({"base": base, "lid": lid}) # or a mapping of shapes already placed asm = Assembly.from_cadquery(cq_assembly) # parts named by path, bodies by top-level child table = relation_table(asm, Tolerances(), cache=PairCache(".cadagent-cache/gates")) table.get("lid", "base") # Relation, oriented as asked (see "Pairs the table did not examine") table.get("lid", "far", exact=True) # run the kernel for a pair not yet examined table.contacts() # touching + overlapping + contains: the contact-graph edges table.interferences() # overlapping + contains: shared volume table.of("lid") # every exact relation of one part, with it as `a` table.stats # parts, pairs, candidates, kernel_queries, cache_hits, seconds, states table.to_dict() # JSON-ready evidence, with the kernel version relate_pair(asm, "ball#3", "hopper_cavity", cache=cache) # one named pair (parts or cavities), cached relate(shape_a, shape_b) # two bare shapes, uncached asm.posed("arm", location) # a copy with one body (and its cavities) moved; static parts untouched ``` A multi-solid part whose solids share volume is refused by `Assembly.add` (fuse it first): the boolean kernel reports the common of such a compound with anything as done and empty. `solid_interference(shape)` lists the offending solid pairs. ### From a dossier build `Assembly.from_build(result)` takes a `cadagent.dossier` `BuildResult`, from the build itself or from `load_geometry("exports/dossier-geometry.json")` in another process: - parts and placed fasteners from `assembly_shapes(with_hashes=True)`, already in assembly coordinates, keyed as the build keys them (`"hopper"`, `"ball#3"`, `"screw_0"`), each with the **content hash recorded at build time**; a shape read back from BREP is never re-hashed, so the pair cache stays warm across processes; - `Part.kind` from `result.kind(key)` (`"part"` or `"fastener"`) and `group` = the part's section; - every cavity through `add_cavity`, with its recorded hash; - `asm.joints` and `asm.fastener_axes` copied from the result for the motion and fastener gates; - body labels from `moving_bodies(joints, fasteners, grounded)` (the rule is final; see the motion gate): a joint's `moving` part seeds its body, which grows transitively across `fixed` joints and fasteners (the fastener, what it clamps and what it threads into), never into a grounded part or a joint's `fixed` part. ### States Following build123d-mcp's `clearance` (Apache-2.0, vendored; semantics adapted, no code copied): | State | Meaning | Witness | |---|---|---| | `apart` | no shared volume, gap > `touch_mm` | `gap_mm`, closest points `point_a`, `point_b` | | `touching` | no shared volume, gap ≤ `touch_mm` | as apart, plus `contact_area_mm2` (0 for point or line contact, and for a gap inside `touch_mm`) | | `overlapping` | shared volume, neither inside the other | `overlap_mm3`, `a_outside_b_mm3`, `b_outside_a_mm3`, `overlap_centroid`, `overlap_box` | | `contains` | one item has at most `contain_mm3` outside the other | as overlapping, plus `contained` (`a_in_b`, `b_in_a`, `both`), `wall_mm` and its `inner_point`, `outer_point` | | `unknown` | the kernel failed, or its volumes disagreed (`error` says which) | a gate must fail on it | | `unchecked` | from `get()` only: a pair excluded by `only=` or `skip_pairs` whose boxes do not prove a gap | a gate must fail on it or ask `exact=True` | **`contains` between two parts is an interference.** A pin buried in a block is `contains`, not `overlapping`; `Relation.interferes` and `table.interferences()` cover both. Only against a declared cavity solid is `contains` the goal (the containment gate); between parts it needs an intended-overlap declaration like any other overlap. ### Tolerances (absolute) | Field | Default | Meaning | |---|---|---| | `touch_mm` | 0.01 | a gap at or below this, with no shared volume, is touching | | `overlap_mm3` | 0.001 | shared volume above this is an overlap (a 0.1 mm cube) | | `contain_mm3` | 0.001 | an item with no more than this outside the other is inside it; a ball resting on a cavity wall has exactly 0 outside | | `near_mm` | 1.0 | broad-phase margin: pairs whose boxes are within this get exact queries, so a 0.5 mm hover is measured | | `volume_check_mm3` | 0.01 | allowed disagreement in `vol = common + cut` (plus 1e-6 of the larger volume) before a pair is `unknown` | ### Method, fail-safe rules and cost AABB broad phase (`BRepBndLib.AddOptimal`, numpy over all pairs). For each candidate: `BRepAlgoAPI_Common` for the shared volume and `BRepAlgoAPI_Cut` for each item's volume outside the other, all non-destructive (OCCT otherwise raises the arguments' tolerances in place, changing the caller's shapes), then `BRepExtrema_DistShapeShape` for the gap when nothing is shared. OCP 7.9 binds no error report on these operations (`HasErrors` is absent; `DSFiller()` reports warnings but crashes the interpreter at exit), and `IsDone()` stays true on invalid input. So every answer is cross-checked: `vol(a) = common + cut(a, b)`, and the same for `b` when they overlap. A mismatch, an exception, a missing solid or a self-interfering compound gives `unknown`, never `touching`, `apart` or `contains`. Measured with `tools/gate_timing.py` on the synthetic 100-part assembly (4,950 pairs, 90 candidates), on a workstation at load average 11: broad phase 0.006 s, narrow phase 1.9 s cold, 0.03 s warm from the cache. The cross-check's extra `Cut` roughly doubled the cold narrow phase. Real parts with hundreds of faces cost 10–100× more per pair. ### Pairs the table did not examine `get(a, b)` answers `apart` (with `exact=False` and `gap_lower_bound_mm`) only when the boxes prove a gap over `touch_mm`; a pair the table skipped (`skip_pairs`, or outside `only=`) with touching boxes reads `unchecked`. ### The pair cache `PairCache(directory)` stores one JSON file per pair, keyed by the sha256 of both items' keys, the tolerances, the relation version and the OCP version (a kernel upgrade re-queries everything). An item's key is its content hash (`cadagent.evidence.shape_content_hash`, shared with the dossier: sha256 of the BREP text without triangulation, flag lines stripped) plus its placement matrix rounded to 1e-9. A rebuild that moves or changes one part re-queries only that part's pairs, across processes. Keep the cache out of `exports/`: the default is `.cadagent-cache/gates/`, which is gitignored; deleting it is always safe. ## Verdicts ```python from cadagent.gates import Check, verdict, write_verdict checks = [Check("wall:hopper", "fail", "thinnest 0.800 mm < 2.4 mm", parts=["hopper"], evidence={...})] data = verdict("wall", checks, assumptions=["rays along the inward normal ..."], limits={"tol_mm": 0.01}, exclusions=[{"parts": ["insert_0", "hopper"], "reason": "heat-set knurl"}]) write_verdict("exports/gates/wall.json", data) # unbound: `check` reads it as stale (see Inputs below) ``` The file (`schema: cadagent.gate/1`) has `pass`, `status` (`pass`/`warn`/`fail`), `counts`, `checks`, `findings` (the checks that are not pass), `exclusions`, `limits`, `assumptions` and `physical_fit_verified: false`. `pass` is false when any check fails **or when no check ran**. Warnings never fail the gate. At least one assumption is required, every exclusion needs a reason, and the file has no timestamp, so an unchanged run writes identical bytes. **Inputs.** `cad-agent gates` adds `inputs` to every verdict, so a verdict from an earlier build is never taken as current: ```python from cadagent.gates.verdict import bind_inputs, stale_reason, geometry_digest, record_hashes bind_inputs(data, record, record_file="exports/dossier-geometry.json", declarations=decl, options=opts) # data["inputs"] = {"geometry": {"file", "digest", "hashes": {key: content hash}}, "declarations": sha256, "options": sha256} stale_reason(data, record, declarations=decl, options=opts) # None, or why the verdict is stale ``` `geometry_digest(record)` is the sha256 of the geometry record's canonical JSON without the fields that say who built it or where: `actor`, `parameters`, `shapes_dir`, `out_dir` and `hash` at the top level, `declared_at` and `file` in each part, cavity, joint, fastener, instance and step (only those fields: a part named `file` is still digested). A rebuild that changes a shape, a flag, a joint, a fastener or a step changes it; the same geometry rebuilt by someone else does not. `hashes` lets the stale message name the shapes that changed. `toolkit` records the cad-agent version; it is not compared (an editable install keeps one version across code changes, and a release would void every verdict whether its gate changed or not), so rerun the gates after an upgrade. A verdict without `inputs` is stale. `cad-agent check` and the dossier lint (`verdict-stale`) compare the geometry, the gate's declarations and its options; `lint()` called without a project compares the geometry only. The gates' `out=` argument writes an unbound verdict: a gate run by hand, or a project's own gate writing into `exports/gates/`, binds it with `bind_inputs` first, or `check` reports it as stale. ## Minimum wall (`cadagent.gates.wall`) ```python from cadagent.gates.wall import check_wall, wall_gate, mesh_thickness, brep_thickness, brep_thickness_near check_wall("hopper", 2.4, mesh=shape_or_stl_or_trimesh, shape=brep) # -> Check wall_gate({"hopper": {"wall_mm": 2.4, "mesh": m, "shape": s}, ...}, out="exports/gates/wall.json") ``` Two samplers measure thickness by rays along the inward surface normal: - **mesh rays** from every triangle centroid (sub-sampled by area to `max_rays`, seeded), using trimesh with `rtree` or an exact numpy fallback. The mesh is repaired first (vertices within 1e-5 mm merged, degenerate and duplicate triangles dropped): CadQuery's tessellation leaves open seams at sphere poles, cone apexes and fillet corners; - **exact B-rep rays** from an n × n UV grid on every face, n ≈ √area / `spacing_mm` (2 mm), between `grid` (3) and `max_grid` (16), so a small face is probed whatever its area and a large curved one is not under-sampled. With both given, every thin mesh sample (up to 500, thinnest first) is **re-measured exactly** from the nearest B-rep point along its surface normal (`brep_thickness_near`); readings the B-rep does not confirm are chord or facet-normal error and are dismissed (`dismissed_as_chord_error` in the evidence). A mesh still open after repair is then a warning, with the B-rep rays as the evidence; without a B-rep it fails. A check fails when a sample (a confirmed one, with a B-rep) is thinner than `wall_mm - tol_mm` (default 0.01 mm), or when more than 1 % of rays find no exit. Evidence: per-sampler summaries (min, p1/p5/p50, thin samples and area) and up to ten witnesses with entry point, exit point and thickness. Limits. Walls met at a slant and regions between samples are not measured. A sharp edge (a cone's apex or rim, a chamfer meeting a face at an acute angle) reads thin near its tip, which is correct. A mesh alone reads curved walls thin by its chord error (0.03–0.09 mm seen on 2.4 mm shells tessellated at 0.05 mm), and on thick curved bodies a sliver triangle's tilted normal can exit through a neighbouring face (16.7 mm read on a 20 mm filleted block): pass the B-rep, or set `tol_mm` to cover it. The threshold is the part's own (the dossier's `material.wall_mm`). ## Containment (`cadagent.gates.containment`) ```python from cadagent.gates.containment import containment_gate, check_contains, Contains, contains_declarations containment_gate(asm, [{"container": "hopper", "cavity": "hopper_cavity", "what": "ball", "count": 8}], out="exports/gates/containment.json") containment_gate(asm, contains_declarations(dossier)) # every card's `contains:` entry containment_gate(asm, decls, poses={"home": {}, "turned": {"turret": cq.Location(...)}}) # per pose containment_gate(asm, decls, support="fail", down=(0, 0, -1), support_mm=0.05) containment_gate(asm, decls, meshes={"ball#8": "scan.stl"}) # mesh-only items: sample-only containment_gate(asm, decls, exclusions=[{"parts": ["ball#0", "hopper"], "reason": "..."}], cache=cache) containment_gate(asm, [{..., "closed_by": ["lid"]}]) # the cavity may reach into the lid's box check_contains(asm, decl, pose="turned") # -> [Check] for one declaration, one pose ``` The cavity must be a registered `@cavity` solid (`Assembly.add_cavity`); there are no derived cavities. Instances of `what` are the parts named `what` or `what#0`, `what#1`, ... Checks per declaration (ids `contains::...`, prefixed `/` when `poses` is given): | Check | Fails when | Evidence | |---|---|---| | `:inside` | more than `contain_mm3` of the instance is outside the cavity, or the kernel was unsure | `outside_mm3`, `outside_fraction`, `depth_mm` and the `furthest_outside_point` with its `nearest_cavity_point`; for a contained one its `clearance_mm` from the boundary | | `:wall` | the instance shares more than `overlap_mm3` with the container's solid (touching is the intended rest) | the relation, with the overlap volume and centroid | | `:interpenetration` | two instances share volume | every overlapping pair with `overlap_mm3` and its centroid | | `:count` | the instances found differ from `count` (or none is found) | declared, found (B-rep and mesh) | | `:support` | `support="fail"` and an instance rests on nothing; a warning with `"report"` (default); skipped with `"off"` | `unsupported`, `on_cavity`, `rests_on` | | `:cavity` | the cavity is not registered (`unknown-cavity`) or belongs to another part (`cavity-wrong-part`), the dossier lint's codes | `lint` | | `:cavity-fit` | the cavity shares volume with the container's material, or its box reaches outside the box of the container and its `closed_by` parts (by more than `touch_mm`) | the relation, `cavity_box`, `host_box`, `box_overshoot_mm` per axis | - **A cavity drawn too big** would pass contents that are not in the container: a ball beside the box, or stacked above its rim, touches no material, so the wall check cannot see it. The `cavity-fit` check looks at the cavity itself. It does not catch a cavity that stays within the container's box and outside its material (beside an L-shaped container): contents in that extra volume read inside. - **Tangent contact is inside.** A ball resting on a cavity wall has 0 mm³ outside and is `touching` the container. - **Contact probe.** OCCT's surface intersection can miss a shallow near-tangent crossing: a 4 mm sphere pushed 0.03–0.1 mm through a cone wall read 0 mm³ outside at 5 of 50 positions tried, a consistent common = volume, cut = 0 that the `vol = common + cut` cross-check cannot see. So every instance whose exact gap to the cavity boundary is within `touch_mm` is re-checked: points on its surface are classified against the cavity with `BRepClass3d_SolidClassifier`, which uses no surface intersection. The points are the tessellation nodes (`probe_deflection_mm`, 0.01) plus a UV grid of about `probe_spacing_mm` (0.5) on every face, trimmed to the face: BRepMesh puts no node inside a plane or along a cylinder's or cone's straight direction, so nodes alone are blind to a crossing in the middle of such a face. A point outside by more than `touch_mm` fails the instance with its depth (`kernel_missed_crossing`; the volume is not measured). Blind spots: a crossing whose footprint falls between samples (the grid is at most 64 × 64 per face, so coarser than the spacing on faces over 32 mm across), or no deeper than `touch_mm`. On a 4 mm ball against a cone wall, every crossing of 0.015 mm or more was caught at 16 angles; at 0.01 mm (= `touch_mm`) the boolean and the probe both missed 5 of 16. It costs about 0.3–0.5 s per touching ball; `probe=False` turns it off. **This limit applies to the relation table too** (a near-tangent `touching` between two parts can be a shallow overlap); only the containment gate probes for it. - **Furthest point outside.** The furthest of the vertices of a tessellation of the escaped piece (`cut(instance, cavity)`), then climbed to the piece's extreme point along the outward direction: exact for a ball through a flat or cylindrical wall, a lower bound in general. - **Support.** An instance rests on X when moving it `support_mm` along `down` pushes more than `overlap_mm3` *further* out of the cavity or into another instance than before the move, so volume already outside or overlapping is not support (boolean volumes; for a pair sharing no volume and within `support_mm`, the surface probe as fallback, counting only points the move takes across); supported means resting on the cavity, or on a supported instance, followed down the chain. A static contact test along one direction, not a settling or stability simulation; other parts (a board in the hopper) do not count. - **Poses.** `poses={name: {body: location}}` applies `Assembly.posed` per body. A container that turns while its contents are modelled in fixed positions (the incident) fails in the pose where they part. - **Mesh-only items** (`meshes={key: trimesh | STL path | shape}`) are **sample-only**: points uniform in the mesh's box are kept when inside it (generalised winding number, no `rtree` needed; computed in chunks sized to keep about 128 MB of temporaries whatever the triangle count, so time, not memory, grows with the mesh) and classified against the exact cavity and container solids, so the outside and wall volumes are Monte Carlo estimates with a standard error (`mesh_samples`, 4000, seeded); the mesh's vertices give the furthest-outside witness. Mesh items get no interpenetration or support check. - **Cost.** The 8-ball gate bench: about 2 s cold (the four balls on the floor are probed); 40 balls in a cone: about 9 s cold on a loaded workstation. Every query goes through `cache` (`PairCache`), keyed on the content hashes and placements, so an unchanged rerun reads the cache. ## Connectivity and floating parts (`cadagent.gates.connectivity`) Does every part hang on something, and is every declared connection really there? ```python from cadagent.gates.connectivity import Declarations, connectivity_gate, connectivity_from_build, analyse, to_dot decl = Declarations.from_build(result, dossier, extra) # build record + YAML dossier + plain-data extras v = connectivity_gate(Assembly.from_build(result), decl, cache=cache, out="exports/gates/connectivity.json") v = connectivity_from_build(result, dossier, out=...) # the same in one call decl = Declarations(grounded=["base"], free={"ball": "loose in the hopper"}, # or by hand interfaces=[{"id": "I7", "between": ["hopper", "base"], "kind": "fastened", "clearance": 0.2}], joints={"I9": {"kind": "revolute", "moving": "flap", "fixed": "hopper"}}, # None: assembly.joints fasteners={"m3": {"clamps": "lid", "into": "insert_0", # axes and length let a fastener "axes": [((17.4, 22.4, 47), (0, 0, -1))], "length_mm": 8}}, # without a solid count contains=[{"what": "ball", "cavity": "hopper_cavity"}], # container: the cavity's part intended_overlaps=[{"parts": ["hopper", "insert_0"], "reason": "heat-set"}], strict_interfaces=False, touch_mm=None, max_joint_gap_mm=1.0) ``` `from_build` joins: `grounded` and `free` flags, fastener axes and lengths from the build record; `gates:` (`grounded`, `free_parts`, `intended_overlaps`, `ignore_pairs`, `strict_interfaces`, `touch_tol_mm`) and every interface and card `contains` from the dossier; the same keys from `extra` (the bench passes its plain-data declarations there). A `contains` with `free: true` makes its items allowed-free with that as the reason. Interfaces declared twice are merged when equal; differing ones keep the first and warn (`duplicate:interface:`). **The graph.** Nodes are the assembly's keys (instances and placed fasteners). Edges: | Edge | From | |---|---| | `contact` | the relation table: `touching`, `overlapping` or `contains` between two parts (`ignore_pairs` dropped) | | `contained` | an instance of a declared `contains` that lies inside its registered cavity (`relate_pair`), touching or not | | `fastener` | a fastener with no modelled solid whose axis, from the seat over `length_mm`, comes within `touch_mm` of what it clamps and of what it threads into (`fastener_reach`) | | `joint` | every joint instance pair, with the measured gap and whether it `holds` | **Bodies.** Each non-fixed joint's `moving` instances seed a moving body, which grows transitively across `fixed` joints and fastener declarations (the fastener, what it clamps, what it threads into). A link from a body into a grounded part, a joint's `fixed` part or another body fails `bodies:` (a moving part bolted to what it moves against). Parts labelled into a moving body by the assembly keep that label; everything else is the static frame. Group ids are `static` and `body:`. **Rigid edges.** An edge holds something up when both ends are in one group, neither end is an allowed-free part, and it is not a joint (a `fixed` joint counts when it holds). A contact across groups (the arm on its stop) or with a loose part is kept in the graph but holds nothing up: a static part touching only a moving body floats (it stays behind when the body moves), and so does a part resting only on a loose ball. **Joints hold when measured.** A non-fixed joint holds a moving instance when the instance is within the declared `clearance` of an interface with the joint's id, else within `max_joint_gap_mm` (1 mm), of a `fixed` instance; a `fixed` joint needs contact or a declared clearance. Every instance is checked. A component is grounded when it holds a grounded part, or a held moving instance whose `fixed` part is in a grounded component (chains followed). **Checks** (ids in the verdict): | Id | Fails when | |---|---| | `ground` | no grounded part, a grounded name not a part in the assembly (sections are refused), or a grounded part in a moving body. `ground:` warns when a grounded part has several instances (each is ground) | | `connect:static`, `connect:body:` | a component of the group is not grounded. One check per group; each island has its witness: the nearest grounded part (same group first), the exact gap, both closest points, what it touches across bodies or among loose parts, and the joints that do not hold it | | `joint:` | a party is not a part; a moving instance not within its limit of a `fixed` instance; the `fixed` part in the moving body itself or not grounded; a `fixed` joint across two bodies | | `bodies:`, `body-label:` | a declared rigid link ties a moving body to ground, a joint's fixed part or another body (fail); a part's given moving-body label differs from the derived one (warn) | | `interface:` | not two parties; a mechanism kind (`revolute`, `slider`, `cylindrical`, `planar`, `ball`) with no joint of that id, or one between other parts; any other geometric kind with no contact between its parties and no gap within `clearance` (`gap` needs a `clearance`). Parties are part names, instance keys or section ids; one contact between any instances realises it | | `fastener:` | a modelled solid that does not touch what it clamps or what it threads into; a fastener without a solid whose measured axis misses either. Without a solid and without axes or length it warns and joins nothing | | `contact:\|` | a contact no interface, joint (its parts or `intended_contacts`), fastener, `contains` or intended overlap declares, and neither part free: a **warning**, a failure with `strict_interfaces` | | `free:` | an allowed-free part with no reason (fail; it is then checked like any part) or one naming nothing (warning) | | `relation:\|` | the kernel could not decide the pair (`unknown`) | | `tolerances` | (warning) the declarations' `touch_mm` differs from the given table's, which is used | An island made only of allowed-free parts is an `exclusion` with their reasons. `not_checked` lists what the gate could not check: non-geometric interfaces (`electrical`, `thermal`, `fluid`, `other`), fasteners without a solid that could not be measured, `contains` whose cavity is not registered, interfaces whose pairs are all ignored, and pairs the table skipped. The verdict's other extra keys: `graph` (`nodes` with `group`, `body_given`, component, `grounded`, `free`, `held`, `floating`; `edges` with `rigid` and `declared_by`; `components` with `grounded` and `via_joint`; `groups`; `missing`: unrealised declarations), `floating`, `undeclared_contacts`, `unrealised` and `relation_stats`. `to_dot(verdict)` / `write_dot(verdict, path)` give a Graphviz graph (one cluster per group, floating parts red, joints that do not hold and unrealised declarations red); `connectivity_gate(out=...)` writes it beside the verdict as `.dot`. The site's gates page draws it as SVG (`cadagent.dotsvg.dot_to_svg`: Graphviz when installed, else a built-in layout); `dot -Tsvg` renders it by hand. The toolkit does not need Graphviz. A relation table passed in (`table=`) is read, never changed: witness and joint queries go through `relate_pair` and the cache. `analyse(nodes, relations, declarations, contained=..., measure=..., lower_bounds=..., fastener_reach=...)` is the kernel-free core: it runs on any relation rows, which is how the rules are tested in core CI. Limits. The graph is the model's geometry within the relation tolerances: contact in the model does not prove that printed parts meet, stay put or carry load. A joint within its allowed gap is trusted as a connection; the gap is reported. A fastener without a solid is judged by its axis line alone (no thread, no head seat). A multi-instance grounded part makes every instance ground. Body labels are derived from declarations, not from motion. Witness search spends at most three extra exact queries per island or interface on pairs the table did not measure; a joint measures every moving × fixed instance pair. **Where this differs from the plan (§4.2).** The plan's single graph becomes per-group graphs, so a cross-body or loose-part contact cannot hold a part up; items in a declared cavity count as held (`contained`); a joint is a connection only within a measured gap; a mechanism interface is realised by its joint, not by contact; contacts involving a free part are not reported as undeclared; the graph is exported as DOT for the site (T15) to render as SVG. ## Fasteners and stacks (`cadagent.gates.fasteners`) Does every screw seat, clamp, engage and stop where the model says, and can its tool reach it when it goes in? ```python from cadagent.gates.fasteners import (fastener_gate, fastener_gate_from_build, fasteners_from_build, FastenerSpec, FastenerOptions, evaluate, measure, table_markdown, tool_envelope) v = fastener_gate_from_build(result, dossier, extra, out="exports/gates/fasteners.json") # + fasteners.md beside it v = fastener_gate(asm, {"lid_screws": FastenerSpec(name="lid_screws", clamps="lid", into="insert", axes=[...], length_mm=8, diameter_mm=3, thread_start_mm=3, min_engagement_mm=1.5, tool="hex 2.5", stack=["lid", "gasket"], head_d_mm=5.5)}, steps=result.steps, tools={"hex 2.5": {"envelope_d": 6, "length": 60}}, options=FastenerOptions(tip_clearance_mm=0.5), cache=cache, intended_overlaps=[{"parts": ["screw", "foam"], "reason": "screw pierces the foam pad"}]) print(table_markdown(v)) # the lesson-04 table ``` `fasteners_from_build` reads the geometry record's `fasteners` (key, clamps, into, tool, the stack numbers, every axis and placed solid) and adds gate options from the dossier's `gates: fasteners:` and `extra["fasteners"]`, keyed by fastener name or catalogue key: `stack` (the clamped parts, head side first; default `[clamps]`), `head` (`flat` for a countersunk head), `head_d_mm`, `head_h_mm`, `pitch_mm`, `hole_clearance_mm`, `tip_clearance_mm`. Tools come from `gates: tools:` and `extra["tools"]` (`tool_envelope`): `{envelope_d, length}`; `{segments: [{d, length}, ...]}` (a driver's shaft then handle, nearest the head first); `start: head | seat` (a key in the socket, or a socket over the head); `offset_mm`; `swing: {radius, at, thickness, min_deg}` for an L key's arm, which needs a free arc of `min_deg` (60) in 12 sectors. **How it reads the model.** Everything is in the screw's own frame: `t` is the distance from the seat along the advance direction, and the head is at `t < 0`. Lines parallel to the axis are cut through the solids (`IntCurvesFace_ShapeIntersector` crossings, the spans between them classified with `BRepClass3d_SolidClassifier`): - the *annulus*: `lines` angles (8), each with three rings at 0.3, 0.55 and 0.8 of the way from the shank to the head rim, where the head and the clamped parts bear. The outer ring must bear at every angle (an edge or a pocket shows there); the inner rings may run through a chamfer at the hole, but material above the seat on any ring is a burial. The stack needs one clean ring per angle, so a chamfer or lead-in at one radius does not decide; - the *axis*; - the *thread probe*: 4 lines at 0.92 d/2 and 4 just outside d/2 (by 0.2 `hole_clearance_mm`, inside the smallest clearance hole); the set whose thread starts first wins, so a receiver modelled at its minor or at its nominal diameter is found (the inner set would meet only a nominal blind bore's bottom). A receiver whose only "thread" is where its material crosses the axis below a wider bore is reported as no thread modelled. Gaps up to 1.5 pitches (0.5 d without a pitch) are bridged, so thread crests and relief grooves read as one region; it is kept where every line agrees, and engagement is the sum of its stretches up to the tip; - *radial rays* at three depths and 8 angles in each clamped part below the seat, out to the head rim plus 1 mm, for its hole radius (a part with a hole wider than every ring is found on lines just outside the head). The placed screw solid gives the length and head (its box in the axis frame) and its overlaps (the relation table, cached; each overlap's extent along the axis from the common solid). Without a solid, the head is the declared `head_d_mm` x `head_h_mm`, or else 1.6 d x 1.0 d, and the length is the record's. **Checks** (ids `fastener::`; `fastener::declaration` for a fastener that cannot be placed): | Check | Fails when | |---|---| | `seat` | at some angle the first part under the head (on the outer ring) is not `clamps`, or stands more than `seat_tol_mm` (0.05) off the seat or above it (head buried); at some angle nothing, or only another part further below, is under the head (it overhangs an edge or a pocket); the head's volume overlaps any part (a pocket wall). For a `flat` head, the solid must touch `clamps` within `seat_tol_mm` (warns without a solid) | | `stack` | a `stack` part is missing under the head or out of order; an undeclared part sits inside the stack; a gap over `gap_tol_mm` (0.05) between two parts, or under the last one (nothing pulls it down) | | `holes` | a clamped part has material on the axis, a hole under `d + hole_clearance_mm` (0.1, diametral), or a hole no smaller than the head (pull-through); the receiver has material on the axis at the thread start (no pilot or tap hole); the screw's solid overlaps a clamped part below the seat, or the receiver above its thread | | `engagement` | no thread of `into` on the axis; the screw stops short of it; the axial overlap with the receiver's thread is under `min_engagement_mm` (else `min_engagement_d` x d, 0.8, labelled in the evidence) | | `tip` | a solid on the axis before the tip (past the stack: the host, another part, the receiver's own bottom); the next solid past the tip is closer than `tip_clearance_mm` (0.5, bottoming); the solid overlaps another part, or the receiver past its thread; the kernel could not decide a pair | | `length` | the placed solid's length differs from the record's by over `length_tol_mm` (0.05); the length is outside the window the stack allows (thread start + minimum engagement, up to the next solid on the axis less `tip_clearance_mm`); the record's `thread_start_mm` differs from the model by over `stack_tol_mm` (recompute the stack). Warns on a `thread_depth_mm` that differs from the model and on an uncommon stock length (the metric list for a metric d, the inch list otherwise, or `stock_lengths_mm`); the evidence lists the stock lengths that fit | | `tool` | no tool declared (on the fastener or its install step), no envelope for it, the envelope overlapping a part present at the install step (more than `overlap_mm3`, or undecided), or the swing's free arc under `min_deg`. Warns when steps exist but none installs the fastener (then it is checked against every part), and when a part fitted at a later step overlaps the envelope, so it must come off before the screw can be removed for service (`service_access`, on by default) | **Bodies and poses.** `poses={body: location}` runs every check and tool sweep with those bodies moved (`Assembly.posed`). A fastener moves with its body: its placed solid's, else its clamped part's, else its receiver's. From a dossier build these come from `relations.moving_bodies`, the closure over fixed joints and fastener `clamps`/`into` (final, shared with the connectivity and motion gates), so the screws that hold a moving part, and their axes, go with it. Each table row names its `body`, and `limits.poses` records the matrices. The assembly steps are taken in the pose given: a fastener driven with a flap turned to its service position is checked by posing the flap. **Install step and the parts present.** The step that installs a fastener is the first to list it by name in `fasteners`; failing that, the first to list its catalogue key and to fit or sit on a part it clamps or threads into; failing that, the first to list its key at all. Present at that step: the parts fitted at it or before; parts that no step fits, which are counted as present (conservative); and the solids of fasteners installed at it or before (or installed by no step). Without steps, everything counts as present. **Output.** The verdict (`cadagent.gate/1`) carries `table`, one lesson-04 row per instance: - the fastener, its stack and receiver; - the seat and thread-start points; - grip, measured thread start (and the record's), thread depth, engagement in mm and threads, the minimum; - length, whether it is a stock length, the window that fits, and the thread depth that length requires; - tip clearance; - the tool, its step and whether it is clear; - the status and the failed checks. With `out=`, a Markdown copy of the table (`table_markdown`) is written beside the JSON as `.md`. For an axis along a world axis, the `seat` and `thread start` columns show the coordinate along it (`Z 47.00`); for a tilted axis they show the whole point. `evaluate(spec, measurement)` is the kernel-free core: core CI feeds it hand-written measurements. `measure` and the gate need OCP. `tool_access(assembly, spec, i, tool, envelope, present, step)` runs the tool sweep alone and `tool_check(id, instance, spec, result)` turns it into the `tool` check; the assembly gate uses both. Cost: on the gate bench, about 0.6 s for four screws (28 checks). Limits. Threads are envelopes: engagement is the axial overlap with the receiver's modelled thread region, not a pull-out rating. A receiver bored wider than d + 0.4 `hole_clearance_mm` has no thread, and a plain hole between d and that is taken for thread (for M3 at the default 0.1, Ø3.00 to under Ø3.04); between that and the smallest clearance hole (Ø3.10) a hole is neither, and fails. A break in the thread wider than the bridge counts as no thread there. The annulus and the radial rays are samples, so a feature between them is missed. There is no minimum bearing width: a head that overlaps its hole by the outer ring's margin passes. A countersunk seat is judged only by contact. The tool is a straight sweep of a declared envelope along the axis: there is no angled approach and no hand room beyond the envelope. Parts no step fits are assumed present. **Where this differs from the plan (§4.4).** - The stack is read from the model along the axis rather than computed from the declared numbers. The numbers are compared with the model (`length`), and any drift fails. - A hole is a measured radius, not a boolean of shank plus clearance. - Seat, stack contact and pull-through are checks the plan did not list. - The tool sweep is a fused solid checked with `relate`, not `connectors.check_ports`. Port windows are a 2D panel model and do not fit an axis sweep. - An uncommon length warns rather than fails. ## Motion-range interference (`cadagent.gates.motion`) Does anything that moves hit something over its declared range? ```python from cadagent.gates.motion import motion_gate, motion_from_build, run_motion v = motion_gate(asm, out="exports/gates/motion.json") # joints from Assembly.from_build (asm.joints) v = motion_gate(asm, {"I9": {"kind": "revolute", "moving": "flap", "fixed": "hopper", # or by hand "axis": ((31, 0, 70), (0, 1, 0)), "range": [0, 70], "unit": "deg", "intended_contacts": [("flap", "hopper_stop")], "samples": 24}}, cache=PairCache(), samples={"I9": 30}, default_samples=12, max_step_deg=2.0, max_step_mm=1.0, combine=[["I9", "I10"]], intended_contacts=[("flap", "seal")], strict_contacts=False, intended_overlaps=[{"parts": ["brush", "drum"], "reason": "bristles"}], ignore_pairs=[("a", "b")], refine_steps=8, max_poses=2000, contains=[{"container": "cup", "cavity": "cup_void", "what": "ball"}], containment_options={}) v = motion_from_build(result, dossier, extra) # from_build + gates: (motion_samples, intended_overlaps, # ignore_pairs, touch_tol_mm) + cards' contains ``` **What moves.** A joint's `moving` part's body, as `Assembly.from_build` labels it with `moving_bodies`: the transitive closure over `fixed` joints and fastener `clamps`/`into`, stopped at grounded parts and at any joint's `fixed` part. This settles the open question in `dossier.md` and is final (the connectivity gate uses the same rule). A hand-built `Assembly` uses its own `body=` labels. A chain therefore hangs each child joint's `fixed` part on its parent's moving part itself, or labels the bodies by hand: a bracket fastened to the parent body and also a child joint's `fixed` part stays in the static frame, which the connectivity gate reports (`bodies:`). **Sampling.** Ranges are offsets from the pose the model was built in (0 = as modelled); axes are in assembly coordinates at that pose. A revolute joint rotates (right-handed about the axis direction), a slider translates along it, a cylindrical joint does both on a grid of the two. A joint needs the ranges of its kind (a revolute `range_deg`, or `range` with `unit: deg`; a slider `range_mm`; a cylindrical both), else `joint:` fails. Each axis gets `samples={id: n}` (the caller's override), else the joint's own `samples`, else `default_samples` (12; `gates.motion_samples` from the dossier), raised until the step is at most `max_step_deg`/`max_step_mm`, evenly spaced with both ends included. A zero-length range is one pose and a `range:` warning. `planar` and `ball` joints have no range: a warning, listed in `not_checked`; `fixed` joints are rigid links, not sampled. **Chains and combinations.** A joint whose `fixed` part lies in another joint's moving body rides on it: the chain is one group, sampled on the full grid (parent samples × child samples), each child's transform carried by its parent's. Independent joints are sampled one at a time with the others at home (0 clamped into their range); `combine=[[...]]` grids them together. Independent groups whose swept boxes come within `near_mm` of each other get a `combination:` warning, since nothing sampled them moving at once. A grid over `max_poses` (2000) is coarsened evenly on every axis, with a `density:` warning. **Pairs and cost.** Per pose: every part of a moving body against every part outside it (the static frame, other bodies, both bodies of a chain). Pairs that never move relative to each other, static against static and parts of one body, are the same in every pose and are never queried: they are the relation table's and the connectivity gate's. Broad phase: each part's exact home box, its corners moved with the pose (a box enclosing the moved part), against `near_mm`; the narrow phase is `relate` (contact area off), memoised in memory and through `cache` on both parts' content hash and posed placement (the pose is in the key), so a repeated pose costs nothing and an unchanged rerun reads only the cache. | Check | Fails when | Evidence | |---|---|---| | `collision:\|` | a moving part shares more than `overlap_mm3` with another (overlapping or contains) at any pose, unless declared in `intended_overlaps`; also an intended contact driven into its face (`intended_contact: true`) | `first_pose`: the colliding pose nearest home (the sum over axes of the distance from home as a fraction of the range; ties in sweep order), the one met first moving out from rest; its `overlap_mm3`, `overlap_centroid` and `overlap_box`; `last_clear_pose`, its clear grid neighbour nearest home (one sample away on one axis), with `last_clear_gap_mm` and the closest points `point_a`/`point_b`; `colliding_poses`, `worst_pose`, `worst_overlap_mm3`; the `onset` bisected between the two along that axis (`joint`, `clear`, `hit`, the overlap at the hit side and the gap and closest points at the clear side; `others` gives the other axes' values, held fixed, on a grid) | | `contact:\|` | (warning) a moving part touches one that is not an intended contact; fails with `strict_contacts` | first pose, number of poses, closest points | | `relation:\|` | the kernel could not decide a pair at some pose (`unknown`) | the relation | | `motion:` | any of the above in the group; passes otherwise | per axis `density` (samples, declared samples, `step` in deg or mm, the largest radius from the axis of the parts it carries, over the sampled poses, and `max_travel_per_step_mm`); the smallest exact clearance (touching included) among the non-intended pairs measured (`min_clearance_mm`, `at`: pair, pose, points) and, when smaller, the box distance of the nearest pair never measured (`unmeasured_gap_at_least_mm`: its gap is at least this); the intended contacts and the poses where they touch | | `joint:` | a joint's part is missing, its `fixed` part is in its own moving body, two joints drive one body, joints carry each other in a loop, or a ranged joint has no range; a warning for `planar`/`ball` | | | `combination:+`, `density:` | (warnings) see above | | | `containment@//...` | with `contains=`: the containment gate's checks at every pose of a group that moves the container's cavity or the contents | as the containment gate | **Intended contacts.** A joint's `intended_contacts`, its own `moving`/`fixed` pair (the bearing) and the `intended_contacts` argument may touch at any pose: an arm resting on its stop at the range end passes. Shared volume between them over `overlap_mm3` still fails. The onset is where shared volume first exceeds `overlap_mm3`, so it lies slightly past the first touch (about 0.02° for a corner 45 mm from the axis at the default tolerances). **Density, honestly.** The verdict reports the step between samples and the furthest any carried point moves in one step: step × the largest distance from the axis of the corners of the carried parts' boxes, over every sampled pose (a child joint's motion included, so a turret carrying an arm slid out is measured at full reach). It bounds the travel at the sampled poses; on a chain, a child pose between samples can reach a little further. A collision shorter than one step, or a feature thinner than that travel, can pass between samples: raise `samples` or set `max_step_*` where the smallest clearance is small next to the travel per step. The verdict never claims a swept volume. **Containment per pose.** With `contains=` (`motion_from_build` passes the cards' and `extra`'s), every declaration whose cavity or contents a group moves runs through `containment_gate` at that group's poses; the others are listed in `not_checked`. The cost is the containment gate's per pose. `run_motion(world, joints, ...)` is the kernel-free core: `world` gives part names, body labels, home boxes and `relate(mats, a, b)` per pose (`AssemblyWorld` wraps an `Assembly`), which is how the rules are tested in core CI. **Cost.** Gate bench, one joint at 12 samples: 12 poses, 48 exact queries, 0.8 s cold (on a workstation at load 6). The synthetic 100-part assembly (`tools/gate_timing.py --motion 20`) with three joints at 20 samples each: 60 poses, 92 candidate pairs plus 32 bisection queries, 1.7 s cold, 0.07 s warm from the pair cache. The broad phase keeps the queries to the pairs that come near: real parts cost more per query (see the relation table), not more queries. Limits. Sampled poses, not a swept volume. Rigid bodies on ideal axes: no backlash, play, deflection or tolerance stack, so a clear pose in the model does not prove printed parts clear. Contacts are classified by the relation table's tolerances (`touch_mm`; the near-tangent blind spot of the boolean kernel applies, see containment). Joint limits are the declared ranges; nothing checks that a stop actually ends the motion there. ## Assembly-sequence access (`cadagent.gates.assembly`) Can every part go in, in the declared order, along its declared direction, and can each screw's tool reach it then? ```python from cadagent.gates.assembly import assembly_gate, assembly_from_build, run_sequence, AccessOptions, steps_markdown v = assembly_from_build(result, dossier, extra, out="exports/gates/assembly.json") # + assembly.md beside it v = assembly_gate(asm, result.steps, fasteners=fasteners_from_build(result), tools={"hex 2.5": {...}}, options=AccessOptions(max_step_mm=1.0, margin_mm=1.0, refine_steps=12, missing_direction="warn"), intended_overlaps=[{"parts": ["insert", "boss"], "reason": "heat-set"}], cache=cache, service=["board"]) # or True: removal for service, as warnings (off by default) print(steps_markdown(v)) ``` `assembly_from_build` takes the steps from the build (`steps_from`: the registry's `assembly.step()` calls, plus YAML steps in `project.yaml` whose order the build does not declare; the lint reports steps that disagree), the fasteners from `fasteners_from_build`, the tools from `tools_from` and the intended overlaps from `gates:` and `extra`. For a preassembled rigid module, use the additive Python `subassemblies={name: {members, steps}}` and `service_after={name: removed_parent_keys}` arguments. Its internal sequence is checked separately; every member and attached fastener then moves rigidly through the parent path. Prerequisite removal remains explicitly unverified. See [Rigid subassembly insertion](https://cad-agent.valpatel.com/docs/subassembly-insertion.md) for the strict scope, examples and limits. **Checks** (ids in the verdict): | Check | Fails when | |---|---| | `steps` | no steps, or two steps share an order (or an order is not an integer); nothing else runs then | | `cover:` | an instance of the part is fitted by no step, or by more than one. `fit` names a part (all its instances) or one instance key (`ball#3`) | | `cover:` | no step installs the fastener, or steps list it by name more than once | | `step::refs` | `fit` is not a part (or is a fastener); an `onto` part does not exist, is the fitted part, or is fitted at this step or later; a `fasteners` entry names no fastener or catalogue key, installs nothing (every fastener of that key goes in at another step: listed twice), or a key listed k > 1 times where a different number go in; a fastener named twice in one step; a direction that is not `+X`..`-Z` or a vector | | `step::fasteners` | a fastener installed here clamps (any `stack` part) or threads into a part fitted at a later step | | `step::insert` | an instance of the fitted part shares volume with a present part on its way in, or at its seat (`state: seat`), or the kernel could not decide (`unknown`), or the path needed over `max_samples`; a warning (`missing_direction="fail"`: a failure) when the step has no direction | | `step::tool:` | the fastener gate's `tool` rule (`fasteners.tool_check`) on its sweep (`fasteners.tool_access`) at this step: no tool or envelope, or the envelope overlapping a part present when the screw is driven. `step::tool` warns when a step declares a tool but installs no fastener | | `service:` | (warning, with `service=`: True, part names or instance keys) pulled back out along its direction from the finished assembly, the part meets a part fitted or a fastener installed later, other than its own fasteners (those that clamp it or thread into it, listed as `own_fasteners_first`); or the kernel could not decide (`undecided`). Parts no step brings in are not counted | **Which step installs a fastener**, and what is present when it is driven, are the fastener gate's rules (`install_step`, `present_at`), so both gates agree. A catalogue key listed once in a step means "these fasteners"; listed once per screw, the count must match. **What an insertion is checked against.** The parts fitted at earlier steps and the solids of fasteners installed at earlier steps. Parts no step fits are not obstacles (`cover:` fails them; `not_checked` lists them), and instances fitted at one step are not checked against each other (their order within the step is not declared). A pair declared in `intended_overlaps` (a press fit, a heat-set insert) is left out, with its reason, as an exclusion. **The insertion sweep.** The instance moves along `seat - s * direction`, `s` from `margin_mm` beyond the far side of the obstacle's box down to 0 (one obstacle at a time). This is conservative advancement: - from each position the part steps by its gap to the obstacle: the box distance while the boxes are at least `max_step_mm` apart, the exact gap once they come closer. Moving by less than the gap cannot close it, so far from contact nothing is skipped and a long approach costs a few queries; - only where the gap is under `max_step_mm` (sliding along a face, passing close by) is the line sampled at `max_step_mm`; - shared volume over `overlap_mm3` at a sample blocks the step, and the first contact is bisected (`refine_steps`) between the last clear sample and the first hit; - at the seat (`s = 0`) touching is the expected end and shared volume fails. The relations are exact B-rep (`relate`, contact area off), memoised and cached in `PairCache` on the content hash plus the translated placement, so an unchanged rerun reads only the cache. **Witnesses.** For a blocked pair: `first_contact_mm` = [last clear s, first hit s] (the distance before the seat where the part first meets the obstacle), the overlap's centroid there (the contact point) and the gap and closest points on the clear side. For a clear step: the parts it touches on the way or at its seat (`touching`), and the closest approach to any other present part as two numbers that bracket it: `min_clearance`, the smallest exact gap measured (at the seat or within `max_step_mm` of an obstacle's box; the closest approach is at most this), and `clearance_at_least`, a bound that holds over the whole path, between samples too (moving by h changes a gap by at most h, so between samples h apart with gaps g1 and g2 the gap is at least (g1 + g2 - h) / 2; for a pair whose swept box never comes near, the box gap). The step table shows `lo..hi`, one number when they agree, or `≥ lo`. **Output.** The verdict (`cadagent.gate/1`) carries `steps`, one row per step: fit, onto, direction, how many parts are present, the clearance bracket, what it touches, the fasteners it installs with each tool's status, and what blocked it; plus `not_checked` (steps without a direction, tools with nothing to drive, parts no step brings in) and `world_stats` (kernel queries, cache and memo hits, tool sweeps). With `out=`, `steps_markdown` writes the step table beside the JSON as `.md`. `run_sequence(world, steps, fasteners, ...)` is the kernel-free core: `world` gives part names and kinds, boxes, the relation of a translated part to another (`relate_at`) and the tool sweep (`AssemblySequenceWorld` wraps an `Assembly`), which is how the rules are tested in core CI. **Cost.** Gate bench, 15 steps, 25 parts, four tool sweeps: about 3.7 s cold, 238 exact queries (a part sliding along a face, like the board down the hopper wall or the pin through its bores, is sampled every `max_step_mm`), on a workstation at load 6. Limits. Straight-line insertion along one declared direction: no rotation, tilt or wiggle, no room for hands or fixtures. Rigid parts at their modelled size: no tolerance stack, flex or print shrink, so a clear path in the model does not prove the parts go together. Where the part is within `max_step_mm` of an obstacle the line is sampled: a feature thinner than the step, crossing another between two samples, can be missed (set `max_step_mm` smaller where clearances are tight). The near-tangent blind spot of the boolean kernel applies (see containment). The tool sweep has the fastener gate's limits. ## Running the gates (`cad-agent gates`) ```bash cad-agent gates # every gate that applies, then the dossier lint cad-agent gates --only motion assembly # just these (names: connectivity containment fasteners motion assembly cad-agent gates --skip motion # wall dossier-lint) cad-agent gates --jobs 3 --no-cache # three gates at once; every query exact, nothing read from the pair cache cad-agent gates --build # build the geometry first (cad.gates.build) cad-agent gates --dry-run # what would run, what does not apply and why ``` It needs a geometry record (`exports/dossier-geometry.json`, written by the registry's `build()`): the build step writes it, or `cad.gates.build` names the script that `build_module` builds when the record is missing or with `--build`. It exits 1 when a gate fails and 2 when the project cannot be gated (no `dossier:` or `gates:` key, a bad configuration, no geometry record and no build). **What runs.** A gate runs when it has something to check, worked out from the record and the declarations without the kernel: connectivity when there are parts, containment when a card or the declarations file has a `contains`, fasteners when the record has fasteners, motion when a joint moves (any kind but `fixed`), assembly when there are steps (registry or dossier), wall when a part declares a minimum wall (a card's `material.wall_mm`, the declarations file's `wall_mm`, or `cad.gates.wall.parts`). A gate with nothing to check is listed as not applicable and its old verdict files are removed. The dossier lint runs last when the project has a dossier, in the same process, because it reads the other verdicts; it writes `exports/dossier-status.json` too. **Worker processes.** Each geometric gate runs in a process of its own (`python -m cadagent.gates.run `, with this interpreter): it loads the geometry with `load_geometry`, runs the gate's `*_from_build` function with the dossier and the declarations file, binds the verdict to its inputs and writes `exports/gates/.json` and `.md` (the findings, exclusions, assumptions and limits, plus the fastener table or the assembly step table), and `connectivity.dot`. The verdict is removed before the run, and a gate that exits abnormally, times out (`cad.gates.timeout_s`, 3600 s) or writes nothing gets a failing verdict naming the reason and the end of its log (`.cadagent-cache/logs/gate-.log`). So an old passing verdict never survives a run that did not finish. When `--build` (or the build of a missing record) fails, no gate runs and every geometric verdict is removed, so `check` fails "not run" rather than passing on the previous build. A gate skipped in `cad.gates.skip` loses its verdict files too (the dossier lint would otherwise still count them); `--skip` leaves them for that run. `--jobs N` (or `cad.gates.jobs`) runs N gates at once. That is safe because the gates share only the pair cache, whose writes are atomic renames, and each writes its own files. Each process holds the whole geometry, so memory grows with N. **Configuration** (`cad-agent.yaml`, every key optional): ```yaml cad: dossier: dossier/project.yaml gates: geometry: exports/dossier-geometry.json # default: /dossier-geometry.json build: cad/build.py # build_module() it when the record is missing, or with --build declarations: dossier/gate-declarations.yaml # plain-data declarations the YAML schema does not cover cache: .cadagent-cache/gates # the pair cache (gitignored); --no-cache runs without it jobs: 2 timeout_s: 3600 skip: {motion: "slow; run with --only motion before a review"} # a reason is required connectivity: {tol: {touch_mm: 0.01}} containment: {support: fail, probe_spacing_mm: 0.5} fasteners: {tip_clearance_mm: 0.5, stock_lengths_mm: [6, 8, 10, 12]} motion: {default_samples: 24, max_step_deg: 2.0, combine: [[I9, I10]]} assembly: {max_step_mm: 2.0, margin_mm: 1.0, refine_steps: 8, max_samples: 5000, service: true} wall: {tol_mm: 0.01, max_rays: 20000, parts: {bracket: 3.0}} ``` | Gate | Options (`cad.gates.`) | |---|---| | all but wall | `tol`: `Tolerances` fields (`touch_mm`, `overlap_mm3`, `contain_mm3`, `near_mm`, `volume_check_mm3`); default `touch_mm` from the dossier's `gates.touch_tol_mm` | | containment | `support`, `down`, `support_mm`, `mesh_samples`, `seed`, `deflection_mm`, `max_points`, `probe`, `probe_deflection_mm`, `probe_points`, `probe_spacing_mm` | | fasteners | the `FastenerOptions` fields (`seat_tol_mm`, `gap_tol_mm`, `hole_clearance_mm`, `tip_clearance_mm`, `stack_tol_mm`, `length_tol_mm`, `min_engagement_d`, `look_ahead_mm`, `lines`, `service_access`, `stock_lengths_mm`) | | motion | `samples`, `default_samples`, `max_step_deg`, `max_step_mm`, `combine`, `intended_contacts`, `strict_contacts`, `refine_steps`, `max_poses`, `containment` (per-pose containment on or off), `containment_options` | | assembly | the `AccessOptions` fields (`max_step_mm`, `margin_mm`, `refine_steps`, `max_samples`, `tool_access`, `missing_direction`) and `service` | | wall | `tol_mm`, `max_rays`, `grid`, `spacing_mm`, `max_grid`, `parts` (part: minimum wall in mm) | Unknown keys, and bad values of `tol` and of the assembly and fastener options, are refused (exit 2) before anything runs; another bad value (a containment, motion or wall option) fails that gate's run, and its verdict says why. The assembly gate samples every `max_step_mm` wherever a part slides close along a face, so a long, tight slide costs many queries; a coarser `max_step_mm` (with the clearance it can then miss stated in the verdict's limits) is the knob for it. **Declarations.** The dossier (`gates:`, the interfaces, cards' `contains` and `material.wall_mm`, `assembly`) and the declarations file are passed to the gates as in the sections above. The file holds what the YAML schema cannot say yet, with the keys `extra` takes: `grounded`, `free_parts`, `interfaces`, `contains`, `intended_overlaps`, `ignore_pairs`, `strict_interfaces`, `max_joint_gap_mm`, `fasteners`, `tools`, `motion_samples` and `wall_mm`. A pair exemption that names a fastener (a screw's thread envelope inside its insert) belongs in the dossier now: `gates.intended_overlaps` and `gates.ignore_pairs` take part names, instance keys and fastener names or instance keys, and the lint checks each against the geometry record (`exemption-unknown-party`, [dossier.md](https://cad-agent.valpatel.com/docs/dossier.md#rules)). The dossier's `gates: tools:` and `gates: fasteners:` are read by the fastener and assembly gates as before. **`check`, the report and the site.** With a `dossier:` or `gates:` key, `cad-agent check` and the report have an "Integrity gates" feature (`cad-gates`): one row per gate. A gate that applies fails when its verdict is missing, unreadable, stale (its inputs differ from the current geometry record, the declarations or the gate's options) or failing; it passes with its check and warning counts and links its `.md`. A gate with nothing to check, or skipped in `cad.gates.skip`, is a skip row with the reason. Other verdicts in `exports/gates/` (a project's own gates) get a row too; the dossier lint stays the `cad-dossier` feature. The site overview of a project that uses the gates shows how many verdicts pass on the current geometry, and how many are stale, linked to the gates page (`site/review/gates.html`, [dossier.md](https://cad-agent.valpatel.com/docs/dossier.md#on-the-review-site)): a row per gate with its state and files, the connectivity graph and every finding with its witness. `cad-agent all` runs the gates after `export-3mf` and before the report, rebuilding the record with `cad.gates.build` first unless a build step already runs that script; when they cannot run (no geometry record), it says so and still runs the dossier lint. Without a `dossier:` or `gates:` key nothing changes. The parent side (configuration, the plan, staleness, the report rows) needs no CAD kernel, so it runs in core CI; only the workers load OCP. ## The gate bench (`tests/fixtures/gate_bench/build.py`) A generic mechanism declared through the dossier registry: a base, a hopper with filleted corners and a declared cavity, eight loose balls (one part, eight instances), a board, a lid held by four fasteners into heat-set inserts, a bracket on a boss, an arm on a pin (a revolute joint) with a filleted stop and an overhead frame, and the assembly steps. What belongs in the YAML dossier (interfaces, intended overlaps, `contains`, decisions, walls, tool envelopes) is plain data until the bench gets a `dossier/` directory. `build(seed)` returns the `BuildResult`, `Assembly.from_build` of it and those declarations, for the clean bench or one of the plan's ten seeded defects (`SEEDS`). In the `gusset_over_screw` seed, the lid goes on in one step and the gusset in the next, and the lid's screws are driven at the gusset's step. The fastener gate's tool sweep at that step meets the gusset. A gusset fitted after the screws are driven is not in the way then; the gate warns that it must come off before they can be removed. In the `lid_before_board` seed, the steps are reordered so the lid goes on before the board and the balls; the assembly gate finds the board (and the balls) meeting the lid's top 40.6 mm above their seats. `tests/test_gate_bench.py` runs every gate on the clean bench (no finding allowed) and on every seed (its gate must fail with a finding that names the seeded part; the wall seed's witnesses must lie on the thinned wall). The bench is also a project, `tests/fixtures/gate_bench/project/`: `cad-agent.yaml` with `cad.gates`, a `cad/build.py` that registers the bench with the seed named in `cad/parameters.json`, the YAML dossier (four sections, a card per part, the interfaces, decisions, requirements and the screw-in-insert overlaps, which name the fasteners). `tests/test_gates_cli.py` runs `cad-agent gates --build` on it: the clean bench passes all seven gates with no finding and every section `gated` (about 10 s with three jobs on a loaded workstation), each geometric seed fails its gate through the CLI with a finding naming the seeded part (the other verdicts then read stale in `check`), and the decision seed fails the lint. Every gate of the plan now has an adapter; a gate added later starts with one that raises `NotImplementedError` and strict-xfail cases, and its implementer writes the adapter and removes the mark. ## Where this differs from the plan - **Cache location and key**: `.cadagent-cache/gates/` (gitignored), not `exports/gates/cache/`; the shared content hash plus placement, the tolerances and the OCP version, not OCP's identity hash. - **Containment tolerance**: absolute (`contain_mm3`), not 1e-6 relative. - **Wall probe**: exact B-rep rays replace the negative-offset probe. `BRepOffsetAPI_MakeOffsetShape` by −t/2 returned a shell or a null shape on a 0.8 mm rib and on a 2.4 mm-walled box, the cases it was meant for. - **Derived cavities** are not built; containment takes a declared cavity solid. - **Containment, mesh-only items**: classified against the exact B-rep cavity (`BRepClass3d`), not with trimesh `contains` against a tessellated cavity (the cavity is always a B-rep; trimesh `contains` needs `rtree`). - **Containment, additions**: the contact probe for near-tangent crossings, the support check and per-pose runs are not in the plan; the plan's "40 spheres in a cone, one moved 5 mm up through the wall" moves it 5 mm along the wall's outward normal (the cone opens upwards, so "up" leads away from the wall). - **Motion**: no pose renders (the verdict gives the poses and witness points; the section review packet, [review-packet.md](https://cad-agent.valpatel.com/docs/review-packet.md), renders each joint's range ends); chains are always gridded and independent joints combined on request (`combine`), where the plan grids only joints declared `coupled`; pairs that do not move relative to each other are never queried rather than taken from the full per-pose table; the collision onset of a one-axis joint is bisected; containment can run per pose. - **Assembly access**: the insertion is conservative advancement along the line (gap-sized steps, sampled only near contact, the first contact bisected), not a fused swept solid: OCCT's prism of a solid is refused and the prism of a curved face is not a valid solid, so an exact sweep is not robust. The sweep runs one obstacle at a time from beyond that obstacle's box, rather than from outside the whole present set's box (equivalent: nothing beyond the obstacle's box can be hit). Parts no step fits are not insertion obstacles (the coverage check fails them). Additions: catalogue-key counts per step, the fastener order check and the optional service removal. - **Seed harness file**: `tests/test_gate_bench.py` (pytest only collects `test_*.py`), fixture directory `gate_bench` (importable name). - **`cad-agent gates`**: gates run in worker processes (crash and timeout isolation, `--jobs`); the dossier lint is part of the command; gate options live in `cad-agent.yaml` (`cad.gates`), with the declarations in the dossier plus a plain-data declarations file for what its schema cannot express; verdicts carry their inputs, and `check` fails a stale one. The plan had the slow gates as separate `cad-agent.yaml` steps (§4.9); `--only`/`--skip`, `cad.gates.skip` and the pair cache cover that. ## Rendered simulation pivots The motion gate poses CAD bodies; it does not execute web viewer transforms. Use the separate [rendered-motion trace check](https://cad-agent.valpatel.com/docs/rendered-motion.md) to compare actual scene landmarks against independent CAD joint frames and component ownership. Keep both checks when a project publishes an articulated simulation. --- Source: https://cad-agent.valpatel.com/docs/pcb.md # PCB and assembly review workflow cad-agent supports electronics and mechanical evidence in the same local project site. KiCad remains the electrical source of truth; cad-agent consumes its native reports and displays the files. The same embedded review groups work for a PCB, printed housing, robot assembly or any combination. ## Embedded review pattern Set `cad.site.review: exports/review.json` in `cad-agent.yaml`: ```json {"groups":[{"title":"Two-port adapter","summary":"Engineering prototype; fit testing pending","views":[{"title":"Schematic","kind":"pdf","file":"exports/2port/schematic.pdf"},{"title":"Top copper","kind":"svg","file":"exports/2port/pcb-top.svg"},{"title":"Housing","kind":"image","file":"preview/2port-open.png"}],"reports":[{"title":"Electrical CAD","file":"exports/2port/pcb-evidence.json"}],"downloads":[{"title":"PCB source","file":"hardware/2port/board.kicad_pcb"}]}]} ``` All drawings are embedded directly in the status page. Variants and views use reusable controls shipped in the web kit (`webkit/review.js`), inlined into the page so a static copy of `site/index.html` works without the local server; without JavaScript every view is shown in turn. `image`, `svg`, `pdf`, local `html`, and `model` views are supported. A `model` file names the existing cad-agent viewer manifest and embeds that viewer; no second 3D engine is created. The viewer itself is served by `cad-agent site --serve`, so a static copy shows the frame with a note saying so. Configure `cad.view.groups: [{id: two, title: Two-port, prefix: "2port_"}]` to export a separate `exports/view/two.json` with only that assembly, reusing the same STL meshes. The id `view` is reserved (it is the full manifest), and every group must match at least one part; `cad-agent view` checks this before writing anything and reports a configuration error. Every view, report and download path must name a regular file inside the project root (symlinks are resolved): a missing file or one outside the root shows as "Pending asset" or "Pending download", and a report shows UNAVAILABLE, never a pass. Supply meaningful captions stating geometry provenance and qualification limits. The local server reloads after source changes, including KiCad, CSV and PDF files. Use `?live=0` to disable reload for an inspection. ## Source-bound electrical evidence Run KiCad's full ERC and DRC, including schematic parity, immediately before binding the results: ```sh kicad-cli sch erc --format json --severity-all -o exports/erc.json hardware/board.kicad_sch kicad-cli pcb drc --format json --severity-all --schematic-parity -o exports/drc.json hardware/board.kicad_pcb ``` `--severity-all` is required, and the adapter enforces it. kicad-cli 10 writes the severities it was asked for into the report as `included_severities`: the default run writes `["error","warning"]`, which leaves excluded violations out, and `--severity-error` drops warnings too. A report whose `included_severities` does not contain `error`, `warning` and `exclusion` fails with "report filtered severities". DRC reports must also include `violations`, `unconnected_items` and `schematic_parity`. Do not reuse old reports after changing a board. Binding rejects reports with filesystem timestamps older than any listed source. This guard applies at local snapshot creation; subsequent freshness uses hashes so repository checkouts do not depend on timestamps. The adapter does not invoke KiCad and cannot prove that an arbitrary report was actually produced from an arbitrary source; the build orchestrator must run checks before the snapshot. An evidence manifest supplies: ```json {"reports":{"erc":"exports/erc.json","drc":"exports/drc.json"},"sources":["hardware/board.kicad_pcb","hardware/board.kicad_sch","hardware/board.kicad_pro","hardware/board.kicad_dru"],"release_gates":[{"id":"physical-fit","pass":false},{"id":"protection-tests","pass":false}],"waivers":{"missing_courtyard":"Mounting holes and logos have no courtyard by design; checked in the enclosure fit review"}} ``` Run `cad-agent pcb-check hardware/evidence-manifest.json --output exports/pcb-evidence.json`. The command exits 2 without writing anything when the manifest is malformed: `reports` must be an object whose keys are only `erc` and `drc`, `sources` a list of path strings, `release_gates` a list of objects, and `waivers` (optional) an object mapping each check key to a nonempty reason. Every source and report must be a regular file inside the project root after resolving symlinks; a path outside the root (`../`, an absolute path, a symlink out) or a directory makes the bundle fail with the reason in `problems`, and hashes are keyed by root-relative paths. Both ERC and DRC plus a nonempty source list are required for a passing bundle. What fails a check: a missing or malformed report, filtered severities, an unknown violation severity, any error or warning, unconnected items, parity findings, and every ignored check or excluded violation that is not waived. KiCad lists the checks a project switched off in the report's `ignored_checks` (by key, for example `missing_courtyard`), and an excluded violation keeps its check key in `type`. Each such key is copied into the check's `ignored_checks` or `excluded_checks` and fails the check unless `waivers` names that key with a reason; waived keys stay listed under `waived` with their reasons, and the bundle records the waivers. A waiver is a reason (`"clearance": "reviewed net ties"`), which accepts every excluded instance of that key, or `{"reason": ..., "max": n}`, which accepts at most n excluded instances; more fail. Each check records how many instances each waiver accepted (`waived_counts`) and lists waivers that matched nothing (`unused_waivers`). Prefer fixing the design or reviewing each excluded instance in KiCad before waiving. Changing a recorded source or report invalidates freshness in the site: the design card, the review workbench and the `report` check row all use the same root-confined hash check. Hash equality establishes the recorded snapshot, not physical correctness. Design cards can reference `pcb_evidence: exports/pcb-evidence.json`; they show CAD status separately from open release gates and production qualification. Never turn fabrication, fit, transient protection, CAN timing, stock or supplier orientation gates green based only on clean CAD checks. ## JLC assembly files `cadagent.pcb.check_jlc_files(bom, cpl)` checks required columns, exact C-codes, duplicate references, finite coordinates, layers and BOM/CPL set equality. Build separate explicit populations for SMT-only versus SMT+THT assembly, and keep manual/consigned components visible in another file. CSV parity does not verify part availability, library zero-angle convention, solder process, panelization or quoted pricing. ## Mechanical enclosure checks Import the native PCB STEP in the documented coordinate frame, and also check the nominal PCB outline and holes. Include connector mating envelopes, screws, captive nuts, cable paths and tool access. A board-only collision proxy misses components. Use measured/manufacturer dimensions when available and flag generic geometry. For ribbon relief, model hard stops, actual jacket thickness, compliant pads and a physical pull-test target; a cable hole alone is not strain relief. Generic lessons from the first PCB and enclosure project that used this workflow are in [lessons/11-pcb-evidence-and-review.md](https://cad-agent.valpatel.com/lessons/11-pcb-evidence-and-review.md). Board-specific incident notes stay in that product's repository. ### Enclosure fasteners Import `cadagent.fasteners` for dimension-driven screw and insert envelopes and `blind_insert_stack` for recessed-head axial checks. Supply sourced dimensions; there is no implicit vendor catalog. Check screw-tip clearance and minimum thread engagement across tolerances. A heat-set knurl deliberately intersects the as-printed pilot: report this separately from unintended assembly collisions. A supplier login or CAD download failure must remain visible. Never present an original dimensioned envelope as an official vendor STEP file. Final assembly pictures should render the exported STEP readback as well as the in-memory model. Reimport for independent placement review, especially for text, repeated instances and exploded scenes. Record export/source hashes and keep nominal envelopes visibly distinct from official supplier geometry. A successful CAD boolean, valid BREP and a watertight print mesh are separate checks: tangent boss/wall contacts can pass one and fail another; use positive overlap at joins. Release packagers should select the validated public manifest, not recursively include every file in hardware folders. KiCad creates ignored `.kicad_prl` UI state next to designs. Compare local and clean-checkout ZIP membership and hashes; a passing source manifest does not automatically constrain a separate glob. Use `filleted_wall_rib(thickness=…, reach=…, height=…, root_radius=…, overlap=…)` for a boss-to-wall brace with explicit curved roots. Its wall is at `Y=reach`; rotate/translate it into the assembly and overlap the floor and boss deliberately. Check post-cut web widths around seam grooves and counterbores. Keep clamp ribs outside the cable lane and provide driver access with the relevant lid state. Side mounting ears need their own screw seats, filleted roots and installation clearance; their mounting hardware is separate from the enclosure's insert screws. ### Prototype pads and user-facing controls Preserve raw signals without promising software support from a net name. Trace the module pin and its ownership in current platform documentation before advertising GPIO/PWM/I²C access. Expose host power through explicit default-open links and verify that no prototype rail bypasses them. Bare isolated pads should carry no net and be excluded from assembly BOM/CPL; suppress automatic copper fill through the free grid. Document the row/column pitch, power-row connections and allowable component/lead envelopes. Re-run native DRC and schematic parity after the final router import: a successfully exported route session can still leave a power branch disconnected. Put illustrated controls beside native layers in the shared review groups, including which shunt enables each termination resistor. --- Source: https://cad-agent.valpatel.com/docs/rendered-motion.md # Rendered joint motion validation The maintained [trace protocol](https://github.com/Valpatel/cad-agent/blob/dev/skills/cad-motion-validation/references/trace-protocol.md) ships with the [cad-motion-validation skill](https://cad-agent.valpatel.com/skills/cad-motion-validation/SKILL.md), including when the workflow installer copies the skill to a different machine. It documents the independent CAD reference, actual renderer capture, `cad-agent motion-trace` CLI, required coverage, diagnostics and evidence limits. --- Source: https://cad-agent.valpatel.com/docs/review-packet.md # The section review packet (`cadagent.render.review_packet`) A fixed set of captioned renders of one dossier section, with the measurements that come first: the gate verdicts and findings for the section, their witness points, the content hash of every part shown, and the render checklist the reviewer answers. The skill [`skills/design-process`](https://cad-agent.valpatel.com/skills/design-process/SKILL.md) tells an agent when to make one; the plan behind it is [`design-process-plan.md`](https://cad-agent.valpatel.com/docs/design-process-plan.md) §5.1–§5.2 (task T10). **What a packet is for.** Renders are for *diagnosis*, not proof. The gates measure (containment, contact, gaps, engagement, clearances, counts); the renders let a reviewer see what the numbers describe and catch what no gate was declared for (a part that is a plain box, a mating feature that is missing, a part in the wrong place). The research behind this ([`research/design-workflows.md`](https://cad-agent.valpatel.com/research/design-workflows.md) §5): vision models do well on a few named views with specific yes/no questions and measurements given first; they do badly at counting, small gaps, hidden parts and unusual viewpoints. So the packet puts the measurements first, cuts and x-rays the section to show the hidden parts, and the checklist never asks an image for a count or a gap. ## Making one ```python from cadagent.render import review_packet manifest = review_packet("S3", root=".") # renders exports/review/S3/*.png + manifest.json manifest = review_packet("S3", root=".", size=(1400, 1000), explode=1.0, context="neighbours", only=None) ``` ```bash cad-agent review-packet S3 # one section; with no section: every section of the dossier cad-agent review-packet --list # the views each section would get, without rendering (no CAD kernel) cad-agent review-packet S3 --view solid-iso exploded # re-render these; the others are kept while current ``` It reads, from the project at `root`: the geometry record (`exports/dossier-geometry.json` and its BREP shapes, via `cadagent.dossier.load_geometry`), the YAML dossier (`cad: dossier:`), every gate verdict in `exports/gates/`, and `exports/dossier-status.json` when it exists. It needs CadQuery, VTK and Pillow (the `cad` extra); without them it raises `MissingDependency`. Run `cad-agent gates` first: a packet made from stale or missing verdicts says so in its measurements, it does not hide it. `cadagent.packet` has the kernel-free half: `plan_packet(section, record, dossier=None, verdicts=None, status=None, lint=None, expected=None, ...)` works out the views, cut planes, framing, explode offsets, poses, findings, witness points and the checklist from the JSON alone and returns the manifest without image hashes; `plan_project_packet(section, root=...)` does the same from a project's files; `project_sections(root)` lists the sections. `review_packet` renders the plan (`render_views`) and writes it (`write_manifest`). | Argument | Default | Meaning | |---|---|---| | `section` | | a section id of the dossier, or (without a dossier) a `section=` value the build registered | | `root` | `.` | the project root (with `cad-agent.yaml`) | | `out_dir` | `/review/
` | where the PNGs and `manifest.json` go | | `size` | `(1400, 1000)` | image size in pixels | | `explode` | `1.0` | scale of the exploded offsets (0 turns the exploded view off) | | `context` | `"neighbours"` | other sections' parts drawn faint: `neighbours` (sharing an interface, a fastener or a joint, or whose box is within 10 % of the section's size), `all`, or `none` | | `only` | `None` | render only these view ids; another view keeps its earlier image while that image is current (same geometry digest, section hash and planned view, and the file still matches its hash), else it gets `file: null` and its old image is removed | ## The views Every image is captioned with the section id and title and the view's name, with the section hash and the part count in the subtitle, and a note saying what the view is for. Section parts are coloured one colour each (listed in the manifest); parts allowed to float (`free`) are a contrast orange; placed fasteners are dark steel; context parts from other sections are light grey and translucent. Each view is framed on its own box (`fit`): the section, the cavity's part, the joint's parties, a box around the screw, the exploded or posed parts; context outside it is cropped. | View id | Kind | When | What it shows | |---|---|---|---| | `solid-iso` | `solid` | always | the section in place, front iso | | `solid-rear` | `solid-rear` | always | the opposite iso, from behind and the other side | | `xray-iso` | `xray` | always | the section's parts translucent, free items and fasteners opaque: what sits inside what | | `cut-cavity-` | `cut` | per registered cavity of a section part | a vertical plane through the cavity's box centre, normal +Y, seen from −Y | | `cut-cavity--plan` | `cut` | the same | a horizontal plane through the centre, seen from above: the walls as a ring around the contents (an item through a wall shows here even when another item hides it in the side cut) | | `cut-joint-` | `cut` | per joint with a party in the section | a plane containing the joint axis, as near vertical as possible | | `cut-fastener-` | `cut` | per group of fasteners with one catalogue key and one clamped part | a plane containing the first screw's axis: head, stack and thread (the fastener gate measures every screw; alike screws need one look) | | `exploded` | `exploded` | when steps fit section parts | each part moved back out along its step's direction, later steps further; fasteners back out along their own axis from where their install step's part went | | `access-from-` | `access` | per distinct fastener axis direction | looking along the screws' advance from the tool's side (`top`, `bottom`, `front` = −Y, `back`, `left`, `right`, or the rounded vector), with neighbouring parts opaque since they can block the tool | | `pose--min`, `-max` | `pose` | per ranged joint whose moving part is in the section | the moving body at each end of its declared range | | `findings` | `findings` | when a failing or warning finding has a witness point | `solid-iso` with a numbered callout at each witness | ## `manifest.json` Written next to the images, `exports/review/
/manifest.json`. Schema `cad-agent.review-packet/1`. File names in `views[].file` are relative to the manifest's directory; every other path is relative to the project root. The file has no timestamp: an unchanged packet writes identical JSON. ```json { "schema": "cad-agent.review-packet/1", "toolkit": "0.1.0", "dir": "exports/review/S3", "section": {"id": "S3", "title": "Hopper and feed", "purpose": "...", "requirements": ["R4", "R7"], "status": "gated", "blocked_by": ["no current reviewer record"], "file": "dossier/sections/hopper.yaml"}, "geometry": {"file": "exports/dossier-geometry.json", "digest": "", "actor": "agent-12"}, "section_hash": "", "parts": { "hopper": {"sha256": "", "kind": "part", "count": 1, "free": false, "grounded": false, "color": "#3b7dd8", "function": "", "decisions": [{"id": "D41", "decision": "..."}], "status": "gated", "bbox": {"min": [0, 0, 0], "max": [60, 60, 80]}, "volume_mm3": 41200.5, "instances": [{"key": "hopper", "sha256": "..."}]}, "screw_0": {"sha256": "...", "kind": "fastener", "count": 1, "key": "m3_shcs", "clamps": "lid", "into": "insert_0", "color": "#4a4f55", "instances": [{"key": "screw_0", "sha256": "..."}]} }, "context": [{"key": "base", "section": "S1", "kind": "part", "sha256": "..."}], "measurements": { "gates": {"containment": {"file": "exports/gates/containment.json", "sha256": "", "status": "pass", "pass": true, "stale": null, "checks": 12, "section_checks": [{"id": "contains:hopper_cavity:ball#0:inside", "status": "pass", "message": "..."}]}}, "missing_gates": ["motion"], "lint": {"file": null, "computed": true, "sha256": "", "status": "pass", "pass": true, "stale": null, "section_checks": []} }, "findings": [ {"id": "F1", "gate": "containment", "check": "contains:hopper_cavity:ball#3:inside", "status": "fail", "message": "...", "parts": ["ball#3", "hopper"], "scope": "section", "stale": null, "witness": [{"label": "furthest_outside_point", "point": [31.0, 4.2, 40.0]}], "views": ["findings"]} ], "views": [ {"id": "cut-cavity-hopper_cavity", "kind": "cut", "file": "cut-cavity-hopper_cavity.png", "sha256": "", "caption": "S3 Hopper and feed: cut through hopper_cavity, side", "note": "...", "camera": [0, -1, 0], "clip": {"origin": [30, 30, 40], "normal": [0, 1, 0]}, "fit": {"min": [8, 8, 2], "max": [62, 62, 84]}, "target": {"cavity": "hopper_cavity", "part": "hopper", "contains": ["ball"]}, "keys": ["hopper", "ball#0", "..."], "offsets": {}, "pose": null, "callouts": []} ], "rendered": 11, "dossier_digest": "", "options": {"explode": 1.0, "context": "neighbours"}, "render": {"size": [1400, 1000]}, "checklist": { "schema": "cad-agent.render-checklist/1", "instructions": ["Read measurements and findings before the images", "..."], "answers": ["yes", "no", "unclear"], "measured_only": [{"id": "count", "topic": "how many items there are", "answered_by": ["gate:containment"]}], "items": [{"id": "contained-inside", "kind": "cut", "per": "cavity", "bad_answer": "no", "gate": "containment", "question": "In the cut through {cavity}, is every {what} ..."}], "questions": [ {"id": "contained-inside:hopper_cavity:ball", "item": "contained-inside", "question": "In the cut through hopper_cavity, is every ball fully inside the inner wall of hopper?", "views": ["cut-cavity-hopper_cavity", "cut-cavity-hopper_cavity-plan"], "bad_answer": "no", "gate": "containment"} ] }, "not_rendered": [{"what": "joint I12", "why": "planar joints have no range to pose"}], "limits": ["Renders are for diagnosis, not proof: ...", "..."], "physical_fit_verified": false } ``` Field notes: - **`parts`** holds the section's parts and the placed fasteners that clamp or thread into them. `sha256` is the hash the geometry record holds for the part (`parts..sha256`, a hash of its instance hashes; for a fastener, of its placed instances): the same hash a reviewer record (`dossier/reviews/*.json`, T11) binds in its `hashes`. `section_hash` changes when any of them does. - **`measurements`** comes before the views on purpose: a reviewer (and the site) reads it first. Per gate: `sha256` is `verdict_digest` of the verdict quoted (its canonical JSON), `status` and `pass` its own, `checks` how many it has; `stale` is null for a current verdict, else the reason (`cadagent.gates.verdict.stale_reason` against the geometry record and, for a project, the gate's declarations and options, as `cad-agent check` judges it: an old verdict is shown as stale, not as a pass). `section_checks` are the verdict's checks that name a section part, an instance of one, a fastener of the section, or a cavity of a section part. `missing_gates` lists the gates a section card names in `checks` that have no verdict. `lint` is the dossier lint, which `review_packet` runs itself when it plans (the lint file records no inputs, so it cannot say whether it is current), so its `file` is null and `computed` true (a lint passed in from a file names the file, is marked stale, and makes the packet stale); its `section_checks` are only those that are not passing. `section.status` and `blocked_by` come from the same lint run. - **`findings`** are the failing and warning checks among them (and every failing check that names no known part, with `scope: "unattributed"`, since it may concern any part). `stale` is the stale reason of the verdict it comes from (null when current). `views` names the views that show it (`findings` when it has a callout). `witness` collects the 3D points in the check's evidence (keys containing `point` or `centroid`, nested witnesses included), at most four per finding. The `findings` view numbers them F1, F2, ... as callouts; a finding without a point is listed, not drawn. - **`views`**: `file` is null for a view not rendered (`only=`); `rendered` counts the views with an image (0 in a plan). `options` are the planning options and `render` the rendering ones (`size`; null in a plan): an earlier image is reused by `only=` only when both, the section hash, the geometry digest and the context all match. `keys` are the assembly keys drawn in the section's colours (plus the context parts in an access view); `offsets` (`{key: [dx, dy, dz]}`) the exploded displacements; `pose` (`{interface, kind, value, unit, body, keys, axis}`, plus `slide_mm` for a cylindrical joint) the joint position shown, as an offset from the modelled position, right-handed about the axis; `callouts` (`[{code, finding, label, point, text}]`) the numbered witness points; `fit` the box the camera frames. `camera` is a named view (`iso`, ...) or the direction from the scene to the camera; `clip` keeps the side its normal points to. - **`not_rendered`** lists what the packet could not show and why: a card with no shape, a planar or ball joint (no range to pose), a fastener without axes, parts no step places (drawn in place in the exploded view). - **`checklist`** is the machine-readable render checklist (below), instantiated for this section: one question per applicable item and target, each naming the views that answer it. - **`context[]`**: each context item's assembly key, its section (empty for a fastener), `kind` (`part` or `fastener`) and content hash. - **Staleness.** `cadagent.packet.packet_stale(manifest, record, verdicts=None, expected=None, lint=None)` is None while the packet is current evidence, else the reason: the geometry record changed; the packet quoted a verdict that was already stale, or a lint read from a file; or, given the current verdicts (with each gate's expected declarations and options) and lint, one of them was rewritten, added or removed, or is stale now (a dossier edit that changes a gate's declarations makes the quoted verdicts stale without touching the geometry). With `current=` (the packet planned again), it is also stale when the dossier text it quotes changed: `dossier_digest` covers the section's title, purpose and requirements, each part's function and decisions and the checklist questions (top-level `dossier_digest` in the manifest). `project_packet_stale(manifest, root)` gathers all of that for a project, kernel-free. A packet is evidence only for the hashes and verdicts it lists. ## The render checklist (`cadagent.packet.CHECKLIST`) Specific yes/no questions, each tied to a view kind (plan §5.2). `per` says what an item is asked for: once per section, or once per cavity and contents (from the cards' `contains`), joint, fastener group, access direction or pose. `answers` are `yes`, `no` or `unclear`; `unclear` is a valid answer and is never read as a pass. `bad_answer` is the answer that is a finding. | Item | Asked of | Question (templated) | Finding when | |---|---|---|---| | `contained-inside` | `cut-cavity-*` (side and plan) | In the cut through {cavity}, is every {what} fully inside the inner wall of {part}? | no | | `joint-bearing` | `cut-joint-*` | In the cut through {interface}, does {moving} turn or slide on a real bearing feature of {fixed} (a bore around the pin, a slot around the slider), with no material in its path? | no | | `fastener-stack` | `cut-fastener-*` | In the cut through {fastener}, does the head sit on {clamps} and does the thread end inside {into}? | no | | `mating-feature` | `exploded` | In the exploded view, does every part have a visible mating feature (bore, boss, slot, seat, screw hole) that meets one on its neighbour? | no | | `assembly-order` | `exploded` | Does the explode order (later steps further out) look buildable: nothing shown going in through a part fitted before it? | no | | `plain-box` | `solid-iso`, `solid-rear` | Does any part appear as a plain box with no features where a mechanism or interface should be? | yes | | `matches-card` | `solid-iso`, `solid-rear` | Does every part look like the function its card states (`parts.*.function`)? | no | | `floating` | `solid-rear` | Is anything floating in the opposite iso: a part with nothing visibly holding it? | yes | | `hidden-overlap` | `xray-iso` | In the x-ray, does any part sit inside another part's material where no insert or press fit is declared? | yes | | `fastener-access` | `access-from-*` | Looking along the screws' axis from the tool's side, is every head of {fasteners} visible and unobstructed? | no | | `motion-contact` | `pose-*` | At this end of {interface}'s range ({value}), does {moving} touch anything other than its declared contacts ({contacts})? | yes | | `findings-located` | `findings` | Is each numbered callout on the parts its finding names? | no | **Not asked of an image** (`MEASURED_ONLY`): how many items there are, gaps and clearances under about a millimetre, wall thickness, thread engagement and screw length, overlap volumes, and whether something moves clear between the drawn poses. The gates measure those; the checklist names the gate for each and tells the reviewer to read it from `measurements` instead. When a measurement and an image disagree, the measurement wins and the disagreement is itself a finding (a wrong declaration, or a view that misleads). ## How a reviewer uses it The reviewer pass ([`dossier.md`](https://cad-agent.valpatel.com/docs/dossier.md#the-reviewer-pass), plan §5.3) copies this manifest and its images into the reviewer's bundle (`cad-agent review-section
`) with the section YAML, the gate verdicts and the diff since the last sign-off. The reviewer reads `measurements` and `findings` first, then answers every question in `checklist.questions` from the views listed with it, and submits a record bound to the part hashes in `parts.*.sha256`. Each answer names the views it was read from and says whether it rests on the images alone: ```json {"checklist": [{"id": "contained-inside:hopper_cavity:ball", "answer": "yes", "evidence": "...", "views": ["cut-cavity-hopper_cavity-plan"], "image_only": false, "measurements": ["gate:containment"]}]} ``` A sign-off needs the packet to be current (`project_packet_stale` is None): render it again after the gates run. ## Limits The packet shows the model, not the part: renders do not prove fit, strength or that a mechanism works. A view is tessellated (chord error), so a small gap or overlap can look closed or open; that is why the checklist does not ask for one. Cut planes are fixed (through a cavity's box centre, through an axis); a defect away from the plane is only seen in the other views. Poses are the declared range ends, not the path between them (the motion gate samples the path). Exploded offsets are illustrative: their direction comes from the declared steps, their length from the section's size. --- Source: https://cad-agent.valpatel.com/docs/subassembly-insertion.md # Rigid subassembly insertion A preassembled module needs two separately checked sequences: its internal bench assembly and its insertion into the parent. Treating its components as independent parent steps invents an installation order and can claim tool access in the wrong surrounding geometry. Additive Python API (issue #68): ```python assembly_gate(assembly, parent_steps, fasteners=specs, tools=tools, subassemblies={ "electronics": { "members": ["carrier", "board", "insert", "board_screw#0", "board_screw#1"], "steps": bench_steps, }, }, service_after={"electronics": ["bumper", "retainer#0", "retainer#1"]}, ) # parent_steps contains an ordinary fit="electronics", direction="-X" step. ``` `members` names exact existing geometry instances, including every attached fastener solid. Members must be known, unique and disjoint; a module name cannot shadow a part or fastener. Nested modules and changed internal poses are outside this first API. Internal fasteners must connect only members of that module, with all their solids represented. External retainers remain parent fasteners with their actual geometric identities. The internal plan is executed against only its members, using the same geometry, insertion directions and tool envelopes. Missing coverage, failed checks or unperformed insertion/tool checks prevent a qualified parent result. Scoped qualification therefore rejects `tool_access=False`; all steps need a direction. Internal screws are not driven again at the parent step. The parent treats the module as rigid: every member receives the same translation, and every member, including attached screw solids, participates in collision checks. A module name is not a blanket intended-overlap exemption; existing explicitly declared internal pair overlaps keep their recorded reason. Cross-boundary overlap exemptions are unsupported and rejected. `service_after` is conditional evidence: listed parent parts/fastener instances are assumed already removed. Unknown, duplicate, target-member or otherwise ambiguous prerequisites are rejected. All external retaining fasteners must be listed. The whole module's reverse insertion path is checked against all remaining final geometry. This does not prove that the listed prerequisites can themselves be removed, that their tools can reach, or that harnesses are disconnected. Those claims need separate evidence. Internal fasteners travel with the module and are not removal prerequisites. The ordinary `service=True` or `service=[module]` mode cannot silently stand in for module prerequisites: requesting a module that way requires its `service_after` entry. Explicit service targets must resolve to parent parts or modules. Individual module members (including instance-owner aliases), unknown names and fastener-only targets are rejected before geometry; member teardown is outside this API. A module's conditional withdrawal does not answer a request to remove one of its members. Supported parent part aliases are expanded to exact instances, each with its own service check. Other individual parent parts retain their existing service behavior. A conditional pass means only that withdrawal is clear in the stated remaining geometry; it is not an unconditional service-ready claim. Existing single-part calls and their verdicts remain unchanged. The first interface is Python-only, through `run_sequence`, `assembly_gate` and the keyword pass-through in `assembly_from_build`; it does not silently extend registry/YAML/CLI schemas. Internal results and parent results are separate in JSON/Markdown. The adapter does not change the separate fastener-engagement, connectivity, tolerance or motion gates. Declaring a module rigid is an assumption, not proof that its joints hold it rigid. Validation in `tests/test_gate_subassembly.py`: kernel-free structural/order/coverage and scoped-tool tests; rigid member and attached-screw path blockers; rejected cross-boundary/partial fastener declarations; failed internal-plan propagation; conditional removal with retained versus removed obstacles; unknown kernel responses; small BREP positive/negative fixtures. Existing assembly and fastener suites remain regressions. The same straight-line sampling, rigid geometry and tolerance limits apply: a model pass does not prove printed fit, strength, service ergonomics or hardware readiness. --- Source: https://cad-agent.valpatel.com/docs/toolkit.md # cad-agent toolkit: verification and review reports for 3D-printed CAD projects `cad-agent` runs a CAD project's build and checks, verifies its print files, and turns the results into a standalone HTML report and a review package. It is generic. The geometry and the design-specific checks stay in the project; the toolkit (Python package `cadagent`) provides the runner, the 3MF writer, shared geometry screens, the renderer, link validation, the report, packaging and a local server. ```bash pip install -e ".[cad]" # in the cad-agent checkout; or install it into the project's own venv cd my-cad-project cad-agent all # build -> export-3mf -> check + report -> package cad-agent check # re-collect results; exit 1 if anything fails cad-agent report --open # exports/cad-agent-results.json + docs/cad-agent-report.html cad-agent serve --port 8000 # needed for "Open in Bambu Studio" links ``` Without the console script, `python -m cadagent.cli ` does the same. The toolkit started as `vptest cad` in the vptest framework; projects set up for it keep working (see [Coming from vptest](#coming-from-vptest)). ## The project contract ``` my-cad-project/ cad-agent.yaml # cad: section (below) cad/parameters.json # the single parameter authority (dimensions, clearances, revision) cad/build.py # builds every part; exposes print_parts() and the print rotations tools/*.py # project-specific checks, renders, docs; one JSON per check exports/ # .stl/.3mf/.step, geometry-checks.json, .json, logs preview/ # renders (part-.png per part) + gallery index.html docs/ # engineering review pages, BOM, registers, cad-agent-report.html ``` 1. **One parameter authority.** Every dimension, clearance and the `revision` live in `cad/parameters.json`; scripts read it, nothing restates a number. 2. **`cad/build.py`** exposes `print_parts() -> {name: cadquery.Shape}` and a print-rotation table. Its `__main__` writes, per part, STEP in assembly coordinates and STL in print orientation, and the parts file `exports/geometry-checks.json`: `{part: {cad_valid, solid_count, watertight, print_bounds_mm, ...}}`. `cadagent.geometry` does the generic part: ```python from cadagent.geometry import export_print_stl, solid_report, pairwise_intersections report = {n: export_print_stl(s, OUT / f"{n}.stl", rotation=PRINT_ROTATION.get(n)) for n, s in print_parts().items()} # raises on a non-watertight STL ``` 3. **Each check is a script** in `tools/` that writes `exports/.json` with a top-level boolean `"pass"`. A check that cannot compute its answer writes `"pass": false` with the reason; it never omits the file or the key. 4. **Renders** go to `preview/`; `preview/part-.png` is picked up as the evidence image for that part. `cadagent.render.Renderer` renders cadquery shapes or STL files offscreen with a caption bar, subtitle, note bar and leader labels; set `CAD_AGENT_RENDER_ONLY=a.png,b.png` to re-render a subset. 5. **A gallery page** (`preview/index.html`) can embed the per-part download table from `cadagent.report.downloads_snippet(project, page_dir=...)`: thumbnail, STL / 3MF / STEP links and a slicer link. ### Honest evidence - CAD checks never claim physical fit. Every check JSON that touches fit should carry `"physical_fit_verified": false` (and `"slicer_run": false` for print screens) until someone has printed and fitted the part. The report records both as false and says so in a banner. `cad-agent` has no way to set them true. - A check is green only when its `pass_key` value is the boolean `true`. A missing file, missing key, `"yes"`, `1` or a list all count as failures. - In the results and the report, a row status other than the known pass, fail and skip spellings counts as a failure, never as a pass. - The overhang screen (`geometry.downward_area`) is a screen, not a slicer. - `export-3mf` re-reads every 3MF it writes and deletes it unless triangle count, edge-manifold watertightness and volume (within 0.1 %) match the STL. ### Print files | File | Orientation | Notes | |------|-------------|-------| | `exports/.stl` | print orientation, lowest point on z=0 | degenerate faces removed; must be watertight | | `exports/.3mf` | print orientation, centred on `printer.bed_mm` | written by `cad-agent export-3mf`, read back and verified; byte-reproducible (fixed zip dates), so a rebuild of unchanged geometry keeps the slice report current | | `exports/.step` | assembly coordinates | for CAD review and re-use | "Open in Bambu Studio" uses `bambustudio://open?file=&name=`. The slicer fetches the file over http, so the link only works when the page is served (`cad-agent serve`); from `file://` the link downloads the 3MF instead. ## The `cad:` section of `cad-agent.yaml` `cad-agent` reads `/cad-agent.yaml` when it exists, otherwise `/vptest.yaml`. Only the `cad:` section is read; other top-level sections (such as vptest's `project:` and `projects:`) are ignored. The first file found is the config: a `cad-agent.yaml` without a `cad:` section is an error even if `vptest.yaml` has one. | Key | Default | Meaning | |-----|---------|---------| | `name`, `title` | folder name | identity in reports and package names | | `revision` / `revision_from` | none | literal, or `"path/file.json:dotted.key"` read at report time | | `python` | `/.venv/bin/python`, else cad-agent's python | interpreter for the steps | | `exports_dir`, `preview_dir`, `docs_dir` | `exports`, `preview`, `docs` | | | `steps` | `[]` | `{name, run, log?, timeout_s=1800, needs?}`; `run` is a script relative to the root, run with cwd = root and `CAD_AGENT_PROJECT` set to the root; `log` is a file in `exports_dir`; `needs` names earlier steps | | `parts_file` | `exports/geometry-checks.json` | part -> `{cad_valid, solid_count, watertight, print_bounds_mm}` | | `part_ids` | `{}` | part -> display id (`P1`) | | `checks` | `[]` | `{id, title, file, pass_key="pass", evidence?, links?}`; `pass_key` may be a list (all must be `true`) | | `printer` | none | `{name, bed_mm: [x, y, z]}`; parts must fit; 3MF files are centred on it | | `package` | none | `{name, include: [globs]}`; zip + `-manifest.json` (sha256) in `exports_dir` | | `release` | every part, default source globs | the print release (`cad-agent release`): see [the print release](#the-print-release-cad-agent-release) | | `serve` | `127.0.0.1`, port 0 (any free) | `{host, port}` | | `html_pages` | `[]` | HTML / Markdown pages whose local links must resolve | Unknown keys are reported as a warning, not an error. ### Worked example: j501-agx (Jetson AGX Orin enclosure) ```yaml cad: name: j501-agx title: reServer J501 / Jetson AGX Orin enclosure revision_from: cad/parameters.json:revision printer: {name: Bambu Lab P1S, bed_mm: [256, 256, 256]} steps: - {name: build, run: cad/build.py, log: build.log} - {name: export-3mf, run: tools/export_3mf.py, needs: [build]} - {name: clearances, run: tools/check_clearances.py, log: carrier-clearances.log, needs: [build]} - {name: shells, run: tools/check_shells.py, log: shell-clearances.log, needs: [build]} - {name: assembly-paths, run: tools/assembly_paths.py, log: assembly-paths.log, needs: [build]} - {name: print-review, run: tools/print_review.py, needs: [build]} - {name: design-metrics, run: tools/design_metrics.py, log: design-metrics.log, needs: [build]} - {name: mounts, run: tools/check_mounts.py, needs: [build]} - {name: storage, run: tools/check_storage.py, needs: [build]} - {name: docs, run: tools/write_docs.py} - {name: check-sheet, run: tools/check_sheet.py} - {name: detailed-assembly, run: tools/detailed_assembly.py, log: detailed-assembly.log} - {name: render, run: tools/render.py, log: render.log} - {name: package, run: tools/package_review.py} - {name: validate, run: tools/validate_review.py} part_ids: {base: P1, base_mount: P1M, cover: P2, front_gmsl: P3, front_no_gmsl: P4, rear_io: P5, mount_fit_template: P9, side_hatch: P10, m3_insert_coupon: P11, rail_coupon: P12, rail_key: P13, countersink_coupon: P14} checks: - {id: carrier, title: Carrier and hardware clearances, file: exports/carrier-clearances.json, evidence: [preview/open.png]} - {id: shells, title: Open-shell clearances, file: exports/shell-clearances.json} - {id: paths, title: Plate insertion paths, file: exports/assembly-paths.json, evidence: [preview/exploded.png]} - {id: mounts, title: Mounting tabs and driver access, file: exports/mount-checks.json, evidence: [preview/mount-clearance.png]} - {id: storage, title: Storage trays and underside slots, file: exports/storage-checks.json, evidence: [preview/storage-stack.png]} - id: review title: Review validation file: exports/review-validation.json # this file has no top-level "pass"; every listed key must be true pass_key: [all_parts_single_valid_watertight, plastic_intersections_zero, carrier_and_hardware_pass, open_shell_pass, mount_checks_pass, assembly_paths_pass, nominal_P1S_bounds_pass] package: name: j501-agx-fit-check include: [README.md, requirements.txt, cad/parameters.json, "docs/*", "exports/*.stl", "exports/*.3mf", "exports/*.step", "exports/*.json", references/sources.json] html_pages: [preview/index.html, README.md, docs/engineering-review.html] ``` Migration notes for this example. `tools/export_3mf.py`, `tools/package_review.py` and `tools/serve_preview.py` duplicate `cad-agent export-3mf`, `package` and `serve`; once those commands are adopted, drop the matching steps. Otherwise `cad-agent all` writes the 3MF files twice (harmless) and two packages. `tools/validate_review.py` can stay as a project check or give way to `cad-agent check`, which covers parts, print files, the configured checks, local links and, with `cad: dossier:` (or `cad: gates:`), the integrity gates' verdicts and the dossier lint. A YAML flow mapping ends a value at the first comma, so a title with a comma must be quoted: `{id: storage, title: "Storage trays, cable routes", file: ...}`. Unquoted, the title is cut at the comma and the rest becomes a stray key. ## Commands | Command | Does | Exit code | |---------|------|-----------| | `cad-agent build [--from STEP] [--only STEP ...]` | runs steps in order with the project python; per-step timing; stops at the first failure and prints the log tail | 1 on a failed step | | `cad-agent export-3mf` | `.stl` -> verified, bed-centred `.3mf` for every part in the parts file | 1 on a read-back mismatch | | `cad-agent holes [PART ...]` | checks every hole in every part is clear of material: finds the cylindrical holes and countersinks in `exports/.step` (concave, at least 200° of wall, up to 30 mm across), probes each with a cylinder or cone just inside its wall, and writes `exports/hole-checks.json` bound to each STEP's sha256; `all` runs it and the report's "Holes clear" rows fail on material in a hole, a stale result or a check that did not run (config `cad: holes:`, [below](#holes-clear)) | 1 if a hole has material inside | | `cad-agent check [--json]` | collects results (no build), prints a table or the results JSON | 1 if any row fails | | `cad-agent report [--open]` | writes `exports/cad-agent-results.json` (schema 1.0, the vptest web shape, so `vptest web validate` and its renderer still accept it) and a standalone `docs/cad-agent-report.html` with inline styles (light and dark) that opens from `file://` | 1 if any row fails | | `cad-agent package` | zip + sha256 manifest from `package.include` | | | `cad-agent release [--out DIR] [--no-zip] [--revision TEXT] [--date D]` | the print release: `release/-rev/index.html` (parts with thumbnails, print settings, slicer mass and time, STEP/STL/3MF links; assembly pages; hardware; source) and a reproducible zip next to it ([below](#the-print-release-cad-agent-release)) | 2 on a bad config (a path outside the project), a missing part file or a foreign `--out` folder | | `cad-agent serve [--port N] [--host H]` | serves the project root; writes the URL to `preview/server-url.txt` | | | `cad-agent site [--serve]` | builds `site/` from milestones, decisions, a designs summary, Markdown pages and git history (config `cad: site:`); `--serve` rebuilds on change and serves the web kit at `/_cadagent/kit/`. One top bar links every page: `site: nav:` (`[{id, title, href}]`, hrefs from the project root) or, when unset, the overview, docs, 3D model and each `html_pages` page plus the check and simulation reports, the design comparison and the search leaderboard. The server adds the bar to every HTML page it sends (files on disk unchanged; a bar the page mounts itself wins; `` opts a page out). A project with a `dossier:` or `gates:` key also gets the review pages in `site/review/`: a page per dossier section, the requirements table and the gates page with the connectivity graph ([dossier.md](https://cad-agent.valpatel.com/docs/dossier.md#on-the-review-site)) | | | `cad-agent view` | writes every part in assembly position to `exports/view/` (STL + `view.json`) for the 3D viewer (config `cad: view:` for colours, hidden variants, explode directions, extra parts from STEP or STL, list categories) | | | `cad-agent webkit [DEST]` | copies the web kit (3D stage, model viewer, HUD panels, three.js, fonts) into the project, default `web/kit` | | | `cad-agent dossier [--json] [--no-write] [--min-status S]` | lints the design dossier (`cad: dossier:`) against the build's geometry record, the gate verdicts, the configured checks and the reviewer records; writes each section's and part's status to `exports/dossier-status.json` and the lint verdict to `exports/gates/dossier-lint.json` ([dossier.md](https://cad-agent.valpatel.com/docs/dossier.md#lint-and-status)) | 1 on a lint error or a section below `--min-status` (default `gated`) | | `cad-agent review-packet [SECTION ...] [--list] [--view V ...] [--size WxH] [--explode F] [--context C]` | renders each dossier section's review packet into `exports/review/
/` with `manifest.json` (`cad-agent.review-packet/1`): the gate measurements and findings first, captioned views with content hashes, the render checklist ([review-packet.md](https://cad-agent.valpatel.com/docs/review-packet.md)); `--list` plans without rendering | 2 on an unknown section or with no geometry record | | `cad-agent review-section SECTION [--bundle-dir DIR] [--since REV]` | writes the reviewer's prompt bundle into `exports/review-bundle/
/` (`cad-agent.review-bundle/1`): the section YAML, the gate verdicts for its parts, the packet manifest and images, the checklist, the diff since the last sign-off and a record template ([dossier.md](https://cad-agent.valpatel.com/docs/dossier.md#the-reviewer-pass)) | 1 while a sign-off would be refused (stale packet, section below `gated`, open decision, unknown builder); 2 on an unknown section or with no build | | `cad-agent review-section SECTION --submit RECORD` / `--check RECORD` | checks a filled-in reviewer record against the current build (hashes current, the reviewer, `CAD_AGENT_ACTOR`, is not the section's builder or author, every question, decision and requirement answered; for a sign-off: packet current, no open decision) and writes `dossier/reviews/
-.json` (`--check` writes nothing) | 1 when the record is refused | | `cad-agent gates [--only G ...] [--skip G ...] [--jobs N] [--no-cache] [--build] [--dry-run]` | runs every integrity gate that applies to the dossier geometry, each in its own process, then the dossier lint; writes `exports/gates/.json` and `.md`, bound to their inputs (config `cad: gates:`, [gates.md](https://cad-agent.valpatel.com/docs/gates.md#running-the-gates-cad-agent-gates)) | 1 if a gate (or its build) fails; 2 without a `dossier:`/`gates:` key, on a bad config, or with no geometry record and no build | | `cad-agent all` | build, export-3mf, holes, gates and the dossier lint (when `cad: dossier:` or `cad: gates:` is set), check + report, package | 1 if anything failed | | `cad-agent catalogue [--json] [--errors-only]` | checks the part catalogue and every design against it (config `cad: explore:`, [explore.md](https://cad-agent.valpatel.com/docs/explore.md)) | 1 on an error | | `cad-agent compare [DESIGN ...] [--swaps] [--workers N] [--json] [--open]` | evaluates the designs with the project's evaluator, ranks them (Pareto fronts over the objectives) and writes `comparison.json`, `compare.html` and site design cards; `--swaps` tries every one-part swap | 1 if a design could not be evaluated | | `cad-agent optimise [--trials N] [--workers N] [--sampler S] [--fresh]` | searches slots and parameters (random, grid; NSGA-II and TPE with optuna) and keeps a leaderboard | 1 if no trial is feasible | `--project PATH` (default: cwd), before or after the subcommand, selects the project. Exit code 2 means the project or environment is unusable: a missing or invalid `cad:` section, a missing build output, or a missing optional dependency. cad-agent names the dependency and the `cad` extra and does not skip the work silently. ## Python API ```python from cadagent.project import load_project # CadProject, Step, Check, CadConfigError from cadagent.geometry import (solid_report, export_print_stl, pairwise_intersections, min_gap, downward_area, within_bed) from cadagent.mesh3mf import write_3mf, read_3mf, verify_3mf, export_parts_3mf, is_edge_manifold from cadagent.render import Renderer from cadagent.report import collect, write_report, downloads_snippet, bambu_studio_url from cadagent.htmlreport import render_page, canonical_status, SCHEMA_VERSION from cadagent.links import missing_links from cadagent.connectors import (PLUG_FAMILIES, plug_segments, window, window_margins, PanelFrame, envelope, check_ports, draw_ports) from cadagent.package import build_package from cadagent.serve import make_server from cadagent.fea import Study, MATERIALS, Material, plane_at, cylinder_at, in_box, near, any_of from cadagent.robot import (Body, combine, box_body, cylinder_body, solid_mass_properties, printed_part_body, Link, Joint, Visual, Collision, Motor, write_urdf, write_mjcf, check_urdf) from cadagent.explore import (explore_config, load_configured, check_catalogue, load_design, resolve, bom, load_evaluator, evaluate_design, compare_designs, comparison, write_comparison, swaps) from cadagent.explore.optimise import run_search from cadagent.gates import (Assembly, Tolerances, relation_table, relate_pair, relate, solid_interference, PairCache, shape_content_hash, Check, verdict, write_verdict) from cadagent.gates.wall import check_wall, wall_gate, mesh_thickness, brep_thickness, brep_thickness_near ``` ### Integrity gates (`cadagent.gates`) Assembly-level checks: the pairwise relation table (apart, touching, overlapping, contains, with witnesses and a content-hash pair cache), the verdict file every gate writes, and the minimum-wall gate. Reference and limits: [gates.md](https://cad-agent.valpatel.com/docs/gates.md). ### Explore and compare designs (`cadagent.explore`) For any product built from bought and made parts. A catalogue of parts (specs, prices with the date checked, sources, confidence flags), designs that pick one part per slot plus parameters, and the project's own evaluator (a Python function or any command speaking JSON) give every design its metrics and checks. `cad-agent compare` ranks the designs into Pareto fronts over the configured objectives, tries one-part swaps, and writes a standalone page and design cards for the site; `cad-agent optimise` searches parts and parameters (optuna's NSGA-II with the `explore` extra). The physics stays in the project. The full guide is [explore.md](https://cad-agent.valpatel.com/docs/explore.md); a worked example is [`examples/explore-two-wheel`](https://github.com/Valpatel/cad-agent/tree/dev/examples/explore-two-wheel/). ### Mating connectors (`cadagent.connectors`): standard practice for every port Every external port's window is derived from its **mated plug**, never from the receptacle's bounding box. A bounding box includes pins below the board, which pushes the window low, and it misses plugs that are larger than the receptacle: a screw-flange terminal plug is 15 mm tall on an 8.4 mm header. 1. **Identify the receptacle and its mate.** Get the receptacle's exact part from the schematic. For the mating plug, prefer the manufacturer's drawing or STEP; otherwise use a catalogue or typical value, recorded as an assumption in `basis`. 2. **Measure the face and axis.** Slice the board model at the mating face; exclude pins and pegs below the board. 3. **Describe the port in `parameters.json`** as a `mate` spec in the panel frame (u along the wall, v up, w out): ```json "mate": {"face": [u0, v0, u1, v1], "face_w": 2.08, "centre_on": "receptacle", "clearance": 0.5, "plugs": [{"family": "rj45", "cavity": [u0, v0, u1, v1], "latch": "up"}], "basis": "..."} ``` Families: `rj45`, `trs35`, `terminal_plug_flanged` (Phoenix MSTB STF), `overmold` (USB/HDMI/...), `face_plug` (plug already in the board model) and `box`. Add a family when a new plug type shows up. 4. **The build calls `window(mate, wall_w)`.** Only plug segments that start inside the wall's outer face (they pass through it on insertion) size the window. `centre_on` picks the visual centre: the receptacle face or the plug. 5. **A check step calls `check_ports()`** with the printed parts in assembly position. It sweeps each plug in from outside and requires: * zero interference; * zero interference after growing the plug by the clearance (corner radii included); * no plug-to-plug clash. `draw_ports()` makes the evidence image. Add the check to `checks:` and the review `pass_key`. Worked example: j501-agx `cad/ports.py`, `tools/check_ports.py` and `exports/port-checks.json`. ### 3D viewer (`cad-agent view` + the web kit) Works for any model: a housing, a bracket set, a mechanism or a robot. `cad-agent view` reads each part's STEP (assembly coordinates) and writes a binary STL per part plus `exports/view/view.json`. `viewer.html` in the web kit shows the assembly with a parts list (show/hide, colour), exploded view, section cut and standard views; the site links it when the manifest exists. Wheel zoom (and trackpad pinch) uses one-quarter sensitivity for closer inspection; middle-button drag and touch pinch keep full speed (`dragZoomSpeed`), and small trackpad deltas remain continuous at any device pixel ratio. Custom stages can pass `createStage(el, {zoomSpeed: 1})` for the earlier wheel sensitivity. Update copied web kits, or restart a served site after updating cad-agent. The viewer is scriptable for screenshots: `viewer.html?explode=0.5&view=iso&panel=0`. Show everything the parts fit around in the same viewer, not in a second one: `extra` entries add bought parts and reference boards from a STEP or an STL (`offset: [x, y, z]` mm moves the part into assembly position; `hidden: true` starts it off). A `category` on an entry (`parts_category` for the parts file; `categories: {Test coupons: [part, ...]}` moves parts by name) heads a section of the parts list with one box that shows or hides the whole section, e.g. *Printed parts*, *Cooler*, *Carrier board*. An extra whose file is missing is listed as not exported, so reference models that are not redistributed drop out of a fresh checkout. Configuration errors stop the export before anything is written: two parts with the same name (including an `extra` glob that matches a part from the parts file, which used to overwrite its mesh silently), an `offset` that is not three numbers, and a `categories` entry that is not a list, names a part that does not exist or lists a part twice. ### Stiffness and strength (`cadagent.fea`) `pip install -e ".[cad,fea]"`. `Study(shape_or_step, material, mesh_mm)` meshes any part with gmsh (P2 tetrahedra), `fix()`/`force()`/`torque()` pick faces with selectors (`plane_at`, `cylinder_at`, `in_box`, `near`, `any_of`). A `torque()` is a pure couple about the loaded faces' own centroid (no net force, exactly the torque along the axis); its `point` does not move the axis, so a force at a distance needs its own `force()`. Then `solve()` returns deflection, stiffness, the design von Mises stress and the safety factor against `MATERIALS` (printed materials carry their weak, layer-direction strength). Stress spikes in the elements touching a clamp or a load patch are artefacts of the idealised support, so the design stress and every failure view leave them out ("support patches": capped at the design stress, never shown as failed, their share of the surface reported). Stresses are element corner peaks, never averaged. The P2 strain is linear in a tetrahedron, so its extremes sit at the corners; the safety factor, the hot spots and the per-vertex colours all use the highest corner stress there. The volume-weighted average is exported too and shown as "smoothed". Where it falls well below the peak (a hot spot says so), the mesh is coarse for that gradient. `overstressed_share(load_factor)` is the share of the volume in elements whose peak is past the strength. It counts whole elements, so it is an upper bound on the overstressed volume. `animate()` and the viewer both report it. Showing how it fails, for the report: - `render(png)`: the deformed part coloured by stress. - `animate(mp4)`: the load ramped from zero through the design load to first failure (x safety factor) and past it; regions past the strength turn black, and every frame gives the overstressed volume share (an upper bound). - `export_viewer(json)`: data for the interactive viewer in the web kit (`fea-viewer.js`, `fea.html`). The boundary surface only (a few MB at most at ~100k tets), with per-vertex displacement, peak and smoothed von Mises stress, each triangle's element peak, held/loaded/support flags, the loads, the overstressed volume as an exact step curve over the load factor, and up to eight **hot spots**: the highest stresses away from the supports, at least 4 mesh sizes apart. Each has the load factor at which it reaches the strength, a plain hint from the CAD faces it lies on ("edge of a Ø5.5 hole", "R4 inside fillet", "flat face, 1.9 mm from an R4 inside fillet", "sharp inside corner"), and a mesh note when the smoothed value is far below the peak. Open a case with `kit/fea.html?case=../../exports/fea/-.json` (the kit copied by `cad-agent webkit`, or the copy `cad-agent site --serve` serves at `/_cadagent/kit/fea.html`). The viewer has: - a load-factor slider from 0 to 1.3 x the failure load, and a play button that ramps it (pausing at the design load and at first failure). Deformation and colours scale linearly. Triangles whose element is past the strength turn black (support patches never do), and the status card gives the peak stress and the overstressed volume share; - colour by stress/strength, stress, local safety factor or displacement, with a legend; - deformation exaggeration (true scale up to about 40 x), held faces in grey, and load arrows (curled for torques) labelled with the load at the current factor. Support patches can be greyed out ("not checked"); - the hot-spot list (click to fly there; numbered markers on the part), a section cut (axis and position; it cuts the surface skin, not the stress inside), a hover readout and a pinned probe (the peak stress, the smoothed stress, % of strength, local safety factor and displacement at a point; on a support patch it says "not checked"); - the assumptions: linear elastic, small deflection, isotropic with the weak-direction strength for prints. It shows where and at what load the part first fails, not crack growth. Links and screenshots can set the state: `&load=1.5&field=ratio|stress|sf|disp&spot=1&clip=x:0.4&exag=20&panel=0&play=1`. Values that do not parse are ignored, and `panel=0` hides the panel for that visit only. The page loads only cases and `back` links on its own origin, and `root` must be a relative path. From your own page, call `mountFeaViewer(element, url, { title, back, panel })`. It builds the kit layout inside the element and returns `setLoad`, `setField`, `setExaggeration`, `setClip`, `flyTo`, `play`, `fit` and `dispose` (call it before mounting another viewer on the same page). ### Robot export (`cadagent.robot`, optional) Turns CAD parts into a simulator model with real mass properties: - `solid_mass_properties(shape, density)` reads mass, centre of mass and inertia from the solid (OCP `BRepGProp`). - `printed_part_body(shape, density, wall_mm, infill)` estimates a printed part: solid perimeters and skins plus sparse infill, with the inertia scaled to that mass. It is an estimate, so weigh the printed part. - `box_body`, `cylinder_body` and `point_body` model bought parts from their catalogue mass. `Body` objects add with the parallel-axis theorem, so a link is the sum of everything bolted to it. - `write_urdf` and `write_mjcf` write the same link tree. The MJCF adds motors, joint armature and friction, an IMU site and a floor. Both refuse a closed loop: model a four-bar or five-bar leg as an equivalent serial chain. - `check_urdf` checks the tree, inertias (positive definite, triangle inequality), mesh files and joint limits. text-to-cad's `cadgen urdf validate` is a second, independent validator. ## Simulation and optimisation (standard practice for anything that carries load or heat) Built on mature open-source tools, never reimplemented: OpenFOAM for airflow and heat, gmsh + scikit-fem for stiffness and strength, pyMOTO for topology optimisation, Bambu Studio for real filament grams, PyVista/VTK for pictures and animations. The survey that chose them, with licences and sources, is [`research/simulation-tools.md`](https://cad-agent.valpatel.com/research/simulation-tools.md). Install: `pip install -e ".[cad,fea,sim]"`, plus Docker (OpenFOAM runs in the official `opencfd/openfoam-default` image) and Bambu Studio for slicing. **Standard practice.** For every design that holds a heat source, carries a load or will be printed in quantity: 1. **Slice it** (`cad-agent slice`): grams and print time per part, with the profile named. This is the number a material-reduction claim is judged on, never the CAD volume. The report shows `slicer_run: true` only while the sliced 3MF files are unchanged (sha256). 2. **Load it** (`cadagent.fea`): the real load cases (handling, stacking, mounting shock) with the printed material's layer-direction strength; report the safety factor and deflection, and the overload animation. 3. **Cool it** (`cadagent.cfd`): a conjugate-heat-transfer case of the enclosure in room air, from the same CAD. Report the hottest part against its limit, the fan's operating point, the air entering the fan (recirculation), and the wall temperatures against the plastic's heat-deflection temperature. 4. **Optimise with evidence**: topology optimisation (`cadagent.topopt`) shows where material carries load; the design change is made in the parametric CAD and re-checked by 1-3. Compare revisions side by side. 5. **State the limits** in every result (each module writes them): steady RANS, no radiation, compact models, linear elastic, isotropic. Simulation does not prove fit, strength or temperature; a test does. ### Airflow and heat (`cadagent.cfd`) `ThermalCase(name, domain, cell_mm, ambient_c)` takes CAD (CadQuery shapes, trimesh meshes or STL), in mm: - `wall(name, shape, level)`: no-slip adiabatic obstacles (enclosure, boards); - `solid(name, shape, material, power_w, level)`: a conducting region, optionally a heat source (`SOLIDS` table); - `contact(a, b, thickness_mm, k)`: a thermal pad or paste as an interface resistance; - `fan(Fan(...))`: an actuator disc with a straight-line fan curve between the datasheet's free-air flow and zero-flow pressure, so the operating point comes out of the simulation; - `porous_box(name, lo, hi, d, f, e1, e2, heat_w)`: a compact fin block (Darcy-Forchheimer, nearly closed across the fins) that releases the heat into the air passing through it; - `refine(lo, hi, level)`. `write()` produces a complete OpenFOAM case (snappyHexMesh, splitMeshRegions, chtMultiRegionSimpleFoam, function objects); `run()` runs it natively or in Docker; `results()` returns temperatures, fan flow and pressure, the air temperature through each fan, residual and temperature histories, the heat leaving with the air against the heat put in (`energy_balance_pct`), the cells each fan and fin zone caught, and the limits. `converged` needs the temperatures, fan flow and heat leaving to have stopped changing *and* the energy balance within 5 %. Exchanger fin blocks are set to their power by `calibrate_fins()` (secant steps on the fin temperature, only once the heat leaving has settled; `calibrated` says whether it got there). Two choices matter for honest numbers, and both are covered by tests: - **The open boundary is hydrostatic** (`p_rgh` uniform): still room air stays still. A uniform *static* pressure there drives a fake draft through the room (0.6 m/s mean in a still, unheated test room), which once made every J501 result optimistic. The opt-in test `test_still_room_stays_still` runs that case for real. - **Fan and fin sources are zone totals** (`volumeMode absolute`), so the fan's pressure rise and the fins' conductance do not depend on how many cells the zones catch. ### Heatsinks (`cadagent.heatsink`) `PlateFin(...)` is the analytic plate-fin model (developing laminar channel flow, fin efficiency, spreading resistance, entrance and exit losses; sources cited in the module): `resistance(q)`, `pressure_drop(q)`, `operating_point(hs, FanCurve)`, and `porous_coefficients()` for the CFD fin block. It runs in microseconds, so it is the tool for heatsink studies (fin pitch, thickness, height against a fan curve), with CFD to confirm the chosen design. ### Topology optimisation (`cadagent.topopt`) `TopOpt(lo, hi, voxel_mm, E_mpa)` with `design()`, `keep(shape)`, `void(shape)`, `fix(lo, hi)`, `load(lo, hi, N)`, then `run(volfrac, filter_mm, overhang='+z', maxit)`: minimum compliance with SIMP, a density filter (minimum member about twice `filter_mm`) and Langelaar's overhang filter for support-free printing, solved with MMA. `render()`, `animate()` (density evolution) and `export_stl()` show the result. It is a design guide: turn it into ribs, pockets or vents in the CAD and check that with FEA. ### Pictures and animations (`cadagent.simviz`) `CFDResult(case)` loads a finished case in mm and °C. `section()`, `flow()` (streamlines), `walls()` (wall temperature with the region past a limit in the critical colour, named in the legend), `animate_flow()` (dashes moving at the air's speed while the camera turns), `animate_sweep()` (a section sweeping through) and `convergence_chart()`. Colour follows the job: one-hue ramps for temperature (orange) and speed (blue), never a rainbow. ### Real grams (`cadagent.slicer`, `cad-agent slice`) Slices each part's 3MF with Bambu Studio's command line (an external AGPL program, run as a subprocess). The system profiles resolve `inherits`, then ordered `include` templates, then explicit child settings (unresolved profiles can report 0 g or retain generic startup G-code). Missing, cyclic, duplicate, malformed or escaping profile dependencies fail explicitly. `flatten(kind, name)` returns the resolved settings; `resolve_profile(kind, name)` also returns `files`, the relative paths and SHA-256 hashes of the exact consumed bytes. Individual slice results record those dependency hashes and the settings JSON hashes. These identities describe the captured inputs, not ongoing filesystem stability or validation of the resulting executable G-code. Inspect actual temperature waits and startup order before printing; a filament label or successful slice does not establish physical readiness. Configure under `cad.slicer`: `printer`, `process`, `filament` (`pla`, `petg` or a profile name), `overrides` (process settings) and `parts`. Writes `exports/slice-report.json`. ### The print release (`cad-agent release`) What someone who wants to *print and build* the design downloads, as opposed to the review package (`cad-agent package`), which carries the evidence. `cad-agent release [--out DIR] [--no-zip] [--revision TEXT] [--date YYYY-MM-DD]` writes `release/-rev/` (or `DIR`) and `.zip` next to it (with `--out DIR`, the zip is `DIR.zip`): | Path | Content | |---|---| | `index.html` | one standalone page (inline CSS, light and dark, no scripts, opens from `file://`): the release's parts and totals, the recommended print settings (`cad.slicer`, else those of a current slice report; with neither, the page says no slicer settings are configured), then each group of parts (with its own count, mass and time) as cards: thumbnail, quantity, material, notes, mass and time per copy, and the STEP, STL and 3MF; then the assembly pages, the hardware table and the source | | `cad/` | `.step`, `.stl`, `.3mf` from `exports_dir`, plus `extra_cad` | | `images/` | the thumbnails | | `docs/` | the `pages`, and the images they link to, at their project paths, so their relative links keep working; links to files the release carries elsewhere (`cad/`, `source/`) are rewritten in the copies; Markdown is also rendered to HTML (with the `markdown` package) | | `source/` | the files matched by `include` minus `exclude` | | `manifest.json` | `format: cad-agent.release/1`, `revision`, `date`, `git` (`commit`, `dirty`, `modified`, `untracked_released`), `slicer`, `parts` (`part`, `group`, `optional`, `pick_one`, `quantity`, `grams`, `minutes`), `build` (`title`, `parts`, `grams`, `minutes`; null without one; `grams` and `minutes` are null while any of its parts is not sliced), `warnings`, and `files` (the sha256 of every file) | Rules the command enforces: - **Inside the project only.** Every configured path and glob must be relative, without `..`; a file that resolves outside the project (a symlink) is skipped with a warning, and nothing is written outside the release folder. - **No hidden or git-ignored files** from globs (`.env`, a gitignored vendor STEP; tracked files, even force-added ones, are kept); each skip is a warning, and so is a git error that stops the ignore check. Any file over `max_file_mb` (default 20) is a warning. - **Slicer numbers only while current:** a part's mass and time are shown only when the slice report was made with the settings `cad.slicer` names now and the part's 3MF still has the sha256 recorded for it; otherwise the card says **not sliced** and the totals leave it out (run `cad-agent slice`). - **Honest counts:** the page counts the parts in the release and each group's parts. It does not claim a number of pieces to print, because groups may hold alternatives (`pick_one: true`: one of two bases; its count line says "print one of these" instead of a total, and needs two parts or more) and optional parts (`optional: true`). With a pick-one group the page leaves out the filament and time over every part, which would add up the alternatives. For what one build takes, name it under `build`: the page's first figure is then that build's filament and time, at the listed quantities, while every part in it is sliced (otherwise it says "not sliced"). A build takes at most one part from a pick-one group; one that takes none from a group that is not optional is a warning. - **What was built:** the release copies the working tree; uncommitted changes to tracked files are a warning and are recorded in the manifest (`git.dirty`, `git.modified`) and on the page; released files that are not in git at all are a warning too (`git.untracked_released`), since the recorded commit cannot rebuild them. The date is `--date`, `cad.release.date`, `SOURCE_DATE_EPOCH`, else the last commit's date; outside git with none of them it is today, with a warning, so the release is not reproducible across days. - Parts in the parts file that no group lists are a warning. The folder is rewritten from scratch each run (a non-empty folder that is not an earlier release is refused); the zip has sorted entries with fixed dates, so the same files give the same bytes. ```yaml cad: release: title: J501 enclosure # default: cad.title revision: 0.9.1 # default: --revision, else cad.revision / revision_from groups: # or `parts: [a, b]`; default: every part in the parts file - {title: Enclosure, parts: [base, cover, rear_io], note: "Every build: one base and the cover."} - {title: Front plate (print one), parts: [front_gmsl, front_plain], pick_one: true} - {title: Drive storage, parts: [storage_35], optional: true} build: {title: Desktop build, parts: [base, cover, rear_io, front_plain]} # one complete build, totalled quantities: {rail_key: 2} # default 1 material: PLA # default: the filament in cad.slicer notes: # label -> text, shown on the part's card in this order cover: {orientation: roof down on the bed, supports: none} # quote values that contain commas thumbnail: "{preview}/part-{part}.png" # the default; only {part} and {preview} (preview_dir) extra_cad: [exports/assembly.step] # never vendor geometry pages: [ASSEMBLY.md, "docs/assembly/*"] hardware: docs/hardware.csv # or a list of mappings: [{item: M3 x 10, qty: "4"}] include: ["cad/**", "tools/**", cad-agent.yaml, "requirements*.txt", README.md, "LICENSE*"] # the default exclude: ["**/__pycache__/**", "**/*.pyc"] # the default max_file_mb: 20 date: 2026-10-29 ``` **Publishing.** The default `release/` folder is inside the project: add `release/` to `.gitignore`. Attach the **zip** to the GitHub release: GitHub serves release assets as downloads and does not render an attached `index.html`, and on its own that page's `images/`, `cad/` and `docs/` links would not resolve. Say in the release notes to unpack the zip and open `index.html`, or publish the folder with GitHub Pages to browse it online. ## Tests ```bash pip install -e ".[cad,dev]" pytest # or, with a CAD project's venv that already has cadquery, trimesh, vtk and PIL: PYTHONPATH=src /path/to/project/.venv/bin/python -m pytest tests -q ``` The geometry and render tests skip unless cadquery and vtk are importable. ## Coming from vptest | vptest cad | cad-agent | |------------|-----------| | `vptest cad ` | `cad-agent ` (same subcommands, options and exit codes) | | `vptest.yaml` | `cad-agent.yaml`; `vptest.yaml` is still read when `cad-agent.yaml` is absent | | `exports/vptest-cad-results.json` | `exports/cad-agent-results.json` (same schema) | | `docs/vptest-cad-report.html` | `docs/cad-agent-report.html` | | `VPTEST_CAD_PROJECT` (set for steps) | `CAD_AGENT_PROJECT`; the old name is still set | | `VPTEST_CAD_RENDER_ONLY` | `CAD_AGENT_RENDER_ONLY`; the old name is read when the new one is unset | | `from vptest.cad.x import ...` | `from cadagent.x import ...` | Pages that link to the old report or results file names need their links updated, or `html_pages` will report them as broken. ## Holes clear A rib, boss or skirt unioned after the holes were cut refills part of a hole. The part is still one valid watertight solid, and fit, FEA and slice checks all pass. `cad-agent holes` (run by `all`) catches it from each part's STEP with no declarations. A hole is a set of concave cylindrical faces on one axis whose wall reaches at least 200° around the circle, so a rib right across a hole still leaves one, while fillets and slot ends do not count. Holes are up to 30 mm across. Each hole is probed with a cylinder 0.05 mm inside its wall over its full depth; material inside above 0.05 mm³ fails, with the hole's axis, diameter, depth range and the centre of the intrusion. Coaxial holes in opposite walls are separate holes. Two same-size bores with a smaller hole through the solid between them are a stepped bore: the **ledge** is listed for a person to confirm, not failed. **Countersinks** (and any concave cone on a hole: chamfered mouths, drill-point tips) are checked the same way: the cone's faces are grouped by axis, apex and angle, and probed with a truncated cone 0.05 mm inside the surface. A rib that reaches into a countersink without touching the bore below it fails as `kind: countersink`, with the included angle and the two end diameters; a flat-head screw would not seat there. Cones are labelled by shape: `countersink` (truncated, 55–130° included), `drill point` (closes to its apex) or `tapered hole` (drafted holes and pockets); material in any of them fails. `max_diameter_mm` applies to a cone's largest diameter, so countersinks over 30 mm (M16 flat heads and up) are only checked with a larger limit, and a countersink cut beside a wall takes its largest diameter from the notch it leaves there. The result (`exports/hole-checks.json`) is bound to each STEP's sha256 and to the check's version and settings. A changed STEP or a changed `cad.holes` makes it stale, and a stale result fails its row like a failing one. `cad-agent holes PART` rechecks one part and keeps the others' rows. `--out FILE` writes elsewhere, for a reviewer who must not change the checkout. Expect about 10 s for a large enclosure part. ```yaml cad: holes: exceptions: # holes with material in them on purpose, each with its reason base: - {at: [1.25, 46.5, 62], diameter_mm: 6.6, reason: anti-rotation flat for the RP-SMA bulkhead (D-hole)} # at: a point on the hole's axis (part coordinates); radius_mm: how far from the axis it may be (default 0.5) skip: {gasket: printed in TPU without fastener holes} # parts not checked, with the reason # tol_mm: 0.05, min_volume_mm3: 0.05, min_arc_deg: 200, max_diameter_mm: 30 ``` Limits: - Only cylindrical and conical (countersink) holes are checked, not polygonal or helical-thread geometry. Only material inside the hole's own wall is measured, not tool or screw-head clearance beyond it. - **Known misses.** A hole capped at one end by a later feature reads as a blind hole. A counterbore blocked right across, with a smaller hole cut through the block, reads as a ledge. Look at the listed ledges and the blind holes in a section view. - A block from one side that covers half the hole or more over its full depth leaves too little wall to count as a hole; it reads as a slot end. Look at blocked holes in a section view. - Concentric openings under 30 mm that are not holes fail: a small fan guard's rings, or a recess around a post. List them as exceptions. [common-issues.md](https://cad-agent.valpatel.com/docs/common-issues.md) lists this check with the other recurring review findings. --- Source: https://cad-agent.valpatel.com/docs/volume-precision.md # Curved-solid volume precision Tracking: issue #70. This investigation concerns numerical volume measurement in the generic relation gate. It does not qualify hardware fit or weaken collision/volume-conservation tolerances. A private manufacturer screw/insert fixture from Gyropup produced a default measured cut volume larger than the original solid. Holding the Common/Cut BREP results fixed and changing only the `BRepGProp.VolumeProperties` integration overload changed the results: | Integration | A − Common − Cut(A,B), mm³ | B − Common − Cut(B,A), mm³ | | --- | ---: | ---: | | Default overload | -14.11652803 | -4.58890513 | | Adaptive, requested relative error 1e-6 | 0.00023872 | 0.00007781 | | Adaptive, requested relative error 1e-8 | 0.00027073 | 0.00006440 | The diagnostic ran against cad-agent `28abd34` / OCP7.9.3.1.1 and took151s. This identifies a measurement problem in that fixture; it does not establish that every previously unknown Boolean relation was correct. Manufacturer geometry stays private. Numerical receipts, input hashes and a generic reproducer will accompany the implementation. ## Error estimates must match the actual algorithm Independent review of draft PR #71 found that Gauss `VolumeProperties_s(..., Eps)` returns the **maximum face-relative error**, not a whole-solid relative bound. Multiplying it by net solid volume can underestimate uncertainty when positive and negative face contributions cancel. The initial two-refinement draft did this incorrectly and must not qualify parts. The corrected draft uses `VolumePropertiesGK_s` with two requested tolerances, 1e-6 and 1e-8, `OnlyClosed=False`, `IsUseSpan=True`, and centroid/inertia flags disabled. In [OCCT 7.9.3 BRepGProp](https://github.com/Open-Cascade-SAS/OCCT/blob/V7_9_3/src/BRepGProp/BRepGProp.cxx), this routine sums absolute face-error estimates at a common reference point, then divides by positive mass only above `Epsilon(1)`. The tiny positive-mass branch already returns an absolute error; multiplying it by mass would lose its uncertainty. The underlying [GK integrator](https://github.com/Open-Cascade-SAS/OCCT/blob/V7_9_3/src/BRepGProp/BRepGProp_VinertGK.cxx) still supplies an estimate, not a mathematical interval proof. The retained uncertainty is the larger of the fine aggregate absolute-error estimate and the two masses' difference. That uncertainty **consumes** the unchanged conservation budget, `0.01 + 1e-6 × max(part volumes)` mm³. Overlap and containment thresholds are unchanged. A threshold-straddling estimate or failed conservation check is `unknown`, never a clearance pass. Cache algorithm version 4 prevents results from older integration policies being reused. Containment's support test also retains uncertainty when subtracting before/after common volumes and when measuring how much was already outside. An unresolved threshold fails that support query; its surface-probe fallback cannot override the uncertainty. Its separate cache now includes the relation measurement policy and uses containment version 3, invalidating prior scalar-volume support decisions. ## Validate geometry before measuring it Solid count alone accepts an open shell wrapped as a solid. `BRepCheck_Analyzer` alone accepts an inverted closed solid whose interior includes infinity. Before broad-phase acceptance, assembly inputs must contain valid solids, exact face surfaces, no free faces outside their solids, and every solid must classify infinity as `OUT`. Source geometry is rejected, not silently repaired or reoriented. Boolean results receive the same geometric checks before measurement. A valid empty or lower-dimensional Boolean result has zero volume; it cannot serve as an assembly part. ## Centroids are optional witnesses Enabling GK centroid moments made a public full-sphere refinement take 3.8–5.7 seconds instead of 0.0003–0.0007 seconds; span splitting did not fix this. Mass decisions therefore do not compute centroid moments. An overlap's optional centroid uses separate Gauss refinements and is labeled approximate. If those points differ by more than 1e-6 mm or are invalid, the point is omitted with its reason. This does not change the relation decision. In particular, mass-only GK properties do **not** provide a computed centroid. ## Reproduce without publishing supplier geometry `tools/volume_probe.py prepare --a --b --location-a '' --location-b ''` computes Common/Cut once and saves private BREP inputs with hashes and transforms. Run `tools/volume_probe.py measure --method default`, `--method gauss`, and `--method gk` against those fixed files. Receipts record input and BREP hashes, source hashes, exact commit/runtime, residuals, estimated error, elapsed time and the unchanged budget. Preparation refuses to overwrite an existing bundle. The BREP bundle stays private; sanitized numerical receipts may be committed. Public tests use analytic asymmetric Bezier solids of revolution and thin shells, rigid placements, independently calculated centroids, invalid/open/inverted solids, missing surfaces, invalid integration outputs, tiny-mass error units, threshold uncertainty, conservation-budget consumption and old-cache invalidation. Existing sphere, curved-cap, containment and synthetic 100-part timing tests remain applicable. ## Private fixed-BREP rerun The detached compute-host run at toolkit `773477b1835a945f51962d8c3650f297ffead11c`, OCP7.9.3.1, Python3.12.3 completed on 2026-10-29. Local and remote STEP hashes matched. [Inputs and fixed BREP hashes](https://github.com/Valpatel/cad-agent/blob/dev/docs/validation/volume-2026-10-29/inputs.json), [default](https://github.com/Valpatel/cad-agent/blob/dev/docs/validation/volume-2026-10-29/default.json), [Gauss](https://github.com/Valpatel/cad-agent/blob/dev/docs/validation/volume-2026-10-29/gauss.json), and [GK](https://github.com/Valpatel/cad-agent/blob/dev/docs/validation/volume-2026-10-29/gk.json) numerical receipts preserve source hashes and transforms without supplier geometry. | Method | Residual A, mm³ | Residual B, mm³ | Error-budget consumption A / B, mm³ | | --- | ---: | ---: | ---: | | Default | -14.11652768 | -4.58890516 | no aggregate estimate | | Gauss 1e-8 | 0.00027073 | 0.00006440 | no aggregate estimate | | GK refinements | 0.00012211 | 0.00002023 | 0.00021169 / 0.00002909 | Both GK budget consumptions fit the unchanged 0.01009256 mm³ conservation limit. Measuring all five fixed BREPs took 2.877 seconds (Boolean preparation is separate). **Common volume remains 3.288196 mm³: this is an interference to interpret, not clearance or proof of a correctly modeled threaded mate.** The result establishes numerical consistency for this fixture only. The broader real-geometry suite at `be90a73` initially reported 73 passes and one failure on the aarch64 compute runtime. An existing test assumed a tangent cone/ball pair always passed. Independent direct `BRepCheck_Analyzer` checks found valid A, B, Common and Cut(A,B), but **invalid Cut(B,A)**; the strict validator correctly returned `unknown` and failed the containment gate. An x86_64 control with the same OCP version produced valid outputs and passed. This observes a runtime difference, not a proven architecture-only cause. The regression now branches on independently checked Boolean validity: all original passing assertions remain for valid outputs; only the observed cone-wall/invalid-cut-B case must explicitly fail closed. Any different invalidity fails the test. No production geometry, kernel tolerance or acceptance threshold was changed for this test. Next: obtain independent review and current CI, including the downstream support changes. No installed toolkit or product geometry changes before review. Passing numerical controls does not qualify thread engagement, assembly access, printed fit, material strength or electrical readiness. --- Source: https://cad-agent.valpatel.com/docs/web-diagrams.md # Mermaid diagrams in published documents `webkit/diagrams.js` exports `renderDiagrams(root=document)` and `observeDiagrams(root=document.body)`. The observer renders existing and subsequently inserted `pre > code.language-mermaid` and `pre.mermaid` blocks. Its `dispose()` disconnects observation; removed nodes never receive a late result. Include `diagrams.css` before the module so pending source is represented as a loading indicator instead of flashing code. Mermaid 11.4.1 is pinned and self-hosted in `vendor/mermaid`, with its MIT license and SHA-256 receipt. The helper loads it only when a diagram exists. It uses explicit queued render calls, strict security mode, generic fonts, unique SVG identifiers and isolated error/retry feedback. Source is available in a closed disclosure; a failed graph cannot remove other diagrams. No CDN connection is required. A runtime load has a 20-second bound; a retry issues a fresh request. Set `data-diagram-theme="light"` or `"dark"` on a containing element to match the actual page surface. Otherwise CSS `color-scheme` then the system preference determines the theme at insertion. Existing diagrams are not recolored by an unrelated theme toggle. Large diagrams retain readable text and scroll inside their own keyboard-focusable region. A visible sideways-scroll hint appears only when the graph overflows, and updates after viewport resizing. Tests scroll to the rightmost semantic node on phones and desktop. The Python site builder ships these assets in `site/assets` and initializes the observer on every generated page. Bootstrap failures show a reload control; runtime and syntax failures show an individual retry control. Browser tests require actual visible SVG geometry and semantic labels, hidden default source, dynamic rendering, both themes, phone overflow containment and failure recovery. Merely finding a Mermaid fence in generated HTML is not evidence that it rendered. --- Source: https://cad-agent.valpatel.com/docs/web-parts.md # Shared parts browser and build tracking Shared modules for [issue 62](https://github.com/Valpatel/cad-agent/issues/62). The table/tracker implementation is available on the feature branch; the original-file viewer is being implemented alongside it. The webkit will provide reusable sortable part tables, browser-local build status, and direct interactive viewing of STL, STEP and 3MF files. Projects supply their own catalogs, images, engineering context and downloads. Checking a status box never places an order. The table must preserve stable part identities through sorting and filtering, sort numbers numerically, retain unknown values, support keyboard and touch use, and explain whether state was saved. Exported state must identify its project and schema; invalid or unrelated imports must not overwrite current progress. The viewer must display geometry from the selected original file, fit its bounds, support fine wheel/pinch zoom and orbit/pan, and retain an original-download link. Loading and parse failures must remain readable and recoverable. CAD parsing must not freeze the interface. Format, units and triangulation limitations belong in the integration documentation and evidence. Acceptance evidence includes generic fixtures, actual project files, narrow/landscape layouts, numeric/text sorting, persistence and import/export, malformed/stalled responses, invalid geometry, and the existing viewer/packaging checks. CAD display does not certify fit, manufacturability or physical safety. This feature is developed on `feat/web-parts`, targeting `dev`. Gyropup may test the feature worktree while its PR is open, following CONTRIBUTING.md. Toolkit integration and release still follow the repository's review and explicit merge-instruction rules. ## Tables and status Include `part-table.css`, then progressively enhance a semantic table. Keep numeric source values in `data-sort-value` rather than asking the component to guess units, pack quantities or currency from display text. Unknown values use an empty sort value and remain last in either direction. Put totals in `tfoot` (or mark a body total `data-fixed`). ```html
Frame parts
PartCost
Bracket$12.50
``` ```js import { createPartTracker } from './kit/part-tracker.js'; import { enhancePartTable, mountProgressTools } from './kit/part-table.js'; const tracker = createPartTracker({ project: 'my-project' }); const tools = mountProgressTools(document.getElementById('progress'), tracker); const table = enhancePartTable(document.getElementById('parts'), { tracker, statuses: [{ id: 'ordered', label: 'Ordered' }, { id: 'inHand', label: 'In hand' }], }); // Before replacing the table or navigating within a single-page application: table.dispose(); tools.dispose(); tracker.dispose(); ``` `enhancePartTable` supplies visible header sort buttons, `aria-sort`, an alternate native sort selector, text search and visible row counts. It preserves the row nodes and original order. Sorting/filtering never changes a part's tracking identity. Each trackable table requires unique `data-part-key` values; use stable project identifiers, not the row index or translated display label. Different tables may intentionally share an identity and tracker. `createPartTracker({project, fields, storage, events})` defaults to `ordered`, `inHand` and `printed`; projects may configure other Boolean fields. Its API provides `get`, `set`, `subscribe`, `export`, `import` and `dispose`, plus `persistent` and `warning`. Statuses are independent: an already-owned part can be in hand without being ordered. Storage is scoped by project and schema version, and valid cross-tab updates synchronize. Blocked storage or a full quota preserves in-tab changes and shows an export reminder instead of claiming a save. Imports validate the entire file before changing anything, require the same project/schema and configured Boolean fields, then merge by part identity. Explicit imported false values replace the corresponding field; unrelated parts/fields remain. Files are limited to 10,000 parts and 2 MB, including after merging, so exported state remains importable. No server, account, order submission, inventory quantity reconciliation or multi-user conflict resolution is supplied. ## Validation Run `npm ci`, `npm run test:webkit-unit`, and `npm run test:part-table`. Set `PLAYWRIGHT_CHANNEL=chromium` for the installed Playwright browser or `PLAYWRIGHT_BROWSER=webkit` for WebKit. The browser harness exercises native controls, correct numeric and natural text sorting, unknown values, filter counts, row identity, reload/cross-tab persistence, atomic import, actual file download, blocked storage and four phone/tablet layouts. Unit cases cover validation and state-file limits. These checks do not certify arbitrary application styling or other projects' part identifiers. --- Source: https://cad-agent.valpatel.com/lessons/01-verify-from-source.md # 01 — Verify from source Board facts that the design depends on (connector side, voltages, designators, threads, which button is which) must come from a primary source: the schematic, datasheet photos or the vendor's 3D model. They should not come from the user's recollection or from the agent's assumptions. In this session the user's assumptions and the agent's were each wrong at least once. ### Confirm which side of the board a connector is on, from photos - **Rule:** Before designing a cable route, confirm the board side and mating direction of each connector from datasheet photos or drawings. Treat generic-height bodies in a vendor STEP as placeholders. - **Incident:** The user asked how to "connect the cables to the top side of the board where we see sata and sata power connectors". In the STEP, no single solid matched a SATA connector, and the SATA bodies found were "3.81 mm-tall generic placeholders". The agent rendered the datasheet's top and bottom board photos at high resolution. The result: both SATA ports and their power headers are *vertical connectors on the underside* and plug in from below. The whole storage design (floor opening, chimney, trays under the unit) follows from this. - **Check:** Every connector entry in the parameters has a `basis` that names its evidence (photo, STEP solid IDs, schematic sheet), for example "Two vertical SATA 7-pin receptacles on the underside (5063/5064; datasheet photo)". Flag any body whose height is a library default (3.81 mm here) as non-authoritative. J26 is an example: its placeholder was replaced by a 13.85 mm envelope. - **Source:** 2026-10-24T17:33, 17:36, 17:39; 2026-10-25T00:36; `cad/parameters.json` (`underside_envelopes`), `README.md`. ### Read supply voltages off the schematic image, pin by pin - **Rule:** When a voltage matters, render the schematic sheet as an image and read the net name on each pin. Do not rely on text extraction, and do not take the user's word for it. Then trace the rail back through the power tree. - **Incident:** The user said: "12V won't come from some external connector, the 12V connector is next to the sata connector, one for each sata port on the pcb, review the schematic." The agent rendered sheet 27. On J22 (and identically J23), pin 1 is not connected, pins 2–3 are ground and pin 4 is SATA_5V. SATA_5V is switched by Q59, with a 2 A bead per port. The power tree on sheet 3 shows that VDD_12V feeds only the camera expansion connector. The first crop cut off the SATA_5V net label, so the agent widened it. The answer was "no 12 V on its SATA power connectors". The crops went on the website, and the agent offered to redesign if a photo showed a 12 V header that is missing from the v1.0 schematic (dated 2024-05-29). - **Check:** For each power pin used, record the sheet, pin and net, plus the upstream rail. Publish the crop with the net label visible. - **Source:** 2026-10-25T01:27, 01:28, 01:29, 01:30; `cad/parameters.json` (SATA power `basis`). ### Re-derive designators from the schematic; name by position when the model can't tell - **Rule:** Check every reference designator in docs and BOMs against the schematic. If the 3D model does not say which physical part is which designator, write the instruction by position and list the designator mapping as unverified. - **Incident:** The agent had J23 and J24 swapped. Per the schematic, data is J21 (port 0) and J24 (port 1), and power is J22 and J23. The STEP does not record which physical connector is J21, so the wiring rule now says "the 3.5 in drive takes the **front** SATA connector". The parameters tell the builder to check the SATA1/SATA2 silkscreen. - **Check:** Search the docs, BOM and diagrams for each designator and compare against the schematic. Each designator-to-location mapping carries a field-verification note (silkscreen or photo). - **Source:** 2026-10-25T01:29, 01:30; `cad/parameters.json` (`cables.wiring_rule`). ### Infer the thread from hole sizes when nobody can tell you - **Rule:** If the thread of a mating part is unknown, infer it from the clearance holes in the vendor model, and state the inference. - **Incident:** The user: "The screw thread you should get approximately from the screw hole size, but M3 sounds right." The NVIDIA module model has Ø3.5 holes through the module PCB and Ø3.7 holes through the thermal plate. Both are M3 clearance holes, so M3 was adopted for the tie-down screws. - **Check:** Measure the hole diameters in the STEP and compare them with a clearance-hole table for candidate threads. Record both diameters in the fastener's `basis` ("M3 from the module holes: D3.5 PCB, D3.7 thermal plate"). - **Source:** 2026-10-25T00:37, 00:38; `cad/parameters.json` (`heatsink_screw`). ### Say what the sources do not tell you, and keep the label provisional - **Rule:** When the sources leave a fact open, say so, use a neutral label and ask the user one specific question. Do not guess. - **Incident:** The schematic shows SW1 = FORCE_RECOVERY and SW2 = power/reset, both Alps SKQJLBA010 right-angle tact switches. The STEP does not record which actuator is SW1, and Seeed's docs did not say either. The rear plate was labelled "RST/REC", and the user was asked which one is closer to the board corner. The question was repeated later and is still unanswered in the transcript. - **Check:** Keep an open-questions list on the progress page. Every feature that depends on an open question has a provisional marker. - **Source:** 2026-10-24T16:35, 17:10; 2026-10-25T00:36; `cad/parameters.json` (button `basis`). ### Use vendor-model bodies as proxies for missing data, and say they are proxies - **Rule:** When the board model lacks copper and drill data, look for bodies that stand in for it, such as mount-pad proxies. Use their size, label it an assumption, and ask for one measurement. - **Incident:** The carrier STEP has no copper or drilled holes. It does have a body at each mounting hole that matches the pad: Ø6.9 at the four board holes and Ø7.14 at the four cooler holes (solids 5155–5162). The agent took those as the pad sizes, assumed the underside pads are the same, and asked the user to "measure one with calipers". - **Check:** The parameter `landing_basis` names the solid IDs and the assumption. Physical measurement stays on the pre-print checklist. - **Source:** 2026-10-24T16:39, 17:10; `cad/parameters.json` (`landing_basis`). ### Find parts by name or reference designator, never by size alone - **Rule:** Locate a component in a vendor STEP by its product name or reference designator (XCAF labels), then confirm it against a photo or silkscreen. Matching bounding-box size finds look-alikes. - **Incident:** The pin holes for the reset and recovery switches were first placed over two parts at PCB X 16.6 and 22.9, chosen because their size matched a right-angle tact switch. The user described the real layout ("REC button is closest to USB/IO headers, RST button is closest to the far board edge"). A name search then found the actual SWITCH4-8X7X7MM instances at X 137-158, between the USB-C ports and the right-hand corner. The holes, sleeves and labels were moved. - **Check:** Every connector or switch used for a cut-out records its STEP product name or refdes and the solid/shell index in `parameters.json`. A test fails if a cut-out's basis names only a size. - **Source:** 2026-10-25 (session record; `cad/parameters.json` `service_buttons.basis`). --- Source: https://cad-agent.valpatel.com/lessons/02-user-intent-and-requirements.md # 02 — User intent and requirements The requirements arrived in pieces, often as short mid-turn messages, and were refined by later corrections. What worked: capture each one in the user's own words, turn it into a named feature or check, and recompute anything that depends on it. ### Capture each mid-turn requirement verbatim and give it a feature - **Rule:** Record each user requirement word for word, map it to the feature that satisfies it, and carry it through every later revision. - **Incident:** During the first review the user added: "we need counter-sunk holes under the heat sink hole location because the heat sink is most of the mass of the assembly... so the heatsink mass is attached to our housing". This became the cooler tie-down: four printed columns under the 92 × 79 cooler holes, with flat-head screws driven up from under the floor. It was also saved as a standing project memory. - **Check:** A requirements table lists each quote, its feature, its check and its status. The final report names every requirement. - **Source:** 2026-10-24T07:07, 07:41. ### Pin down what a screw threads into before computing its length - **Rule:** For each fastener, identify the mating thread (part and feature) explicitly. Do not infer it from the part's general role. - **Incident:** Rev 0.5 said that the tie-down screws thread "into the cooler", and asked the user to confirm the direction. The user corrected this: "the standoffs between the orin AGX and the heatsink have female threads. So our screw lengths ... need to go into those standoffs, that sets our lengths", and "(the screws through the base of the housing plastic go up into the standoffs)". The agent later listed "docs saying 'thread into the cooler'" as one of the session's mistakes. - **Check:** The fastener table has a "threads into" column (for example "female standoff between AGX module and heatsink, thread starts at Z 40.76"). - **Source:** 2026-10-24T07:41; 2026-10-25T00:32, 02:18. ### Derive values your own geometry sets; ask the user only for outside facts - **Rule:** If a value follows from the model (stack heights, screw lengths), compute it and decide. Ask the user only for facts the model cannot supply, such as part identity or a physical measurement. - **Incident:** Rev 0.5 asked for "the original screw's length + 19.6 mm". Later the agent asked whether to keep the head flush or raise it. The user: "the screw length is basically set by the top of the AGX and the bottom of the housing, assuming we just go something like 8 threads into the standoff", then "I can't give you the screw lenght, that depends on your assembly." The agent then made the call itself (M3 × 35 with a raised seat; see 04). - **Check:** Before asking a question, test whether the answer is computable from `parameters.json` and the STEP. If it is, present a decision with its reasoning instead of a question. - **Source:** 2026-10-24T07:41; 2026-10-25T00:34, 00:36, 00:37, 00:38. ### Model every interface layer the user mentions, and recompute what it moves - **Rule:** Add every stack-up layer as a named parameter (thermal pads, gaskets, washers), then recompute both the fasteners and the clearances it affects. - **Incident:** The user: "(also watching out for thermal interface pad, standard thickness between heatsink and AGX)". A 0.5 mm pad became `cooler.tim_thickness`. It lifts the heatsink but not the start of the standoff thread, so the screw length is unchanged. It does take 0.5 mm off the cooler-to-roof gap: `assembly-checks.json` now reports 14.742 mm, where Rev 0.5.1 reported 15.24 mm. - **Check:** Each layer has a nominal value and a "measure the fitted part" note. Dependent outputs (screw engagement, roof gap) are regenerated after any change to it. - **Source:** 2026-10-25T00:34, 00:36; `cad/parameters.json` (`tim_note`); `exports/assembly-checks.json`. ### Treat "doesn't block disassembly" as a hard requirement - **Rule:** Before placing anything that is tethered to the internals, decide which part never comes off and attach it there. - **Incident:** The user asked "where can we have RPSMA connectors for wifi and cellular without blocking disassembly?", then added "(2x each)" and "those are not mounted on the PCB, they would be extension cables with a RPSMA sticking through the housing with a nut". All four bulkheads went into the fixed base side walls, so the lid, both I/O plates and the side door lift away with the antennas still fitted. - **Check:** For each removable part, list what is attached to it. No removable part may hold a cable that runs to the board. - **Source:** 2026-10-24T16:35, 16:36, 17:10. ### Turn a configuration list into named test cases - **Rule:** When the user lists the configurations they want, make each one a named case in the check output. - **Incident:** The user: "We want to be able to stack 1 or 2 of them and have them be 2.5" or 3.5" skinny 2.5" sata ssd or thick regular 3.5" hdd", then "(1 of each or 2 of each, or 1 total)", and "The cables can be exposed but need entry/exit points between the drives and the Orin carrier." `storage-checks.json` has seven named stacks, among them "1 total: 2.5 skinny SSDs on clips", "1 of each: 2.5 over 3.5, on plate" and "2 of each". Each one has its own plastic, hardware, chimney and bend-radius results. - **Check:** Each case the user named appears in the check JSON with `pass: true`, and the review site shows it. - **Source:** 2026-10-25T00:39, 00:40, 01:23; `exports/storage-checks.json`. ### Report constraints that undercut the request, with a recommendation - **Rule:** When a hard limit makes part of a request impractical (ports, power), say so plainly and recommend a default. - **Incident:** After the 5 V finding the user asked: "Ok so this can only support the ssd SATA drives realistically?" The answer: yes, 2.5 in drives (SSD or spinning) run from the board, while 3.5 in drives need an added 12 V source. The board has only two SATA ports, so "2 of each" only makes sense with an M.2 SATA adapter. The agent recommended the 2.5 in tray as the standard option and offered to drop the 3.5 in tray. - **Check:** Compare the number of drives in each configuration against the port count and the power budget (see 06). - **Source:** 2026-10-25T01:34, 01:36. ### Make "show it on the website" part of done - **Rule:** For this user, a feature is not done until the review site shows it with evidence. - **Incident:** Requests included "Also show them on the website..." (the drive options), "Launch the server again and show me how you are making progress", and "Where's the base for stacking with drives?". The last one was answered by stating that P1 is the stacking base and adding a "Which base do I print?" section. See 08. - **Check:** Every change in the report links to the section or image on the site that shows it. - **Source:** 2026-10-25T00:31, 00:40, 01:30, 01:36. --- Source: https://cad-agent.valpatel.com/lessons/03-mechanical-design-rules.md # 03 — Mechanical design rules These rules come from printed-enclosure work (FDM, Bambu P1S bed) around a real carrier board. ### Tie tall slender columns into the nearest wall - **Rule:** A tall screw column near a wall should be fused to the wall: a full-height buttress, concave fillets where it meets the wall, and a fillet at its base. It should not stand free. - **Incident:** The user: "P1M and P1 base have 4 long tall skinny standoffs for the lid, can you make them better connected to the outside walls ... so they don't snap off like toothpicks?" Each lid-screw column was Ø10 × 68 mm, with its centre 8.8 mm from the outside face, so it reached only 0.2 mm into the 4 mm wall. The fix had three parts: - a full-height buttress from the wall face to the column centre (a D-shaped pillar); - 4 mm concave fillets on both sides where it meets the wall; - a 45° flare at the floor, later replaced by a 3 mm radius fillet. The lid's 34 mm hanging columns got the same treatment. Clearance to the antenna spot faces stayed at 1.25 mm, and to the nearest vent at 1.0 mm. - **Check:** For each post, compute how far it reaches into the adjacent wall and its height-to-diameter ratio. Flag a post if the wall overlap is near zero. After the change, re-check the clearance to nearby features. - **Source:** 2026-10-25T01:39, 01:45, 02:15. ### When one weak feature is found, survey all similar ones - **Rule:** After fixing one slender feature, rank every similar feature by risk and either fix it or write down why it is left alone. - **Incident:** The user: "What other skinny parts might snap...?" The survey found three more risks: - the board posts and cooler columns, whose flat landing shoulder carries the cooler clamp load; - the storage-tray standoff sleeves, which touched the 3 mm tray wall by only 0.3 mm; - the P9 template posts. Some features were kept on purpose. The lid tongue is supported along its full length, the ribs are 3 × 3 mm, and the rails are fused to the floor. The SSD clip got no fillet, because a fillet in its inside corner would push the drive off. - **Check:** A table of slender features with risk, decision and reason. Fixed items are re-checked for overlaps. - **Source:** 2026-10-25T01:50, 01:51, 01:57. ### Fillet both ends of a stepped standoff with true radii - **Rule:** Put concave radius fillets at the base of each standoff and at each diameter step. Keep the fillets below the bearing face, and re-apply component reliefs after filleting. - **Incident:** The user pointed at a render: "the small standoffs need the same fillet on the bottom that connects them to the base of the housing, as well as the top of the standoff (the small section) needing a fillet connecting to the fat part". The agent had used 45° chamfers there. These became radius fillets: 3 mm at the floor and 2 mm at the landing shoulder, on the board posts, the cooler columns and the P9 template. The lid columns and tray sleeves were given 3 mm floor or roof fillets for consistency. The fillets narrow as they rise, so they print without supports. - **Check:** The landing-top area is unchanged after filleting. The component-relief cuts run after the fillets, so the carrier clearance check still reports 0 collisions. - **Source:** 2026-10-25T02:04, 02:15; `cad/parameters.json` (`support_fillet_note`). ### Size landings to the pad's annular ring, limited by nearby components - **Rule:** Make the plastic landing under a PCB mounting hole as large as the pad, for full contact. Keep a set margin to the nearest component, and give a fit template the same landings and reliefs. - **Incident:** The user asked whether the fit template needed built-in standoffs, and: "Do you have the mounting hole pad sizes to match the plastic to the annular ring ... max contact area without colliding with components?" The landings were set to Ø6.9 (board) and Ø7.14 (cooler), from the STEP pad bodies. The nearest underside part is at least 4.14 mm from any hole centre, so the edges clear components by at least 0.5 mm. The P9 template became a cradle with the base's posts and reliefs on a 3 mm frame, 12 mm tall, with 2.5 mm under the lowest component. - **Check:** Landing radius + margin ≤ the distance from the hole centre to the nearest component's bounding box. The template must pass the same carrier clearance check as the base. - **Source:** 2026-10-24T16:39, 16:42, 17:10; `cad/parameters.json` (`landing_basis`). ### Envelope underside cards and relieve the supports around them - **Rule:** Model envelopes for the cards that plug into underside slots, including their plugs, and cut reliefs into any support they reach. - **Incident:** The user asked whether there was "height clearance on the bottom side of the board for the large double stacked pcie slots". The stacked Mini PCIe and M.2 Key B cards need about 10 mm of the 16 mm available. The M.2 Key E Wi-Fi card envelope clipped a support by 74 mm³, so that support was cut back with a 0.5 mm margin. - **Check:** For each envelope, the intrusion into plastic after relief is 0 mm³ and the gap to the floor is reported (`underside_envelopes` in `storage-checks.json`). - **Source:** 2026-10-24T17:33; 2026-10-25T00:36; `README.md`; `exports/storage-checks.json`. ### Size connector windows to the mating plug's overmold - **Rule:** Work out where the receptacle face sits relative to the outside of the panel. If the plug's overmold has to enter the window, size the window to the overmold, not to the receptacle. Bridge webs that would be too thin. - **Incident:** In Rev 0.4 the 6 mm panels put receptacle faces 7–7.6 mm behind the outside, and the USB-C slot was 5.7 mm tall where typical overmolds are 6.5–8 mm, so plugs could not mate. Rev 0.5 uses 3 mm plates with the inner faces unchanged. Windows were sized to typical overmold envelopes: USB-A 16.5 × 8.5, USB-C 13 × 8, HDMI 11.5 × 21.5 (rotated). Webs under 1.8 mm were bridged. These sizes are typical values, still to be checked against the user's cables. - **Check:** For each port: receptacle depth behind the outside face, window size ≥ envelope, web ≥ the minimum (1.8 mm). Record the sizing basis per port in `ports`. - **Source:** 2026-10-24T07:41; `README.md` (Port windows); `cad/parameters.json` (`plug_envelopes_note`). - **Superseded check:** a size-only check missed windows that were 2 mm off-centre and a plug taller than its header. Model the mated plug and sweep it in (see 05, "Model every mating connector …"). ### Build printable lead-ins into the cutter, and confirm with an overhang screen - **Rule:** Make lead-ins and transitions self-supporting (45° or a shape that narrows as it builds). Prefer tapered cutters to edge chamfers on complex edges. Compare the overhang screen before and after each change. - **Incident:** An edge chamfer on the bridged windows failed (StdFail_NotDone), so the agent built the 45° lead-in into the cutter as a tapered extrude. The horizontal antenna and 12 V jack holes are teardrops with a 45° roof (`teardrop_x` / `teardrop_y` in `cad/build.py`), capped below the split line. Fillets were checked to narrow as they rise. - **Check:** Downward-facing area from `print-review.json` before and after the change. For example, the lid stayed at 803.5 mm² after the cell chamfer was added. - **Source:** 2026-10-24T07:17, 14:50, 17:10. ### Make grilles finger-safe, and shape the fan side for flow - **Rule:** Keep the smallest opening of a grille over a fan below the test-finger size. Taper the inlet side rather than leaving a sharp edge, and do not claim an airflow gain without testing it. - **Incident:** The Rev 0.4 roof cells (25 × 29 mm) let a finger reach the fan. Rev 0.5 has 216 hex cells, 10 mm across flats, which a 12 mm test finger cannot pass. On request ("internal chamfer on the fan side"), each cell got a 1.0 mm deep inlet that opens to 11.2 mm across flats, with a 0.6 mm web edge. That is 31° off the cell axis, chosen over 45° for a longer lead-in on thin webs. The outer 5 mm keeps full 1.8 mm webs. The airflow gain was stated as untested. - **Check:** Smallest opening < test-finger diameter. The overhang screen is unchanged. Any airflow claim is labelled as untested until CFD or a physical test. - **Source:** 2026-10-24T07:41, 14:42, 15:09. ### Keep the shell flush and let the lid capture the loose plates - **Rule:** Prefer a flush shell without overhangs or protruding rails. Have the closing part hold the slide-in panels down, so one set of screws retains everything. - **Incident:** Rev 0.4 overhung by 6.4 mm and had rails protruding 11 mm. Rev 0.5 is a flush R8 shell, 206 × 181 × 112 mm. Plates and the hatch slide down channels, and the lid's ledges over the plates and its skin over the hatch keep all three from lifting. Four M3 screws go in from the top, so the lid opens while the base is bolted to a wall. - **Check:** `assembly-paths.json` shows that the plates slide vertically (0.37 mm front, 0.25 mm rear clearance). Every loose part is either screwed or captured by a screwed part. - **Source:** 2026-10-24T07:41; `README.md` (Assembly step 9). ### Cut every hole last, after all the added features - **What happened:** Rev 0.9 of the J501 lid (2026-10-29) added four stiffening ribs from the fan to the lid-screw corners. The build cut the counterbores and clearance holes first and unioned the ribs afterwards. Each rib ran into its socket-head counterbore, and the lid was printed that way. Every check passed: the lid was a valid watertight solid, it cleared the cooler, and its FEA and slice were fine. - **Rule:** in the build, cut fastener holes, counterbores, countersinks and insert bores after every rib, boss, skirt and gusset has been added. Run `cad-agent holes` (part of `cad-agent all`), which probes every cylindrical hole and countersink in each part's STEP for material inside it. A finding is a build-order bug unless it is a deliberate D-flat or bridge layer, which is listed in `cad.holes.exceptions` with its reason. - **Index:** [docs/common-issues.md](https://cad-agent.valpatel.com/docs/common-issues.md) lists this and the other recurring review findings. - **Source:** mvalancy/j501-agx-orin#9; Valpatel/cad-agent#85. --- Source: https://cad-agent.valpatel.com/lessons/04-fasteners-and-assembly-access.md # 04 — Fasteners and assembly access Bought hardware (screws, nuts, washers, inserts, pins, bearings, standoffs, rod ends) is chosen and placed with [`skills/mcmaster-parts`](https://cad-agent.valpatel.com/skills/mcmaster-parts/SKILL.md): a real McMaster-Carr part number, its STEP model normalised into a known frame, and its specification table in the hardware catalogue, never an envelope. ### Check tool access for every fastener at the step where it is installed - **Rule:** For each fastener, check that a real tool can reach it and turn it at the point in the assembly where it is fitted, not only in the finished assembly. - **Incident:** The first storage-stack design needed standoffs to be turned "inside narrow bores where no tool fits". The 6.6 mm `column_bore` in `parameters.json` holds the 5.5 mm AF hex standoff (6.35 mm across corners), which leaves no room for a 5.5 mm nut driver (about 8 mm OD). In the redesign, the standoffs hang from the part above: the male end passes up through that part's 2 mm ledge, the hex bears in a 2 mm counterbore, and the female end takes the next tray. Every standoff is turned while fully exposed. Earlier, the wall-mount check found that a 20 mm driver envelope collided with a gusset; the fix was projection 24 → 26 mm and hole offset 13 → 15 mm. - **Check:** Sweep a tool envelope (driver diameter or wrench across flats plus swing) to each fastener head at its assembly step. Any intersection with plastic fails. `mount-checks.json` records `driver_access` with a 20 mm tool. - **Source:** 2026-10-24T08:35 (summary of the 07:00 work); 2026-10-25T00:36; `cad/parameters.json` (`interface_note`); `exports/mount-checks.json`. ### Choose the assembly direction that keeps fasteners exposed - **Rule:** If a stack can only be fastened from one side, write the assembly order to match. Flipping the build (upside down) is acceptable. - **Incident:** The storage stack is built upside down on the lid. Working from the top tray down: drive the drive screws in from below, route the leads, offer up the tray, and fit four M3 M-F standoffs through its ledges into the part above. - **Check:** The README assembly steps state the order and which way up the assembly is. Each step names the tool and the access side. - **Source:** 2026-10-25T00:36; `README.md` (Assembly step 10); `cad/parameters.json` (`interface_note`). ### Compute screw length from the stack plus thread engagement - **Rule:** Length = distance from the head seat to the start of the mating thread, plus the required engagement. Then check the result against the depth of thread actually available. - **Incident:** The user defined it: housing bottom to the top of the AGX, plus "something like 8 threads into the standoff". The standoff thread starts at Z 40.76, and 8 threads of M3 (0.5 mm pitch) is 4 mm, so a head flush with the floor would need 44.3 mm. The agent pointed out that an M3 × 45 only fits if the standoff thread is at least 5.2 mm deep. - **Check:** A table per fastener: seat Z, thread-start Z, engagement in threads and mm, the stock length chosen, and the minimum thread depth that length requires. - **Source:** 2026-10-25T00:34, 00:36; `cad/parameters.json` (`heatsink_thread_engagement_note`). ### Move the head seat to reach a standard screw length - **Rule:** If the computed length is an uncommon size, move the seat (counterbore or countersink depth) so a stock length gives exactly the intended engagement. - **Incident:** A 44.3 mm flat head is uncommon. The countersink seat was raised to Z 9.76 inside each Ø14 column, up a Ø7 pocket from the floor, so a standard M3 × 35 (DIN 7991 / ISO 10642) gives exactly 4.0 mm of engagement (9.76 + 35 = 40.76 + 4). This also puts the clamping cone in the solid column instead of the thin floor, and the base still sits flat. - **Check:** Seat Z + stock length − thread-start Z = target engagement, within a stated tolerance. Recompute whenever a layer in the stack changes. - **Source:** 2026-10-25T00:38; `README.md` (Assembly step 5); `cad/parameters.json` (`heatsink_seat_note`). ### Print coupons for inserts and countersinks before the big parts - **Rule:** Give each process-sensitive fastener feature a small test coupon, printed in the intended filament and profile before the main part. - **Incident:** P11 is the M3 heat-set insert pilot coupon (nominal pilot 3.9 mm). P14 is the countersink coupon, used to check that the flat head sits at or below the floor. P12/P13 test the plate channel and key. The filament is unspecified, so insert retention and fits are not qualified. - **Check:** Every insert pilot and countersink in the design has a matching coupon in the part list, and the assembly steps say to print it first. - **Source:** 2026-10-24T07:41; `README.md` (Parts, Assembly step 1); `cad/parameters.json` (`board_insert_pilot`, `lid_insert_pilot`). ### Limit screw penetration into drives to the SFF spec - **Rule:** For screws into third-party devices, take the allowed thread penetration from the device spec. Pick the length so that (length − material under the head) falls inside that range. - **Incident:** The drive hole patterns and penetration limits come from SFF-8301 Rev 1.9 and SFF-8201 Rev 3.4: - 3.5 in: a 6-32 × 3/16 in pan head through 1.9 mm of material, spec penetration 2.39–3.56 mm. - 2.5 in: an M3 × 4 button head through 2.3 mm, spec 0–2.5 mm. - 7 mm SSD side screws: penetration at most 2.0 mm (3.0 mm for thicker drives). By arithmetic (not stated in the session), the chosen screws penetrate about 2.86 mm (6-32) and 1.7 mm (M3 × 4), both inside the ranges. - **Check:** `storage-checks.json` `drive_fastening` records the material under the head and the spec range per drive type. Add a computed-penetration field and assert that it is inside the range. - **Source:** 2026-10-25T01:23; `cad/parameters.json` (`drives`, `ssd_clip`); `exports/storage-checks.json`. --- Source: https://cad-agent.valpatel.com/lessons/05-cables-connectors-and-routing.md # 05 — Cables, connectors and routing ### Model every mating connector, place it on the measured axis, and derive the window from it (standard practice) - **Rule:** For every external port, identify the receptacle's exact part, then look up its mating plug: the manufacturer's drawing or STEP, otherwise a catalogue or typical envelope labelled as assumed. Next: - measure the receptacle's mating face and axis by slicing the board model at the face; - place the plug there in its mated position; - derive the panel window from the part of the plug that passes through the wall on insertion, plus clearance. Never size a window from a receptacle bounding box. Then sweep each plug in along its axis against every printed part and require zero overlap, the stated clearance all round (corner radii included) and no plug-to-plug clash. Use `cadagent.connectors`: plug families, `window()`, `check_ports()` and the `draw_ports()` evidence image. - **Incident:** On 2026-10-25 the user reported: "The DC input connectors, ethernet connectors, and audio connector cutouts do not appear centered properly. … The DC input connector is not able to fit and mate properly." Then: "connector cutouts need to move up a bit, away from the bottom of the PCB." The cause had two parts. - Windows were "solid face bbox + 0.5". A bbox includes the through-hole pins about 2.3 mm below the PCB, so the RJ45 and DC windows sat about 2 mm low. The audio windows sat 0.5 mm low because they used the shell's centre, not the nose's. - The DC header J2 is a 2-pin 5.08 mm screw-flange header, 8.4 mm tall. Its mating plug, the Phoenix MSTB 2,5/2-STF-5,08, is 15 mm tall and 20.17 mm wide, and its 9.9 mm body sits outside the header face, right through the 3 mm plate. The old window stopped 5.9 mm below the plug top, a 354 mm³ sweep collision. Run against the old windows, the new check fails all four front ports. Against the derived windows, all 13 ports in three plate configurations pass. - **Check:** `exports/port-checks.json` `pass` (one row per port, with plug basis, margins, centring offsets and interference volumes), plus `preview/port-fit.png`. The review's `pass_key` includes `port_plug_fit_pass`. - **Also label what the user must get right at the port:** after the fit fix the user asked: "power connector should have polarity and voltage labeled on it, same with ethernet speeds". Take polarity from the schematic net names (J2 pin 1 = DCJ_IN through F1). When the model cannot tell which physical pin is pin 1, say so and ask for a meter check; don't present the guess as fact. - **Source:** 2026-10-25 (J501 session, port-fit fix); `cad/parameters.json` (`ports.*.mate`); `cad/ports.py`; `tools/check_ports.py`; Phoenix 1777989 datasheet. ### Route cables from the connector's real mating direction - **Rule:** Start every route at the connector's actual board side and mating axis, and give the cable a defined entry and exit through the housing. - **Incident:** The SATA receptacles and PH power headers are vertical connectors on the underside (see 01), so the plugs mate straight up from below. The route is as follows: - the leads drop through a base-floor opening directly beneath the connectors; - they continue down a chimney that lines up through every tray floor; - they turn into a cable gutter on the left side that runs the full depth of each tray; - they run straight into the drive plug. A printed cap (P18) closes the floor opening when there is no storage. This answered the user's "cables can be exposed but need entry/exit points". - **Check:** For each stack, the chimney zones (`sata_plugs`, `sata_power_plugs`) report 0 mm³ blocked in `storage-checks.json`. - **Source:** 2026-10-25T00:36, 00:39; `README.md` (Cable routing); `exports/storage-checks.json`. ### Model plug bodies and a straight lead before the first bend - **Rule:** A cable cannot bend at the connector face. Model the rigid plug length and a straight lead-in at each end, then start the bend. - **Incident:** The model uses SATA data plugs of 12 mm at the board and 11 mm at the drive, with an 8 mm straight lead. The PH power plug is 7 mm at the board, the drive power plug 11 mm, with a 6 mm straight lead. Lead diameters are modelled (2.2 mm data, 1.8 mm power, 2.0 mm for 12 V). - **Check:** Each route's geometry includes plug and straight segments sized from `cables` in `parameters.json`. - **Source:** 2026-10-25T01:23; `cad/parameters.json` (`cables`). ### When no standard exists, state the assumed bend radius and check every route - **Rule:** If no published minimum bend radius exists, choose conservative values, label them as assumptions with their basis, and check each route against them in every configuration. - **Incident:** The user asked: "What about the bend radius for the sata cables coming out of drives and so on?" No published SATA bend-radius standard was found. The basis notes that 3M sells a foldable SATA cable and that a general rule of thumb is at least 15× the cable thickness. The design values are 12 mm for data, 6 mm for the 4-wire PH power lead and 8 mm for 12 V. The tightest route is the 3.5 in data lead: 12.06 mm available against 12 mm required. - **Check:** For every route in every stack, `max_uniform_bend_available_mm` ≥ `min_bend_required_mm`. Show the table on the review site. - **Source:** 2026-10-25T00:58, 01:23; `exports/storage-checks.json` (`cables.basis`, `bend_radius`). ### Move the connector to make room for the bend; don't squeeze the cable - **Rule:** When a route fails its bend radius, move the mating connector (or the device) to lengthen the run before the bend. Do not shrink the radius. - **Incident:** After the bend checks were added, two configurations failed: "1 of each: 2.5 over 3.5, on plate" and "2 of each, on plate". In both, the rear SATA connector fed the 3.5 in drive, leaving only 9.1 mm of bend radius against the 12 mm required. The fix was the wiring rule: the front connector feeds the 3.5 in drive. An earlier round had already found routes with only 3.6–11.5 mm available. The drives were moved so their connector ends sit at X 48 (3.5 in) and X 62 (2.5 in) from the left outer face. The 3.5 in drive also shifted rearward, clear of the standoff columns. - **Check:** Re-run the bend and clearance checks for all stacks after the move. All seven stacks pass in `storage-checks.json`. - **Source:** 2026-10-25T01:08, 01:23; `cad/parameters.json` (`connector_x`, `connector_x_note`). ### Write down the wiring rules the geometry forces - **Rule:** If only one port-to-device pairing satisfies the routing limits, publish it as a wiring rule, written in terms the assembler can see. - **Incident:** The 3.5 in drive must take the front SATA connector and a 2.5 in drive the rear one. The other way round, the turn into a 3.5 in drive's data plug allows only about a 9 mm bend. The rule is phrased by position because the STEP does not map J21/J24 to physical connectors. - **Check:** The wiring rule appears in the parameters, the README, the cable diagram and the assembly steps, with the same wording. - **Source:** 2026-10-25T01:23, 01:30; `cad/parameters.json` (`cables.wiring_rule`). ### Keep cable-tie anchors out of the cable paths - **Rule:** Place tie anchors beside the route, against a wall, and include them in the cable collision check. - **Incident:** As part of the bend-radius rework, the cable-tie anchors were moved against the tray wall, out of the cable paths (`gutter_anchors` at X 9). Before that, the cable-clearance check had caught the port-2 data lead to the second 2.5 in bay running through the gutter tie anchor at X 22, Y 90 (7.5 mm³). A flat SATA cable would thread under the anchor's bridge, but the round tube model could not. - **Check:** `cable_route_hits` is empty for every stack in `storage-checks.json`. - **Source:** 2026-10-25T01:23; `cad/parameters.json` (`gutter_anchors`); `exports/storage-checks.json`. --- Source: https://cad-agent.valpatel.com/lessons/06-enclosure-features.md # 06 — Enclosure features ### Put antenna bulkheads in the part that never comes off - **Rule:** Mount panel connectors that carry cables to the internals (RP-SMA pigtails and the like) on the fixed base. Size the wall locally for the hardware stack-up: body, washer, nut and mating thread. - **Incident:** There are four RP-SMA holes, Ø6.6, in the base side walls: WIFI 1/2 on the left and CELL 1/2 on the right, 92 mm apart on each side, above the vents and below the lid joint, with debossed labels. Each hole is spot-faced on the inside to a 2.5 mm wall, so a short-thread jack still leaves room for the washer, the nut and about 4 mm for the antenna. The pigtails fit from inside, with the nut outside. The jack's thread length was left for the user to check against the parts they buy. - **Check:** A ¼-inch rod passes all four holes. The jack models report no collisions. The wall thickness at the spot face plus the nut and washer must be less than the jack's thread length (a supplier check). - **Source:** 2026-10-24T16:40, 17:10; `README.md` (Assembly step 7). ### Give recessed buttons a pin hole and a guide sleeve that clears installation - **Rule:** For right-angle buttons behind a panel, add a pin hole with a lead-in and an inner guide sleeve. Stop the sleeve short of the board, so the panel still installs along its path. - **Incident:** The reset and recovery switches sit 4.8 mm behind the rear plate. The plate has two Ø1.6 pin holes, angled at the opening, for a SIM tool or paper clip. A guide tube on the inside of each stops 0.35 mm short of the board edge, so the plate still slides straight down. - **Check:** A 1 mm pin passes through both bores. A slide-in sweep check confirms that nothing is in the sleeve's path. The label stays provisional ("RST/REC") until the switch identity is confirmed (see 01). - **Source:** 2026-10-24T16:39, 16:40, 16:53, 17:10. ### Build storage trays from the drive specs and make them stack in any order - **Rule:** Take drive outlines and hole patterns from the official form-factor specs. Design the trays as identical interfaces, so any mix stacks in any order. - **Incident:** P15 holds one 3.5 in drive (SFF-8301, all three 6-32 hole pairs) and is 32 mm tall. P16 holds two 2.5 in drives up to 15 mm (SFF-8201, M3 holes) and is 22 mm tall. They are clamped by M3 M-F hex standoffs (M3 × 30 for P15, M3 × 20 for P16), with heat-set inserts in the base column feet. Two of each tray add 108 mm, for 220 mm overall. - **Check:** Every stack the user named passes in `storage-checks.json` (no plastic or hardware overlap, chimney open, bends OK). Hole positions are traceable to the spec table in `parameters.json` `drives`. - **Source:** 2026-10-24T17:48; 2026-10-25T00:36, 01:23; `README.md` (Trays); `cad/parameters.json` (`basis`). ### Support thin drives that lack bottom holes - **Rule:** Check which mounting holes the spec makes optional, and provide a holder that uses the holes that must be there. - **Incident:** SFF-8201 makes bottom holes optional on drives 7 mm or thinner. P19 is a printed clip, four per drive. It takes an M3 × 5 through its wall into the drive's side hole (14.0 / 90.6 mm from the connector, 3.0 mm above the bottom) and an M3 × 6 from under the tray into a captive nut. It uses the tray's existing bottom-hole pattern, so no extra holes are needed. - **Check:** The clip stacks report no `drive_clip_overlaps`. Side-screw penetration stays within 2.0 mm (see 04). - **Source:** 2026-10-25T01:23; `cad/parameters.json` (`ssd_clip`, `bottom_holes_note`). ### Provide a mount plate and a blanking cap for the configurations - **Rule:** Every configuration needs a defined bottom part and every unused opening needs a closure. Say which base to print for which use. - **Incident:** - P1 is the base for desktop use and for every storage stack. - P1M is only for wall or robot mounting without storage. - With storage on a wall or robot, use P1 + trays + P17 (storage mount plate). - P18 (chimney cap) plugs the SATA floor opening when no storage is fitted. - **Check:** A "Which base do I print?" table on the review site and in the README. Each configuration in the checks names its bottom part. - **Source:** 2026-10-25T00:36, 01:36; `README.md` (Parts). ### Do the power budget before promising a device type - **Rule:** Before supporting a device class, compare its supply rails and current draw with what the board actually provides (from the schematic), and design the extra power entry if one is needed. - **Incident:** Each SATA power header supplies switched 5 V only, with a 2 A bead per port. One transistor, Q59 (CJ3407, marked "30V; 4.3A" on schematic sheet 27), switches both ports. The agent's rough figures were 0.5–1 A at 5 V for a 2.5 in SSD and about 1 A at spin-up for a 2.5 in HDD. These are general estimates, not taken from a drive datasheet. A 3.5 in HDD also needs 12 V (about 2 A peak at spin-up, also an estimate). The 3.5 in tray therefore got an 8 mm "12V IN" hole and tie anchors for a 12 V-to-SATA adapter. The board input is 12–36 V, so DC-IN can be shared only if the supply is 12 V. - **Check:** A power table per configuration: rail, source, per-port limit, worst-case draw (cite the drive datasheet once chosen). - **Source:** 2026-10-25T01:30, 01:36; `README.md` (Power). ### Connect insert bosses to the enclosure with filleted load paths - **Rule:** A cable-clamp insert should have a continuous load path through its boss to the floor and nearby wall. Use broad ribs with filleted roots and positive boolean overlap. Check the remaining material after pilots, counterbores and seam grooves, not just the uncut wall or boss diameter. State a project-specific structural minimum; cosmetic engraving strokes are a different feature class. - **Incident:** The Thor clamp's central 2 mm blade did not brace the two inserts to the rear wall. Its 10 mm bar depth left only 1.8 mm beside a 6.4 mm counterbore; a 3 mm wall around a 1.4 mm tongue plus 0.25 mm clearance left 1.35 mm outside the groove. The revised project uses two 3.2 mm ribs with R1.6 roots, a 40 × 12 mm clamp and a 4.5 mm wall around a 2 mm tongue. Two side mounting ears use R2 roots and recessed M3 clearance holes. - **Tool:** `cadagent.fasteners.filleted_wall_rib` builds explicit curved roots and wall overlap. Supply actual dimensions, then fuse to the boss/floor/wall and check one valid solid, insertion, driver, cable and board clearances. The regression test checks curved edges and nonzero overlap with the receiving wall. - **Limits:** CAD connectivity and nominal web width do not prove insert pull-out, fatigue, print anisotropy, creep or safe tightening torque. Print coupons and perform the project's load tests before setting ratings. - **Source:** 2026-10-26, Thor adapter prototype/mounting iteration; `tests/test_fasteners.py` reports three passing checks. ### Fuse engraved text before cutting it, and check the printable mesh - **Rule:** Fuse the glyphs of each text item into one solid before the boolean cut, keeping the union to that one item rather than a whole panel. Judge engraving by the exported mesh being watertight, not by how the BREP looks. - **Incident:** "AUX + PROTOTYPING INSIDE" in DejaVu Sans Bold at 2.2 mm had a 0.0000426 mm³ overlap between two kerned glyph solids (0.6 mm extrusion). Cutting the raw glyph compound gave a non-watertight STL although the lettering looked right. - **Check:** `tests/test_markings.py::test_kerned_bold_letters_export_as_a_watertight_engraving` failed before the fix (`GeometryError: non-watertight STL`); `cadagent.markings` now fuses each item's glyphs, as it already did for a line's overlapping stroke caps. - **Source:** 2026-10-26, Thor adapter lid rebuild. --- Source: https://cad-agent.valpatel.com/lessons/07-verification-and-evidence.md # 07 — Verification and evidence ### Collision-check everything and investigate every hit - **Rule:** Check plastic against plastic, plastic against the vendor board, and plastic against the modelled hardware. Treat every non-zero overlap as a finding until it is explained, however small it is. - **Incident:** Real defects caught this way: - The hatch tongues ran past the bottom V-groove (0.29 mm³, then 0.144 mm³ after the first fix). - The wall-mount driver envelope collided with a gusset. - A mount ear overlapped the panel channel. - The M.2 Key E card envelope clipped a support (74 mm³). - The hardware check reported 4 collisions because the RP-SMA thread cylinder in the visual model pointed vertically instead of through the wall. That was a bug in the agent's own hardware model, not in the enclosure. - The trays' rounded chimney corner (R3) clipped the SATA plug drop zone by 2.0 mm³ per tray. The fix was a narrower plug zone and an R2 corner moved to Y 27.5. - The storage mount plate's tab gussets (copied from P1M) intruded 3.3 mm³ into the lowest tray wall. They are now trimmed at the tray outline. - The data lead to a second 2.5 in bay ran through a tie anchor (7.5 mm³). False alarms, explained and then excluded on purpose: - Each drive's visual connector strip overlapping its own drive body. - The tie-down screws overlapping the P9 fit template (4 × 19.5 mm³). The template is used without those screws. - **Check:** `assembly-checks.json` (all part pairs = 0 mm³), `carrier-clearances.json` (4902 pairs, 0 collisions), `storage-checks.json` (per stack). Each hit is triaged as a design defect, a model defect or intended. - **Source:** 2026-10-24T07:25, 08:35 (summary), 16:53; 2026-10-25T00:36. ### Classify false positives explicitly; don't silence them - **Rule:** When a check flags something that is not a defect, add a named exclusion or category. Do not loosen the check globally. - **Incident:** - A drive's visual connector strip overlapped its own drive body. These strips were excluded from the drive-to-drive comparison. - The link validator reported JavaScript template strings as missing files. The regex was tightened to require a quote or `?` after the link. - Heat-set inserts overlap their holes by design. They are reported separately as `intended_insert_interference` (8 in the carrier check, 4 per stack). The brief also mentioned a fit-template false positive involving the tie-down screws. It does not appear in the transcript. - **Check:** Each exclusion is listed in the check's JSON with its reason. Intended interferences have their own list and count. - **Source:** 2026-10-24T07:40; 2026-10-25T00:44; `exports/carrier-clearances.json`. ### Check motions, not just final positions - **Rule:** For parts that slide, swing or get driven, sweep the motion path and the tool envelope. - **Incident:** - The plates' slide-in path was checked (0.37 mm front, 0.25 mm rear). - A sweep check was added for the button guide sleeves on the way down. - Wall-mount screws are checked with a 20 mm driver envelope. - The first stack design failed this kind of reasoning: its standoffs had no tool access (see 04). - **Check:** `assembly-paths.json`, `mount-checks.json` `driver_access`, and the sleeve sweep in the clearance run. - **Source:** 2026-10-24T07:41, 16:40; 2026-10-25T00:36. ### Never claim what was not tested, and make the checks strict - **Rule:** Every report states what was not verified (physical fit, slicer, thermal, airflow, loads). A check passes only on an explicit true. A missing file or key is a failure. - **Incident:** The Rev 0.5 report opened with "nothing has been printed, run through a slicer, or test-fitted". The hex chamfer report said "the airflow gain is untested". The Bambu link was reported as found in the binary but not tested. `assembly-checks.json` carries `physical_fit_verified: false`. The shared `vptest cad` report "always records 'physical fit not verified' and 'slicer not run', and nothing can set those to true". A missing file, a missing key or a value like "yes" counts as a failure. - **Check:** The report schema has unverifiable flags that cannot be set to true, plus an explicit limits list (the README lists slicer, insert pullout, thermal, vibration, creep and mounting load). - **Source:** 2026-10-24T07:41, 15:09, 17:10; 2026-10-25T01:44; `README.md` (Checks). ### Re-run the pipeline after geometry changes; ignore stale notifications - **Rule:** When geometry changes, stop any run that is checking the old geometry and re-run everything downstream: checks, docs, renders and package. Match each completion notice to the run it belongs to. - **Incident:** - The agent stopped runs whose results would go stale three times (interface change, screw-seat change, fillet change). - Twice it noticed that a completion notice belonged to an older run and kept waiting for the current one. - It found Rev 0.4 numbers left in the measurement list (roof Z 102 instead of 106, gap 11.24 instead of 15.24). - Drift remains in the project files: `README.md` is titled "Rev 0.5", but the exports say `revision: 0.6-storage-modules`. The README checks table says "4 stack combinations", while `storage-checks.json` has seven. - **Check:** Every artefact carries the revision string. The validator fails when a doc's revision or counts disagree with the JSON it summarises. - **Source:** 2026-10-24T15:09, 17:09, 18:02; 2026-10-25T00:38, 01:46, 01:51; `README.md`; `exports/storage-checks.json`. ### Look at every render before showing it - **Rule:** Open each render and check that it is current, lit well enough, and shows the feature in question before linking it. - **Incident:** - Renders being reviewed turned out to be old Rev 0.4 images; the new render pass had not started. - The hex-inlet close-up was too dark and its angle hid the chamfer, so it was re-rendered lighter and more square-on. - The user once pointed at a render made before the latest change, and the agent had to explain that it was stale. - **Check:** Each render's modification time is later than the build that produced its geometry. A visual pass is logged for each new image. - **Source:** 2026-10-24T07:33, 14:58, 15:09; 2026-10-25T02:04. ### Verify generated files by reading them back - **Rule:** After exporting a fabrication file, read it back with an independent parser and compare it with the source mesh. - **Incident:** trimesh's 3MF export needed networkx, so the agent wrote a small standalone exporter. Its self-check then failed, because the read-back also went through trimesh, and the pipeline stopped there. The read-back was switched to a plain XML parse. The check compares triangle count, watertightness and volume; the shared tool uses a 0.1% volume tolerance and deletes any file that does not match. - **Check:** For each 3MF: triangle count equal, closed surface, volume within 0.1%, otherwise delete the file and fail. - **Source:** 2026-10-24T16:43, 16:54; 2026-10-25T01:44. --- Source: https://cad-agent.valpatel.com/lessons/08-review-site-ux.md # 08 — Review site UX The user reviewed the work almost entirely through a locally served preview page. Each request below came from something they could not see or could not act on. ### Keep a progress section during long work - **Rule:** During multi-hour work, keep a progress section at the top of the review page with three lists: done, running and needs your decision. Mark images that are stale until the rebuild finishes. - **Incident:** "Launch the server again and show me how you are making progress." The agent added a "Rev 0.6 progress" section that listed the finished items, the background jobs and two open questions (screw seat, button identity). It said that some renders were from earlier runs. - **Check:** The progress section is updated at each milestone, and each item links to its evidence. - **Source:** 2026-10-25T00:31, 00:36, 01:45. ### Give every printed part a thumbnail and one-click files - **Rule:** Each part row needs a thumbnail and STL, 3MF and STEP downloads. Add a slicer deep link where possible, verify the link format, and say whether it was tested. - **Incident:** The user asked: "'CAD, print files and validation' section needs thumbnails" and "Each printed component section needs STL download links for printer, ideally bambu studio compatible links?" The agent confirmed the `bambustudio://open?file=` handler in the installed binary and added it. It did not open the app to test it, and warned that the link may not accept a local non-https URL, with the 3MF download as the fallback. The 3MFs are in print orientation and centred on the 256 mm bed. - **Check:** Every part in the build has a thumbnail, STL, 3MF, STEP and a deep link. The link checker passes. "Untested" is shown next to any unverified link type. - **Source:** 2026-10-24T16:37, 16:43, 17:10. ### Draw cable routes explicitly, in 2D and 3D - **Rule:** Show cable routing as a labelled 2D diagram and 3D cutaways, with colour-coded leads, numbered steps and a live check table. A render of the parts alone does not show the route. - **Incident:** "cable routing between sata drives and the orin carrier is not at all clear in the preview page". The new "Cable routing" section has: - a labelled 2D side view of the whole route and a plan view of a tray; - two 3D cutaways with translucent plastic (red SATA data, yellow board 5 V, orange 12 V); - six routing steps; - a live table of required and available bend radius per route. - **Check:** Each route in the checks JSON appears in the diagram and the table, with the same names. - **Source:** 2026-10-25T00:46, 01:23; `README.md` (`preview/cable-route-diagram.png`). ### Put the source evidence on the page - **Rule:** When a design decision rests on a datasheet or schematic, show the crop next to the decision, so the user can check it without opening the PDF. - **Incident:** After the 12 V dispute, crops of schematic sheet 27 (SATA power pinout) and sheet 3 (power tree) were added to the Cable routing section, and the user was pointed to `#cables`. - **Check:** Every "per the schematic" or "per the datasheet" claim on the page has an image or a link with the sheet or page number. - **Source:** 2026-10-25T01:29, 01:30. ### Answer recurring "which part" questions with a table and a labelled render - **Rule:** When the user is unsure which variant to use, add a decision table on the page, not just a chat answer. - **Incident:** "P1M and P! don't support stacking the drives right?" and "Where's the base for stacking with drives?" The agent added a "Which base do I print?" section with a labelled underside render: desktop P1; wall or robot without storage P1M; any storage P1 + trays (+ P17 for wall or robot). - **Check:** Each variant in the part list appears in exactly one row of the table. - **Source:** 2026-10-25T01:30, 01:36. ### Provide close-ups of the feature under discussion, at stable URLs - **Rule:** For each detailed feature, render a close-up or half-section and give it a stable file name, so the user can point at it. - **Incident:** The agent made close-ups for each detailed feature: - `heatsink-tiedown.png`, a section through the tie-down; - `hex-inlet.png`, showing the cell chamfer; - `lid-column-detail.png`, showing the column buttress; - `support-detail.png`, a half-cut of both standoff types. The user used the URL of `lid-column-detail.png` to show exactly which fillet they meant. - **Check:** Every feature changed in a revision has at least one close-up linked from its section. - **Source:** 2026-10-24T07:18, 15:09; 2026-10-25T01:45, 02:04, 02:15. ### Keep the review URL stable - **Rule:** When restarting the preview server, reuse the same address so the user's open tab keeps working, and save the URL to a file. - **Incident:** "can you serve it again?" The agent checked that port 43871 was free, served on it again, and confirmed that the page and images load. `tools/serve_preview.py` writes the address to `preview/server-url.txt`. - **Check:** After a restart, fetch the page and one image at the saved URL. - **Source:** 2026-10-24T14:40. ### Make wheel zoom fine enough to inspect small features - **Rule:** Use a moderate shared zoom sensitivity and preserve continuous wheel/trackpad deltas. Do not add a different zoom implementation to each project. - **Incident:** During a later CAN-housing review on 2026-10-27, the user asked for more fine-grained wheel control in the standard viewer. The shared stage now uses OrbitControls `zoomSpeed: 0.25`; custom integrations can override it. This also affects dolly and pinch gestures. - **Check:** In the browser at device pixel ratio 1, a 100-pixel wheel event changed camera distance by 1.29059%, versus 5.26316% at the prior sensitivity. Four fine increments matched one old increment; reversing the wheel restored distance. A 1-pixel delta moved only 0.012824%, with no JavaScript exceptions. - **Source:** CAN breakout review, 2026-10-27; `src/cadagent/webkit/stage.js` and a browser check on the served project viewer. This is a later-project follow-up, not evidence from the original J501 transcript. --- Source: https://cad-agent.valpatel.com/lessons/09-agent-process.md # 09 — Agent process ### Fold mid-turn messages into the current work and report on each - **Rule:** Treat each mid-turn message as a requirement that joins the current plan. Acknowledge it, and account for it in the final report. - **Incident:** Within five minutes the user sent several new requests in a row: - reset and flashing button cutouts; - RP-SMA positions "(2x each)", which are pigtails with nuts; - thumbnails; - STL and Bambu links; - fit-template standoffs. The report opened with "All four of your requests are now in Rev 0.5.2" and covered every item, including the open question. - **Check:** The final report has one entry per user message since the last report, each with a status: done, partial or needs input. - **Source:** 2026-10-24T16:34–16:39, 17:10. ### Give short status during long work, and say what is stale - **Rule:** During long runs, post one-line status updates: what is running, what is waiting, and which outputs are out of date. - **Incident:** The agent posted lines such as "The build is running in the background; I'll pick up when it finishes" and "Some images are still from earlier runs until the full rebuild finishes." The user still asked to see progress, which led to the on-page progress section (see 08). - **Check:** No silent gap longer than one pipeline stage. Every report says which artefacts are stale. - **Source:** 2026-10-24T14:44, 16:46; 2026-10-25T00:36, 01:23. ### Ask before outward or irreversible actions - **Rule:** Before committing, pushing, opening PRs, publishing repos or installing third-party plugins, ask and wait for a clear yes. Build locally first. - **Incident:** - The `vptest cad` work stayed uncommitted in a separate worktree: "I'll ask before committing or opening a PR." - For the proposed public repo, the agent proposed building locally first and publishing only after review, because public content may be copied or indexed. - Installing text-to-cad or a KiCad MCP server "runs their code on your machine", so the agent asked for a go-ahead and had the survey install nothing. - The private repo was created only after the user asked for one. - **Check:** A list of outward actions (git commit/push, repo create, PR, package or plugin install, public upload) that need explicit user approval in the current conversation. - **Source:** 2026-10-25T00:36, 01:45, 02:18, 02:19, 02:20, 02:23. ### Check whether it already exists before building it - **Rule:** Before scaffolding a new tool or framework, search for prior art. Record licence, activity and fit, and prefer adopting it and contributing the gaps. - **Incident:** The agent had started planning a new cad-agent framework ("I'm starting Phase 0 now"). The user said: "I can almost 100% guarantee this already exists?" A search found text-to-cad (MIT, about 23,000 stars at the time), which already ships CAD and DFM skills for Claude Code, Codex and others, plus several KiCad MCP servers. The plan changed to: adopt the best tools, survey the field properly, test-drive them on this project, and contribute only the real gaps (designing an enclosure around a real board, cable bend radius, fastener stack length, evidence-backed review). - **Check:** A landscape file (here `research/landscape.md`) exists before any new framework code. Each gap claimed is backed by a search result. - **Source:** 2026-10-25T02:18, 02:19. ### Mine and handle only the data the user selects - **Rule:** Limit history mining and data handling to the sources the user names. Write that scope down where future runs will see it. - **Incident:** "You shouldn't mine -all- session histories, only this one", and "This one with J501 is important." The repo README and a saved standing preference record the scope. The extractor keeps only user and assistant prose and strips personal details. - **Check:** The miner takes an explicit session ID, never a glob. Its output header names the single source. - **Source:** 2026-10-25T02:22, 02:25. ### Diagnose desktop problems without disrupting the user's session - **Rule:** For problems on the user's machine, diagnose first. Use the least disruptive fix, restore any setting you touch, and never log out, restart or kill the user's processes without asking. - **Incident:** "my mouse cursor looks broken ... I can't click anything". A sub-agent was told to diagnose first and avoid anything that could log the user out. The X server's grab report showed that Bambu Studio held an active pointer grab from a stuck "Seam position" tooltip. Escape and clicking did not release it. The sub-agent briefly enabled the X keyboard grab-break option, sent the ungrab key, and then restored the keyboard options. No apps were closed, and the long-running CAD jobs were left alone. The user was told the project in Bambu was unsaved. - **Check:** Before and after: grab report, keyboard options, pointer position. The final note lists every setting touched and confirms it was restored. - **Source:** 2026-10-25T02:21, 02:23. ### Hand long, independent work to background jobs and agents, and check what comes back - **Rule:** Run slow pipelines and separable sub-projects in the background or in isolated worktrees. Keep working on the main thread, resume agents after interruptions, and verify their claims before relaying them. - **Incident:** - The roughly 15-minute build-and-check pipeline ran in the background while the agent edited docs and the site. - The user asked to "standardize the tooling for the report under some vptest repo sub-folder for CAD designs". One sub-agent first mapped vptest's conventions read-only. A second built `vptest cad` in a separate worktree. - The second agent was cut off by a session restart before it wrote anything, and was restarted. - Its report included a fixed fallback-parser bug with a test that catches it, failing tests compared against a clean HEAD before being called pre-existing, and a tracked file the test suite had rewritten, which it restored. - **Check:** Background agents work in a worktree or scratch copy, never the user's checkout with uncommitted changes. Their reports state what was not done. The parent re-runs at least the key check before reporting success. - **Source:** 2026-10-24T17:35, 17:40, 17:58; 2026-10-25T00:36, 01:44. ### Check facts directly instead of answering from memory - **Rule:** When the user asks a factual question about the current design or the outside world, inspect the artefact or primary source before answering. - **Incident:** On "P1M and P! don't support stacking the drives right?" the agent said: "I'll check both parts directly rather than answer from memory." On prior art it said it was "checking the biggest one directly rather than relying on search snippets". - **Check:** The answer cites the file, part or URL that was inspected. - **Source:** 2026-10-25T01:30, 02:19. --- Source: https://cad-agent.valpatel.com/lessons/10-simulation-and-material.md # 10 — Simulation, material and airflow ### Identify the exact bought part before sizing anything around it - **Rule:** before an envelope, a clearance or a thermal model depends on a bought part, pin down its exact SKU and datasheet, and record the source next to the number. "Same vendor, same board, same purpose" is not the same part. - **Incident:** the J501 enclosure was sized for a 110 × 100 × 50 mm cooler taken from Seeed's *reComputer mini J501* heatsink datasheet (SKU 100099421). The README's reference cooler is a different Seeed product, the WS-RDN-601 (p-5970), 100 × 87 × 34 mm. The lid was about 16 mm taller than it needed to be, and the intake gap was wrong for airflow. Found on 2026-10-26 while building the thermal model from the datasheet. - **Check:** every part dimension in the parameters names its source document. Where the user owns the part, ask them to measure it before printing. ### Judge material savings on slicer grams, not CAD volume - **Rule:** a filament-reduction claim uses the slicer's grams and print time with a named profile (`cad-agent slice`). CAD volume misleads: walls and floors print as perimeters, skins and sparse infill. - **Incident:** the Rev 0.7 base is 293 cm³ of solid, which would be 363 g of PLA, but it slices to 196 g. The lid is 198 cm³ but slices to 164 g, because its 6 mm hex grille is mostly perimeters. The two parts don't scale the same way. - **Gotcha:** Bambu Studio's command line needs flattened profiles (the system profiles inherit from parents; unflattened, the filament density is 0 and it reports 0 g) and `from: system` (a `User` process fails the printer compatibility check). ### A thinner shell must be re-checked under load, and the check must catch it - **Incident:** the first Rev 0.8 lid had a 3.5 mm roof (down from 6 mm). Under 300 N on the lid it: - deflected 2.4 mm, twice Rev 0.7's 1.19 mm; - reached a 99.9th-percentile stress of 19.4 MPa, a safety factor of 1.3 on PLA's layer strength. Rev 0.7 had 6.4 MPa. Its "design" stress (the peak away from the supports) read 56 MPa, but that was one element at a notch. - **Ribs:** the roof topology study showed radial ribs from the intake ring to the walls and corners. - **Sweep:** a parametric sweep (roof 3.5 / 4 / 4.5 mm × no, cross or star ribs, nine FEA runs) picked a 4.5 mm roof with the star ribs: 0.87 mm, 11.7 MPa (safety factor 2.1), 133 cm³ against Rev 0.7's 198 cm³. - **Where the stress was:** the hottest elements located the remaining weak points. Ribs ended on the lid's vent slots, and the rib-to-ring junctions were sharp inside corners. - **Rule:** report deflection, a percentile stress (99.9th) and the peak with its location. One element at a notch is a mesh-dependent spike; a stiffness loss is real. Compare against the previous revision computed the same way, with the acceptance criteria fixed before the sweep. ### Thermal models of printed enclosures - Check the plastic, not just the chip. PLA's heat-deflection temperature is 57 °C and PETG's is 69 °C (Bambu TDS); air leaving a cooler can exceed that. Report the walls' air temperature against a design limit below the HDT. - A fan is a curve, not a flow rate. Model it on its datasheet curve (`cadagent.cfd.Fan`) so the grille, fins and vents set the operating point. - **Check that still air stays still before believing a buoyant case.** The first J501 runs used a uniform *static* pressure on the open boundary (`prghTotalPressure`). Real room air needs the hydrostatic gradient, so the model blew a fake wind through the room: about 350 m³/h, nine times the fan's free-air flow. That made the intake air read as room temperature and every number optimistic. The separate review agent on the cad-agent PR caught it from the boundary velocities (air going *down* through the top of a heated room). Fixed with `p_rgh` uniform (hydrostatic). An unheated test room now stays at 0.009 m/s mean, against 0.62 m/s before. All results were re-run. - **Close the energy balance.** The heat leaving with the air must equal the heat put in. That check would have caught both the draft and a calibration that targeted the wrong power. - Stacked 0.2-0.3 mm fins cannot be meshed at enclosure scale. Use a compact model: a porous block fitted to an analytic plate-fin model (`cadagent.heatsink`). Then turn the CFD's fan flow and intake temperature into the plate temperature with the same model, and say so. - Measure recirculation directly: the air temperature entering the fan, against the room. An intake ring from the grille to the fan is the fix when it is above room temperature. ### Steady CHT of a box in an open room converges slowly: calibrate on local quantities - With the stable settings (velocity relaxation 0.3, energy 0.7), the heat leaving the room and the intake temperature drifted for more than 2000 iterations. That's the buoyancy-coupled circulation settling, not the linear solver: multigrid for energy changed nothing. Tighter energy solves, less energy relaxation, and frozen-flow energy passes all diverged in the first iterations. - What works: - start cautious; - restart with faster relaxation once the flow has formed; - set the fin temperature from the block's own heat, exactly hA (T_fin - mean block air temperature), which is local and settles early; - keep the room's energy balance as the honest "has everything caught up" indicator; - give the room only modest margins (30 mm sides, 40 mm above): every extra litre of slow air is more iterations. - The fan flow changed by 2× between a 16 mm and an 8 mm base mesh: thin shroud plates and grilles need their cells. Compare designs on the same mesh, and say which mesh it was. ### Compute is shared: one heavy solver at a time - MPI CFD runs only as fast as its slowest rank. With topology optimisation, FEA (MKL/PARDISO threads) and another session's jobs on the same 24 cores, the solver slowed from 6 s to about 60 s per iteration. Pause or nice the others (`kill -STOP` on the ranks is safe), and never `pkill -f` a pattern that appears in your own command line. - **Source:** 2026-10-26 (J501 session, Rev 0.8 thermal and material work); `cad/parameters.json` (`thermal`, `cooler`); `tools/thermal.py`, `tools/structure.py`, `tools/topology.py`; Seeed datasheets S03 and S05. --- Source: https://cad-agent.valpatel.com/lessons/11-pcb-evidence-and-review.md # 11 — PCB evidence, enclosure readback and release packaging Distilled from the incident notes of a second project: a two- and four-port CAN breakout PCB with a printed housing, designed with KiCad 10 and cad-agent on 2026-10-26 (repository `mvalancy/pcb-agx-thor-can-breakout`). The board-specific detail (coordinates, net names, project tools) stays in that repository's `.agents/` notes; each **Source** line names the note. Nothing here was electrically or physically tested; the lessons are about CAD evidence. ### Treat ERC, DRC, connectivity and schematic parity as separate evidence - **Rule:** a routed board with zero unconnected items is not a verified board. Keep ERC, DRC violations, unconnected items and schematic parity as separate results, and never reduce a DRC report to its `violations` array. - **Incident:** the first routed KiCad 10 reports showed 0 unconnected items but 100/131 schematic parity warnings. The PCB net names lacked the schematic's root `/` namespace, no-connect pads had no isolated net, and footprint fields were missing. Netlist-based synchronisation and board-only mount attributes closed the parity gap. - **Check:** `cad-agent pcb-check` (docs/pcb.md) fails on any error, warning, unconnected item or parity finding. - **Source:** `.agents/inbox/2026-10-26-pcb-evidence.md` (cad-agent PR #1). ### Run every severity, and account for every ignored check - **Rule:** generate ERC/DRC with `--severity-all`. A default kicad-cli run leaves excluded violations out of the report and `--severity-error` drops warnings, yet both reports look clean. Checks a project switched off (`ignored_checks`) are findings too: accept each one only by name, with a written reason. - **Incident:** the committed DRC report recorded `included_severities: ["error","warning"]` and five ignored checks (for example `missing_courtyard`). The first evidence adapter passed it with no trace of either. A review of the adapter caught it by re-running DRC with `--severity-error`, which still passed. - **Check:** the adapter fails a report whose `included_severities` lacks error, warning or exclusion, and fails every ignored or excluded check key the evidence manifest does not waive with a reason. - **Source:** cad-agent PR #1 review (finding on `pcb.py`); `exports/2port/drc.json` in the PCB project. ### Re-run DRC on the bytes you saved - **Rule:** bind reports to source hashes taken after the last write to the sources. `kicad-cli pcb drc --refill-zones --save-board` writes the board after its JSON, so the report describes the board before the save; run DRC again on the saved board without saving. Touching a report's timestamp is not evidence. - **Incident:** a refill-and-save run produced a report older than the board it described; the export sequence now runs a second, read-only DRC. - **Check:** the bundle records `source_sha256` and `report_sha256`; binding rejects reports older than any source. - **Source:** `.agents/inbox/2026-10-26-report-freshness-formats.md`. ### Check each report's declared hashes with one shared helper - **Rule:** different reports declare different evidence (source hashes, report hashes, mechanical sources, exported solids). Verify whichever maps a report declares, all through one containment-aware function, and use it everywhere a pass is displayed. An empty or invalid declared map fails. - **Incident:** the review workbench applied the PCB-bundle checker to every report with `source_sha256`, so current routing and fabrication reports showed STALE. The standalone report had the opposite gap: it trusted `pass: true` without checking hashes, so a changed solid still passed. Later, the design card and the workbench disagreed about a bundle whose source path left the project root. - **Check:** `cadagent.evidence.hashes_are_current`; regression tests change one source byte and expect STALE in the workbench, the report row and the design card. - **Source:** `.agents/inbox/2026-10-26-report-freshness-formats.md`; cad-agent PR #1 review. ### Measure routing geometry and the return plane; DRC does not - **Rule:** connectivity and clearance checks say nothing about differential pairing or return-path continuity. Measure the shortest layer-aware copper path per signal (excluding stubs such as termination branches), require the protection pads on that path, compare pair imbalance and layer changes, and sample the reference copper under each trace. Route constrained pairs first and reserve the plane beneath them before routing anything else. - **Incident:** boards that passed ERC, DRC and parity had separated pair conductors, and an auxiliary track left a 19.3 mm gap in the reference plane under one pair. A total-copper-length sum hid it by adding the termination branch. - **Check:** `cadagent.pcb_routing` graph/path and paired-spacing measurements, with project-defined limits. They do not establish impedance, data rate, EMC or transient immunity. - **Source:** `.agents/inbox/2026-10-26-can-routing-is-not-connectivity.md`. ### Prove a stitching via joins the main plane, not another island - **Rule:** a via touching copper on both layers may join two isolated islands. When a zone-to-zone ratsnest remains, inspect the filled, connected regions and re-run native connectivity after each stitch. Never disable the unconnected check or treat a zone-to-zone finding as cosmetic. - **Incident:** one DRC `unconnected_items` finding persisted after stitching; the via had joined a small bottom island to a separate front region. A second via where the island overlapped the main-plane region closed it. - **Check:** DRC with `--refill-zones` reports zero unconnected items. - **Source:** `.agents/inbox/2026-10-26-ground-islands-need-a-real-bridge.md`. ### A part substitution is a new design: re-source and re-check it - **Rule:** identify a bought part by manufacturer plus exact MPN plus full supplier code. A short name, pitch or category is not substitution approval. After changing a footprint, rerun electrical and mechanical checks, even within the same connector family. - **Incident:** exact connector pad geometry replaced a generic footprint of the same family and changed clearances, which needed a fresh DRC. A TVS part was referred to by a short family name that matches several products. - **Check:** BOM rows carry exact C-codes (`cadagent.pcb.check_jlc_files`), and a C-code is still not proof of stock or orientation. - **Source:** `.agents/inbox/2026-10-26-pcb-evidence.md`, `.agents/domains/pcb.md` (cad-agent PR #1). ### Align a PCB STEP with a rotation, never a mirror - **Rule:** reorient native or manufacturer STEP assemblies with proper rotations and translations (determinant +1). A mirror can make a symmetric outline fit while reversing every asymmetric connector. Record the transform and apply the same map to labels, port centres, mating envelopes and clamps. - **Incident:** the enclosure import used `mirror('XZ')` to make the board's Y positive. Outline collision checks passed while the ribbon header sat on the mirrored side. A 180° rotation about Z plus a translation fixed it. - **Check:** check one asymmetric feature (a header, a polarised connector) in housing coordinates after import. - **Source:** `.agents/inbox/2026-10-26-preserve-pcb-handedness.md`. ### Give joined features positive overlap; a valid BREP can still mesh badly - **Rule:** a boss or rib that only touches a wall tangentially can give a valid single solid and a non-manifold STL. Joined features need positive volume overlap. Check the solid, the STL and the 3MF separately after every change. - **Incident:** a Ø9.2 mm corner boss touched the inner wall with no overlap; the CAD was valid but the STL had six edges not shared by exactly two faces. Ø9.6 mm (0.2 mm overlap) fixed it. - **Check:** the watertight-mesh check in `cad-agent check`. - **Source:** `.agents/inbox/2026-10-26-tangent-boss-mesh.md`. ### Render final review images from the exported STEP - **Rule:** final assembly pictures should show the deliverable: re-import the exported STEP for each scene and compare visible placement to model coordinates, especially for text, repeated instances and exploded views. - **Incident:** an in-memory render showed two lid labels run together although their centres were 42 mm apart; the STEP readback rendered them correctly. The exported solids were right; the preview was not. - **Check:** render from readback; record export hashes next to the images. - **Source:** `.agents/inbox/2026-10-26-review-export-readbacks.md`. ### Engrave panel artwork in bounded operations - **Rule:** do not subtract one compound of many overlapping text and stroke tools. Cut item by item, fusing only each polyline's own stroke and cap solids, and validate the final solid count and the exported mesh. - **Incident:** one aggregate subtraction ran for more than twelve minutes without finishing; the staged cuts finished a probe in about 109 s with one valid solid. - **Check:** `cadagent.markings.PanelMarkings.engrave`; timing depends on the artwork and is not guaranteed. - **Source:** `.agents/inbox/2026-10-26-panel-engraving-overlap-cost.md` (cad-agent PR #1). ### Package releases from the validated manifest, not a folder glob - **Rule:** release membership comes from the validated public file list. Compare the member hashes of a local package with one built from a clean checkout. - **Incident:** local PCB packages contained KiCad's Git-ignored `.kicad_prl` UI-state files; CI packages built from clean checkouts did not. Both passed source validation because the glob, not the manifest, chose the files. - **Check:** package candidates are filtered through the allowlist; local and clean-checkout ZIP membership match. - **Source:** `.agents/inbox/2026-10-26-release-manifest-allowlist.md`. ### Don't let `.github/README.md` replace the repository overview - **Rule:** GitHub picks the landing README from `.github/`, then the root, then `docs/`. When the root README is the overview, do not create `.github/README.md`; document CI in `.github/workflows/README.md`. After publishing, check the hosted `/readme` API and the visible page, not only local links. - **Incident:** a README-in-every-folder generator created `.github/README.md`, and the public page showed only "CI validation and prerelease publication". - **Check:** `gh api repos///readme --jq .path` returns `README.md`. - **Source:** `.agents/inbox/2026-10-26-github-readme-precedence.md`; https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-readmes ### Keep CAD checks, fit and qualification as separate gates - **Rule:** a clean CAD check never turns fabrication, physical fit, transient protection, bus timing, stock or supplier orientation green. Check the enclosure against the native PCB STEP with its components, mating plugs, cables, fasteners and tool access, not a bare-board proxy; record physical qualification as pending until measured. - **Incident:** the review playbook for the breakout listed eight steps from carrier revision to physical qualification; the evidence bundle keeps `physical_fit_verified` and `electrical_tested` false and release gates open. - **Check:** design cards show CAD status and open release gates separately (docs/pcb.md). - **Source:** `.agents/playbooks/pcb-review.md`, `.agents/issues/pcb-evidence.md` (cad-agent PR #1). --- Source: https://cad-agent.valpatel.com/lessons/12-original-file-viewer.md # Original-file previews ### Parse the requested original and preserve its identity during recovery - **Rule:** A file preview must parse that file's actual bytes, units and component transforms. Do not substitute a companion with a similar name. If a failed replacement leaves an earlier model visible, name the retained file explicitly and keep the requested download clearly identified. - **Incident:** On 2026-10-29, real barrel STL/3MF print exports had dimensions about 30 × 35 × 100 mm while the STEP was 100 × 35 × 30 mm. Those different export orientations were real; using a sibling would have hidden the difference. Independent review also identified a potential mismatch between candidate title/download and a retained older mesh during an error. - **Check:** `node tests/webkit-file-viewer-browser.mjs` checks known-dimension originals, declared-inch 3MF, translated/reflected instances and `A→failedB→retry` identity. The remote 495-file parser receipt is in `docs/validation/file-viewer-corpus-results.json`; this does not replace browser visual checks. - **Source:** 2026-10-29, PR 63 / issue 62; direct barrel probe and independent actual-parser review. No CAD geometry was changed. ### Verify bundled parser behavior rather than assuming a newer API - **Rule:** Test the pinned parser's actual classifier, unit behavior and dependency recovery. Keep CPU work in a terminable worker and ensure the page can still respond to Cancel. - **Incident:** Three r160's 3MF loader reads unit metadata without applying its scale. Its bundled fflate 0.6.9 ignores a modern unzip filter option, so the wrapper initially tried to validate relationship XML as a model. The STEP runtime internally retries a failed WASM fetch, requiring a sustained dependency failure in the error fixture. These were found using real originals and explicit failure injection. - **Check:** The browser suite validates millimeter/inch conversion and reflected winding; worker wrappers filter actual model keys, preflight STL/ZIP admission, and validate finite indexed surfaces. A busy-worker fixture proves main-page animation frames continue and native Cancel calls worker termination. Failed WASM, stalled response body and failed entry-module fixtures all recover through native Retry. - **Source:** 2026-10-29; `file-mesh-worker.js`, `file-step-worker.js`, `file-loader.js` and the dedicated browser/validator tests. Limits remain explicit: texture images omitted, required 3MF extensions rejected, capped input/triangles, parser deadlines. ### Check the distributed wheel, including dependency notices - **Rule:** Inspect the built package for required runtime, license and source-reference files; a source-tree listing is insufficient. Use visible filenames for required metadata that packaging globs may omit. - **Incident:** Independent review flagged that hidden `.gitmodules` could be absent from a wheel. The viewer supplies its exact upstream contents as `SUBMODULES.gitconfig`, alongside licenses, importer sources and pinned full-source/build/replacement links. The 7.6 MB WASM fits the static host's per-file limit. - **Check:** `uv build --wheel --out-dir /tmp/cad-file-viewer-wheel`; compare every file in `vendor/file-viewer-sources.json` against the wheel's bytes and confirm all seven entry/parser files plus notices are present. - **Source:** 2026-10-29, PR 63 packaging check. Source availability and replacement instructions apply to the pinned build and need fresh review when dependencies change. --- Source: https://cad-agent.valpatel.com/lessons/README.md # Lessons from the J501 enclosure session Lessons for an AI hardware-design agent, mined from **one** Claude Code session: the J501 AGX enclosure design session (session `d8e464a0-f4a5-4edd-9190-6fd66a0508b0`, 2026-10-24 to 2026-10-25). In that session the agent and the user designed a 3D-printed enclosure for a Seeed reServer J501 carrier with a Jetson AGX Orin module, plus storage trays, SATA cable routing and a review website. No other session histories were mined; the user asked for this one only. ## How they were mined - **Input:** a scrubbed, chronological transcript produced by `mining/extract_session.py`. It keeps only the prose of user messages, mid-turn user messages and assistant replies. Tool calls and tool output are left out. Personal data (addresses, paths, hostnames) is replaced with placeholders such as `` and ``. - **Gaps in the transcript:** - Context was compacted at 2026-10-24T08:35. A summary covers the 07:00–07:41 work. - The session restarted between 2026-10-24T18:03 and 2026-10-25T00:31. Most of the storage-tray, chimney and standoff work happened in that window, so it appears only in later assistant summaries. - **Cross-check:** facts were confirmed read-only against the project files `README.md`, `cad/parameters.json` and `exports/*.json`. When a lesson relies on a project file, its **Source** line names the file. - **What counts as a lesson:** user corrections, requirements as the user stated them, assumptions that turned out wrong, checks that caught real bugs (or raised false alarms), and process choices the user endorsed or pushed back on. - **Limits:** - Nothing in this session was printed, sliced or test-fitted. Lessons about fit describe design intent and CAD checks, not physical validation. - Where the brief for this mining run mentioned an incident that could not be found in the transcript or the project files, the lesson says so rather than inventing details. File 08 also includes an explicitly attributed later CAN-project wheel-zoom follow-up (2026-10-27). File 11 comes from a different project: the incident notes of a CAN breakout PCB and housing designed on 2026-10-26. Its Source lines name those notes, which stay in that project's repository. ## Index | File | Topic | |---|---| | [01-verify-from-source.md](https://cad-agent.valpatel.com/lessons/01-verify-from-source.md) | Confirm board facts from schematics, photos and models, not from assumptions | | [02-user-intent-and-requirements.md](https://cad-agent.valpatel.com/lessons/02-user-intent-and-requirements.md) | How the requirements evolved, and the user's corrections | | [03-mechanical-design-rules.md](https://cad-agent.valpatel.com/lessons/03-mechanical-design-rules.md) | Strength, printability, landings, reliefs, windows, grilles, shell | | [04-fasteners-and-assembly-access.md](https://cad-agent.valpatel.com/lessons/04-fasteners-and-assembly-access.md) | Tool access, assembly order, screw length, inserts, drive screws | | [05-cables-connectors-and-routing.md](https://cad-agent.valpatel.com/lessons/05-cables-connectors-and-routing.md) | Mated-plug port windows (standard practice), SATA routing, plug lengths, bend radius, wiring rules | | [06-enclosure-features.md](https://cad-agent.valpatel.com/lessons/06-enclosure-features.md) | Antennas, service pins, storage trays, mount plate, cap, power budget | | [07-verification-and-evidence.md](https://cad-agent.valpatel.com/lessons/07-verification-and-evidence.md) | Checks that caught bugs, false positives, honest claims, stale outputs | | [08-review-site-ux.md](https://cad-agent.valpatel.com/lessons/08-review-site-ux.md) | What the user wanted to see on the review site, and why | | [09-agent-process.md](https://cad-agent.valpatel.com/lessons/09-agent-process.md) | Mid-turn messages, status, consent, prior art, scope, delegation | | [10-simulation-and-material.md](https://cad-agent.valpatel.com/lessons/10-simulation-and-material.md) | Exact bought parts, slicer grams, re-checking thinner shells, thermal models of printed enclosures, shared compute | | [11-pcb-evidence-and-review.md](https://cad-agent.valpatel.com/lessons/11-pcb-evidence-and-review.md) | KiCad report severities and waivers, parity, source hashes, routing geometry, STEP handedness, readback renders, release allowlists (from a second project; see below) | | [12-original-file-viewer.md](https://cad-agent.valpatel.com/lessons/12-original-file-viewer.md) | Original-file identity, parser units/frames, cancellable loading, dependency and wheel evidence | ## Lesson format ```markdown ### - **Rule:** one or two sentences, usable on any project. - **Incident:** what happened in this session (specific and factual; brief user quotes where they matter). - **Check:** how an agent or tool verifies it (a concrete check, threshold or artefact). - **Source:** transcript timestamp(s) such as 2026-10-24T16:35, and/or a project file. ``` Timestamps are UTC as they appear in the transcript, truncated to the minute. Part numbers (P1, P9, P15 and so on) are the project's printed-part IDs. ## Later rendered-motion incidents The 2026-10-28 Gyropup pivot validation lesson is maintained in [the rendered-motion domain](https://github.com/Valpatel/cad-agent/blob/dev/.agents/domains/rendered-motion.md), with a [symptom index](https://github.com/Valpatel/cad-agent/blob/dev/.agents/SYMPTOM-INDEX.md). It is not part of the original J501 transcript mining. --- Source: https://cad-agent.valpatel.com/research/agent-toolkits-2026-10.md # Agent toolkits for mechanical CAD, DFM/DFA and FEA: refresh since 2026-10-24 (checked 2026-10-31) For Valpatel/cad-agent #96 (landscape refresh) and #94 (compliant-mechanism engineer). Extends `research/landscape.md` (2026-10-24); it does not repeat the KiCad or benchmark sections. **Method.** Licence, stars, created and pushed dates and commit counts come from `gh api repos/OWNER/REPO` on 2026-10-31. Capabilities come from each README, manifest or source file as read on that day. Candidates came from `gh search repos … --created >=2026-10-15`, `gh search code "snap-fit filename:SKILL.md"` and web search. **(s)** means a search snippet only. **(u)** means unverified. **Nothing was installed or run.** Stars below 20 mean "new, unproven", not "bad". ## 1. Changes to projects already in landscape.md | Project | Change since 2026-10-24 | Effect on our recommendation | |---|---|---| | [earthtojake/text-to-cad](https://github.com/earthtojake/text-to-cad) | **v0.8.0 (2026-10-27) → v0.8.4 (2026-10-30)**. PR #568: the "xbf engine" replaces the 0.7 file pipeline with warm workers and a `Document` API. The migration guide says "no authoring compatibility layer… no shims, no aliases… no codemod" ([migrating-0.7-to-0.8.md](https://github.com/earthtojake/text-to-cad/blob/main/docs/migrations/migrating-0.7-to-0.8.md)). The plugin now pins `cadgen==0.8.4` ([claude.mcp.json](https://github.com/earthtojake/text-to-cad/blob/main/claude.mcp.json)). Telemetry is **still on by default** once cadgen has shown its notice; `DO_NOT_TRACK=1` or `CADGEN_TELEMETRY=0` turn it off ([cli/telemetry.py](https://github.com/earthtojake/text-to-cad/blob/main/packages/cadgen/src/cadgen/cli/telemetry.py)). The PCB PR #518 was **closed unmerged** on 10-28. Issue #626 (large-STEP viewer memory) was closed as completed on 10-28. The sheet-metal unfold proposal #614 is still open | Still adopt, but **pin 0.7.26 or 0.8.4 deliberately**. 0.7 scripts break on 0.8. Re-test the viewer with the J501 STEP. The PCB-to-enclosure path via text-to-cad is gone for now | | [gumyr/build123d](https://github.com/gumyr/build123d) | `BuildSheet` sheet metal is on the dev branch: `flange`, `bend`, `corner_relief`, `bend_relief`, `hem`, `jog`, `miter`, `thicken`, and **`unfold`** to a flat pattern from thickness, K-factor and bend radius ([docs/sheet_metal](https://github.com/gumyr/build123d/tree/dev/docs/sheet_metal)). First commit 2026-07-21, latest 2026-10-21. It is **not in v0.13.0** (2026-09-21): the file 404s at that tag | Watch; plan to adopt when released. It covers text-to-cad #614 and our sheet-metal checks | | [jdilla1277/agentcad](https://github.com/jdilla1277/agentcad) | v0.7.1 (2026-10-30), comments in the live viewer; 191★ | No change | | [KittyCAD/mcp](https://github.com/KittyCAD/mcp) | v0.33.0 (2026-10-30) | No change (closed cloud engine) | | [pzfreo/build123d-mcp](https://github.com/pzfreo/build123d-mcp) | No release since v0.3.91 (2026-10-15) | No change | ## 2. New, or missing from landscape.md ### 2a. Slicers (process-aware loop) | Project | Licence | Activity | What it does | Security | Rec. | |---|---|---|---|---|---| | **OrcaSlicer 2.5.0 built-in MCP server + Design tab** ([v2.5.0-beta](https://github.com/OrcaSlicer/OrcaSlicer/releases/tag/v2.5.0-beta), 2026-10-27; [`McpControl.cpp`](https://github.com/OrcaSlicer/OrcaSlicer/blob/main/src/slic3r/GUI/CAD/McpControl.cpp)) | AGPL-3.0 | Official project; OCCT 8.0.1; kernel tests in CI ([scripts/CAD/README.md](https://github.com/OrcaSlicer/OrcaSlicer/blob/main/scripts/CAD/README.md)) | Turned on under Preferences › General › AI features › MCP server. Loads, arranges and slices models, sets presets, supports and modifiers, runs calibration, and models parts in a parametric Design (CAD) tab | Server "accepts connections from that computer only" ([PRIVACY.md](https://github.com/OrcaSlicer/orcaslicer-mcp-skills/blob/main/PRIVACY.md)). Needs a running GUI; the GUI checks need OpenGL. Beta | **Watch** until 2.5.0 is stable, then **adopt as an external process** (AGPL: never link). It could replace our flattened-profile Bambu CLI for slice evidence | | [OrcaSlicer/orcaslicer-mcp-skills](https://github.com/OrcaSlicer/orcaslicer-mcp-skills) | AGPL-3.0 | Created 2026-10-29, 5 commits, official org | CC and Codex plugin: one SKILL.md and references (actions, settings, supports, calibration, cad, output) | No code, no network; plugin only ([PRIVACY.md](https://github.com/OrcaSlicer/orcaslicer-mcp-skills/blob/main/PRIVACY.md)) | **Adopt as a plugin** with Orca 2.5. Do not copy its text into our MIT repo (AGPL) | | [MaxEllis/orcaslicer-mcp](https://github.com/MaxEllis/orcaslicer-mcp) | AGPL-3.0 | 86★, v0.1.17 (10-12) | Third-party Orca MCP | — | Skip; the built-in server supersedes it | ### 2b. build123d / CadQuery agent toolkits (nearest to cad-agent) | Project | Licence | Activity | What it does on our tasks | Security | Rec. | |---|---|---|---|---|---| | [KD5RYN/b123d-print](https://github.com/KD5RYN/b123d-print) | MIT | Created 2026-10-17; at least 100 commits; PyPI; docs site; CI; 2★; one maintainer | MCP + CLI + CC plugin. `check` runs: valid, overlap, overhang (bridge vs cantilever), bridge length, wall/thin (knife edges), holes (round-top horizontal holes sag), bed-edge fillet, **tip-over** (Orca auto-brim formula), **gap/pip_clearance** for print-in-place, fit, probe, mate/align ([checks.md](https://github.com/KD5RYN/b123d-print/blob/main/docs/using/checks.md)). `suggest_orientation` ([orient.py](https://github.com/KD5RYN/b123d-print/blob/main/src/b123d_print/orient.py)). Examples: flip hinge, detent, overhang/bridge coupon | `.mcp.json` pins `b123d-print==0.3.2` via `uvx`; SECURITY.md (it runs AI-written code and can serve several users over HTTP/SSH); no telemetry hits in code search | **Vendor ideas / borrow checks** (MIT): tip-over, round-top hole, bed-edge fillet, pip-gap. Watch for maturity | | [ballerabdude/agent-cad-works](https://github.com/ballerabdude/agent-cad-works) | Apache-2.0 | Created 2026-10-26, 4 commits, 1★ | build123d motion sweep for collisions, single watertight solid, nothing printing in mid-air, OrcaSlicer times, fit against vendor STEP, BOM, FreeCAD exploded views, Blender renders; CC plugin; 16 tests | `uv sync` pinned environment | **Watch**; it overlaps cad-agent almost one for one. Compare its gates with ours | | [gjaloyan/print3d-designer](https://github.com/gjaloyan/print3d-designer) | MIT | Created 2026-10-28, 13 commits | Skill: measures a reference picture, builds from tested shapes, silhouette IoU ≥ 0.92 per view, slices with the user's presets, **reads the G-code back layer by layer** | — | Watch; G-code readback is a good evidence idea | | [SiddhuYen/eitri](https://github.com/SiddhuYen/eitri) | NOASSERTION (no standard LICENSE found) | 72 commits since 10-18 | Local agent: build123d, geometry report (holes, thinnest wall, knife edges, clearances), browser editor | — | Skip code (licence unclear); ideas only | | [pzfreo/step-diff-demo](https://github.com/pzfreo/step-diff-demo) + [pzfreo/specify-core](https://github.com/pzfreo/specify-core) | Apache-2.0 | 16★; specify-core-rust 0.1.5 (2026-10-31) | A STEP diff comment on every PR: named feature changes ("depth 11 → 13"), before/after picture, volume/area/bbox, and manufacturing answers carried to the new revision | GitHub Action | **Adopt candidate** for the review packet (#97 coverage, PR reviews). Trial on one product repo | | [naman113/Designstudio](https://github.com/naman113/Designstudio) ("Buildcore") | MIT | 8★, 3 commits | STEP → auto-dimensioned PDF drawings, grouped BOM, GLB viewer, parts catalogue (Docker) | Docker compose, local port 8800 | Watch | | [heartland-eng/bd_hardware](https://github.com/heartland-eng/bd_hardware) | **none** | 1 commit, README only | "Function-first hardware feature library for build123d … snap fits" | — | Skip (no code, no licence) | ### 2c. Vendor MCP servers missing from landscape.md (closed CAD) | Server | Status | Rec. | |---|---|---| | Autodesk **Fusion MCP** (local; Fusion must be running) and **Fusion Data MCP** (cloud) | Announced around 2026-04 ([engineering.com](https://www.engineering.com/autodesk-announces-fusion-mcp-servers-and-more-ai-updates/); [Autodesk help](https://help.autodesk.com/view/fusion360/ENU/?guid=FMCP-OVERVIEW)) | Skip (closed; no Linux Fusion) | | Autodesk **Fusion Compute MCP** | Public beta, cloud-hosted, no local Fusion needed (s) ([blog](https://www.autodesk.com/products/fusion-360/blog/fusion-compute-mcp/); the page returned 403 to fetch) | Watch | | PTC **Onshape FeatureScript MCP** | Launched 2026-08-13 via Onshape Labs / App Store ([press release](https://www.placera.se/pressmeddelanden/ptc-ptc-advances-leadership-in-ai-driven-design-with-launch-of-onshape-featurescript-mcp-server-20260813)) (s) | Skip (SaaS) | | SolidWorks community MCPs, e.g. [soheil-mp/solidworks-mcp](https://github.com/soheil-mp/solidworks-mcp) (MIT, 10★, created 10-23) | Windows-only | Skip | ### 2d. FEA / simulation agents | Project | Licence | Activity | What it does | Security | Rec. | |---|---|---|---|---|---| | [novasolver/novasolver-studio](https://github.com/novasolver/novasolver-studio) | GPL-3.0 | Created 2026-10-25, at least 100 commits | Local CAE with an MCP server: CalculiX structural/thermal, OpenFOAM, and a built-in quadratic-tet solver. Every result passes **ran → converged → verified** gates (Roache GCI mesh convergence; closed-form templates). The README lists nonlinear (plasticity, large deformation), and contact (mortar, tied) plus fatigue "0.7" with a Hertz check ([README](https://github.com/novasolver/novasolver-studio)) | "No account, no upload, no telemetry"; Windows download; browser demo | **Watch**; run only as an external process (GPL). **Borrow the 3-gate idea** for `cadagent.fea` | | [WifiLast/freecad-mcp](https://github.com/WifiLast/freecad-mcp) | **none** | 2 commits (10-17) | FreeCAD FEM + gmsh + CalculiX statics; SIMP topology optimisation keeping mounting faces exact; dimension optimisation | **Binds `0.0.0.0:2022` by default** (streamable HTTP) | Skip | | [V0v1kkk/freecad-engineering](https://github.com/V0v1kkk/freecad-engineering) | MIT | 3★, 2026-08 | Skills: `fdm-structural-review` (cites ASTM F3489/F3529, ISO; a material-profile validator script), `mechanical-strength-review`, `freecad-fem-check`, `mechanical-constraint-analysis` | — | **Vendor ideas** for the compliant-mechanism agent's evidence checklist | | [n3r/AgentCAD](https://github.com/n3r/AgentCAD) `fem-workflow` skill | Apache-2.0 | 7★, 100 commits, macOS-first | gmsh + FEM workflow; says contact and nonlinearity are out of scope | — | Watch | | [google/feabench](https://github.com/google/feabench) ([arXiv 2504.06260](https://arxiv.org/abs/2504.06260)) | Apache-2.0 | — | LLM agents on FEA problems; **needs a COMSOL licence**; no agent fully solved a problem (s) | — | Skip (COMSOL); cite as prior art | ### 2e. DFM / DFA | Project | Licence | What it does | Rec. | |---|---|---|---| | [Vivek-Boora/dfm-agent](https://github.com/Vivek-Boora/dfm-agent) | **none** | STEP → optimal pull direction, faces classed core/cavity/undercut, draft against pull, parting-line loop, Three.js view | Ideas only, for the moulding checks (no licence) | | [shayya/dfm-agent](https://github.com/shayya/dfm-agent) | none | Claude Agent SDK + pythonOCC; "day 4", topology counts only | Skip | | Boothroyd Dewhurst [DFM Design Review Checklist](https://www.dfma.com/resources/pdf/dfm-design-review-checklist.pdf) | vendor PDF | Process prompts (§3 of fea-and-process-tools.md) | Cite; seed #97 | ### 2f. Published skills for compliant mechanisms and snap fits **Yes, several exist**, all created in 2026, all prompt-only, and none with tested calculators: | Skill | Licence | Activity | Content | Quality notes | Rec. | |---|---|---|---|---|---| | [static74/3dp-compliant-mechanisms](https://github.com/static74/3dp-compliant-mechanisms) | MIT | Created 2026-10-26, 0★ | FDM-only skill: selection table (grip-fin lids, snap-in tabs, S-bracket clips, forked detent, flexible T-slot, fluted pins, printed vs metal springs with capture rules, glue features); `fundamentals.md` with an orientation rule, a tagged allowable-strain table (`[derived]` from Bayer rules × Prusament/Polymaker yield), perimeter quantisation and a clearance table; 3 evals with graders; sources in `docs/research` (largely Slant 3D videos) | Every number is tagged with its provenance. Its allowables are about 2× our TDS-derived values for PETG (compliant-mechanisms.md §6). No code, no FEA | **Vendor selectively**: the selection table, eval cases and provenance-tag scheme, with attribution. Recompute the strain table | | [n3r/AgentCAD](https://github.com/n3r/AgentCAD) `skills/snap-fits` | Apache-2.0 | 2026-08 | Cantilever and annular sizing, L ≥ 5t, taper rule, `material_strain.json` (per-material %, FDM derate 50–70 % in-plane), `cantilever_lid.py` | Some rows attributed to "Bayer 1998" (ABS 4–6 %) are not in the Bayer copy we read | Borrow the JSON-schema idea; check every value | | [MP-AI-20/mechanical-engineering-skills](https://github.com/MP-AI-20/mechanical-engineering-skills) `snap-fits`, `design-for-assembly` | MIT | 10★, one push (2026-06-09), "647 skills" | Generic textbook prompts (y = εL²/1.5h, strain tables with ranges) | Looks bulk-generated; few primary citations | Skip | | [dwragon/Fusion360SnapFitGenerator](https://github.com/dwragon/Fusion360SnapFitGenerator) | MPL-2.0 | Created 2026-10-24, v1.1.0 | Fusion add-in (not an agent skill): 5 snap types with strain and force calculation | — | Reference for parameters | | [Kayforkind/skill-slice](https://github.com/Kayforkind/skill-slice) `3d-articulated-print-in-place` · [laurigates/mcu-tinkering-lab](https://github.com/laurigates/mcu-tinkering-lab) `printed-mechanism` · [ghbalf/freecad-ai](https://github.com/ghbalf/freecad-ai) `enclosure` (LGPL-2.1, 601★) | MIT / MIT / LGPL | — | Print-in-place articulation; servo mechanism clash sweep; snap-fit lid option (fixed 1 mm gap, 0.5 mm tab) | Shallow on snaps | Skip | No published skill or agent: - reproduces design-guide worked examples in tests; - carries **process- and orientation-specific allowables from TDS** (beyond static74's derived table); - covers flexure pivots (notch, CAFP, LET) or bistable beams; or - links a snap design to large-deflection FEA or to a printed coupon result. That is the gap cad-agent #94/#95 fills. ## 3. Security notes for anything we might run - **Network binding.** WifiLast/freecad-mcp listens on all interfaces. b123d-print can serve over HTTP/SSH (stdio by default per its plugin config). Orca's server is local-only. text-to-cad's viewer has no auth on 127.0.0.1 (landscape.md). - **Arbitrary code.** Every code-CAD MCP (b123d-print, text-to-cad, agentcad, eitri) runs model scripts written by the agent. Run them in the project venv or a container, never with fleet credentials in the environment. - **Supply chain.** Pin plugin versions. Both text-to-cad and b123d-print use `uvx` with an exact version but unhashed dependency ranges (landscape.md §2 for text-to-cad). - **Telemetry.** text-to-cad is on by default (off with the env vars above). NovaSolver and Orca state none. The others were not audited. - **Licences.** AGPL/GPL tools (Orca, NovaSolver, MaxEllis) run only as separate processes. The Orca skills plugin is AGPL text: install it, do not copy it. Repos without a licence (WifiLast, Vivek-Boora, bd_hardware, eitri) are ideas only. ## 4. Recommendations | # | Item | Decision | |---|---|---| | 1 | OrcaSlicer 2.5 MCP + official skills | **Adopt when 2.5.0 is stable** (external process). Pilot now on a non-fleet workstation for slice evidence and supports | | 2 | text-to-cad 0.8 | **Keep pinned**; decide 0.7.26 vs 0.8.4 explicitly and record it in `docs/toolkit.md`. Keep telemetry env vars | | 3 | step-diff (specify-core) | **Trial** in one product repo's PRs (Apache-2.0) | | 4 | b123d-print checks | **Borrow** tip-over, round-top hole, bed-edge fillet, print-in-place gap as cad-agent checks with seeded-defect fixtures (#97) | | 5 | static74 skill | **Vendor selectively** (MIT, attribution) into `skills/compliant-mechanisms/references/` after recomputing allowables from TDS | | 6 | NovaSolver Studio | **Watch**; copy the ran/converged/verified gate idea, not the code | | 7 | build123d sheet metal | **Watch**, adopt on release | | 8 | Fusion / Onshape / SolidWorks MCPs, FEABench | **Skip** (closed, non-Linux, or COMSOL) | --- Source: https://cad-agent.valpatel.com/research/compliant-mechanisms.md # Compliant mechanisms and single-part joints: references, equations, allowables, process effects (2026-10-31) For Valpatel/cad-agent #94 (compliant-mechanism engineer), #95 (calculators), #97 (pitfalls), #99 (process profiles). Extends `research/landscape.md` (2026-10-24) and `research/simulation-tools.md` (2026-10-26). Neither covers this field. **How this was checked.** "Read" means the text was pulled from the PDF or page on 2026-10-31 (`pdftotext` or the page's own Markdown) and the number is quoted from it. **(s)** means the number comes only from a search-engine snippet of the page, not from reading the page. **(u)** means unverified: from memory, or from a source that could not be opened. **[derived]** means I calculated it from cited inputs; the method is stated beside it. Citation keys such as [BAY] are listed in §10. ## 1. Feature catalogue | Feature | Use it when | Governing equations / reference | Typical failure modes | Process notes | |---|---|---|---|---| | **Cantilever snap**: constant section, tapered to h/2, or width tapered to b/4 | One-off or occasional assembly of a lid, cover or panel; you want the joint stress-free once seated | Bayer Table 1 [BAY]: y = K·ε·l²/h, P = b·h²·E_s·ε/(6l), W = P(μ+tanα)/(1−μ·tanα). DuPont: S = FLc/I, y = FL³/3EI [DUP]. See §2 | Root crack at a sharp fillet; strain over the allowable because the undercut was guessed; creep loss of retention when left deflected; weld line at the hook [DUP] | Moulded: a 90° undercut needs a side core or a slot in the part [DUP]. FDM: deflect within the layers and never stack the beam in Z [KLA][HUBS-SF] | | **U-shaped / L-shaped snap** | Not enough straight length for the strain: folding the arm makes it longer in the same envelope | Bayer recommends a curved-beam analysis and gives no closed form [BAY]. BASF manual pp. V-1…V-4 [BASF] (s). A Fusion add-in implements U-shaped and overlay snaps [F360SF] | Strain peaks at the inner radius of the U; the fold becomes a stress raiser | FDM: the U must lie flat in XY | | **Torsion snap** | Release by pressing a lever; the hook rotates rather than bends | φ_pm[°] = (180/π)·γ_pm·l/r; γ_pm ≈ (1+ν)·ε_pm ≈ 1.35·ε_pm; P₁l₁ = P₂l₂ = γ·G·I_p/r, with I_p = πr⁴/2 for a round bar [BAY] (the PDF text is partly garbled; the form is standard torsion) | Shear fracture of the torsion bar; posts too weak | FDM: run the bar along a layer; posts in Z are weak in bending | | **Annular snap** | Round, rotationally symmetric parts: caps, lamp covers, bearings | y_pm = ε_pm·d with one part rigid (halve the strain if both parts are equally flexible); P = y·d·E_s·X; X_N = 0.62·√[(d₀/d−1)/(d₀/d+1)]/{[(d₀/d)²+1]/[(d₀/d)²−1]+ν} (the square root is on the rendered page; pdftotext drops it; lamp cover: 1.69e-3, Bayer 1.7e-3); a groove far from the tube end needs up to 3× the force [BAY]. DuPont: hub-expansion force from interference stress and geometry factor W [DUP] | Hoop crack at a weld line [DUP]; an inseparable return angle needs a split-cavity mould [BAY] | FDM: a full ring is too stiff, so slot the collar into fingers [F360SF] | | **Living hinge** | Lid and box in one part, many cycles, moulded PP/PE | Moulded PP: 0.012 in (0.30 mm) thick, about 0.060 in (1.5 mm) spacing, a 0.008 in reduction on the side it folds towards, and a 0.030 in radius on the tension side [PL-LH]. Land 1.5 mm, thickness 0.2–0.4 mm [PT-LH] (s). Strain ε = t·θ/(2L) under uniform bending [derived] | Cracks at once if not flexed while warm (K-resin) [PL-LH]; knit line across the hinge [PL-LH] | A 180° fold of 0.3 mm over 1.5 mm reaches ε ≈ 31 % [derived], which only orienting PP/PE survives. FDM PLA/PETG hinges are short-life: print them flat with lines across the hinge axis (u). BOSL2 `living_hinge_mask` models a printed hinge by layer height [BOSL2] | | **Leaf flexure / parallel-guided stage** | Short-stroke guidance without backlash: probe mounts, stages, grippers | Small deflection: one leaf K_θ = EI/L under end moment; a fixed-guided leaf k = 12EI/L³, so two leaves give k = 24EI/L³ and σ_max = 3E·t·δ/L² [derived, beam theory]. Large deflection: PRBM γ = 0.85, K_Θ ≈ 2.65 for end force [PRB][CHEN-PRB] (s). References: Howell [HOW], Smith [SMITH] | Fatigue at the clamp; buckling under axial load; parasitic motion | FDM: leaves in XY, at least 2 perimeters, no infill in the leaf (u) | | **Notch hinge** (circular, elliptical) | Precision pivots with small rotation | Paros & Weisbord 1965, simplified (t ≪ R): θ/M ≈ 9πR^½ / (2E·b·t^{5/2}) (u: the constant was not checked against the 1965 paper; see Lobontiu [LOB]) [PW] | Stress concentration at the throat limits the angle to a few degrees | Needs isotropic, notch-tough material; poor in FDM (the notch sits across layers) (u) | | **Cross-axis flexural pivot (CAFP) / cross-spring** | Larger rotation than a notch, with less centre shift than a single leaf | Haringx 1949: the pivot point is near where the leaves cross and shifts with angle [HAR]. PRBM/CBCM model of CAFPs [CAFP]. The often-quoted K ≈ 8EI/L for leaves crossing at mid-length is **(u)** | Centre shift; stiffness depends on load direction [HAR] | FDM: leaves that cross in the XY plane print. Leaves crossing in Z need bridges (u) | | **LET joint** (lamina emergent torsion) | A rotational joint cut from flat sheet: laser-cut, waterjet or planar-printed | Jacobsen, Chen, Howell, Magleby 2009 give the force-deflection equations: torsion bars and bending segments combined as series/parallel springs [LET]. The equations were not read here (the PDF is bot-blocked): (u) | Parasitic motion, so the off-axis stiffness is low [LET]; torsion-bar shear | Planar by design: printed flat in XY, or cut from sheet. BYU LET-joint chair model [BYU] | | **Bistable latch** (curved beam) | Two stable states without a spring: switches, latches | Qiu cosine double beam, centrally clamped, second mode constrained: bistable when Q = h/t > 2.31; for Q ≥ 6, f_top ≈ 740·EIh/l³, f_bot ≈ 370·EIh/l³, d_mid = 1.33h, d_top ≈ 0.16h, d_bot ≈ 1.92h [QIU] (read) | Second-mode buckling with only one beam; asymmetric clamping; stress relaxation flattens the curve | FDM: the beams must lie in XY; the amplitude h must be ≥ several line widths | | **Detent** (cantilever and bump, ball and spring) | Indexed positions, holding a lid open | Cantilever equations from §2. Holding force uses the same W formula with the return angle α′ in place of α [BAY][derived] | The bump wears; the holding force drifts with friction (the μ ranges in §4) | Printed bumps on sloped faces chatter across the stair-steps [KLA] | | **Spring tab** (printed cantilever or serpentine) | Preload against a part, anti-rattle, battery contacts | Cantilever formulas; design for sustained strain well below the snap allowable (u) | Creep: a preload carried by a printed polymer relaxes. No study of permanently deflected printed snaps was found (§9) | A metal spring is safer for sustained load. static74's skill offers a printed/metal choice for every spring [S74] | | **Crush / press ribs** | A press fit into a drafted or variable hole without tight tolerance | At least three undrafted ribs along the hole [PL-CR]; rib about 2.5 mm diameter with about 0.25 mm interference [RD-CR] (s) | Ribs shear off; hoop crack in a thin boss | Printed: no published values were found, so print a coupon. FDM ribs in Z are strong in shear | | **Print-in-place joint** | Hinges and chains that come off the bed already assembled | XY clearance 0.2–0.4 mm for a 0.4 mm nozzle (forum and third-party guides, not official) [PIP]; FDM 0.5 mm, SLA/SLS/MJF 0.3 mm [HUBS-SF]. PrusaSlicer merges bodies closer than about 0.5 mm unless the slice-gap closing radius is changed [PIP-PS] (s) | Fused joint; elephant's foot locks the first layer | Make Z gaps a multiple of the layer height [PIP] (s). b123d-print's `gap` check fails solids closer than `pip_clearance` [B123] | | **Sheet-metal spring clip / tab** | A metal latch or ground spring cut and bent from tempered strip | Beam formulas with the yield of the temper: 301 full-hard has UTS ≥ 1276 MPa, yield ≥ 965 MPa, elongation ≥ 9 % [ULB] (s). Associated Spring flat-spring design stresses (u, not found) | Cracks at the bend in hard temper; fatigue at the root | Protolabs: features within about 4T of a bend distort; tab width ≥ max(2T, 3.2 mm) [PL-SM]. Bend radius against temper (u) | ## 2. Snap-fit equations and worked examples for tests (Bayer guide, read) Source [BAY]: Bayer MaterialScience, *Snap-Fit Joints for Plastics – A Design Guide* (26 pp). Covestro is the successor company and the issue cites the Covestro edition [COV]; the copy read here is the Bayer-branded PDF, and the Covestro edition prints the same equations and worked examples. Cite them, do not redistribute them. - **Undercut = deflection.** Partially crystalline resins may be strained almost to yield in a single brief snap. Amorphous resins may be strained to about 70 % of the yield strain. Glass-filled resins (no yield point) may be strained to about half the elongation at break. For frequent separation, use about 60 % of the single-use value. - **Taper.** A beam whose thickness falls linearly to h/2 (Table 1 design 2) allows **more than 60 % more deflection** than a constant section: y = 1.09·ε·l²/h. A constant section (design 1) has y = 0.67·ε·l²/h and a width taper to b/4 (design 3) has y = 0.86·ε·l²/h. These two constants are **(u)**: Table 1 is an image in this copy. They agree with the 1.09/0.67 ≈ 1.63 ratio, and 0.67 is exact beam theory for a constant rectangular section (y = PL³/3EI with ε = PLh/2EI gives y = (2/3)·ε·l²/h) [derived]. The Covestro edition's worked examples print 1.09 (design 2) and the general-section 0.55 (design 2) in text (read). The polarised-light figure shows a constant-section arm with 46 % higher strain and 17 % more material than the optimum. - **Root fillet.** Stress concentration levels off near R/h = 0.6, but a large radius makes a thick section that sinks. Use a compromise, and **never less than 0.015 in (0.38 mm)**. DuPont's general rule is a fillet of t/2 and a corner of at least 0.020 in (0.5 mm) [DUP]. - **Mating force:** W = P·(μ+tanα)/(1−μ·tanα). E_s is the strain-dependent secant modulus, taken at the design strain. | Gold test case | Inputs | Expected output [BAY] | |---|---|---| | Example I, tapered hook (design 2, rectangular section) | Makrolon PC; l = 19 mm; b = 9.5 mm; y = 2.4 mm; α = 30°; ε_pm = 4 % with design ε = ½ε_pm = 2 %; E_s(2 %) = 1815 MPa; μ = 0.5×1.2 = 0.6 | h = 1.09·0.02·19²/2.4 = **3.28 mm**; P = **32.5 N**; (μ+tanα)/(1−μ·tanα) = 1.8; W = **58.5 N** | | Example II, ring-segment hook (design 2, shape C) | Bayblend PC/ABS, frequent separation: ε = 0.6×2.5 % = 1.5 %; l = 25.4; r₂ = 20; r₁ = 17.5; h = 2.5; θ = 75°; K₂ = 2.67 (Fig. 10) | y = 1.64·K₂·ε·l²/r₂ = **2.11 mm**. Cross-check: c₂ = 2.52 mm, y = 0.55·ε·l²/c = 2.11 mm | | Annular lamp cover | Makrolon; d = 200; t = 2.5; undercut 2×1 mm (equal stiffness); α = 45° taken as 30° effective; E_s = 2200 MPa; d₀/d = 1.025 → X_N = 1.7e-3 | ε = 0.5 %; P = **748 N**; W = 748×1.8 = **1346 N** | These three cases are the acceptance tests the issue asks for ("calculators reproduce the design guide's worked examples"). ## 3. Allowable strain tables (moulded resin) **Bayer Table 2** [BAY] (read): short-term strain limits at 23 °C for a single assembly; use about 60 % for frequent separation. | Resin | Single | Frequent (×0.6) | |---|---|---| | Apec high-heat PC | 4 % | 2.4 % | | Bayblend PC/ABS | 2.5 % | 1.5 % | | Makroblend PC blends | 3.5 % | 2.1 % | | Makrolon PC | 4 % | 2.4 % | | Makrolon PC, 10 % glass | 2.2 % | 1.3 % | | Makrolon PC, 20 % glass | 2.0 % | 1.2 % | **DuPont Table 10.06** [DUP] (read): suggested allowable strain for lug-type snap fits. | Resin | Used once (new material) | Used frequently | |---|---|---| | Delrin 100 (POM) | 8 % | 2–4 % | | Delrin 500 (POM) | 6 % | 2–3 % | | Zytel 101 (PA66), dry | 4 % | 2 % | | Zytel 101, 50 % RH | 6 % | 3 % | | Zytel GR (glass), dry | 0.8–1.2 % | 0.5–0.7 % | | Zytel GR, 50 % RH | 1.5–2.0 % | 1.0 % | | Rynite PET GR | 1 % | 0.5 % | | Crastin PBT GR | 1.2 % | 0.6 % | | Hytrel (TPC elastomer) | 20 % | 10 % | - **BASF** *Snap-Fit Design Manual*, p. IV-4, has allowable strain and friction for eight resins, with worked examples for an Ultradur PBT grade and an Ultraform POM grade [BASF]. The values were **not read**: the tables are images, and only unofficial Scribd/studylib copies exist. - **Celanese/Ticona**: a dynamic strain limit up to 70 % of the yield strain for resins with a yield point, and 60 % of that for repeated engagement [CEL]. These figures are (s): the OCR is garbled. - **Older Bayer table.** Third-party skills quote ABS 4–6 % and PA 4–6 % "from the Bayer 1998 manual" [AGCAD]. That is **(u)**: the copy read here has only the six Bayer rows above. Do not carry those values over without the original. ## 4. Friction coefficients (Bayer Table 3, plastic on steel; same-material factor in brackets) [BAY] (read) PTFE 0.12–0.22 · PE rigid 0.20–0.25 (×2.0) · PP 0.25–0.30 (×1.5) · POM 0.20–0.35 (×1.5) · PA 0.30–0.40 (×1.5) · PBT 0.35–0.40 · PS 0.40–0.50 (×1.2) · SAN 0.45–0.55 · PC 0.45–0.55 (×1.2) · PMMA 0.50–0.60 (×1.2) · ABS 0.50–0.65 (×1.2) · PE flexible 0.55–0.60 (×1.2) · PVC 0.55–0.60 (×1.0). ## 5. FDM anisotropy: in-plane (X-Y) vs inter-layer (Z) data from manufacturer TDS (read) All specimens were 100 % infill, ISO 527 / GB/T 1040. Z means pulled across the layers. | Material (TDS version) | E XY / Z (MPa) | UTS XY / Z (MPa) | Elong. at break XY / Z (%) | Z/XY strength | Z/XY strain | Source | |---|---|---|---|---|---|---| | PLA Basic, Bambu (V2.0) | 2680 / 2160 | 39 / 35 | 12.2 / 7.5 | 0.90 | 0.61 | [BBL-PLA] | | PLA Matte, Bambu (V2.0) | 1960 / 1670 | 32 / 25 | 32.8 / 4.8 | 0.78 | 0.15 | [BBL-PLAM] | | PolyLite PLA, Polymaker (V5.4) | 3427 / 3065 | 52.3 / 40.5 | 6.3 / 1.8 | 0.77 | 0.29 | [PM-PLA] | | PETG Basic, Bambu (V2.0) | 1460 / 1120 | 48 / 39 | 11.2 / 5.7 | 0.81 | 0.51 | [BBL-PETG] | | PETG HF, Bambu (V1.0) | 1810 / 1540 | 34 / 23 | 8.6 / 5.1 | 0.68 | 0.59 | [BBL-PETGHF] | | PETG-CF, Bambu (V2.0) | 2460 / 1340 | 59 / 38 | 10.4 / 4.7 | 0.64 | 0.45 | [BBL-PETGCF] | | PolyLite PETG, Polymaker (V5.4) | 2117 / 1899 | 50.8 / 42.8 | 8.4 / 3.3 | 0.84 | 0.39 | [PM-PETG] | | ASA, Bambu (V2.0; annealed and dried 80 °C/12 h) | 2450 / 2120 | 52 / 35 | 9.2 / 4.6 | 0.67 | 0.50 | [BBL-ASA] | | PolyLite ASA, Polymaker (V5.4) | 2379 / 1965 | 43.8 / 32 | 6.7 / 1.65 | 0.73 | 0.25 | [PM-ASA] | | PolyMide CoPA, dry (annealed 80 °C/6 h) | 2703 / 2209 | 78.0 / 45.8 | 12.7 / 1.3 | 0.59 | 0.10 | [PM-COPA] | | PolyMide CoPA, wet (6.16 % moisture) | 724 / 660 | 34.3 / 13.7 | 13.2 / 1.7 | 0.40 | 0.13 | [PM-COPA] | | Fiberon PA6-CF20, dry (annealed 100 °C/16 h; V1.1) | 8637 / 3760 | 109.3 / 54.0 | 2.1 / 1.9 | 0.49 | 0.90 | [PM-PA6CF] | | Fiberon PA6-CF20, wet | 2508 / 1056 | 54.7 / 25.5 | 7.0 / 6.7 | 0.47 | 0.96 | [PM-PA6CF] | | PA6-CF, Bambu | — | — | 5.8 / 3.7 | — | 0.64 | [BBL-PA6CF] (s; the PDF now returns 404) | | TPU 95A HF, Bambu (V1.0) | 9.8 / 7.4 | 27.3 / 22.3 | >650 / >480 | 0.82 | ≥0.74 | [BBL-TPU] | | PolyFlex TPU95, Polymaker (V5.4) | 33.6 XY (ISO 37) | — | 551 XY | — | no Z data | [PM-TPU] | Reading the table: - **Strain to failure across the layers falls far more than strength.** For PLA, PETG, ASA and CoPA the Z/XY strength ratio is 0.40–0.90 (0.59–0.90 without wet CoPA), but the Z/XY elongation ratio is 0.10–0.61. How much of the *allowable* this costs depends on which limit governs in §6. Where 0.7·UTS/E governs, or the two limits are close (PLA, PETG, Bambu ASA), the allowable across the layers is 78–100 % of the in-plane value. Where half the elongation at break governs across the layers (Polymaker ASA, CoPA) it falls to 26–64 %. The orientation rule below applies either way: the notch effect of layer lines is not in a tensile test. - **Allum et al. 2020** found that the bond between layers has bulk strength in printed PLA. Their explanation for the anisotropy is the extruded-filament geometry: grooves and voids concentrate strain, not incomplete bonding [ALL]. So layer lines act like notches, and Bayer's "half the elongation at break" rule for materials without a yield point is the right model for Z. - **Carbon fibre lowers in-plane strain** (PA6-CF20: 2.1 % XY dry), so CF grades are poor for snap fits even though they are strong. Moisture roughly halves CF-nylon strength [PM-PA6CF]. That matches `cadagent.fea`'s ×0.6 humid factor. - **Orientation rule.** Lay the flexing member in XY and keep the joining direction independent of the bending direction. A cantilever built along Z is "poor", and the stair-step notches add stress concentration [KLA]. Hubs: avoid Z-built cantilevers; MJF root at least 1 mm thick; fillet ≥ 0.5×root thickness; width ≥ 5 mm [HUBS-SF]. Schreiber et al. 2021 found that moulding guidelines transfer poorly to FDM and SLS [SCH] (s). **HP Jet Fusion 5200 (MJF), XY / Z** [HP] (read): | Material | UTS (MPa) | Elong. at yield (%) | Elong. at break (%) | |---|---|---|---| | PA 12 | 50 / 50 | 10 / 8 | 17 / 9 | | PA 11 | 54 / 54 | 25 / 20 | 40 / 25 | | PA 12 GB | 31 / 30 | 8 / 4 | — | | PP | 29 / 29 | 9.5 / 9.5 | 20 / 14 | MJF is isotropic in strength but not in strain. **SLA (Formlabs).** - Tough 1500 V2: E about 1460 MPa, elongation 155 % [FL-T1500] (s). - Formlabs does not recommend Tough 2000 for living hinges [FL-T2000]. - No XY/Z breakdown was found, so SLA isotropy is (u). ## 6. Printed allowable strain: a starting rule to be confirmed by coupons [derived] Rule: 1. Single assembly: ε_single = min(0.7·UTS/E, 0.5·ε_break), applied separately to XY and Z values. 0.7·σ/E is Bayer's amorphous rule using UTS/E as a lower bound on the strain at maximum stress. 0.5·ε_break is Bayer's no-yield rule. 2. Repeated assembly: ε_repeat = 0.6·ε_single. 3. Never exceed the moulded value for the same polymer family (§3). 4. In Z, use min(Z, XY). 5. Bayer's own example designs at ½ε_pm, which is a factor of 2. | Material (TDS) | XY single / repeat | Z single / repeat | static74 skill, in-plane single / repeat / across layers [S74] | AgentCAD table, design value before its printing derate [AGCAD] | |---|---|---|---|---| | PLA (Polymaker) | 1.1 / 0.6 % | 0.9 / 0.5 % | 2.0 / 1.2 / ≤0.5 % | 1.2 % | | PLA Basic (Bambu) | 1.0 / 0.6 % | 1.0 / 0.6 % | — | — | | PETG (Polymaker) | 1.7 / 1.0 % | 1.6 / 0.95 % | 3.6 / 2.1 / ≤1.0 % | 3.0 % | | PETG Basic (Bambu) | 2.3 / 1.4 % | 2.3 / 1.4 % | — | — | | PETG HF (Bambu) | 1.3 / 0.8 % | 1.0 / 0.6 % | — | — | | ASA (Polymaker) | 1.3 / 0.8 % | 0.8 / 0.5 % | 2.0 / 1.2 / 0.3–1.0 % | — | | ASA (Bambu) | 1.5 / 0.9 % | 1.2 / 0.7 % | — | — | | CoPA, dry (Polymaker) | 2.0 / 1.2 % | 0.65 / 0.4 % | 3–4 / 2 / ≤0.4 % | PA 6 % (moulded basis) | | CoPA, wet | 3.3 / 2.0 % | 0.85 / 0.5 % | — | — | | PA6-CF20, dry | 0.9 / 0.5 % | 0.9 / 0.5 % | — | — | | TPU 95A | strain does not govern; size for force and retention | — | ≥10 % | — | - The derived values are **about half** of static74's for PETG, which applied 70 % of an estimated yield strain. AgentCAD's 3.0 % is before its own printing derate. After that derate (1.5–2.1 % in the layer plane, 0.9 % across the layers) it agrees with this table. - Rule 3 has no moulded value to compare PLA, PETG or ASA with: none of the guides read here lists them. Treat every row as a hypothesis until the cad-agent coupon (#94 item 5) measures it. - The repo's existing `cadagent.fea.MATERIALS` already uses weak-direction strengths for printed materials, which is consistent. ## 7. How the process changes each feature | Process | Changes | Key numbers | |---|---|---| | **FDM** | Anisotropic strain (§5); stair-step on slopes (ratchet feel across the steps) [KLA]; walls come in whole perimeters [S74]; bridges sag; creep in PLA (u) | Clearance 0.5 mm for snaps [HUBS-SF]; print-in-place 0.2–0.4 mm XY [PIP]; overhang about 45°, bridges under 5 mm sag least [HUBS-FDM] | | **SLS / MJF** | Near-isotropic strength with lower Z strain [HP]; no supports; powder must escape; PA12 picks up moisture | Clearance 0.3 mm; MJF root ≥ 1 mm [HUBS-SF] | | **SLA** | Choose a tough or durable resin, because standard resins are brittle [HUBS-SF]; post-cure changes the properties | Tough 2000 is not for living hinges [FL-T2000] | | **Injection moulding** | Isotropic but with weld lines [DUP]; undercuts need side actions or slots [DUP]; draft; fillet t/2 [DUP]; flex the hinge warm off the press [PL-LH]; the annular undercut must survive ejection [BAY] | Allowables in §3; draft 0.5° on vertical faces, 2° in most cases, 3° minimum with light texture and 5° or more with heavier texture (PM-T2), 3° minimum on shut-offs [PL-IM] | | **Laser-cut sheet / plastic** | Planar flexures (LET, kerf-pattern hinges) [LET][LH-INK] | Hole ≥ T; edge distance 1.574 mm for T ≤ 0.914 mm, else 3.175 mm [PL-SM] | | **Bent sheet metal** | Spring temper vs formability; relief and edge distances | Flange ≥ 4T; features within about 4T of a bend distort; hem ID ≥ T with a 6T return [PL-SM]; hole-to-bend ≥ 2T + R [BD-CHK] | ## 8. Failure modes that become pitfall-catalogue rows (#97) | Failure | Deterministic check candidate | |---|---| | Strain at the root fillet over the allowable | Calculator: ε(y, l, h, taper) ≤ derated ε_allow(material, process, orientation, cycles); flag a fillet under max(0.38 mm, 0.5·h) [BAY][HUBS-SF] | | Snap beam bending across layer lines | Geometry: the beam's long axis or bending-plane normal against the print-rotation Z; fail when the bending strain direction has a Z component above a threshold [KLA] | | Permanently loaded snap or spring tab (creep) | Assembly state: residual deflection after seating > 0 means "sustained", which needs PETG/ASA/PA or metal, and a stress-relaxation note (u) | | Undercut that cannot be moulded or printed | Moulding: undercut faces against the pull direction (cf. the dfm-agent approach [DFMAG]). FDM: the hook overhang angle in print orientation | | Print-in-place gap too small, or merged by the slicer | Minimum distance between solids of one part ≥ pip_clearance [B123]; Z gaps an integer number of layers [PIP] | | Living hinge in a material or process that cannot take ε = tθ/2L | Calculator plus a material whitelist (PP/PE moulded; TPU or PP printed) [PL-LH] | | Friction assumption (μ) unstated | Lint: every W calculation names its μ source row (§4) | ## 9. Open-source calculators and libraries | Project | Licence | Activity | What it implements | Use | |---|---|---|---|---| | [BelfrySCAD/BOSL2](https://github.com/BelfrySCAD/BOSL2) | BSD-2-Clause | push 2026-10-30, 2441★ | `snap_pin`/`snap_pin_socket`, `rabbit_clip`, `half_joiner`, `dovetail`, `living_hinge_mask` (layer-height aware), `knuckle_hinge`, `snap_lock`/`snap_socket`, `apply_folding_hinges_and_snaps` | Geometry ideas; port shapes to CadQuery (OpenSCAD runtime not needed) | | [dwragon/Fusion360SnapFitGenerator](https://github.com/dwragon/Fusion360SnapFitGenerator) | MPL-2.0 (LICENSE file; GitHub shows NOASSERTION) | created 2026-10-24, v1.1.0, 18 commits | Fusion add-in for FDM enclosures: cantilever, overlay, U-shaped, torsion and annular snaps; material presets (thickness, engagement, clearance, allowable strain, E); detachable 60° vs permanent 90° return angle | Best reference for parameter UX; MPL is file-level copyleft, so reimplement rather than copy | | [Mechanical-Python/SnapFit_Calculator](https://github.com/Mechanical-Python/SnapFit_Calculator) | LGPL-2.1 | last push 2022-03-16 | Cantilever, annular and torsion calculator; ships .exe/.7z binaries | Skip (stale, binaries) | | [n3r/AgentCAD](https://github.com/n3r/AgentCAD) `skills/snap-fits` | Apache-2.0 | push 2026-08-26 | Skill, `material_strain.json`, `cantilever_lid.py` snippet | Data format idea; values need checking (§3) | | [static74/3dp-compliant-mechanisms](https://github.com/static74/3dp-compliant-mechanisms) | MIT | created 2026-10-26, 0★ | Skill with tagged allowables, clearances and 3 evals; FDM only | See agent-toolkits note | | [aatmdelissen/pyMOTO](https://github.com/aatmdelissen/pyMOTO) | MIT | (simulation-tools.md) | `ex_compliant_mechanism*.py` topology-optimisation examples | Adopt for compliant topology optimisation (already the recommended TO engine) | | [ninja7v/TopoptComec](https://github.com/ninja7v/TopoptComec) | MIT | push 2026-10-31, 18★ | 2D/3D topology optimisation of compliant mechanisms, GUI + CLI | Watch | | [GazzolaLab/PyElastica](https://github.com/GazzolaLab/PyElastica) | MIT | push 2026-10-25, 363★ | Cosserat-rod large-deflection beams | Optional fast large-deflection check of slender arms | | [artofscience/flexures](https://github.com/artofscience/flexures) | GPL-3.0 | 2023 | MATLAB/COMSOL/STL flexure material | Reference only | | BYU CMR software page [BYU] | no licence stated | — | MATLAB/XLS large-deflection beam scripts for the *Handbook* examples | Equations only; ask before copying | | [CameronBrooks11/snapfit-scad](https://github.com/CameronBrooks11/snapfit-scad) (MIT), [nohkumado/openscad_snapfit](https://github.com/nohkumado/openscad_snapfit) (GPL-3.0), [jmj3d/snapmates](https://github.com/jmj3d/snapmates) (CERN-OHL-W) | as listed | small | Parametric snap geometry, no strain maths | Skip | | Laser living hinges: [buxtronix/living-hinge](https://github.com/buxtronix/living-hinge) (GPL-3.0), [Gernreich/living-hinge](https://github.com/Gernreich/living-hinge) (CC0) [LH-INK] | as listed | 2024 / 2026-10 | Kerf-pattern SVG generators | CC0 one is freely reusable | | [skyneticist/flexure-hinge-calculator](https://github.com/skyneticist/flexure-hinge-calculator) | MIT | 1★ | HTML/JS hinge thickness and length | Skip | **No maintained Python or JS library** implements the Bayer/BASF/DuPont snap-fit calculations, PRBM flexures or Qiu bistable beams with tests. cad-agent #95 has to build this; it is small (§2 equations plus tables), and the §2 gold cases are its tests. ## 10. References - **[COV]** Covestro LLC, *Snap-Fit Joints for Plastics – A Design Guide* (the Covestro edition of [BAY]); a copy supplied by the project owner, read for the 1.09 and 0.55 factors and Example II; not redistributed. - **[BAY]** Bayer MaterialScience, *Snap-Fit Joints for Plastics – A Design Guide*: https://cdn.sparkfun.com/assets/home_page_posts/1/4/1/0/Plastic_Snap_fit_design.pdf (read); mirror: https://fab.cba.mit.edu/classes/S62.12/people/vernelle.noel/Plastic_Snap_fit_design.pdf - **[DUP]** DuPont, *General Design Principles – Module I*: https://www.emcoplastics.com/assets/pdf/acetal/General%20Design%20Principles%20for%20Engineering%20Polymers.pdf (read) - **[BASF]** https://ulprospector.ul.com/1248/snapfit-3/ (read; points to manual pp. IV-3…IV-5 and V-1…V-4) - **[CEL]** https://www.docketalarm.com/cases/PTAB/IPR2017-01990/Inter_Partes_Review_of_U.S._Pat._6631715/12-12-2017-Patent_Owner/Exhibit-2002-18-Exhibit_2002_Snap_Fits_Celanese/ (s) - **[KLA]** Klahn, Singer, Meboldt, Procedia CIRP 50 (2016) 264–269: https://publikationen.bibliothek.kit.edu/1000142464 (read) - **[SCH]** https://www.doi.org/10.1007/978-3-030-90700-6_31 (s) - **[HUBS-SF]** https://www.hubs.com/knowledge-base/how-design-snap-fit-joints-3d-printing/ - **[HUBS-FDM]** https://www.hubs.com/knowledge-base/how-design-parts-fdm-3d-printing/ - **[ALL]** Allum et al., *Addit. Manuf.* 34 (2020) 101297: https://hull-repository.worktribe.com/output/4511720/interlayer-bonding-has-bulk-material-strength-in-extrusion-additive-manufacturing-new-understanding-of-anisotropy (abstract) - **Bambu TDS mirrors** - [BBL-PLA] https://www.additive-x.com/shop/mpattachments/file/viewonline/id/494/product_id/2236 - [BBL-PLAM] …/id/513/product_id/2261 - [BBL-PETG] …/id/580/product_id/2330 - [BBL-PETGCF] …/id/596/product_id/2346 - [BBL-ASA] …/id/612/product_id/2399 - [BBL-TPU] …/id/823/product_id/2584 - [BBL-PETGHF] https://botland.com.pl/img/art/inne/25540_karta_produktu.pdf - [BBL-PA6CF] https://polyalkemi.no/wp-content/uploads/2024/01/Bambu_PA6-CF_Technical_Data_Sheet-v2.pdf (404 on 2026-10-31) - **Polymaker TDS** (wiki; append `.md` for the text) - [PM-PLA] https://wiki.polymaker.com/polymaker-products/more-about-our-products/documents/technical-data-sheets/pla/polylite-tm-pla - [PM-PETG] …/petg-pet/polylite-tm-petg - [PM-ASA] …/abs-asa/polylite-tm-asa - [PM-COPA] …/nylon/polymide-tm-copa - [PM-TPU] …/tpu/polyflex-tm-tpu95 - [PM-PA6CF] https://cdn.shopify.com/s/files/1/0548/7299/7945/files/TDS_FIBERON_PA6-CF20_V1.1_EN.pdf - **[HP]** HP white paper, Jet Fusion 5200 mechanical properties: https://info.sculpteo.com/hubfs/Material%20documentation/PA11%20HP/TDS%20HP%20pa12%20pa12gb%20pa11%20pp%20(1).pdf (read) - **[FL-T1500]** https://formlabs.com/global/support/Using-Tough-1500-Resin/ (s) - **[FL-T2000]** https://formlabs.com/blog/tough-2000-questions/ (s) - **[PL-LH]** https://www.protolabs.com/resources/blog/living-hinge-basics-for-injection-molding/ (read) - **[PT-LH]** https://www.plasticstoday.com/plastics-processing/can-this-part-be-saved-part-3-living-hinges (s) - **[PL-CR]** https://www.protolabs.com/resources/design-tips/adding-crush-ribs-to-molded-parts/ (s) - **[RD-CR]** https://www.rapiddirect.com/blog/crush-ribs/ (s) - **[PL-SM]** https://www.protolabs.com/services/sheet-metal-fabrication/design-guidelines/ (read) - **[PL-IM]** https://www.protolabs.com/services/injection-molding/plastic-injection-molding/design-guidelines/ (read) - **[BD-CHK]** https://www.dfma.com/resources/pdf/dfm-design-review-checklist.pdf (read) - **[ULB]** https://www.ulbrich.com/blog/301-stainless-steel-strip-for-spring-applications/ (s) - **[PRB]** https://mecheng.iisc.ac.in/suresh/me254/Lectures/PRB.pdf (read: γ = 0.85, Howell & Midha 1995) - **[CHEN-PRB]** https://web.xidian.edu.cn/gmchen/files/20110919_163448.pdf (s: K_Θ = 2.65, c_θ = 1.24) - **[QIU]** Qiu, MIT thesis, eqs. 2.10 and 2.47–2.48: https://pergatory.mit.edu/research/jqiu/thesisscreen.pdf (read); JMEMS 13(2) 2004: https://ieeexplore.ieee.org/document/1284352 - **[LET]** https://scholarsarchive.byu.edu/facpub/1560; patent https://patents.google.com/patent/US9157497 - **[CAFP]** https://scholarsarchive.byu.edu/facpub/6578 - **[HAR]** Haringx 1949: https://www.doi.org/10.1007/BF02120338 (abstract) - **[PW]** Paros & Weisbord, *Machine Design* 37(27) 1965, cited in https://pmc.ncbi.nlm.nih.gov/articles/PMC10358504 - **[LOB]** Lobontiu, *Compliant Mechanisms: Design of Flexure Hinges*, CRC 2002 (u) - **[HOW]** Howell, *Compliant Mechanisms*, Wiley 2001; Howell, Magleby, Olsen, *Handbook of Compliant Mechanisms*, Wiley 2013 (u: not opened) - **[SMITH]** Smith, *Flexures: Elements of Elastic Mechanisms*, CRC 2000 (u: not opened) - **[FACT]** Hopkins, MIT thesis 2007: https://dspace.mit.edu/handle/1721.1/39879; summary: https://en.wikipedia.org/wiki/Freedom_and_constraint_topologies; UCLA BLOT optimiser: https://techtransfer.universityofcalifornia.edu/NCD/29415.html (no FACT software package found) - **[BYU]** https://compliantmechanisms.byu.edu/ (maker resources, books, videos); MATLAB scripts: https://compliantmechanisms.byu.edu/software-analysis - **[PIP]** https://forum.prusa3d.com/forum/prusa-core-one-hardware-firmware-and-software-help/what-are-the-minimum-reliable-printing-distances; https://fab.cba.mit.edu/classes/863.24/people/HayashiAni/2024/09/27/3d-prints.html (s) - **[PIP-PS]** https://forum.prusa3d.com/forum/prusaslicer/prusa-slicer-merging-my-print-in-place-bodies (s) - **[F360SF]** https://github.com/dwragon/Fusion360SnapFitGenerator (README read) - **[S74]** https://github.com/static74/3dp-compliant-mechanisms (`skill/references/fundamentals.md` read) - **[AGCAD]** https://github.com/n3r/AgentCAD/blob/HEAD/agentcad/skills/snap-fits/tables/material_strain.json (read) - **[B123]** https://github.com/KD5RYN/b123d-print/blob/HEAD/docs/using/checks.md (read) - **[BOSL2]** https://github.com/BelfrySCAD/BOSL2 (`joiners.scad`, `hinges.scad` read) - **[DFMAG]** https://github.com/Vivek-Boora/dfm-agent (README read; no licence) - **[LH-INK]** https://github.com/buxtronix/living-hinge; https://github.com/Gernreich/living-hinge **Could not verify:** - Bayer Table 1 constants 0.67 and 0.86 (image). - BASF and Celanese strain tables. - The older Bayer ABS/PA rows. - Paros–Weisbord constant. - CAFP 8EI/L. - LET equations (PDF bot-blocked). - Formlabs XY/Z data. - Associated Spring flat-spring stress limits. - Printed-snap creep (no study found). - FDM living-hinge cycle life. --- Source: https://cad-agent.valpatel.com/research/design-workflows.md # Design workflows for CAD agents: what works, with evidence (verified 2026-10-27) Scope: how the best agentic CAD systems, professional mechanical-design practice and model-integrity verification can be turned into a process that a CAD agent executes. [`landscape.md`](https://cad-agent.valpatel.com/research/landscape.md) covers tool *features*; this survey covers *workflow and method*. The plan that applies it is [`docs/design-process-plan.md`](https://cad-agent.valpatel.com/docs/design-process-plan.md). Method: papers were read on arXiv (abs and HTML, version and date noted); repositories through the GitHub API and raw files; vendored upstreams (`vendor/`) from their pinned source. **(V)** = verified from the primary source; **(u)** = README, vendor page or third-party claim not checked further. Licences are noted before any idea or code is borrowed (§7). Machine-readable copy: [`design-workflows.json`](https://github.com/Valpatel/cad-agent/blob/dev/research/design-workflows.json). Motivating incident, stated generically: a turret assembly in a robot project rendered its ammunition outside the hopper that should hold it, with subassemblies that read as boxes stuck on the side. Every per-part check passed. §6 says which practices would have caught it. ## 1. Summary: the practices that have evidence | # | Practice | Evidence grade | Source | |---|---|---|---| | 1 | **Kernel checks and a hard validity gate before any render** (invalid solid = fail, score 0), with the judge reading measurements before images | strong: the largest gains in every ablation that separates them | CADSmith Table I (median IoU 0.81→0.96 with the judge given kernel metrics and no render; the render adds 0.96→0.963); CADGenBench scoring; Embodied CAD; CAD-Coder RL reward (§2.4) | | 2 | **Decide interfaces, joints and connectors before geometry** (assembly-level decomposition) | strong for assemblies: the largest single ablation effect found | ArtiCAD (late assembly prediction: motion 3.82→3.16, success −11 pt); AssemCAD typed spec, text-to-CAD setting: assembly preservation 87.5 % vs 66.7 % for the best code-centric baseline (§2.4) | | 3 | **Containment and interpenetration as first-class relations** (`apart / touching / containing / interpenetrating`), not only overlap volume | practice, no ablation; it is exactly the incident's missing check | build123d-mcp `clearance`; AssemCAD port checks via `BRepClass3d_SolidClassifier` (§2.2, §4.2) | | 4 | **Author-declared structure expectations** (expected solid count, parts allowed to float, named intended fits) diffed against the built geometry | practice; vendored, Apache-2.0 | agentcad `expect_solids` / `floating`; text-to-cad "intended fits must be named" (§2.2, §4.6) | | 5 | **Spec-driven conformance with evidence tiers** (measured / structural / recognised / unverified; a spec that verifies nothing never conforms) | practice; vendored, Apache-2.0 | build123d-mcp `verify_spec` (§2.2) | | 6 | **Visual review as diagnosis, not proof**: few views chosen for the question, specific yes/no questions, "unclear" allowed, every visual concern converted to a deterministic check | moderate: +18 % point-cloud distance in CADCodeVerify, removes catastrophic outliers in CADSmith, no gain in median accuracy; documented failure modes on gaps and hidden interiors | CADCodeVerify Tables; CADSmith; CADReview; text-to-cad snapshot policy; MAC judge prompt (§2.5, §5) | | 7 | **A judge that did not write the code, given measurements first and images second, with anti-hallucination rules** (accept/halt need a hard data point; hedged language is not evidence; no image ⇒ `unverified`) | practice; MIT | MAC `qa_judge.md`; CADSmith's stronger-model validator (§2.1) | | 8 | **Interface control documents and requirement traces** (requirement → feature → verification method) | professional standard; automatable as a lint | NASA SE Handbook, INCOSE, 29148 (§3.1–§3.2) | | 9 | **Decision records with alternatives rejected**, written with the design | professional standard (ADR/MADR, QOC); evidence on use is thin but the artefact is cheap | §3.4 | | 10 | **DFMEA rows and the DFA part-count questions** as the review checklist for each part | professional standard | AIAG-VDA, Boothroyd-Dewhurst (§3.5–§3.6) | | 11 | **Assembly-sequence and tool-access checks at the step that installs each part** | practice; already partly in cad-agent (lesson 04) | §3.9 | | 12 | **Sign-off invalidated by a geometry hash** (review bound to the bytes it reviewed) | practice; cad-agent already does it for PCB evidence | `cadagent.evidence`, text-to-cad `document_hash` (§2.2) | ## 2. Agentic and AI CAD systems: workflow and method ### 2.1 Pan-Chera/Multi-Agent-CAD (MAC): the judge agent - [github.com/Pan-Chera/Multi-Agent-CAD](https://github.com/Pan-Chera/Multi-Agent-CAD), **MIT (V, API)**, last push 2026-10-17, 1,055 stars. Vendors text-to-cad's runtime under MIT. - **Pipeline (V, `multi_agent_cad/README.md`, `WORKFLOW.md`, `prompts/qa_judge.md`):** Spec Planner (prompt and reference images → `CADBrief`) → Geometric Architect (`ArchitectPlan`) → a deterministic translator from plan steps to build123d (unsupported steps become `TODO` markers for a repair agent) → execution → two QA engines (STEP topology; STL mesh) → **QA Judge** → Aider-based repair. Outer loop of 5 retries; the judge engages only after one blind repair; inner loop of 3 for execution failures. - **What the judge sees (V):** the user request as ground truth, the planner's `special_features`, per-feature bounding boxes measured *before* booleans (white-box instrumentation), the QA engines' error list, the retry count, and, only with `JUDGE_MULTIMODAL`, four 512×512 isometric renders covering all octants. The prompt says: *"You never see the raw 3D mesh, STEP file, or Python source"* and *"When view evidence conflicts with feature_measurements, trust the measurements."* - **Decision schema (V):** `{action: accept|repair|halt, reason, confidence, evidence[], disputed_errors[], semantic_verification: verified|failed|unverified, modification_suggestions}`. Accept or halt requires at least one hard data point in `evidence`, otherwise the pipeline downgrades to repair; hedged words are not evidence; without images `semantic_verification` must be `unverified`, and lack of vision cannot trigger a repair. - **Own evaluation (V, `docs/quantified_quality.md`):** 10 prompts, 141 binary features read off the prompts. A single-agent skill baseline scored 138/141 at 104 M tokens; MAC v1 140/141 at 0.9 M tokens (116× fewer); MAC v2 with visual and geometric judging 140/141 at 1.4 M tokens. The document itself says who judged is unstated, the comparison is "controlled but not isolated", and assembly outputs are curated. **The robust finding is cost, not quality**; 3 vs 1 failures on 141 items is noise. ### 2.2 earthtojake/text-to-cad `cad` skill, build123d-mcp and agentcad (vendored, pinned) - **text-to-cad** ([repo](https://github.com/earthtojake/text-to-cad), **MIT (V, `vendor/text-to-cad/LICENSE`)**, pinned at `2f79365`, imported 2026-10-24; upstream released 0.8.1 on 2026-10-27). Workflow (V, `skills/cad/SKILL.md`, `references/`): a model is a parameterless decorated function; the rebuild is the script run; user-given dimensions are checked by an `assert` in the model or a measurement, *"never read off a picture."* Snapshot policy (V, `snapshot-review.md`): one reviewed snapshot per visible change, views chosen for the feature, *"No fixed set of views proves every face or feature is correct"*, *"Do not loop on snapshots"*, every visual concern becomes a deterministic check (asymmetric holes → measure centres; floating boss → solid count and contact distance; wrong bore → clipped view then measure). Inspection recipes (V, `inspection-and-validation.md`): `overlap_volume` (zero means at most touching; *intended fits must be named*), `closest_points` (does not measure penetration), cheap bounding boxes as a pre-filter, `is_valid`, `self_intersections`, `boundary_edges`; a check that cannot decide raises and *"has shown nothing about the design"*. A scene is bound to a `document_hash`; selector ids are revision-scoped. **No plan-before-build step exists (V)**: the brief says a simple part may need a sentence and a complex assembly a dimension/interface checklist, with no required format. - **build123d-mcp** ([repo](https://github.com/pzfreo/build123d-mcp), **Apache-2.0 (V, LICENSE)**). Its system prompt (V, `default_prompt.md`) orders the loop: `measure` after every `execute` (faces and volume must change as expected after a boolean), `clearance` between mates before any render (`touching` or `apart`, never `interpenetrating`), render only after those pass, `design_audit` (±10 % nudges of every named parameter; `brittle`/`coupling` verdicts) before finishing, `validate` then test-export. Tools (V, `llms.md`): `clearance` returns one of `apart | touching | containing | interpenetrating` with `intersection_volume`, `a_volume_outside_b` and, for `containing`, the worst-direction wall thickness; `verify_spec` (experimental) checks a declared spec with evidence tiers `measured | structural | recognised | unverified`, and *"conforms = no FAILs and ≥1 requirement actually checked"*; `lint_drawing` must return zero violations before a drawing is done. CADGenBench: a build123d-MCP entry leads the leaderboard at 86.9 % (u, via a 2026-10-25 third-party snapshot; the HF Space did not render). - **agentcad** ([repo](https://github.com/jdilla1277/agentcad), **Apache-2.0 (V, LICENSE)**). Validation layers (V, `validation.py`): BRepCheck, shell closure (free edges), mesh manifold, structure, advisory (sliver faces, loose tolerances). Authors declare `expect_solids`, `expect_shells` and `floating: true` per shown object (V, `validation_guidance.py`); a disconnected body that is not declared floating is reported with its nearest part and distance. `edit_risk.py` classifies imported B-reps (face/edge counts, free edges, severe validity errors) before an agent commits to editing them. Shared human/agent review comments live in `.agentcad/reviews/comments.json` with statuses `draft | open | addressed | resolved` (V, `reviews.py`). Its CADGenBench claim (48 % higher score, 2.5× valid outputs with GPT-5 Nano) is a single uncontrolled run by its own README (V, README caveat). ### 2.3 Commercial systems - **Zoo Zookeeper** ([press release 2026-02-05](https://zoo.dev/press/zoo-introduces-zookeeper), V): researches, proposes constraints and parameters and *"generates a clear design plan before producing a CAD model"*; has engine-level tools to inspect, snapshot and debug geometry; output is an inspectable feature tree. No verification method or metrics published. - **Autodesk Assistant (Fusion)**: autodesk.com refused the fetch (403). Trade press (u): assistant GA at AU 2026; an agentic version that can "flag problems, trace knock-on effects" previewed for 2027; Fusion MCP servers released June 2026. No published method. - **Onshape AI Advisor** ([feature page](https://www.onshape.com/en/features/ai-advisor), V): documentation Q&A only; *"does not generate designs, predict errors in real time, or make complex engineering decisions."* - **Leo AI** ([vendor page](https://www.getleo.ai/ai-for-cad), V as a claim, u as fact): finds guidelines and formulas, runs calculations, asks clarifying questions, outputs an editable assembly with a part and feature tree. No accuracy data. ### 2.4 Academic systems with verification or decomposition | System | Source, date, licence | Technique | Measured effect | |---|---|---|---| | **CADCodeVerify** (ICLR'25) | [arXiv 2410.05340](https://arxiv.org/abs/2410.05340) v2 2025-02-28; code repo **no licence file (V)** | compile-repair loop, then the VLM writes 2–5 yes/no questions from the spec and answers them on 4 renders (0/90/180/270°), "Unclear" allowed; only non-yes answers feed back; 2 iterations (no gain beyond) | CADPrompt, GPT-4 few-shot medians: point-cloud distance 0.155→0.127, Hausdorff 0.494→0.419, compile 96.0→96.5 %; CodeLlama compile 67→73.5 %. Ablation on 100 samples: removing the renders 0.126→0.153. A solver with ground-truth access reaches 0.103 (upper bound) | | **CADFusion** (ICML'25) | [arXiv 2501.19054](https://arxiv.org/abs/2501.19054) v3; [microsoft/CADFusion](https://github.com/microsoft/CADFusion) **MIT (V)**, archived | training-time visual feedback: a 7B VLM scores one render 0–10, ~1,500 DPO pairs per round, alternated with sequence learning | Table 1: CD 19.89 vs Text2CAD 30.23; Table 2: visual score 7.69→8.96 but invalidity 4.84→6.20 %; visual feedback alone collapses (invalidity 88.87 %) | | **From Idea to CAD** (Honda RI) | [arXiv 2503.04417](https://arxiv.org/abs/2503.04417) v1 2025-03; no code | requirements engineer / CAD engineer (numbered plan, AST check, exec) / QA agent renders 7 views and lists the 2 most relevant discrepancies | **no numbers**: 5 designs, a qualitative figure; authors note variability and failure on complex parts | | **CADSmith** | [arXiv 2603.26512](https://arxiv.org/abs/2603.26512) 2026-03-27 (V); code licence not stated | planner (JSON plan: components, target bbox, constraints) → coder (RAG over API docs) → sandboxed executor → validator = a stronger model given prompt, code, kernel metrics (volume, bbox, centre of mass, face/edge counts, validity) and 3 renders → refiner; 5 iterations; invalid solid = hard fail | Table I, 100 prompts (IoU and the second CD are medians): zero-shot exec 95 %, IoU 0.8085, mean CD 28.37 / median 0.55; **no-vision** (the LLM judge reads kernel metrics, no render): exec 99 %, IoU 0.9563, mean CD 18.19 / median 0.48; **full** (judge also sees the renders): exec 100 %, IoU 0.9629, mean CD 0.74, **median CD unchanged** (0.48). The render removed catastrophic outliers, not fine error | | **CADDesigner** | [arXiv 2508.01031](https://arxiv.org/abs/2508.01031) v2; licence not stated | ReAct agent; 6-view renders → 0/1 checker drives regeneration; case library | Text2CAD 1k subset: IoU 0.277 / success 100 % vs CADCodeVerify 0.235 / 86.1 %. **No with/without-vision ablation** | | **CAD-Assistant** | [arXiv 2412.13810](https://arxiv.org/abs/2412.13810) v3 2025-08-26; [repo](https://github.com/dimitrismallis/CAD-Assistant) custom licence (NOASSERTION) | VLLM planner emits plan plus Python per step into FreeCAD; observes JSON or renders | SGPBench CQA GPT-4o 0.686/0.782→0.791/0.857 (2D/3D); autoconstrain F1 0.693→0.979; tool-use validity 98.5 %; most residual errors are reasoning or *misreading renders* | | **CAD-Coder** (Doris et al., MIT) | [arXiv 2505.14646](https://arxiv.org/abs/2505.14646); [repo](https://github.com/anniedoris/CAD-Coder) **Apache-2.0 (V)** | fine-tuned VLM image→CadQuery; no loop | 100 % valid syntax; beats GPT-4.5 and Qwen2.5-VL-72B on IoU | | **CAD-Coder** (CoT + geometric reward) | [arXiv 2505.19713](https://arxiv.org/abs/2505.19713) | SFT, CoT cold start, GRPO with a CD-based reward that is 0 for non-executing code | mean CD 6.54 vs Text2CAD 29.29; invalid 1.45 %; w/o CoT 17.34 | | **CADReview / ReCAD** (ACL'25) | [arXiv 2505.22304](https://arxiv.org/abs/2505.22304); [repo](https://github.com/Gary-code/CADReview) **no licence (V)** | trained reviewer: 3 reference views + numbered program + 1 render → feedback generator (DPO) + code editor | error-localisation accuracy GPT-4o 41.5 %→71.8 % (machine-made), 31.8→63.6 % (human-made); CD 4.34→1.43. Accuracy drops on programs with **hidden internal components** | | **ArtiCAD** | [arXiv 2604.10992](https://arxiv.org/abs/2604.10992) v2 2026-04-14 (V); licence not stated | Design agent sets connectors (attachment points, joint parameters) *before* geometry; per-part generation agents; assembly agent; review agent on multi-view renders plus motion keyframes; cross-stage rollback separating code errors from design errors | ArtiCAD-Bench (120 tasks, 1–5): single VLM geo 3.31 / detail 2.82 / motion 3.67 → 3.57 / 3.14 / 3.91. **Ablation: predicting assembly late drops motion 3.82→3.16 and success 100→89.2 %**; w/o rollback 95 %. No ablation removing the review agent | | **AssemCAD** | [arXiv 2607.05123](https://arxiv.org/abs/2607.05123) 2026-07-06 (V) | an axiomatic spec first: typed parts, geometry-backed ports verified by `BRepClass3d_SolidClassifier` point sampling (bore, shaft, flat-face), closed-form mate transforms, mate-aware clash check; then deterministic execution | AssemBench Table 3 (V, HTML), **text-to-CAD setting**: AssemCAD success 89.17 %, assembly preservation (APR) 87.50 %; baselines: Claude-4.8 85.00 % / 0.83 %, GPT-5.5 79.17 % / 66.67 % (the best baseline APR), Qwen3.7-Max 78.33 % / 39.17 %. Image-text setting: AssemCAD 84.17 % / 82.50 %. The paper's Table 4 lists AssemCAD's text APR as 82.50, which disagrees with Table 3 (the paper is inconsistent). The abstract gives no numbers. Appendix H, Proposition 3: Chamfer distance is non-identifiable for assemblies (the page truncated before the proof) | | **Embodied CAD** | [arXiv 2606.31252](https://arxiv.org/abs/2606.31252) 2026-06-30 | step-wise skills; the kernel accepts or rejects each op; GRPO on solver rewards | executable 100 %; exact-policy 93.1→93.2 % only; authors: solver feedback *"necessary but not sufficient"* | | **AADvark** (CAIS'26) | [arXiv 2604.15184](https://arxiv.org/abs/2604.15184) v2 | FreeCAD renders with unique per-face colours and labels; a joint solver as the verification signal | scissors with a revolute joint by iteration 4; 468 LLM calls, 4.1 h, $15.85; qualitative | | **CADGenBench** | [huggingface/cadgenbench](https://github.com/huggingface/cadgenbench) **Apache-2.0 (V)**, data ODC-BY; 81 fixtures | validity gate (watertight manifold, else 0), then 0.4 shape (F1 + IoU) + 0.4 interface (keep-in/keep-out IoU) + 0.2 topology (Betti) | leaderboard (u, third-party snapshot 2026-10-25): build123d-MCP + Opus 5.5 86.9 %, MCP + Opus 5 59.6 %, single-shot baselines 16–36 %, Claude Code + text-to-cad skill 28.0 % vs bare model 27.4 %. Tool-using agents with validity gates dominate; entries are not controlled | | Others (abs only, V) | Text2CAD NeurIPS'24 ([2409.17106](https://arxiv.org/abs/2409.17106), custom licence); CAD-Recode ICCV'25 ([2412.14042](https://arxiv.org/abs/2412.14042), custom); FlexCAD ICLR'25 ([2411.05823](https://arxiv.org/abs/2411.05823), MIT); CAD-Editor ([2502.03997](https://arxiv.org/abs/2502.03997), MIT); CAD-Llama ([2505.04481](https://arxiv.org/abs/2505.04481)); IterCAD ([2606.13368](https://arxiv.org/abs/2606.13368), compiler + runtime + orthographic-render feedback with RL); BenchCAD ([2605.10865](https://arxiv.org/abs/2605.10865), code MIT); survey Zhang et al. ([2505.08137](https://arxiv.org/abs/2505.08137), ACM CSUR 2026) | mostly fine-tuned generators without inference loops | no verification-loop ablations | ### 2.5 What the evidence says - **Kernel feedback is the most reliable signal.** CADSmith's no-vision configuration (an LLM judge reading kernel metrics and validity, no render) gives most of the total gain (median IoU 0.81→0.96); CADCodeVerify's compile loop; Embodied CAD and AssemCAD get 100 % / 89 % executability by gating every op on the kernel; CAD-Coder's zero reward for non-executing code cut invalidity to 1.45 %. CADGenBench's hard validity gate is why tool-using entries dominate. Caveat (Embodied CAD): solver acceptance does not prove intent. - **Assembly-level decomposition (interfaces first) has the largest measured effect.** ArtiCAD's "late assembly" ablation and AssemCAD's typed spec (text-to-CAD APR 87.5 % vs 66.7 % for the best baseline; one baseline, Claude-4.8, scores 0.83 % APR at 85 % success, which shows how little a high success rate says about assembly structure). For single parts, planners appear in CADSmith, Idea-to-CAD, MAC and Zookeeper but **no paper ablates the planner alone**; MAC's 99.3 % vs 97.9 % is within noise. - **Render plus VLM self-critique helps modestly and mainly catches gross errors.** CADCodeVerify −18 % distance and +21 % without images; CADSmith's judge moved the mean, not the median; CADFusion's visual preference raised the score but also the invalidity rate; ArtiCAD, CADDesigner and Idea-to-CAD never ablate the reviewer. The survey (2505.08137 §7) concludes LLMs *"can generally determine whether the design is satisfactory, but they often lack the necessary information to understand how it should be improved."* - **Documented visual-review failure modes.** CADCodeVerify's VQA answers are only 64.6–68.2 % correct with 19–27 % "Unclear"; its metrics miss *"small gaps between a desk's legs and top"*. CADSmith passed a quadcopter with arm-hub gaps on the first try (fixed 3 views, render scale). CADReview drops on **hidden internal components**. BenchCAD: a target render *replacing* text scores near zero (renders carry no numbers); adding one to text barely helps and hurts the strongest thinking model. CADFusion uses one view because *"LVMs perform worse with multiple images."* [2607.05573](https://arxiv.org/abs/2607.05573): a VLM critic diverges most on rotationally symmetric parts (u, abs). CAD-Assistant: most failures were reasoning or *"misreading tool-generated renderings."* Both MAC's judge prompt and text-to-cad's policy encode the same lesson: renders are for relative shape; dimensions, interference and containment come from measurements. ## 3. Professional mechanical design practice an agent can execute For each practice: the canonical reference, the artefact, what an agent can produce or verify from a parametric model plus a YAML dossier, and the honest limit. NPR 7123.1D was read from the published PDF. Paywalled standards are cited, not read. Access failures: DTIC (MIL-STD-1472 tables), ANSI previews, ecss.nl (ESA outage), PTC and FreeCAD wiki pages; values from those are (u). | § | Practice | Canonical reference | Artefact | Agent produces / verifies | Not automatable | |---|---|---|---|---|---| | 3.1 | Top-down, skeleton and interface control documents | NASA SE Handbook SP-2016-6105 Rev 2 App. L, [IRD outline](https://www.nasa.gov/reference/appendix-l-interface-requirements-document-outline/) (V): interface description, responsibilities, coordinate systems, units and tolerances, mass, structural/mechanical (attachment, stiffness, envelope), fluid, power, signal, data, environments; ECSS-E-ST-10-24C ICD DRD (V listing); Onshape *modelling in context*: contexts are snapshots, never auto-updated (V); PTC skeleton models (V, Toolkit docs) | a skeleton part (datums, axes, envelopes, layout dimensions) and one ICD row per interface pair: id, parties, owner, frame, units, pattern, envelope, loads, change authority | every ICD row has a named entity in the skeleton; child parts reference the skeleton, never each other; the mating envelope lies inside the ICD envelope; "ICDs ready to baseline" is a literal PDR entrance item (NPR 7123.1D G-6 6.m, V) | what counts as an interface and who owns it | | 3.2 | Requirements traceability | NASA SE Handbook §5.3 [verification](https://www.nasa.gov/reference/5-3-product-verification/) (V): methods analysis, demonstration, inspection, test; a matrix with bidirectional trace, method, pass/fail and the *versions* verified; NPR 7123.1D PDR criterion 2 (V); ISO/IEC/IEEE 29148:2018 characteristics (u, paywalled) | the verification matrix: `id, text, parent, rationale, method ∈ {I,A,D,T}, success criterion, feature, evidence, status, version verified` | unique ids; every requirement has a parent or is marked derived; every leaf has a method and evidence; every analysis/inspection requirement binds to an executable check; suspect links when parent text changes (Doorstop's fingerprint, V); coverage | whether a requirement is correct or necessary; the method choice | | 3.3 | Design reviews (PDR/CDR) | NPR 7123.1D (2023) App. G Tables G-6, G-7 (V, read from the PDF): PDR entrance includes a preliminary drawing tree, ICDs ready to baseline, single-point-failure list; success includes complete traceable flow-down, margins, manufacturability, FMEA maturity; CDR: technical data package with ICDs, analyses, material tests, manufacturability; ECSS-M-ST-10C / 10-01C (cited) | the review package and checklist with per-criterion status; a RID/RFA log with disposition and closure | a gate script: drawing tree nodes resolve to files; ICD count equals interface count at status ≥ ready; every TBD has an owner and date; margins numeric; FMEA has a row per function; all prior RIDs closed | risk acceptance, margin adequacy, the board | | 3.4 | Design rationale | Nygard ADR 2011 (V): context, decision, status, consequences, never renumber, superseded kept; [MADR](https://github.com/adr/madr) (V, MIT or CC0): decision drivers, considered options, outcome, **confirmation**, pros and cons; IBIS (Kunz & Rittel 1970), QOC (MacLean et al. 1991); evidence: Burge 2008 (V abstract) says most rationale systems had little or no empirical evaluation; the AI EDAM special issue reports consensus on value and equal concern about capture cost | a decision record per decision with ≥ 2 options and a confirmation naming the check that proves it still holds | lint: status, date, ≥ 2 options, outcome, successor for superseded, confirmation resolves to a passing check; orphan-parameter report (named dimensions no decision or requirement mentions) | which decisions deserve a record; honest context | | 3.5 | DFMEA | AIAG-VDA FMEA Handbook 2019 seven steps (V via secondary): planning, structure, function, failure, risk, optimisation, documentation; Action Priority replaces RPN; SAE J1739_202101 (V listing). The 1000-cell AP table is copyrighted; open implementations reconstruct the band structure (V, Schematex docs) | one row per focus element: function, failure effect / mode / cause, S, prevention control, O, detection control, D, AP, action, owner, status | structure tree from the assembly tree; function rows seeded from the trace; completeness (every element a function, every function a mode, S/O/D 1–10, AP from a versioned table); geometric prevention controls ("clearance", "captive", "keyed") resolve to CAD checks | realistic modes and causes; ratings | | 3.6 | DFM/DFA (Boothroyd-Dewhurst) | Boothroyd, Dewhurst, Knight 2011 (u); the three criteria for a theoretically necessary part (V via two secondaries): relative motion, different material for a fundamental reason, separate for assembly or service; DFA index = N_min × 2.93 s / T_total (V secondary); handling and insertion penalty tables are proprietary | the DFA worksheet: part, qty, handling and insertion codes and times, necessary (criterion), notes; N_min, T, index | part and fastener counts; symmetry classes from principal inertia; insertion directions (fraction from above); lead-ins on mating cylinders; criterion 1 from mate DOF, 2 from material, 3 flagged for a human; **no open-source Boothroyd implementation exists (V, negative search)**, so a simplified declared table is needed | criterion 3 and real times | | 3.7 | Tolerance stack-ups | PTC EZ Tolerance summary (V): worst case all contributors at limits; RSS statistical; ASME Y14.5-2018 (R2024) (V listing); ISO 2768-1 class m: ±0.1 (0.5–6), ±0.2 (6–30), ±0.3 (30–120), ±0.5 (120–400) (V secondary); FDM accuracy: Hubs desktop FDM ±0.5 % with a ±0.5 mm floor, industrial ±0.15 % / ±0.2 mm (V); Prusa FAQ 0.1 mm Z, 0.3 mm XY (V) | the stack table per loop: dimension, nominal, ± tolerance, sensitivity, source, contribution; worst-case and RSS results against the requirement | build the loop along a named direction from the mate graph; nominals from the model; tolerances from the dossier, else a general class, else the process table; WC and RSS; flag loops that pass RSS but fail WC; **no open-source stack-up tool found (V)** | datums and loop choice; capability data | | 3.8 | Drawings and GD&T | ASME Y14.5-2018, Y14.100-2017, Y14.1-2020 (V listings); ISO 1101, ISO 7200 (cited); open tools: FreeCAD TechDraw (LGPL, limited GD&T, u), [draftwright](https://github.com/pzfreo/draftwright) (**AGPL-3.0**, V: auto views and sections, ISO 7200 title block, `lint()`), build123d drafting helpers (Apache-2.0), text-to-cad `engineering-drawing` (MIT); build123d-mcp's rule "a drawing is done when `lint_drawing` returns zero violations" (V) | views and sections, title block from the dossier, datums, FCFs on functional features, critical dimensions, revision block | title block from the dossier (rev = tag); every critical dimension appears; every FCF references a declared datum; drawing revision equals model revision; lint zero | datum scheme and which dimensions are critical | | 3.9 | Assembly access and serviceability | MIL-STD-1472 access-opening tables and MIL-HDBK-470A §2.4, §2.11 (V listings; tables **not read**, DTIC 403); ISO 15534-2 / EN 547-2 openings sized for the 95th percentile, reach for the 5th (V listing); ISO 2936 hex-key dimensions (paywalled); Unbrako Engineering Guide counterbore and clearance table: M3 Ø6.5, M4 Ø8.25, M5 Ø9.75, M6 Ø11.25 (V) | the serviceability matrix: item, task, opening, body part, tool, sweep ok, swing available, fastener visible, removal order, captive | the strongest area: sweep a modelled tool along the fastener axis and through its swing arc; line of sight from the opening; opening size against the body-part table; removal-order graph from the contact graph; lesson 04 already does the driver sweep | which tasks are service tasks; posture and strength | | 3.10 | Configuration management | SAE/ANSI EIA-649C (V listing); five functions (V, NDIA slides): planning, identification, change management, status accounting, verification and audit; functional / allocated / product baselines | configuration index (CI, part number, revision, baseline, change id, approver), change log, as-built audit | baseline = annotated tag; revision string stamped in every artefact (cad-agent does this); PR template lists affected items; CI re-exports from the tag and diffs hashes; LFS for binaries with locks (u) | what is a CI; the change board | | 3.11 | Part numbers, BOM, assembly tree, mates | ASME Y14.34-2013 associated and indentured lists (V listing); Onshape mates: fastened 0 DOF, slider 1 translation, revolute 1 rotation, planar, cylindrical, pin slot, ball, parallel, tangent, width (V) | indented BOM: level, item, part number, revision, description, qty, material, make/buy, vendor PN, mass, parent; assembly tree; mate list with DOF | BOM ↔ bodies bijective; qty = instance count; mass roll-up against budget; every bought part has a vendor PN and a STEP matching the fitted geometry (the McMaster skill); every instance fully constrained unless declared a mechanism with the declared joint type | numbering policy, make/buy | ## 4. Model-integrity verification: algorithms, APIs, licences, performance ### 4.1 Contact graphs and interference - **Commercial reference behaviour (V, vendor help pages).** SolidWorks Interference Detection reports a B-rep interference *volume* per pair and has *Treat coincidence as interference* (touching faces reported), *Create fasteners folder* (thread overlaps segregated) and *Include multibody part interferences*; the help warns that faceted curved faces can touch falsely. Onshape's tool reports the interfering region's bounding box, not a volume, and excludes standard content (fasteners) by default. Fusion's `Design.analyzeInterference()` returns interference bodies with volume and can include coincident faces (zero-volume hits). The Fusion 360 Gallery assembly dataset defines a *contact* as two B-rep faces coincident or within 0.1 mm (V, `docs/assembly.md`). - **Liaison graphs.** Bourjault (1984) and De Fazio & Whitney, *Simplified generation of all mechanical assembly sequences*, IEEE J. Robotics & Automation 3(6):640–658, 1987 (V bibliographic; the IEEE page did not serve the abstract): nodes are parts, edges are mating contacts; two precedence questions per liaison give the feasible assembly sequences (u for the question count). This is the graph the connectivity gate builds. - **OCCT primitives** ([OCCT](https://github.com/Open-Cascade-SAS/OCCT), **LGPL-2.1 with the OCCT exception (V)**; CadQuery and build123d use it through OCP): `BRepExtrema_DistShapeShape(a, b)` → `Value()`, witness points and support shapes, `InnerSolution()` true when one shape is a solid and the other lies wholly or partly inside it, `SetMultiThread(true)` (V, refman); `BRepAlgoAPI_Common` plus `BRepGProp::VolumeProperties` for exact overlap volume; `BRepAlgoAPI_Section` for touching faces with zero common volume (u, standard usage); `BOPAlgo_CheckerSI` for self-interference of one compound with `SetLevelOfCheck(0–9)` (V); `BRepClass3d_SolidClassifier.Perform(p, tol).State()` → IN/OUT/ON (V); `BRepCheck_Analyzer` for validity. Distance 0 cannot distinguish touching from overlapping: combine with Common. - **Mesh-level**: trimesh `CollisionManager` (MIT, V `collision.py`) wraps python-fcl (BSD, V PyPI; FCL 0.7 with `DynamicAABBTreeCollisionManager` broad phase, `continuousCollide` giving a time of contact); manifold3d (Apache-2.0, V) for fast robust mesh booleans that error on non-manifold input. - **Measured here (throwaway experiment, 2026-10-27, CadQuery/OCP on a loaded workstation):** 100 synthetic solids (boxes, cylinders, spheres): AABB broad phase over 4,950 pairs → 100 candidates in 3 ms; `DistShapeShape` on the candidates 9.9 ms per pair; exact Common volume 3.4 ms per pair; `SolidClassifier` 24 points in 7 ms; contact graph and components trivial; **1.35 s total**. Real parts with hundreds of faces cost 10–100× more per pair, which keeps a 100-part assembly within minutes cold (an estimate, not measured: `DistShapeShape` on freeform or heavily filleted faces and `Common` on shelled parts can take seconds per pair); a cache keyed by content hashes of the two shapes makes a rebuild that changes one part re-query only its pairs. Script: [`bench/gate_timing.py`](https://github.com/Valpatel/cad-agent/blob/dev/research/bench/gate_timing.py). ### 4.2 Containment No published "components must lie inside their cavity" validator was found (gap); the primitives are all there: 1. **Exact volume test on the B-rep:** `vol(A ∩ cavity) == vol(A)` within tolerance ⇒ inside; `0 < vol < vol(A)` ⇒ straddling the wall; `0` with positive distance ⇒ outside. build123d-mcp's `clearance` returns exactly this as `containing | interpenetrating | apart | touching` with `a_volume_outside_b` and, for containing, the worst-direction wall thickness (V, `llms.md`). The cavity must be a solid: register it from the build, or derive it (hull minus part) and say so. 2. **Point sampling** with `BRepClass3d_SolidClassifier` on the item's vertices and centre: cheap, misses thin protrusions between samples; `DistShapeShape.InnerSolution()` is a one-bit hint. AssemCAD verifies ports this way (V). 3. **Meshes:** `trimesh.Trimesh.contains(points)` casts rays in a direction and its negative with parity, accepts only when both agree, retries disagreements once at random, raises on non-watertight meshes (V, source). ### 4.3 Mates and joints - **Vocabularies (V):** Onshape mates (fastened, revolute, slider, planar, cylindrical, pin slot, ball, parallel, tangent, width; limits on the moving ones). build123d joints `RigidJoint`, `RevoluteJoint(axis, range)`, `LinearJoint`, `CylindricalJoint`, `BallJoint` with `connect_to()`; values outside range raise; no solver (joints are explicit transforms), so consistency means a tree rooted at a fixed part where every `connect_to` succeeded. CadQuery assembly constraints (`Plane, Point, Axis, PointInPlane, PointOnLine, Fixed, FixedPoint, FixedAxis, FixedRotation`) are solved by CasADi/IPOPT as squared residuals (V, `solver.py`); a non-zero residual after solving is an inconsistent or over-constrained mate. - **JoinABLe / Fusion 360 Gallery assembly-joint** (Willis et al., CVPR 2022; [repo](https://github.com/AutodeskAILab/JoinABLe)): code MIT, **dataset "non-commercial research purposes only" (V, LICENSE.md)**. A joint is two occurrences, the B-rep entity pair that defines the axis, offset, angle, flip, and `joint_type ∈ {Rigid, Revolute, Slider, Cylindrical, PinSlot, Planar, Ball}` with limits. Useful as a vocabulary; the data cannot be used commercially. - **URDF/MJCF:** URDF must be a tree (V, spec; `check_urdf`); inertia must be positive definite with the triangle inequality (common practice, u as a single source); cad-agent's `robot.check_urdf` already does this. - **Consistency rules an agent can run (synthesis; no published spec found):** the joint graph is connected and every part reaches a grounded part; loops close within tolerance; mated axes coincide (angle and point-to-line distance under tolerance); pin/hole diameters compatible with positive overlap length; after placement, every joint's two parts are in contact or within the declared clearance, otherwise it is a floating joint. ### 4.4 Kinematic interference - **Exact swept volumes** exist (Sellán, Aigerman, Jacobson, *Swept Volumes via Spacetime Numerical Continuation*, ACM TOG 2021; [code](https://github.com/sgsellan/swept-volumes) MIT but **patent-pending per its README (V)**; maintained in gpytoolbox `swept_volume`, MIT). They are mesh methods, seconds per part, and only needed when the envelope geometry itself is wanted (to cut a clearance pocket). - **Sampled poses are the practice.** MoveIt's Setup Assistant builds the allowed-collision matrix from 10,000 random configurations and a 2025 paper notes undersampling can wrongly disable pairs (V, arXiv 2512.23140). FCL's `continuousCollide` does conservative advancement between two poses (V). MuJoCo: sweep-and-prune broad phase, per-body AABB trees, parent–child filtering, meshes collide as convex hulls (V, docs). SolidWorks *Collision Detection* stops a drag at the first collision; Onshape animates mates and interference stays a static tool (u). - **Recommendation for ~100 parts × N poses:** tessellate once into a `CollisionManager`; per pose, `set_transform` the moving subtree and query `in_collision_internal` and `min_distance_internal`; pick the pose step from the smallest clearance and the largest surface speed between samples (step ≤ d/v misses nothing, the conservative- advancement argument); confirm any mesh hit on the exact B-rep with Common at that pose (facet chord error gives false positives, the SolidWorks caveat). Report the sample density; never claim an exact sweep. ### 4.5 Minimum wall and feature size - **Ray casting along the inward normal** is what text-to-cad's `dfam-check` (MIT, V `SKILL.md`: "fact-only, never pass/fail", mesh only) and Casys `mcp-dfm` (MIT) do; the documented weakness is that thin *diagonal* walls are undersampled when the ray follows the normal (V, mcp-dfm). libigl's `shape_diameter_function` (MPL-2.0) averages a cone of rays, which mitigates it. - **Medial-axis / inscribed-sphere** methods are exact: Attene et al., *ε-shapes*, Computers & Graphics 2017 ([arXiv 1704.08049](https://arxiv.org/abs/1704.08049), V): finds thin walls *and* thin gaps; O(n²) worst case per vertex (2,720 s complete vs 45 s thresholded map in the paper); no code release found. - **Slicers** check per layer only: PrusaSlicer's "detect thin walls" offsets by half an extrusion width and takes a Voronoi medial axis (V, OrcaSlicer mirror); Arachne replaces it with adaptive widths. - **B-rep probes:** build123d-mcp `verify_spec` has `wall_thickness_at(point, direction)` (exact ray through the solid) and an approximate global `min_wall_mm` (V); OCCT negative offset (`BRepOffsetAPI_MakeOffsetShape`) and comparison is possible but negative offsets fail on some geometry (V, tracker issues). ### 4.6 Existing open-source assembly validators and CAD lint | Tool | Licence | What it checks | Status | |---|---|---|---| | [CADCLAW](https://github.com/sunnyday-technologies/CADCLAW) | MIT (V); deps OCCT LGPL, CasADi LGPL-3 | gate chain on CadQuery/OCCT: inventory, **interference** (bbox pre-filter → boolean), **floating** (parts isolated from labelled structure beyond a max gap), adjacency, dimensional, orientation, PMI presence, AP242 round-trip, BOM, parity | the closest thing to an assembly lint; MIT, so its structure can be borrowed with attribution | | [build123d-mcp](https://github.com/pzfreo/build123d-mcp) | Apache-2.0 (V, vendored) | `interference`, `clearance` (four states), `validate`, `verify_spec` (`wall_thickness_at`, `min_wall_mm`, `material_at_point`), `design_audit` | vendored and pinned | | [Casys mcp-build123d](https://jsr.io/@casys/mcp-build123d) | MIT (V) | `observe_assembly_integrity(STEP)`: per-pair contact, intersection volume, min distance, validity, free edges; limited to 32 occurrences | | | [agentcad](https://github.com/jdilla1277/agentcad) | Apache-2.0 (V, vendored) | part-level build/measure/check-spec/diff; `expect_solids`, `floating` declarations; **no assembly checks** | vendored and pinned | | cadpilot (LBurny) | not stated (u) | FreeCAD `verify_assembly`: floating (gap > 1 mm), interferences, anchor distances | ideas only | | [text-to-cad](https://github.com/earthtojake/text-to-cad) | MIT (V, vendored) | `dfam-check`, URDF validator | vendored and pinned | | NIST STEP File Analyzer | free, NIST disclaimer (V) | Part 21 syntax, CAx-IF validation properties, PMI | Windows + Excel | | [STEPcode](https://github.com/stepcode/stepcode) | BSD-3 (V) | schema parsing only | | **Gaps (V, by absence after search):** no open tool checks *joint-graph* consistency; none checks "part inside its cavity" beyond build123d-mcp's pairwise `clearance`; none ties checks to a design dossier; none evaluates section or exploded renders as judge inputs. That is the space the plan fills. ## 5. Visual review of renders by a vision model What the measurements say (details and citations in §2.4–§2.5): - **Works:** a few *named* views chosen for the question (CADCodeVerify's four orthogonal views; CADSmith's iso, high rear and front); specific yes/no questions written from the spec with "unclear" allowed; a judge stronger than the generator; kernel metrics given to the judge first with the validity gate as a hard fail; measurements trusted over images when they conflict (MAC). - **Does not work:** counting many features (VLMCountBench: Gemini 2.5 Flash 0.58→0.30 as compositional difficulty rises, V); small gaps and clearances (CADCodeVerify, CADSmith); interior and hidden parts (CADReview); orientation and unusual viewpoints (3DSRBench ICCV'25: GPT-4o 44.2 %, Claude 3.5 Sonnet 48.2 %, human 95.7 %; orientation 21.6 % / 31.4 %; uncommon viewpoints −13.5 %, V); multi-view reasoning (BLINK ECCV'24: GPT-4V 51.3 % vs humans 95.7 %, specialists +34.6 points, V). - **Unmeasured (gap):** no paper evaluates section cuts, exploded views or per-section colour coding as judge inputs. They are the obvious mitigations for the interior and counting failures (AADvark colours faces uniquely, V, but reports no numbers), so the plan uses them and measures them (plan §6). ## 6. The incident, and what would have caught it Stated generically: a turret assembly in a robot project rendered its ammunition outside the hopper that should hold it; subassemblies read as boxes stuck on the side; every per-part check (valid solid, watertight, within the bed, no plastic-on-plastic overlap) passed. | Symptom | Practice that catches it | Where it comes from | |---|---|---| | ammunition outside the hopper | a declared `contains` relation and a containment gate (`vol(item ∩ cavity) == vol(item)`) | §1 row 3; §4.2; build123d-mcp `clearance(status=containing)` | | subassemblies that only touch by coincidence, or float | author-declared interfaces diffed against the contact graph; floating components flagged unless declared free | §1 row 4; §4.1; agentcad `floating`; CADCLAW floating gate | | boxes with no features where a mechanism should be | interfaces and joints decided before geometry, with a card per part that names its function and mates; a reviewer that did not write it answering "does any part appear as a plain box?" | §1 rows 2, 7; ArtiCAD; MAC judge | | nothing in the pipeline looked | a render packet per section with named views and section cuts through each cavity, reviewed against yes/no questions, with the answers and the geometry hash recorded; sign-off invalidated when the hash changes | §1 rows 6, 12; §5; CADCodeVerify; text-to-cad snapshot policy | | no record of why the design is as it is | spec cards with decisions and alternatives rejected; a lint that fails a part without a card | §1 row 9; §3.4 | ## 7. Licences before borrowing | Project | Licence | What may be taken | |---|---|---| | earthtojake/text-to-cad (vendored, pinned) | MIT (V) | snapshot policy wording, inspection recipes, dfam-check approach, with attribution | | pzfreo/build123d-mcp (vendored, pinned) | Apache-2.0 (V) | the four-state `clearance` semantics, `verify_spec` evidence tiers, `design_audit` idea, with attribution and NOTICE | | jdilla1277/agentcad (vendored, pinned) | Apache-2.0 (V) | structure expectations (`expect_solids`, `floating`), review-comment store shape, edit-risk classification | | Pan-Chera/Multi-Agent-CAD | MIT (V) | judge prompt rules (evidence requirement, measurements over images, `unverified` without images) | | sunnyday-technologies/CADCLAW | MIT (V); depends on CasADi LGPL-3 | gate-chain structure (inventory, interference, floating, adjacency); do not import its CasADi path | | Casys mcp-build123d / mcp-dfm | MIT (V) | assembly-integrity report shape; ray-thickness caveat | | huggingface/cadgenbench | Apache-2.0 code, ODC-BY data (V) | the validity-gate-then-score pattern; a regression eval | | microsoft/CADFusion, anniedoris/CAD-Coder, FlexCAD, CAD-Editor, BenchCAD code | MIT / Apache-2.0 (V) | ideas; none needed as code | | OCCT (through OCP) | LGPL-2.1 with exception (V) | already a dependency via CadQuery | | trimesh, python-fcl, manifold3d, gpytoolbox | MIT, BSD, Apache-2.0, MIT (V) | collision manager for the motion gate; mesh containment fallback | | MADR | MIT or CC0 (V) | the decision-record template | | **Do not copy code from:** CADCodeVerify and CADReview repos (no licence file, V); CAD-Assistant, Text2CAD, CAD-Recode (custom or NC licences); JoinABLe *dataset* (non-commercial research only, V); draftwright (AGPL-3.0, V: run as a separate program only); CADSmith, ArtiCAD, AssemCAD, CADDesigner, Idea-to-CAD (no code licence stated); the swept-volumes code (MIT but patent-pending per its README); the AIAG-VDA AP table and Boothroyd penalty tables (copyrighted: declare simplified tables of our own) | | | Standards (NASA SP-2016-6105, NPR 7123.1D) are US government works and may be quoted; ASME, ISO, SAE, AIAG and ECSS documents are cited by number only. --- Source: https://cad-agent.valpatel.com/research/fea-and-process-tools.md # FOSS large-deflection + contact FEA, orientation, sheet metal and DFM checklists (2026-10-31) For Valpatel/cad-agent #96 (items 3–5), #94 (FEA step), #97 (pitfall catalogue), #99 (process profiles). Extends `research/simulation-tools.md` (2026-10-26), which covers topology optimisation, CFD/CHT, post-processing and slicer CLIs. Notation: **(s)** search snippet only · **(u)** unverified · **[derived]** my calculation · "read" = text read on this date. ## 0. What `cadagent.fea` does today From the module docstring, read from `origin/dev:src/cadagent/fea.py`: - gmsh tetrahedra and scikit-fem P2 linear elasticity. - Solvers: PARDISO, SciPy direct, or CG with pyamg. - "linear and small-deflection; isotropic", with weak-direction strengths for printed materials. - "Contact, bolt preload and buckling are not modelled." A snap engagement needs geometric nonlinearity, contact with friction, and preferably an orthotropic printed material, so the current module cannot model one. Closed-form checks (compliant-mechanisms.md §2) cover sizing. FEA is needed for force-vs-travel curves, U/L shapes, ring segments, both parts flexing, and checking the root fillet. ## 1. Solvers for nonlinear geometry + contact | Solver | Licence | Version / install on our hosts | Large deflection | Contact | Orthotropic (layer frame) | Python driving | Fit for snap engagement | |---|---|---|---|---|---|---|---| | **CalculiX ccx** | GPL-2.0 ([dhondt.de](http://www.dhondt.de/)) | **2.23**, manual dated 2025-10-19 ([ccx_2.23.pdf](http://www.dhondt.de/ccx_2.23.pdf), read). Ubuntu apt `calculix-ccx` **2.21** (checked on this workstation: noble); conda-forge `calculix` **2.23** | `*STEP, NLGEOM` (44 hits in the manual) | `*CONTACT PAIR` NODE TO SURFACE, SURFACE TO SURFACE (penalty), **MORTAR**; `*SURFACE BEHAVIOR` LINEAR / EXPONENTIAL / TABULAR / TIED; `*FRICTION`; `*RIGID BODY` (manual) | `*ELASTIC, TYPE=ENGINEERING CONSTANTS` + `*ORIENTATION` (manual) | Write `.inp` text; read `.frd`/`.dat` (e.g. [ccx2paraview](https://pypi.org/project/ccx2paraview/) 3.2.0, licence (u)) | **Best fit**: mature contact, small binary, apt/conda, Abaqus-like syntax that LLMs know. Run as a subprocess (GPL) | | **FreeCAD FEM** (CalculiX writer) | LGPL-2.1 | FreeCAD **1.1.4** (2026-09-28); 26.3 RC2 (2026-10-18) ([releases](https://github.com/FreeCAD/FreeCAD/releases)) | `GeometricalNonlinearity` adds `, NLGEOM` ([write_step_equation.py](https://github.com/FreeCAD/FreeCAD/blob/main/src/Mod/Fem/femsolver/calculix/write_step_equation.py), read) | Writes `*CONTACT PAIR … TYPE=SURFACE TO SURFACE`, LINEAR (slope) / HARD / TIED, `*FRICTION µ, stick slope`, optional ADJUST ([write_constraint_contact.py](https://github.com/FreeCAD/FreeCAD/blob/main/src/Mod/Fem/femsolver/calculix/write_constraint_contact.py), read) | Material cards (isotropic in the GUI) (u) | `freecadcmd` scripts; the FreeCAD wiki is behind Anubis bot protection | Good for humans and for reading `.inp` examples. Heavier than writing `.inp` ourselves | | **Elmer** | GPL-2+/LGPL mix (simulation-tools.md) | release-26.2 | ElasticSolve, nonlinear elasticity (u) | Mortar contact in `ContactUtils.F90`; **26 contact regression tests** incl. `ContactFriction`, `ContactFrictionNonlin`, `ContactBlunt2D*`, `ContactPatch3D*` ([fem/tests](https://github.com/ElmerCSC/elmerfem/tree/devel/fem/tests), read) | (u) | `.sif` text | Second choice; good docs-by-tests | | **Code_Aster** | GPL-3.0 (+ CeCILL-C/Apache/LGPL parts) | conda-forge `code-aster` **18.1.11** (read via the anaconda API) | STAT_NON_LINE (u) | DEFI_CONTACT (u) | yes (u) | `.comm` Python-like | Most capable; heaviest to learn. Use for difficult validation cases | | **FEniCSx** | LGPL-3.0 (dolfinx 0.11) | Docker/conda (simulation-tools.md) | Native (UFL hyperelastic, Newton) | Core has none; [dolfinx-contact](https://github.com/Wells-Group/asimov-contact) (MIT) is "under heavy development and highly experimental", needs dolfinx 0.11 | Custom UFL | Pure Python/UFL | Not now; penalty contact against a rigid plane is easy to hand-write | | **scikit-fem** | BSD-3-Clause | **12.0.2**, already in cad-agent | Hyperelastic examples ex36, ex43, **ex55 (updated Lagrangian)** | Mortar contact ex04 and **ex51** (2D, linear elastic) ([docs/examples](https://github.com/kinnala/scikit-fem/tree/master/docs/examples), read) | Write our own | In-process | No ready nonlinear-contact example. Writing a 2D total-Lagrangian + penalty contact is feasible later (BSD, in-process) | | **SfePy** | BSD-3-Clause | PyPI **2026.3** | Total-Lagrangian hyperelastic (u) | Penalty contact terms: `elastic_contact_planes.py`, `elastic_contact_sphere.py`, `two_bodies_contact.py` ([examples](https://github.com/sfepy/sfepy/tree/master/sfepy/examples/linear_elasticity), read) | yes (u) | Python | Viable permissive in-process option; worth a spike against ccx | | **Kratos** | BSD-4-clause-style with an advertising clause ([kratos/license.txt](https://github.com/KratosMultiphysics/Kratos/blob/master/kratos/license.txt), read; GitHub shows NOASSERTION) | PyPI `KratosMultiphysics` + `KratosContactStructuralMechanicsApplication` **10.4.4** | yes (u) | Mortar contact app (u) | (u) | `pip`, JSON + Python | Watch; wheels make it the easiest permissive 3D contact install | | MOOSE | LGPL-2.1 | Source/conda build | yes (u) | contact module (u) | yes (u) | `.i` files | Skip (build weight) | | [PyChrono](https://github.com/projectchrono/chrono) / [PyElastica](https://github.com/GazzolaLab/PyElastica) | BSD-3 / MIT | active | Beams, shells / Cosserat rods | Chrono FEA contact (u) | no | Python | Fast large-deflection beam checks only | **Recommendation.** - Add `cadagent.fea_nl`, which writes CalculiX `.inp` from our gmsh mesh and runs `ccx` as a subprocess (GPL boundary as in simulation-tools.md). Validate it on the three Bayer gold cases first. - Keep scikit-fem for the linear checks. - Spike SfePy or Kratos only if we need contact in-process. - Use Elmer or Code_Aster as independent cross-checks on one case. ## 2. Minimal snap-engagement run (CalculiX) Settings verified in the ccx 2.23 manual: - Linear pressure-overclosure K "5 to 50 times the E-modulus of the adjacent materials". - Stick slope "about 100 times smaller than the spring constant"; μ "usually 0.1–0.5". - Convergence advice: try without NLGEOM first, SMALL SLIDING first, and a linear pressure-overclosure. Base the slave surface on faces, especially with quadratic elements. The slave mesh must be at least as fine as the master. - Plane-stress elements are expanded to 3D internally and "must have a small thickness". | Item | Choice for a cantilever-hook engagement | |---|---| | Geometry | The 2D section of the beam with its hook and root fillet, plus the mating ramp or edge. Use 3D only for ring segments, U shapes or annular snaps | | Mesh | gmsh second-order triangles (CPS6) or quads (CPS8), at least 4 elements through the root thickness, refined at the fillet and the contact faces | | Material | Secant E at the design strain (Bayer Fig. 16 for PC), or the printed XY/Z E from TDS (compliant-mechanisms.md §5). For printed beams: `*ELASTIC, TYPE=ENGINEERING CONSTANTS` with E₁ = E₂ = E_XY, E₃ = E_Z, G and ν estimated (u), and `*ORIENTATION` taken from the part's print rotation (`cad/build.py` table) | | Mating part | Rigid: `*RIGID BODY, NSET=…, REF NODE=…` driven by `*BOUNDARY` displacement. Or elastic if both parts flex (Bayer "both mating parts elastic") | | Contact | `*CONTACT PAIR, INTERACTION=SI, TYPE=SURFACE TO SURFACE` with hook faces as slave; `*SURFACE BEHAVIOR, PRESSURE-OVERCLOSURE=LINEAR` K ≈ 10·E; `*FRICTION` μ from Bayer Table 3, λ = K/100 | | Step | `*STEP, NLGEOM, INC=…` + `*STATIC` with a small initial increment. Drive the mate through insertion, then back out for the release force | | Outputs | `*NODE PRINT, TOTALS=ONLY` RF on the reference node, giving the F–δ curve, peak insertion force W and release force; `*EL FILE` S, E giving peak strain at the root; `*CONTACT FILE` CDIS, CSTR | | Acceptance | (1) At small strain, ccx P and W within about 10 % of Bayer closed form (Example I: P = 32.5 N, W = 58.5 N) [target, derived]. (2) The result changes < 5 % on mesh halving (or a GCI study, as NovaSolver does). (3) The F–δ curve returns to ≈ 0 after release. (4) Peak strain in the print frame ≤ the derated allowable: ε_XY in the layer plane, ε_ZZ across layers | | Evidence to store | `.inp`, solver version, F–δ CSV, a strain map PNG, the hash of the STEP it came from (binding as `cad-agent` already does for other checks) | | Run cost | 2D: about 10³–10⁴ elements, seconds to minutes (u). 3D annular: minutes to an hour; use the cad-remote-runs skill for long runs | ## 3. Print-orientation optimisers | Tool / method | Licence | What it optimises | Anisotropy-aware | Use | |---|---|---|---|---| | [ChristophSchranz/Tweaker-3](https://github.com/ChristophSchranz/Tweaker-3) | GPL-3.0 | Overhang/support area, bottom area, contour; evolutionary-tuned weights; 110★, push 2026-03-24 | No | External comparator (subprocess) | | Slicer auto-orient: Bambu/Orca CLI `--orient` (simulation-tools.md §5); Orca 2.5 MCP (agent-toolkits note) | AGPL | Support/overhang | No | Cross-check | | [b123d-print `suggest_orientation`](https://github.com/KD5RYN/b123d-print/blob/main/src/b123d_print/orient.py) | MIT | Overhang plus the tip-over measure (Orca auto-brim formula) ([checks.md](https://github.com/KD5RYN/b123d-print/blob/main/docs/using/checks.md)) | No | Borrow ideas | | Umetani & Schmidt 2013, cross-sectional structural analysis ([Autodesk Research](https://www.research.autodesk.com/publications/cross-sectional-structural-analysis-for-3d-printing-optimization)) | paper / patents (US10239258) | Weakest cross-section under bending, then the orientation | Assumes isotropy (s) | Method idea | | Ulu et al., build orientation for maximum safety factor ([ITU record](https://research.itu.edu.tr/en/publications/enhancing-the-structural-performance-of-additively-manufactured-o/)) | paper | Orientation maximising the minimum FoS with anisotropic properties from coupons, using a surrogate | **Yes** (s) | The method to copy for loaded parts | | Anisotropic multi-component TO with build orientation ([arXiv 1911.10393](https://arxiv.org/abs/1911.10393)) | paper | Joint topology and orientation | Yes | Research only | | pyMOTO `OverhangFilter` (simulation-tools.md §1) | MIT | Topology under a fixed build direction | n/a | Already recommended | **Gap and proposal.** No FOSS tool scores orientation against the *strain direction of declared compliant features*. Proposed deterministic scorer [derived]: for each of the 6 axis-aligned orientations (plus slicer suggestions), compute - overhang area (`cadagent.geometry.downward_area`, already exists); - support volume; - the tip-over measure; - for each declared flexing feature, the fraction of peak bending strain whose direction has a Z component across layers. Reject any orientation that puts a snap or flexure's bending across layers (Klahn 2016, Hubs; compliant-mechanisms.md §5). ## 4. Sheet metal: unfold and bend tables | Tool | Licence | Status | Bend model / tables | Headless | Use | |---|---|---|---|---|---| | **build123d `BuildSheet` + `unfold`** | Apache-2.0 | dev branch only (not in v0.13.0); commits 2026-07-21 → 2026-10-21 | Neutral radius from thickness, K-factor and reference surface; flange, bend/corner relief, hem, jog, miter, thicken ([docs](https://github.com/gumyr/build123d/tree/dev/docs/sheet_metal), read) | yes (Python) | **Adopt on release**: same kernel as text-to-cad, permissive | | [shaise/FreeCAD_SheetMetal](https://github.com/shaise/FreeCAD_SheetMetal) | LGPL-2.1 | 350★, push 2026-10-17 | K-factor ANSI or DIN; a "material definition" spreadsheet mapping R/T → K per bend; example `MDS/mild_steel.csv` (K = 0.40 for 1–6 mm); `tools/calc-unfold.py`; unattended unfold and DXF/SVG export; `SMTests/testKfactor.py` (README and files read) | via `freecadcmd` (u) | Fallback (external process) | | [alexneufeld/YouCanNotUnfold](https://github.com/alexneufeld/YouCanNotUnfold) | LGPL-2.1 | 22★, push 2026-10-09 | "Fast robust sheet metal unfolding tools for FreeCAD" | (u) | Watch | | SendCutSend specs JSON (landscape.md §5) | vendor data | live | 178 materials: K-factor, bend radius, bend deduction, minimum hole and bridge | fetch | Source for the K and R tables (fetch and cite, don't redistribute) | | text-to-cad #614 `cadgen.unfold` | MIT | open proposal | — | — | Contribute or reuse build123d's | Bend allowance BA = θ·(R + K·T); flat length = Σ flats + Σ BA [derived; this is the neutral-axis model the build123d docs describe]. ## 5. Published checklists whose items can become deterministic checks (#97) Sources: the DuPont design manual [DUP]; the Boothroyd Dewhurst DFM checklist [BD]; Protolabs moulding [PL-IM] and sheet-metal [PL-SM] guidelines; Hubs FDM [HUBS-FDM] and snap-fit [HUBS-SF] guides; the UF design-lab checklist [UF] and common-mistakes list [UF-M]; b123d-print checks [B123]; Bayer [BAY]; the Klahn paper [KLA]; Boothroyd DFA criteria [DFA]. "Exists" means cad-agent already has a check, according to `docs/common-issues.md` on origin/dev. | ID | Check | Rule / threshold | Source | Process | Implementation hint | Status | |---|---|---|---|---|---|---| | G1 | Holes and countersinks clear of later features (cylinders **and cones**) | Probe material inside each hole and countersink cone | common-issues.md; #98 | all | Extend `cad-agent holes` to `Cone` faces | partly exists (#98) | | G2 | Tool/driver access along the fastener axis | Swept tool envelope collision-free | [UF] "fastener head types allow adequate motion with tools" | all | Sweep a hex-key or driver cylinder along the axis | exists (assembly-access gate) | | G3 | Thread engagement | ≥ 5 threads (UF lab rule) or project rule | [UF] | all | Stack length vs pitch | exists (fastener gate) | | G4 | Rib/boss ends on air | Every rib end face touches a wall solid | common-issues.md | all | Face adjacency after the boolean | new | | M1 | Rib thickness vs wall | rib ≤ 60 % of the adjacent wall | [BD] (DuPont plots 60 % ribs [DUP]) | moulding | Medial thickness ratio | new | | M2 | Rib base fillet | ≈ ½ rib thickness | [DUP] | moulding | Fillet radius at the rib root | new | | M3 | Draft | ≥ 0.5° on vertical faces, 2° typical, ≥ 3° light texture, ≥ 5° heavier texture (PM-T2), ≥ 3° on shut-offs | [PL-IM]; resin table [DUP] | moulding | Face normal · pull direction | new (cf. text-to-cad `mold_tool.py`) | | M4 | Wall thickness range per resin | e.g. PC 1.016–3.81 mm, PP 0.889–3.81 mm, nylon 0.762–2.921 mm | [PL-IM] | moulding | Ray-cast thickness histogram | new | | M5 | Boss OD | 2–2.5 × hole diameter | [DUP] | moulding / FDM | Annulus measure on bosses | new | | M6 | Blind cored hole | depth ≤ 2 × core-pin diameter; floor ≥ d/6 | [DUP] | moulding | Hole depth vs diameter | new | | M7 | Sharp internal corners | radius ≥ 0.5 mm (0.020 in); fillet ≈ t/2 | [DUP][BD] | moulding | Concave edge radius | new | | M8 | Undercut needs a side action | Faces not visible from ±pull | [BD]; dfm-agent approach | moulding | Visibility per pull direction | new | | C1 | Snap/flexure strain | ε(y, l, h, taper) ≤ derated allowable(material, process, orientation, cycles) | [BAY][DUP]; compliant-mechanisms.md §6 | all | Calculator from #95 on declared features | new (#95) | | C2 | Snap root fillet | R ≥ max(0.38 mm, 0.5·h) | [BAY][HUBS-SF] | all | Fillet at the beam root | new | | C3 | Flexing member bends across layers | Strain direction in the print frame must not cross layers | [KLA][HUBS-SF] | FDM | Beam axis and bending normal vs print Z | new | | C4 | Print-in-place gap | gap ≥ pip_clearance (0.3–0.5 mm FDM; 0.3 mm SLS/MJF); Z gaps = n·layer height | [HUBS-SF][B123] | AM | Minimum distance between the solids of one part | new | | C5 | Snap min width / base (MJF) | width ≥ 5 mm; base ≥ 1 mm | [HUBS-SF] | MJF | Section measure | new | | P1 | Overhang | > 45° from vertical needs support | [HUBS-FDM][B123] | FDM / SLA | `downward_area` | exists | | P2 | Bridge length | warn > 5 mm (Hubs) or > 10 mm (b123d-print default) | [HUBS-FDM][B123] | FDM | Flat downward faces resting on walls on two sides | new | | P3 | Round horizontal hole sags | Use a teardrop or flat roof | [B123] | FDM | Horizontal cylinder with a round top | new | | P4 | Bed-edge fillet flares | Use a chamfer on edges at the bed | [HUBS-FDM][B123] | FDM | Curved faces touching z = 0 | new | | P5 | Tip-over | brim = tip_speed·H²·W/(1920·I) ≥ 5 mm warns | [B123] | FDM | Footprint second moment of area | new | | P6 | Thin wall vs line width | < 1 line width fails | [B123] | FDM | Section scans | exists (wall gate) | | P7 | Small pins | < 5 mm diameter is weak; fillet the base | [HUBS-FDM] | FDM | Cylinder radius + fillet | new | | S1 | Minimum flange | ≥ 4T | [PL-SM][BD] | sheet | Flange length per bend | new | | S2 | Hole diameter | ≥ T | [PL-SM] | sheet / laser | Hole radius | new | | S3 | Hole to edge | ≥ 2T [BD]; Protolabs 1.574 mm (T ≤ 0.914) / 3.175 mm | [BD][PL-SM] | sheet / laser | Edge distance | new | | S4 | Hole to bend | ≥ 2T + R [BD]; features within about 4T of a bend distort [PL-SM] | [BD][PL-SM] | sheet | Distance to the bend line in the flat pattern | new | | S5 | Bend radius | ≥ T [BD]; one radius for all bends [PL-SM] | [BD][PL-SM] | sheet | Radius list | new | | S6 | Bend relief present | relief at every bend that ends inside the blank (sizing from SendCutSend/Protolabs (u)) | [BD] | sheet | Notch at bend ends | new | | S7 | Hems | inside diameter ≥ T; return ≥ 6T | [PL-SM] | sheet | Hem geometry | new | | S8 | Tabs and notches | tab ≥ max(2T, 3.2 mm), length ≤ 5 × width; notch ≥ max(T, 1.016 mm) | [PL-SM] | sheet / laser | Slot widths | new | | A1 | DFA part count | Each part must pass ≥ 1 of: moves relative to its neighbours; must be a different material; must be separate to assemble or service the others. Otherwise it is a candidate for merging, e.g. into a compliant feature | [DFA] | all | BOM + assembly graph prompt (with #40) | new (review prompt) | | A2 | Every fastener modelled; clearance check run on the full assembly | — | [UF] | all | BOM vs solids | exists (gates) | | R1 | Claims backed by evidence | No feature called validated without a coupon (#94 item 5) | common-issues.md | all | Dossier lint | exists (partly) | Checks C1–C5 are the compliant-feature checks for #94; S1–S8 and M1–M8 need process metadata per part (#99). **References** - [DUP] https://www.emcoplastics.com/assets/pdf/acetal/General%20Design%20Principles%20for%20Engineering%20Polymers.pdf - [BD] https://www.dfma.com/resources/pdf/dfm-design-review-checklist.pdf - [PL-IM] https://www.protolabs.com/services/injection-molding/plastic-injection-molding/design-guidelines/ - [PL-SM] https://www.protolabs.com/services/sheet-metal-fabrication/design-guidelines/ - [HUBS-FDM] https://www.hubs.com/knowledge-base/how-design-parts-fdm-3d-printing/ - [HUBS-SF] https://www.hubs.com/knowledge-base/how-design-snap-fit-joints-3d-printing/ - [UF] https://web.mae.ufl.edu/designlab/Lab%20Assignments/EML2322L%20Final%20Design%20Checklist.pdf - [UF-M] https://web.mae.ufl.edu/designlab/TA/Common%20Design%20Review%20Mistakes.pdf (e.g. "not considering room for tools", "less than 5 threads of engagement") - [B123] https://github.com/KD5RYN/b123d-print/blob/main/docs/using/checks.md - [BAY] https://cdn.sparkfun.com/assets/home_page_posts/1/4/1/0/Plastic_Snap_fit_design.pdf - [KLA] https://publikationen.bibliothek.kit.edu/1000142464 - [DFA] Boothroyd–Dewhurst minimum-part criteria as summarised at https://www.wevolver.com/article/dfma-design-for-manufacturing-and-assembly-explained and https://www.dfma.com/design-for-assembly.asp (s) **Could not verify:** - Code_Aster, Kratos, MOOSE and Chrono keyword- and feature-level claims (marked (u)). - Elmer ElasticSolve large deformation. - ccx run times. - FreeCAD wiki pages (bot-blocked). - ccx2paraview licence. - Bend-relief sizing numbers. - Whether FreeCAD SheetMetal unfold runs under `freecadcmd` without the GUI. --- Source: https://cad-agent.valpatel.com/research/landscape.md # Agentic hardware-design landscape (verified 2026-10-24) > **Refreshed 2026-10-31** (issue #96): [agent toolkits since 10-24](https://cad-agent.valpatel.com/research/agent-toolkits-2026-10.md) (text-to-cad 0.8 break, > OrcaSlicer 2.5 MCP, build123d sheet metal, snap-fit skills), [compliant mechanisms](https://cad-agent.valpatel.com/research/compliant-mechanisms.md) (snap > fits, flexures, allowable strains, FDM anisotropy from datasheets) and [FEA and process tools](https://cad-agent.valpatel.com/research/fea-and-process-tools.md) > (large-deflection contact solvers, orientation optimisers, sheet metal, 37 candidate deterministic checks). Scope: public tools that let frontier-model agents (Claude Code first; Codex, Cursor, Gemini) design 3D-printed, sheet-metal, laser-cut, CNC and molded parts, enclosures around real PCBs, and KiCad boards. Method: stars, SPDX licence and last push come from `gh api repos/OWNER/REPO` on this date; capabilities come from each repo's README, manifests or source as read. **(u)** = unverified (README claim or third-party page not checked in source). Machine-readable copy: [`landscape.json`](https://github.com/Valpatel/cad-agent/blob/dev/research/landscape.json). ## 1. Capability matrix Clients: **CC** = Claude Code plugin/skill, **MCP** = MCP server, **Cx** = Codex, **Cu** = Cursor, **Ge** = Gemini, **App** = standalone app. | Project | Licence | Stars | Push | Clients | Kernel / tool | Processes | DFM rules cited | Materials / cost | Render loop | Drawings | Vendor / slicer | Evals | |---|---|---|---|---|---|---|---|---|---|---|---|---| | [earthtojake/text-to-cad](https://github.com/earthtojake/text-to-cad) | MIT | 22,951 | 10-25 | CC, Cx, Cu, Ge, Grok, MCP, skills.sh | build123d 0.11 / OCP 7.9 | FDM, SLA, SLS, MJF, metal PBF (mesh); sheet, CNC, molding (review); laser (DXF); URDF/SDF | Yes: Hubs, Protolabs, ISO/ASTM 52910 | No (SendCutSend specs fetched live) | Yes (headless snapshots + viewer) | Yes, PDF | OrcaSlicer, Bambu Connect/Studio, SendCutSend, step.parts | None in repo | | [amagine-ai/Amagine3D](https://github.com/amagine-ai/Amagine3D) | Apache-2.0 | 6,259 | 10-22 | App (OpenAI-compatible key, Codex SDK) | build123d, OCP, Manifold | FDM enclosures, multi-part | (u) | (u) | Yes (5-view) | (u) | none seen | none | | [Adam-CAD/CADAM](https://github.com/Adam-CAD/CADAM) | GPL-3.0 | 5,328 | 10-15 | App (web) | OpenSCAD | general | No | No | Yes | No | none | Showcase prompts only | | [KittyCAD/modeling-app](https://github.com/KittyCAD/modeling-app) + [KittyCAD/mcp](https://github.com/KittyCAD/mcp) | MIT / MIT | 1,321 / 10 | 10-25 | App; MCP (needs `ZOO_API_TOKEN`) | KCL on Zoo's closed cloud engine | general | (u) | (u) | Yes | (u) | none | none | | [Pan-Chera/Multi-Agent-CAD](https://github.com/Pan-Chera/Multi-Agent-CAD) (MAC) | MIT | 1,048 | 10-17 | App + web UI | build123d | FDM parts, assemblies, URDF | No | No | Yes + judge agent | No | none | Own 10-prompt / 141-feature set | | [OzAILabs/Claude-To-Print](https://github.com/OzAILabs/Claude-To-Print) | MIT | 4 | 08-15 | CC skills (from MAC) | build123d | FDM, multicolour 3MF | No | No | Yes | No | Bambu | none | | [jdilla1277/agentcad](https://github.com/jdilla1277/agentcad) | Apache-2.0 | 173 | 10-23 | CLI, MCP, skill | build123d (CadQuery mode) | general | No | No | Yes, diffing | No | none | CADGenBench runs | | [pzfreo/build123d-mcp](https://github.com/pzfreo/build123d-mcp) | Apache-2.0 | 130 | 10-15 | MCP | build123d | general | No | No | Yes | SVG/DXF export | none | CADGenBench ([pipeline](https://github.com/pzfreo/cadgenbench-build123d)) | | [flowful-ai/cad-skill](https://github.com/flowful-ai/cad-skill) | **PolyForm-Noncommercial-1.0.0** | 692 | 07-14 | CC skill | CadQuery | FDM | No | No | Yes | No | Prusa/Bambu mentions | none | | [Svetlana-DAO-LLC/cad-agent](https://github.com/Svetlana-DAO-LLC/cad-agent) (was clawd-maf) | Custom "CAD Agent License" | 53 | 02-17 | MCP | build123d | FDM | No | No | Yes | No | none | none | | [MikeSuiter/claude-build123d-starter](https://github.com/MikeSuiter/claude-build123d-starter) | MIT | 1 | 06-08 | CC template | build123d | FDM, CNC, laser (u) | No | No | Live preview | No | none | none | | [neka-nat/freecad-mcp](https://github.com/neka-nat/freecad-mcp) | MIT | 3,040 | 10-25 | MCP | FreeCAD (LGPL, external) | general, FEM (u) | No | No | Screenshots | (u) | none | none | | [jhacksman/OpenSCAD-MCP-Server](https://github.com/jhacksman/OpenSCAD-MCP-Server) | MIT | 208 | 09-07 | MCP | OpenSCAD | FDM | No | No | Preview images | No | none | none | | [altendky/onshape-mcp](https://github.com/altendky/onshape-mcp) | Apache-2.0 | 20 | 10-24 | MCP | Onshape (closed SaaS) | general | No | No | (u) | (u) | none | none | | [faust-machines/fusion360-mcp-server](https://github.com/faust-machines/fusion360-mcp-server) | MIT | 131 | 10-06 | MCP (CC, Cu, Cx docs) | Fusion (closed) | general | No | No | (u) | (u) | none | none | | [ahujasid/mcp-for-blender](https://github.com/ahujasid/mcp-for-blender) | MIT | 32,261 | 10-24 | MCP, Cx plugin | Blender (mesh, not B-rep) | visual only | No | No | Yes | No | Poly Haven, Sketchfab, paid generators | none | | [mixelpixx/KiCAD-MCP-Server](https://github.com/mixelpixx/KiCAD-MCP-Server) | MIT | 2,933 | 10-01 | MCP | KiCad 9+ (kicad-python + SWIG `pcbnew`) | PCB | KiCad DRC | JLCPCB parts + prices | Snapshots | No | JLCPCB, DigiKey, Freerouting | none | | [mixelpixx/Konnect](https://github.com/mixelpixx/Konnect) ([obhox/kicad-mcp](https://github.com/obhox/kicad-mcp) is a fork) | **AGPL-3.0** | 1,399 | 10-25 | KiCad plugin + MCP, CC skills | KiCad 10 IPC (Rust) | PCB | design-review audits (u) | (u) | (u) | No | Freerouting | none | | [oaslananka/kicad-mcp-pro](https://github.com/oaslananka/kicad-mcp-pro) | MIT | 149 | 10-25 | MCP, CC plugin skills, Cx/VS Code config | KiCad 10.0.x (kicad-cli/IPC; no SWIG) | PCB | 8 fab profiles with source URLs | Fab pricing coefficients | 3D render PNG | No | JLCPCB; Nexar/DigiKey/Mouser with keys | `evals/`, `benchmarks/` dirs | | [Seeed-Studio/kicad-mcp-server](https://github.com/Seeed-Studio/kicad-mcp-server) | **No LICENSE file** (MIT only in pyproject) | 168 | 10-22 | MCP | KiCad 8-10 (`pcbnew`) | PCB analysis, device tree | KiCad ERC/DRC | PartReel registry | No | No | PartReel | none | | [lamaalrajih/kicad-mcp](https://github.com/lamaalrajih/kicad-mcp) | MIT | 529 | 2025-10-17 | MCP | KiCad 9 | PCB | DRC | BOM | (u) | No | none | none; stale | | [aklofas/kicad-happy](https://github.com/aklofas/kicad-happy) | MIT | 1,499 | 10-25 | CC plugin, Cx, Copilot, Gemini/Antigravity, GitHub Action | KiCad file parsing, SPICE | PCB review, EMC, Gerber | EMC rules ref | BOM, distributors | No | No | JLCPCB, PCBWay, DigiKey, Mouser, LCSC, element14 | none | | [American-Embedded/kistack](https://github.com/American-Embedded/kistack) | MIT text (GitHub: NOASSERTION) | 411 | 10-24 | skills.sh | KiCad | schematic, layout, Gerber review | prose | No | No | No | none | none | | [i2cjak/Backplane](https://github.com/i2cjak/Backplane) | MIT (ships a KiCad fork) | 731 | 10-25 | App; multi-harness agents | KiCad fork + FreeCAD viewers | PCB + case | No | No | Yes | No | none | none | | [copperheadhq/copperhead](https://github.com/copperheadhq/copperhead) | Apache-2.0 | 627 | 10-24 | CLI, agent-install prompt | KiCad s-expr + kicad-cli | PCB | ERC/DRC | parts (u) | (u) | No | none | none | | [punkfab/circuit-skills](https://github.com/punkfab/circuit-skills) | **No licence** | 10 | 10-06 | CC skills, Cx plugin | kipy, build123d, ngspice | PCB + `pcb-enclosure-fit` | No | No | Blender renders | No | Freerouting | PCBWorld harness | Building blocks: [gumyr/build123d](https://github.com/gumyr/build123d) (Apache-2.0, 3,519), [CadQuery](https://github.com/CadQuery/cadquery) (Apache-2.0 in LICENSE file; GitHub shows NOASSERTION, 5,990), [gumyr/bd_warehouse](https://github.com/gumyr/bd_warehouse) (Apache-2.0, ISO fasteners and threads), [partcad/partcad](https://github.com/partcad/partcad) (Apache-2.0, 509), [sargo-devel/Cables](https://github.com/sargo-devel/Cables) (LGPL-3.0 FreeCAD cable-routing workbench), [wireviz/WireViz](https://github.com/wireviz/WireViz) (GPL-3.0, 2D harness documents). [rjwalters/kicad-tools](https://github.com/rjwalters/kicad-tools) (MIT) keeps a long, licence-annotated survey of KiCad and agent tools in its README. ## 2. Deep dive: earthtojake/text-to-cad (v0.7.26, 2026-10-20) **What it is.** A plugin monorepo. `skills/` holds the product. `packages/cadgen` is the PyPI runtime, MIT, with build123d, OCP, a build daemon, STEP/GLB/3MF/DXF output, a viewer backend and an MCP server. `apps/` holds the viewer, the MCP app, the docs site and `api.texttocad.dev`. It is mostly one maintainer: earthtojake has 1,136 commits, then bots and a few contributors. **Skills** (each is MIT and carries its own `LICENSE`): | Skill | What it does | Code it runs | |---|---|---| | cad | build123d models with `@step/@stl/@threemf/@glb` decorators; `read_step`/`read_scene`; face/edge refs; snapshots; viewer | cadgen via pinned `uvx` | | step-parts | Search and download catalog STEP files (screws, bearings, standoffs, boards), with sha256 check | `download_step_part.py` → `api.step.parts` | | engineering-drawing | Third-angle views, hidden lines, measured dimensions, hole callouts, ISO title block → PDF | `cadgen.eng_drawing` | | dxf | 2D profiles, flat pattern of *already flat* plates, kerf offset | `cadgen.flatten` | | dfam-check | Mesh wall thickness (ray-cast), overhang, support volume, 6 axis orientations; limits table per FDM/SLS/SLA/PBF/MJF | `dfam_tool.py` (trimesh) | | dfm | Guided sheet/CNC/molding review with a cited rule per finding; only molding is measured (draft, undercuts, projected area) | `mold_tool.py` | | sendcutsend | Preflight of DXF/STEP against SendCutSend's live catalog and specs JSON | fetches `cdn.sendcutsend.com` | | gcode | Writes complete OrcaSlicer presets and slices headless | `orca_presets.py` (stdlib) | | bambu-labs | Hands off to Bambu Connect (`bambu-connect://` link) or Bambu Studio; never controls the printer | none | | urdf / srdf / sdf | Robot and simulator descriptions, with validation | cadgen | **Install (Claude Code):** `claude plugin marketplace add earthtojake/text-to-cad#latest` then `claude plugin install text-to-cad@earthtojake`. The `latest` branch holds only the plugin, one commit per release. **Security review.** - `.claude-plugin/plugin.json` declares only `skills` and `mcpServers`. It has **no hooks, commands or agents**. `.githooks/pre-commit` is for development only. - Claude Code starts the MCP server with `uvx --no-config --managed-python --python 3.13 --from cadgen==0.7.26 cadgen mcp`. The first start downloads a managed CPython and cadgen. cadgen is pinned exactly, but its dependencies are ranges with no hash lock: build123d <0.12, cadquery-ocp-novtk 7.9, ezdxf, matplotlib, pillow, shapely, playwright==1.64.0. The wheel bundles Node/browser JS and a native file tracer (`filetrace.c`). The first snapshot downloads a Playwright browser. - **Telemetry is on by default.** It sends counts, a random install ID, the OS and agent app, and crash types to `api.texttocad.dev`, which stores them in PostHog; the server derives country from IP. The README says names, paths, contents and prompts are never sent; I did not audit the payload code line by line. Turn it off with `uvx cadgen telemetry off`, `DO_NOT_TRACK=1` or `CADGEN_TELEMETRY=0`. - **Daily update check** to `api.texttocad.dev/v1/versions`. Turn it off with `CADGEN_UPDATE_CHECK=0`. - The viewer binds 127.0.0.1 with **no authentication** (per its SECURITY.md). Any local process can read files under the opened folder and trigger builds. - The dfam-check and dfm skills `pip install` unpinned `requirements.txt` files into the *active project environment*. - Model scripts are arbitrary Python run by the daemon, which is normal for code-CAD. **Coverage of our J501 workflow:** | J501 capability | text-to-cad today | |---|---| | Vendor STEP ingestion | Yes (`read_step`, `read_scene`, refs). Viewer fails on large PCB STEP: open [#626](https://github.com/earthtojake/text-to-cad/issues/626), "exceeds viewer memory envelope" | | Clearance vs 5000+ carrier solids | Primitives only: `cadgen.geometry.closest_points` and `overlap_volume`. The `interference` module was retired; the caller writes pair selection. No broad-phase or report | | Schematic ingestion | No. PCB support is open PR [#518](https://github.com/earthtojake/text-to-cad/pull/518) (KiCad 11 nightly, vendored kipy) | | Enclosure around a real board | Generic examples only (`electronics_enclosure_base.py`) | | Connector windows from board STEP | No | | Standoffs / fastener stack length | Catalog parts via step-parts. Stack-length checks exist only as model-specific scripts in `models/tendon_hand`, not as a skill | | Cable routing, bend radius | No | | Renders | Yes | | PDF check sheet | Drawings yes; no check-sheet or evidence PDF | | Review / evidence site, STL/3MF/Bambu links | Local unauthenticated viewer; 3MF export and Bambu handoff yes; no publishable evidence page | | DfAM | Walls and overhangs measured; hole size, min feature and bridge are "not checked" (per `process-limits.md`) | | Sheet-metal unfold | No; proposal [#614](https://github.com/earthtojake/text-to-cad/issues/614) | Kernel note: text-to-cad uses build123d. Our J501 code is CadQuery. Both use OCP, so shapes interoperate through `.wrapped`. ## 3. KiCad MCP servers: install and security, then a recommendation | | kicad-mcp-pro | mixelpixx KiCAD-MCP-Server | Seeed kicad-mcp-server | kicad-happy (skills) | |---|---|---|---|---| | Install | `uvx kicad-mcp-pro`. The shipped `.mcp.json` is **unpinned**, so each launch runs latest PyPI | `git clone`; `npm install` runs `prepare` → `npm run build`; pip requirements | `pip install -e .` into KiCad's Python | CC marketplace plugin | | KiCad | 10.0.x primary (10.0.7 verified), 8.x deprecated, 9 dropped, 11 preview; SWIG forbidden by test | 9.0+; mixes kicad-python IPC and SWIG `pcbnew` | 8-10, `pcbnew` | file parsing | | Tools | 387 public; default profile = 24 read-only; writes need a profile | 244 registered | analysis, ERC/DRC, device-tree code | review scripts | | Network | JLCPCB public catalog by default; other distributors only with keys; telemetry **off** by default (opt-in OpenTelemetry) | JLCPCB, DigiKey; README advertises paid "Nimrod" service | PartReel downloads (host allowlist) | datasheets, distributors | | Hygiene | CodeQL, OpenSSF Scorecard, gitleaks, SECURITY.md; one maintainer (1,029 commits), repo created 2026-05-31 | active; new development moved to AGPL Konnect | no LICENSE file | plugin.json declares no hooks | | Deps of note | — | pymupdf (AGPL) at runtime | fastmcp | — | **Recommendation:** use **kicad-happy** for review and **kicad-mcp-pro** for edits and exports. Both are MIT. kicad-mcp-pro targets KiCad 10 with no SWIG, is read-only by default, ships cited fab profiles, and is the most active. Pin its version (`kicad-mcp-pro==4.4.0`) and keep the default profile until edits are needed. Use mixelpixx only if KiCad 9 is required. Treat Seeed as all-rights-reserved until it commits a LICENSE file. Konnect and obhox/kicad-mcp are AGPL: run them only as external programs. ## 4. Mechanical + PCB co-design (ECAD to MCAD) - **KiCad itself:** `kicad-cli pcb export step` takes `--board-only`, `--no-components`, `--component-filter` and `--no-dnp`. There are also `brep`, `glb`, `stl`, `xao`, `dxf` (use Edge.Cuts for the outline) and `ipc2581`. KiCad 10's CLI has **no IDF export**. The KiCad 9/10 IPC API needs the GUI and cannot plot or export; KiCad 11 adds headless operation ([KiCad dev docs](https://dev-docs.kicad.org/en/apis-and-binding/ipc-api/for-addon-developers/), [CLI docs](https://docs.kicad.org/10.0/en/cli/cli.html)). kicad-python on PyPI is MIT, version 0.8.0, KiCad 9+. - **text-to-cad:** PR [#518](https://github.com/earthtojake/text-to-cad/pull/518) (open) adds `@pcb` boards-as-code with DRC on every build. #616 (`pcb-dfm` skill) merged into a feature branch stacked on #518. PR #345 (closed, not merged) was a KiCad 10 fixture in which a USB-C connector is blocked by an enclosure wall and then fixed by a source-level cutout. That is exactly our pattern. - **punkfab/circuit-skills:** a `pcb-enclosure-fit` skill co-registers the board STEP with a build123d shell and gates placement on connectors, cutouts and holes. It has no licence, so ideas only. - **Backplane:** KiCad and FreeCAD viewers that show a case assembly beside the agent. MIT; install is `curl | sh`. - **Amagine3D:** designs the enclosure around internal components from requirements. It does not read KiCad files. - **kicadStepUp** (AGPL per its README): FreeCAD workbench that syncs board outlines and models both ways. External only. - **Gap:** no project closes the loop from KiCad outline, connectors and holes, to enclosure, to clearance report, and back-annotation onto the board. ## 5. Knowledge sources | Domain | Source | Licence / terms | Already embedded in | |---|---|---|---| | DfAM | ISO/ASTM 52910 / 52911-1 | paid standard: cite, don't copy | text-to-cad `process-limits.md` (MIT summary citing Hubs, Formlabs, HP MJF, EOS) | | Sheet / laser | [SendCutSend specs JSON](https://cdn.sendcutsend.com/specs/sendcutsend-specs.json) + catalog: 178 materials, k-factor, bend radius, bend deduction, minimum hole and bridge (inches) | vendor data, no licence stated: fetch live and cite | text-to-cad `sendcutsend` | | Sheet / CNC / molding | [Hubs](https://www.hubs.com/guides/sheet-metal-fabrication/) and [Protolabs](https://www.protolabs.com/services/sheet-metal-fabrication/design-guidelines/) guides | copyrighted pages: cite | text-to-cad `dfm` references | | PCB fab | JLCPCB, PCBWay, Eurocircuits, OSH Park, NextPCB capability pages | vendor terms | kicad-mcp-pro `dfm_profiles/*.json` (MIT, with source URLs and pricing coefficients); kicad-happy jlcpcb/pcbway skills | | PCB design | IPC-2221/2222, IPC-7351 | paid: cite only | text-to-cad PR #518 skill cites IPC-2221 widths | | Materials | FreeCAD `.FCMat` cards | CC-BY-3.0 (some older cards LGPL) | FreeCAD | | Polymers | OpenPoly | CC BY 4.0 (research data) | none | | Fasteners | bd_warehouse | Apache-2.0 | none | | Cost | kicad-mcp-pro `dfm_calculate_manufacturing_cost` (area, layers, vias); slicer time and grams from OrcaSlicer G-code | MIT; OrcaSlicer AGPL (external) | kicad-mcp-pro | No open, cited cost model for FDM, sheet metal or CNC turned up in any agent tool (gap). MatWeb has no bulk export; MakeItFrom's terms are (u). ## 6. Benchmarks and evals | Benchmark | What it measures | Licence / access | |---|---|---| | [CADGenBench](https://github.com/huggingface/cadgenbench) (Hugging Face, 2026) | Drawing → STEP generation, plus STEP editing; validity gate, shape, interface and topology scores; 81 fixtures (per pzfreo submission); ground truth private | Apache-2.0 code; [HF leaderboard](https://huggingface.co/spaces/HuggingAI4Engineering/CADGenBench). Best fit for measuring our agent | | [Text2CAD](https://github.com/SadilKhan/Text2CAD) (NeurIPS'24) | Text → sequential CAD | CC BY-NC-SA 4.0 | | [CADPrompt / CADCodeVerify](https://arxiv.org/abs/2410.05340) (ICLR'25) | 200 prompts → CadQuery; point-cloud distance, compile rate | repo has no licence | | [CAD-Recode](https://github.com/filaPro/cad-recode) (ICCV'25) | point cloud → CadQuery | CC BY-NC 4.0 | | [cadrille](https://github.com/col14m/cadrille) | multimodal reconstruction | Apache-2.0 | | [Text2CAD-Bench](https://arxiv.org/abs/2605.18430) | 600 examples, 4 levels, novice and expert prompts | paper CC BY 4.0; data licence (u) | | P3D-Bench (arXiv 2606.11152), CADBench (2605.10873), BenchCAD (2605.10865), RealCADBench (2609.03773), [CAD Arena](https://cadarena.dev/) | various | (u) | | [PCBWorld](https://github.com/LGAI-Research/PCBWorld) (arXiv 2607.05915) | KiCad-grounded routing for agents and RL; 679 real boards; 8 engine-checked metrics | BSD-3-Clause | | PCB-Bench (ICLR'26), OmniRouting (2608.04434) | LLM placement and routing | (u) | No benchmark covers **enclosure fit around a real board**, DFM correctness or fastener/cable checks. Our J501 session could seed one: the vendor STEP is fetched by URL and hash, never committed. ## 7. Recommendation **Adopt now** 1. text-to-cad plugin pinned to a release, with `CADGEN_TELEMETRY=0`, `DO_NOT_TRACK=1` and `CADGEN_UPDATE_CHECK=0` in the environment. It provides the CAD runtime, drawings, DfAM, slicing, Bambu and SendCutSend. 2. kicad-happy (review) plus kicad-mcp-pro pinned (edits and exports), and `kicad-cli pcb export step` for the board model. 3. build123d-mcp or agentcad as an optional inspection loop; both are Apache-2.0 and have CADGenBench evidence. 4. CADGenBench as the regression eval for generic CAD skill. **Thin add-on repo (cad-agent, MIT)** - `enclosure-around-board` skill: board STEP/KiCad → keep-outs, connector windows, standoffs. - Scalable vendor-assembly clearance report: bounding-box/BVH broad phase, then `closest_points`. - Fastener stack length (bd_warehouse) and cable route with bend-radius check. - Evidence site and PDF check sheet. - `knowledge/` with cited rules, materials and cost. Upstream does not cover any of these. Their contract favours caller-written checks, and their roadmap is PCB, FEA and CAM. **Contribute upstream to text-to-cad** - DfAM hole, min-feature and bridge measurement (documented as unmeasured). - Help with sheet-metal unfold (#614). - Large-PCB-STEP viewer memory fix (#626); our J501 carrier is a reproducer. - A PCB-to-enclosure fit fixture and recipe once #518 lands (successor to #345). - An `inspection-and-validation.md` recipe for many-body clearance with a broad phase. **Licence constraints** - Incorporate code only from MIT, BSD or Apache-2.0 projects, with attribution: text-to-cad, build123d, CadQuery, bd_warehouse, agentcad, build123d-mcp, MAC, kicad-mcp-pro, kicad-happy, KiStack, kicad-python, PCBWorld, CADGenBench. - Never copy code from: flowful-ai/cad-skill (PolyForm Noncommercial), Svetlana cad-agent (custom licence), circuit-skills and Seeed (no LICENSE file), or Text2CAD and CAD-Recode data (NC). - External programs only: CADAM (GPL-3.0); Konnect and obhox/kicad-mcp (AGPL-3.0); kicadStepUp, OrcaSlicer and pymupdf (AGPL); FreeCAD and Cables (LGPL); WireViz and freerouting (GPL); KiCad (GPL). - Vendor data (SendCutSend JSON, fab capability pages) is fetched and cited at run time, not redistributed. --- Source: https://cad-agent.valpatel.com/research/simulation-tools.md # FOSS simulation and optimisation survey for cad-agent (J501 / AGX Orin enclosure) Survey date: 2026-10-26. Licences were checked against each repo's LICENSE/COPYING file (GitHub API or GitLab API) unless marked otherwise. "Last commit" means the newest commit on the default branch at survey time. Docker tags come from the Docker Hub API on the same day. Licence rule used throughout: - **Importing** a GPL/AGPL Python package from our MIT package does not change our source licence. But anyone who distributes the combined program must do so under the GPL, and the FSF treats an import as a combination. So GPL packages stay optional extras or run behind a process boundary. - **Calling** a GPL/AGPL program as a separate process (subprocess or Docker), and exchanging files with it, is ordinary "aggregate" use. It puts no obligation on cad-agent's licence. - AGPL's network clause applies only if we modify the AGPL program and offer it to users over a network. > Existing exposure to flag: cad-agent already does `import gmsh`. Gmsh is GPL-2.0-or-later. Its exception covers combination with Netgen, METIS, OpenCASCADE and ParaView, but not with arbitrary MIT code ([gmsh.info/LICENSE.txt](https://gmsh.info/LICENSE.txt), [PyPI gmsh 4.15.2, "GPLv2+"](https://pypi.org/project/gmsh/)). This is the same situation as foamlib below. Document gmsh as an optional dependency, or call it via its CLI. --- ## 1. 3D topology optimisation from Python | Candidate | Licence (verified) | Maintenance | Install | Fit | |---|---|---|---|---| | **pyMOTO** | MIT ([repo](https://github.com/aatmdelissen/pyMOTO)) | v2.0.1, 2026-05-07; last commit 2026-06-02 ([releases](https://github.com/aatmdelissen/pyMOTO/releases)) | `pip install pymoto[fast]` or `conda install -c aatmdelissen pymoto` ([README](https://github.com/aatmdelissen/pyMOTO#quick-start-installation)) | Best fit. Voxel `VoxelDomain` in 2D and 3D; density filter; robust (eroded/dilated) formulation for minimum length scale; **`OverhangFilter`** (Langelaar AM filter, [example](https://github.com/aatmdelissen/pyMOTO/blob/master/examples/topology_optimization/ex_compliance_overhang.py)); OC, MMA and GCMMA; multigrid-preconditioned CG for 3D; stress constraints; thermal and thermo-mechanical modules (`ex_showcase_thermal.py`, `ex_thermo_mech.py`); `WriteToVTI` to dump the density field every iteration ([io.py](https://github.com/aatmdelissen/pyMOTO/blob/master/pymoto/modules/io.py)). Modules carry their own sensitivities, so non-design regions are just a mask or mapping on the design vector. | | PyTopo3D | MIT ([repo](https://github.com/jihoonkim888/PyTopo3D)) | v0.3.0, 2026-06-16 ([PyPI](https://pypi.org/project/pytopo3d/)) | `pip install pytopo3d` (`[gpu]` extra uses CuPy) | Simple 3D SIMP on voxels, with "obstacle" (non-design) regions from JSON and optional GPU. The README mentions no overhang or robust filter. A good, small fallback or reference. | | dl4to | Apache-2.0 ([repo](https://github.com/dl4to/dl4to)) | Last commit 2024-11-19; not on PyPI | `pip install git+https://github.com/dl4to/dl4to` | PyTorch voxel SIMP plus deep-learning research tooling. Stale. | | FEniTop | **GPL-3.0** ([repo](https://github.com/missionlab/fenitop)) | Last commit 2024-11-01; pinned to **dolfinx 0.7.3** | Manual on a FEniCSx env | Works on unstructured tets with a PDE filter, OC/MMA and MPI. Stale and pinned to an old dolfinx. GPL, so it can only run as a separate process. | | dolfinx (roll our own) | LGPL-3.0 ([repo](https://github.com/FEniCS/dolfinx)) | v0.11.0, 2026-06; active | Docker `dolfinx/dolfinx:v0.11.0` ([hub](https://hub.docker.com/r/dolfinx/dolfinx)) or conda-forge | The right base if we later need tets and MPI scale. LGPL is fine to import. | | jax-fem | **GPL-3.0** per the LICENSE file ([repo](https://github.com/deepmodeling/jax-fem)). The PyPI classifier wrongly says BSD ([PyPI](https://pypi.org/project/jax-fem/)). | v0.0.12, 2026-06; active | pip | Differentiable FE on GPU (JAX), with topopt examples. GPL, so it can only run as a separate process. | | TopOpt_in_PETSc | **Unclear**. GitHub detects no licence; `main.cc` says "The authors reserves all rights" but the repo also ships `lesser.txt` (LGPL text) ([repo](https://github.com/topopt/TopOpt_in_PETSc)) | Last commit 2026-10-17 | Build against PETSc + MPI | Scales to very large structured grids in C++. Avoid until the licence is clarified. | | beso (CalculiX) | LGPL-3.0 ([repo](https://github.com/calculix/beso)) | Last commit 2025-04-03 | Python scripts + CalculiX ≥2.17 + FreeCAD | BESO on tets via CalculiX `.inp` files. Works, but it's file-driven and needs FreeCAD. | | scikit-fem topopt example | BSD-3 ([repo](https://github.com/kinnala/scikit-fem)) | Active (12.0.2, 2026-06) | Already installed | **None found**: docs/examples ex01–ex56 contain no topology-optimisation example. We would have to write SIMP + MMA ourselves on top of our existing P2-tet FEA. | | OpenFOAM adjoint (ESI) | GPL-3 | v2606 | Docker (see §3) | `adjointOptimisationFoam` has `topologyOptimisation` tutorials for **fluid** topology ([tree](https://gitlab.com/openfoam/core/openfoam/-/tree/master/tutorials/incompressible/adjointOptimisationFoam)). This could later shape vents and ducts; it's not for structure. | | SU2 | LGPL-2.1 | v8.5.0 | — | Has a `fea_topology` test case ([TestCases](https://github.com/su2code/SU2/tree/master/TestCases)). Niche. | **Recommendation:** use **pyMOTO** (MIT, importable) as the structural and thermal topology engine on a voxel domain built from our CadQuery solids: - **Non-design regions:** fix them to solid (bosses, insert pockets, port frames, the cooler seat) or to void (cooler, board, connector keep-outs, airflow paths). - **Printability:** add robust/density filtering for minimum feature size and `OverhangFilter` with the build direction = enclosure Z. - **Size:** the enclosure is 206×181×112 mm. That is about 4.2 M voxels at 1 mm and about 0.52 M at 2 mm, so optimise per part or per panel (floor, roof, ribs) at 1–2 mm with pyMOTO's multigrid CG. - **Back to CAD:** convert the result with marching cubes (scikit-image or VTK). Then verify it with our existing scikit-fem P2-tet FEA and a real slice (§5). - **Fallback:** keep PyTopo3D in reserve. If tets are ever needed, build on dolfinx (LGPL) rather than FEniTop. ## 2. Parametric shape/size optimisation drivers (expensive, multi-objective) | Candidate | Licence | Latest | Notes | |---|---|---|---| | **Optuna** | MIT ([repo](https://github.com/optuna/optuna)) | v5.0.0, 2026-09-07 ([release notes](https://github.com/optuna/optuna/releases/tag/v5.0.0)) | v5 made `GPSampler` stable, with **qLogEHVI / qLogCEHVI**: batch multi-objective Bayesian optimisation with constraints. TPE is now the default multi-objective sampler. A new core `trial.set_constraint()` API was added. Storage is RDB or Journal, so runs are resumable and parallel, and it includes Pareto plots. | | pymoo | Apache-2.0 ([repo](https://github.com/anyoptimization/pymoo)) | 0.6.2, 2026-06-28 | NSGA-II/III, MOEA/D, constraint handling, decision making. Better suited to cheap surrogates than to expensive CFD. | | Ax / BoTorch | MIT ([Ax](https://github.com/facebook/Ax), [BoTorch](https://github.com/meta-pytorch/botorch)) | Ax 1.3.1 (2026-06-09), BoTorch 0.18.1 (2026-06-08) | The most capable Bayesian optimisation (qNEHVI, multi-fidelity), but heavier (PyTorch). | | OpenMDAO | Apache-2.0 ([LICENSE.txt](https://github.com/OpenMDAO/OpenMDAO/blob/master/LICENSE.txt)) | 3.45.1, 2026-09-10 | Gradient-based MDO framework. Worth it only once we have adjoint gradients, e.g. pyMOTO or DAFoam. | | scipy.optimize | BSD-3 ([repo](https://github.com/scipy/scipy)) | — | Single-objective local optimisers (SLSQP, COBYQA, differential_evolution). Fine for 1–3 parameter refinements. | **Recommendation:** use **Optuna 5 + GPSampler** as the outer driver over CadQuery parameters (wall thickness, rib count and height, vent area and position, infill). Run it with three objectives and a temperature constraint: - **Objectives:** grams from the real slicer, compliance or max displacement from FEA, and max TTP temperature from the thermal model. - **Constraint:** `trial.set_constraint` with T_wall < HDT − margin. - **Multi-fidelity:** do the bulk of the search on the fast thermal model (§3B), then re-score Pareto candidates with full CHT. Add pymoo only to run NSGA-II on a fitted surrogate. ## 3. CFD + conjugate heat transfer ### OpenFOAM: ESI/OpenCFD vs Foundation | | ESI / OpenCFD (openfoam.com) | Foundation (openfoam.org) | |---|---|---| | Licence | GPL-3 ([v2606 news](https://www.openfoam.com/news/main-news/openfoam-v2606)) | GPL-3 ([COPYING](https://github.com/OpenFOAM/OpenFOAM-dev/blob/master/COPYING)) | | Current | **v2606** (June 2026); source moved to [gitlab.com/openfoam/core/openfoam](https://gitlab.com/openfoam/core/openfoam) | **v14**, 14 Jul 2026 ([download](https://openfoam.org/download/), [OpenFOAM-14](https://github.com/OpenFOAM/OpenFOAM-14)) | | Steady CHT solver | `chtMultiRegionSimpleFoam` (tutorial dir `tutorials/heatTransfer/chtMultiRegionSimpleFoam`) | `foamMultiRun` with `fluid` + `solid` modules (e.g. [circuitBoardCooling Allrun](https://github.com/OpenFOAM/OpenFOAM-14/blob/master/tutorials/multiRegion/CHT/circuitBoardCooling/Allrun)); single-region buoyant flow is `foamRun -solver fluid` | | Closest tutorial | **`cpuCabinet`** ([tree](https://gitlab.com/openfoam/core/openfoam/-/tree/master/tutorials/heatTransfer/chtMultiRegionSimpleFoam/cpuCabinet)): snappyHexMesh multi-region. Regions are `domain0` (air), `v_CPU` (solid with 100 W `scalarSemiImplicitSource` on `h`) and `v_fins` (heatsink). The fan is an **MRF zone** (`MRFProperties`, ω = 209.44 rad/s). This is almost exactly our problem. | `circuitBoardCooling` (baffles), `heatExchanger` (porous region + inter-region heat transfer), `heatedDuct`, `hotWire` (snappy) ([tree](https://github.com/OpenFOAM/OpenFOAM-14/tree/master/tutorials/multiRegion/CHT)) | | Fan models | `fan` cyclic-baffle jump BC with modes velocity / uniformVelocity / **volumeFlowRate** / nonDimensional ([fanFvPatchField.H](https://gitlab.com/openfoam/core/openfoam/-/blob/master/src/finiteVolume/fields/fvPatchFields/derived/fan/fanFvPatchField.H)); `fanPressure`; `swirlFanVelocity`; **`fanMomentumSource`** fvOption: Δp(Q) fan curve applied in a cellZone ([header](https://gitlab.com/openfoam/core/openfoam/-/blob/master/src/fvOptions/sources/derived/fanMomentumSource/fanMomentumSource.H)); MRF; `actuationDiskSource`; `explicitPorositySource` | `fanPressureJump` and `fanPressure` BCs ([src](https://github.com/OpenFOAM/OpenFOAM-14/tree/master/src/finiteVolume/fields/fvPatchFields/derived)); fvModels `actuationDisk`, `radialActuationDisk`, `porosityForce`, `interRegionHeatTransfer`, `heatSource` ([src/fvModels](https://github.com/OpenFOAM/OpenFOAM-14/tree/master/src/fvModels)); MRF ([mixerVessel2DMRF](https://github.com/OpenFOAM/OpenFOAM-14/tree/master/tutorials/fluid/mixerVessel2DMRF)) | | Docker | `opencfd/openfoam-run:2606` (runtime only, 2026-06-23). `opencfd/openfoam-default:2512` / `latest` has tutorials and source; **no 2606 tag yet** ([run](https://hub.docker.com/r/opencfd/openfoam-run/tags), [default](https://hub.docker.com/r/opencfd/openfoam-default/tags)). Starter script: [openfoam-docker](https://dl.openfoam.com/source/latest/openfoam-docker). | **No official images after v11**: the newest on Docker Hub is `openfoam/openfoam11-paraview510`, last updated 2024-06 ([hub](https://hub.docker.com/u/openfoam)). v14 is shipped as Ubuntu apt packages (needs sudo) or source. | ### Other solvers | Candidate | Licence (verified) | Status | CHT / fan fit | |---|---|---|---| | Elmer FEM | GPL-2.0+; ElmerSolver library and some modules LGPL-2.1, most solver modules GPL ([LICENSE.md](https://github.com/ElmerCSC/elmerfem/blob/devel/LICENSE.md), [licence policy](https://github.com/ElmerCSC/elmerfem/tree/devel/license_texts)) | release-26.2 (2026-04-28); active. Docker build repo [ElmerCSC/elmerfem-docker](https://github.com/ElmerCSC/elmerfem-docker) | Strong for conduction, thermal and multiphysics on gmsh tets. Fan-driven turbulent air needs more effort than OpenFOAM. A good **solid-side** thermal and thermo-mechanical solver. | | SU2 | LGPL-2.1 ([LICENSE.md](https://github.com/su2code/SU2/blob/master/LICENSE.md)) | v8.5.0 (2026-04-27) | Has `coupled_cht` and `solid_heat_conduction` test cases ([TestCases](https://github.com/su2code/SU2/tree/master/TestCases)). Aero-oriented, with no snappy-style STL meshing. Not recommended here. | | Code_Saturne | GPL-2.0 ([repo](https://github.com/code-saturne/code_saturne)) | v9.2.0; docker `simvia/code_saturne:9.2.0` (2026-07-13) ([hub](https://hub.docker.com/r/simvia/code_saturne)) | Industrial-grade finite-volume solver with CHT. Smaller ecosystem for STL → mesh. Second choice. | | XLB (Autodesk) | Apache-2.0 ([LICENSE](https://github.com/Autodesk/XLB/blob/main/LICENSE)) | v0.3.1 (2026-01-12); `pip install "xlb[warp]"` | GPU LBM (Warp/JAX), differentiable. The README mentions **no thermal/CHT**, so it's isothermal airflow only. | | lettuce | MIT ([repo](https://github.com/lettucecfd/lettuce)) | 0.3.0 (2026-10-06); `pip install lettucecfd` (the PyPI name `lettuce` is an unrelated package) | PyTorch GPU LBM; no thermal in the README. Research grade. | | OpenLB | GPL-2.0 ([LICENSE](https://gitlab.com/openlb/release/-/blob/master/LICENSE)) | 1.9r0 (2025-12-18) ([download](https://www.openlb.net/download/)) | C++ GPU LBM with thermal examples (`examples/thermal`: Rayleigh–Bénard, porous plate) and a "Conjugate Heat Transfer" showcase listed on the site (not inspected). Works only as a separate process. | | Palabos | **AGPL-3.0** ([COPYING](https://gitlab.com/unigespc/palabos/-/blob/master/COPYING)) | Active (2026-07) | Works only as a separate process. No advantage over OpenFOAM for our case. | | FluidX3D | Non-commercial licence | — | **Rejected** per the brief. | | DAFoam | GPL-3 ([LICENSE.md](https://github.com/mdolab/dafoam/blob/main/LICENSE.md)) | v5.1.1; docker `dafoam/opt-packages:v5.1.1` | Discrete adjoint of OpenFOAM + OpenMDAO. A future option for gradient-based vent shape optimisation. | | CfdOF (FreeCAD) | LGPL-3 ([repo](https://github.com/jaheyns/CfdOF)) | Active | A GUI case generator for snappy and cfMesh, porous regions, buoyant flow. The README shows no multi-region CHT. A useful reference for writing snappy dictionaries. | ### Python case-generation helpers - [foamlib](https://github.com/gerlero/foamlib): **GPL-3.0**, v1.9.0 (2026-10-19). A modern parser/writer with an async runner. - [PyFoam](https://pypi.org/project/PyFoam/): **GPL**, 2026.6. - [fluidfoam](https://pypi.org/project/fluidfoam/): GPL-3.0-or-later. - `ofpp`: MIT, but stale (2023). None of the good options is permissive. **Recommendation:** - Generate the dictionaries ourselves from MIT Jinja/string templates. FoamFile syntax is simple, and the `cpuCabinet` case is a template. - Read results with PyVista's `OpenFOAMReader` (MIT). - Or confine foamlib to an optional GPL extra that runs as a separate process. ### Recommended primary path: steady CHT, a few M cells, 24 cores **ESI OpenFOAM v2606 in Docker (`opencfd/openfoam-run:2606`; use `openfoam-default:2512` for tutorials), `chtMultiRegionSimpleFoam`, starting from `cpuCabinet`.** Rationale: - It's a near-identical tutorial. - It has the richest fan options: `fan` BC in `volumeFlowRate` mode, or `fanMomentumSource`, both driven by the datasheet P–Q curve; MRF if the blades are modelled. - Official Docker images exist and don't need sudo. The Foundation route would need apt (sudo) or a source build. Pipeline: 1. CadQuery exports one STL per region: fluid = enclosure interior minus solids; solids = TTP/module block with a volumetric heat source, heatsink fins + base, and heatpipes as high-k solids; optionally the PLA walls as a solid region so wall temperature is resolved. 2. `blockMesh` → `surfaceFeatureExtract` → `snappyHexMesh` → `splitMeshRegions -cellZones` → `decomposePar -allRegions` → `mpirun -np 24 chtMultiRegionSimpleFoam -parallel`. This is the cpuCabinet `Allrun` / `Allrun.pre` sequence. 3. Model heatpipes as anisotropic or effective high-conductivity solids. This is my engineering assumption and needs calibration; no source verified. 4. Run time for 2–5 M cells on 24 cores is unmeasured; plan on hours, not minutes. ### Fast low-fidelity path for optimisation loops - **(a) Thermal resistance network + fan operating point (milliseconds).** Use the fan P–Q line from the datasheet (§6). Use system impedance from the heatsink and vent pressure-loss correlations in [`fluids`](https://github.com/CalebBell/fluids) (MIT) and fin convection from [`ht`](https://github.com/CalebBell/ht) (MIT). This gives T_TTP and outlet air temperature per design. - **(b) scikit-fem steady conduction** in the printed walls (already in cad-agent), with Robin BCs (h, T_air) taken from (a) or from one calibration CHT run. This checks wall temperature against HDT. - **(c) Coarse single-region OpenFOAM (~0.3–0.5 M cells, minutes; my estimate).** Model the heatsink as a porous zone (`explicitPorositySource`, Darcy–Forchheimer fitted to the fan/heatsink Δp) with a heat source, and the fan as `fanMomentumSource`. Use it to rank vent layouts. - Calibrate (a)–(c) against 2–3 full CHT runs. - XLB on the RTX 3080 is an option for isothermal vent-flow screening only. ## 4. Post-processing, visualisation, animation - **PyVista**: MIT, v0.49.1 (2026-10-11) ([repo](https://github.com/pyvista/pyvista)). `Plotter` has `open_movie` / `open_gif` / `write_frame`, `orbit_on_path`, `export_html`, **`export_vtksz`** (vtk.js scene), **`export_gltf`** (three.js / model-viewer), `export_vrml`, `export_obj` and `save_graphic`. `DataSet.streamlines` / `streamlines_from_source` produce streamlines ([plotter.py](https://github.com/pyvista/pyvista/blob/main/pyvista/plotting/plotter.py)). It reads OpenFOAM cases directly. MP4/WebM encoding uses [imageio-ffmpeg](https://pypi.org/project/imageio-ffmpeg/) (BSD-2). Headless rendering uses EGL/OSMesa VTK; we have an RTX 3080 and the Docker nvidia runtime. - **ParaView / pvpython**: BSD-3 ([conda-forge paraview 6.2.0](https://anaconda.org/conda-forge/paraview)). Since 6.0 there is **one Linux binary** that picks GLX, EGL or OSMesa at runtime (`--opengl-window-backend`, `VTK_DEFAULT_OPENGL_WINDOW`). There is no separate headless package ([6.0 release notes](https://www.kitware.com/paraview-6-0-0-release-notes/)). Get it as the tarball `ParaView-6.2.0-MPI-Linux-Python3.12-x86_64.tar.gz` ([files/v6.2](https://www.paraview.org/files/v6.2/)), which needs no sudo, or via conda-forge. `kitware/paraview` on Docker Hub has only CI tags, so there is **no official runtime image** ([hub](https://hub.docker.com/r/kitware/paraview/tags)). - **trame**: Apache-2.0, v4.0.0 (2026-09-09) ([repo](https://github.com/Kitware/trame)). Base images are `kitware/trame` (e.g. `uv-cuda`, `conda-glvnd`) ([hub](https://hub.docker.com/r/kitware/trame/tags)). Use it for live interactive dashboards. For static, shareable web reports, PyVista's `export_vtksz` with [vtk.js](https://github.com/Kitware/vtk-js) (BSD-3) or `export_gltf` with three.js is lighter. **Recommendation:** - **PyVista** for all scripted images and animations: temperature slices, streamline tubes coloured by T or |U|, density-evolution flipbooks from pyMOTO `WriteToVTI` frames, written to MP4 (H.264) + WebM + GIF. - **glTF/vtksz** for embedded 3D viewers in an HTML report. Keep the report standard: per run, a `report.html` + `summary.json` + PNG/SVG figures + MP4. - **ParaView** tarball only for heavy, ad-hoc work. ## 5. Slicer CLIs for real grams and print time All three slicers are **AGPL-3.0**, so we call them as subprocesses only ([BambuStudio](https://github.com/bambulab/BambuStudio), [OrcaSlicer](https://github.com/OrcaSlicer/OrcaSlicer), [PrusaSlicer](https://github.com/prusa3d/PrusaSlicer)). Latest: Bambu Studio GitHub release v02.08.02.61; the installed snap is **02.08.04.57 (beta channel)**. OrcaSlicer v2.4.2. PrusaSlicer 2.9.6. ### Bambu Studio (tested here, headless, works) The working invocation: ``` bambu-studio --slice 0 --arrange 1 --orient 0 \ --load-settings "machine.json;process.json" \ --load-filaments "filament.json" \ --outputdir OUT --export-3mf out.3mf model.stl # or a .3mf project ``` - Machine: `Bambu Lab P1S 0.4 nozzle`. Process: `0.20mm Standard @BBL X1C`, which lists P1S 0.4 in `compatible_printers`. Filament: `Bambu PLA Basic @BBL P1S 0.4 nozzle`. All are from `/snap/bambu-studio/current/opt/bambu-studio/resources/profiles/BBL/`. - **Gotcha 1 (verified):** the system JSONs use `inherits`. Passed raw, they leave `filament_density` = 0, so the slicer reports **0.00 g**. You must flatten the inheritance chain (merge parent JSONs by `name`) before passing the files. After flattening, a 20 mm cube gave **3.75 g, 542 s** (PLA, 2 walls, 20 % infill). - **Read grams from:** - `OUT/result.json` → `sliced_plates[i].filaments[j].total_used_g` and `total_predication` (s) - `out.3mf` → `Metadata/slice_info.config` → `` and `` - `plate_1.gcode` header → `; total filament weight [g] : 3.75` and `; model printing time:` - **Gotcha 2:** the gcode header's `total filament volume [cm^3]` actually printed mm³ (2974 for 2.97 cm³). - **Gotcha 3:** a "Wayland: Failed to connect to display" error is logged and is harmless. `return_code` was 0. - **Gotcha 4:** snap confinement. I ran in `~/snap/bambu-studio/common/`. Files in host `/tmp` are probably not visible to the snap (not tested). - Other useful flags (from `--help`): `--export-slicedata`, `--mstpp`, `--no-check`, `--info`, `--export-png`, `--debug 0-5`, `--pipe`. ### OrcaSlicer It's the same CLI lineage: `orca-slicer --slice 0 --load-settings "machine.json;process.json" --load-filaments f.json --outputdir OUT --export-3mf out.3mf model.stl`. Flags use hyphens, `--slice 0` means all plates, and the machine file comes before the process file ([Orca wiki CLI](https://www.orcaslicer.com/wiki/cli/cli_mode), [actions](https://www.orcaslicer.com/wiki/cli/cli_actions); third-party reference [printago](https://printago.io/blog/orca-slicer-cli-reference)). Install via the AppImage or flatpak (no sudo). Not tested here. ### PrusaSlicer From the source option table ([PrintConfig.cpp](https://github.com/prusa3d/PrusaSlicer/blob/master/src/slic3r-shared/src/Slic3r/Biz/Config/Legacy/PrintConfig.cpp)): - Options: `--export-gcode|-g`, `--load ` (repeatable), `--output|-o`, `--info`, `--export-3mf`, `--datadir`, `--printer-profile`, `--print-profile`, `--material-profile`, `--query-printer-models`, `--query-print-filament-profiles`. - Example: `prusa-slicer -g --load p.ini --load f.ini --load m.ini -o out.gcode model.stl`. Read `; filament used [g] = …` from the gcode footer (from memory, not re-verified). - No P1S profile was verified. **Recommendation:** use the installed Bambu Studio snap as the ground truth for filament grams and print time, with flattened P1S profiles cached in the repo. Use OrcaSlicer as a cross-check. ## 6. Thermal reference data **Jetson AGX Orin module.** Sources: [datasheet DS-10662-001 v1.8, Sep 2025, mirror](https://static.generation-robots.com/media/Jetson-AGX-Orin-Data-Sheet.pdf) and [Jetson Linux r36.4 power/thermal page](https://docs.nvidia.com/jetson/archives/r36.4/DeveloperGuide/SD/PlatformPowerAndPerformance/JetsonOrinNanoSeriesJetsonOrinNxSeriesAndJetsonAgxOrinSeries.html). - **Module power (datasheet):** - JAO 64GB: 15 W / 30 W / 50 W, "up to 60 W" - JAO 32GB: 15 W / 30 W / 40 W, "up to 40 W" - Industrial: 15 / 35 / 60, up to 75 W - **nvpmodel modes (r36.4):** - 64GB: MAXN, 15 W, 30 W (default), 50 W - 32GB: MAXN, 15 W, 30 W (default), 40 W - **TTP (datasheet):** operating range −25 to **80 °C max** for JAO; −40 to 85 °C for JAOi. Module is 100 × 87 × 16 mm. - **SoC thresholds (r36.4):** SW throttle 99 °C, HW throttle 103 °C, SW shutdown 104.5 °C, HW shutdown 105 °C. The devkit fan "cool" profile tops out at 2900 RPM. - **Thermal Design Guide (TDG-10943-001):** v1.1 is at [Mouser](https://www.mouser.com/pdfDocs/Jetson_AGX_Orin_Series_TDG-10943-001_v11.pdf), but it is **bot-protected and I could not read Table 2-1 or its figures**. Forum posts reference v1.2 and v1.3, and confirm that θ_jp is given for 32GB/64GB but "TBD" for Industrial ([forum](https://forums.developer.nvidia.com/t/agx-orin-industrial-missing-thermal-resistance-values-in-tdg/278394)). **Unverified:** a third-party claim of a 60 W "MAXN SUPER" mode for the 32GB module. **Seeed J501 cooler.** The reference cooler in the repo README is [Seeed p-5970](https://www.seeedstudio.com/Aluminum-heatsink-with-Fan-for-Jetson-AGX-Orin-p-5970.html). The page publishes no numeric specs and it 403s to scripts. Seeed hosts a vendor datasheet under the J501 wiki ([WS-RDN-601, 100×87×34 mm](https://files.seeedstudio.com/wiki/reComputer-Jetson/J501/323011387Jetson_AGX_Orin_Heatsink_100x87x34mm-datasheet.pdf)); the repo has a saved copy: - **Fan:** 8015 PWM fan (80×80×15 mm), ball bearing, 5 V, 0.16 A, 2300 RPM ±10 %, 27.4 dB(A). - **Airflow:** **22.90 CFM** at zero static pressure. **Max static pressure 1.55 mmH₂O (≈15.2 Pa)**; the datasheet swaps the two row labels. - **PWM curve:** 25/50/75/100 % → 700/1380/1920/2300 RPM. - **Heat sink:** "copper pipe + aluminium radiator", rated **100–120 W**. - **Heatpipe count is not stated.** - **Height caveat:** the repo models the cooler top 50 mm above the seat (Z40.758 → Z90.758), but the datasheet says 34 mm. Possibly 34 mm of heatsink plus the 15 mm fan on top; measure it. - **Other coolers:** the different Mini J501 heatsink is listed at 110×100×50 mm and 14.1–55.26 CFM ([openelab](https://openelab.io/products/seeed-studio-recomputer-mini-j501-heatsink)). It is not ours. **Printed-material limits (Bambu TDS; third-party-hosted copies; values read with pdftotext):** | Material | Tg (DSC) | HDT 0.45 MPa | HDT 1.8 MPa | Source | |---|---|---|---|---| | PLA Basic (TDS V2.0) | 60 °C | 57 °C | 54 °C | [TDS](https://www.additive-x.com/shop/mpattachments/file/viewonline/id/494/product_id/2236) | | PETG Basic (V2.0) | 68 °C | 69 °C | 65 °C | [TDS](https://www.additive-x.com/shop/mpattachments/file/viewonline/id/580/product_id/2330) | | PETG HF (V1.0) | 66 °C | 69 °C | 62 °C | [TDS](https://botland.com.pl/img/art/inne/25540_karta_produktu.pdf) | With the TTP allowed up to 80 °C and the fan exhausting warm air, PLA within a few cm of the heatsink, the exhaust path or the cooler clamp is marginal. Use PETG (or better) for parts that carry the cooler, and treat **~50 °C PLA / ~60 °C PETG sustained wall temperature under load** as design limits. Those limits are my margin choice, not a datasheet value. Creep under clamp load is a separate, unmodelled risk. ## Existing projects combining CAD + TO + CFD for 3D printing There is no single mature FOSS project that does CadQuery → topology optimisation → CHT CFD → slicer. The closest building blocks: - **FreeCAD** (LGPL-2.1) + **CfdOF** (LGPL-3), plus FreeCAD FEM + **beso** (LGPL-3): a GUI-centric equivalent. - **DAFoam** (GPL-3) + OpenMDAO: adjoint CFD shape optimisation. - **ESI `adjointOptimisationFoam`**: fluid topology optimisation. - **pyMOTO**: thermal and structural topology optimisation with an AM overhang filter. Reuse the ESI `cpuCabinet` case, pyMOTO examples and Optuna as templates, rather than adopting a framework. ## Could not verify - TDG Table 2-1 and figures (PDF blocked). - Heatpipe count. - The fan P–Q curve beyond its two endpoints. - PrusaSlicer's gcode-footer key in the current version. - OrcaSlicer flags on this machine (not installed). - OpenLB's CHT showcase details. - CHT run times. - Whether ESI images include ParaView. --- Source: https://cad-agent.valpatel.com/skills/cad-motion-validation/SKILL.md # Validate the rendered mechanism A CAD interference sweep does not validate a browser's scene graph. Establish independent mechanical intent, capture the actual rendered transforms, and compare them before claiming the simulation is correct. 1. Inventory all moving joints and components, including shafts/rotors, stationary housings, feed mechanisms, helper geometry and scene attachments. Record ownership, units, axis handedness, home datums and parent chains from CAD source or inspected geometry. Do not take the expected pivots from the suspect animation manifest. 2. Read [the trace protocol](https://github.com/Valpatel/cad-agent/blob/dev/skills/cad-motion-validation/references/trace-protocol.md). Build a reference with at least four noncoplanar landmarks per component. Drive the real renderer using `cad-agent motion-trace reference.json --poses-only`, then capture actual mesh vertices through its current world matrices in the common assembly frame, in mm. 3. Run `cad-agent motion-trace reference.json capture.json --output motion-report.json`. Inspect the worst-point, pivot-anchor and radial errors. Compare axes to physical shaft/bearing geometry; rest-pose agreement and rigid within-mesh distances alone cannot catch a wrong orbit. Keep fixed housings attached to their support. 4. Add captures for firing/operation, combined independent joints, stop/start, repeated cycles and scene transitions. Reproduce a wrong pivot with correct home placement and require failure. Also exercise units, axis/sign, lost-parent and ownership errors. Keep regression checks in the existing product build/test harness. 5. Pair kinematic checks with CAD motion/containment gates and visual inspection of the actual published renderer. Record input/export hashes, tolerance, sample density, untested events and independent reference provenance. A passing trace says nothing by itself about continuous clearance, loads, physical fit or hardware safety. Use the repository's normal independent review and deployment process. This skill adds no publication permission. --- Source: https://cad-agent.valpatel.com/skills/cad-pr-review/SKILL.md # Pull request review An author cannot review their own change: they read what they meant, not what they wrote. Every PR gets a review by an agent or person that did not write it, and the review lives on the PR, where anyone can see it later. ## As the author 1. **Make the PR reviewable.** Description: what and why, how it was checked (commands and their results, the real project it was tried on, renders or reports), effect on other projects, and `Closes #`. For geometry changes, run `cad-agent all` first. Every row passes, "Holes clear" included, and you have walked [docs/common-issues.md](https://cad-agent.valpatel.com/docs/common-issues.md). Push everything; the reviewer reads GitHub and the branch, not your session. 2. **Ask for the review.** In Claude Code, spawn the `cad-pr-reviewer` subagent with the PR URL and the checkout path. In Codex, start a separate session with the `cad-pr-reviewer` skill. Give it the PR, not your conclusions: a reviewer told what to find tends to find it. 3. **Answer every comment on the PR.** Fix it and reply with the commit (`Fixed in : ...`), or explain why not. Then resolve the thread: ```bash gh api repos///pulls//comments//replies -f body="Fixed in : ..." gh api graphql -f query='mutation{resolveReviewThread(input:{threadId:""}){thread{isResolved}}}' # thread ids: gh api graphql -f query='query{repository(owner:"",name:""){pullRequest(number:) # {reviewThreads(first:50){nodes{id isResolved comments(first:1){nodes{databaseId body}}}}}}}' ``` 4. **Merge only under the repo's rules**: CI green, a review exists, every thread resolved, and the user's go-ahead where the repo requires it (cad-agent: the user asks; product repos say in their AGENTS.md). Merge commits, not squash, unless the repo says otherwise. After the merge, check that the linked issues closed and tick the tracking issue (`cad-work-tracking`). ## As the reviewer - Read the diff (`git diff origin/...HEAD` in a checkout of the branch, or `gh pr diff `), the PR text and the linked issue. Do not edit, commit or push to the branch. - **Verify before you post.** Run the tests that cover the change; for a claimed bug, reproduce it with a small script. Report only what you checked. Mark severity: blocking, minor, nit. - For CAD and simulation changes, check the evidence, not only the code: - geometry: does the exported STEP/STL match the claim (bounds, volume, clearance numbers in the reports)? - physical assumptions stated and sourced: directions (fan flow, gravity, load), units, datum and orientation of any photo or drawing fit, which values are measured and which assumed; - simulations: converged and steady, energy or force balance closed, mesh dependence acknowledged; - nothing claims more than CAD can show (fit, strength and temperature are predictions until tested); - the [common-issues index](https://cad-agent.valpatel.com/docs/common-issues.md), every row: each automatic check (for example "Holes clear") present in the report, passing and current. If one is missing, run it yourself without writing into the checkout (`cad-agent holes --out /tmp//holes.json`). Each review-only row is checked by looking at the model and named in the summary. A mistake the index does not list is a finding, and it is added to the index. - Post inline comments on the exact lines (`gh api repos///pulls//comments` with `commit_id`, `path`, `line`, `side=RIGHT`), then one summary review (`gh pr review --comment --body ...`) that says plainly whether anything is blocking. Do not approve or merge unless the repo's rules give the reviewer that role. - Finish with a list of findings (file:line, severity, one sentence) for the author. --- Source: https://cad-agent.valpatel.com/skills/cad-remote-runs/SKILL.md # Long runs on a compute host Heavy runs (OpenFOAM CFD, FEA sweeps, topology optimisation, `cad-agent all`) on the workstation the agents share can exhaust its memory and crash every session on it. Run them on a compute host, detached, and write them down where another computer can find them. ## Before the run - **Pick the host by need**: cores and memory for the solver, a GPU only if the tool uses one, Docker for OpenFOAM. Check its load first (`uptime`, `free -g`); other people's jobs may be running there. - **Give it the code from git**, not a hand copy, where possible: clone the repo there and `git fetch && git checkout ` of your pushed branch. If you must copy a working tree (uncommitted changes, large untracked references), use `rsync -a` with excludes for virtualenvs, caches and run folders. Pull back anything the host produced before you copy to it again. Use `--delete` only right after such a pull (it removes remote files that are not in your copy, including results newer than it); without it, files you deleted locally stay on the host. - **Give it its own environment** (a virtualenv from the project's pinned requirements, `pip install -e` of the cad-agent checkout or version the project uses). Record which cad-agent commit it runs. ## Starting it Detach it from the ssh session and the agent, and make the log say when it is done: ```bash ssh 'cd && mkdir -p build && (setsid nohup nice -n 5 bash -c " > build/.log 2>&1; echo RUN-DONE rc=\$? >> build/.log" < /dev/null > /dev/null 2>&1 &)' ``` Then **record it in the issue** right away (see `cad-work-tracking`): `: (commit ), log , done when "RUN-DONE", results in , collect with `. ## While it runs - Watch it from the agent with a poll every few minutes (`ssh 'tail -n 3 '`) or the harness's monitor; match the failure markers too (`Traceback`, `Error`, `Killed`, `FAIL`, non-zero `rc=`), not only success. - When watching processes over ssh, anchor patterns so they do not match the ssh command line itself (`ps -eo pid,args | grep "^ *[0-9]* "`, or the `[x]` bracket trick). Stop jobs by PID or process group, never with a broad `pkill -f`. - A finding that changes the inputs (a wrong assumption, a geometry fix) means stop the run, fix, push, restart, and note the stopped run in the issue so nobody collects its stale results. ## After it - Check the result before trusting it: converged and steady, balances closed, the inputs it used are the committed ones (hashes or revision recorded in the result file). - Pull the results back (`rsync -a :/ `), commit them with the command that made them in the message, push, and tick the issue. - Leave the host clean: stop containers you started, delete large scratch case folders you no longer need. --- Source: https://cad-agent.valpatel.com/skills/cad-work-tracking/SKILL.md # Work tracking: assume the session ends in 30 minutes Plan as if this session can stop at any moment. Token budgets run out about every 30 minutes, workstations crash, and disks get reformatted. Anything that exists only in this session's context, in an uncommitted file, in a scratch or `/tmp` directory, on one machine's disk or in an agent's private memory is lost when that happens. **GitHub is the record.** A fresh agent on another computer must be able to continue from the issue, the branch and the PR alone. ## The loop 1. **Find or open the issue first.** Search before opening: `gh issue list --state all --search ""`. Open one per piece of work that will take more than a few minutes: `gh issue create --title "" --body-file `. Write the body with [the template below](#issue-template). A large job gets a tracking issue that links smaller ones. **Then check who holds it and claim it.** Several agents on different tools and machines work the same repos. Read the issue's latest [work-claim comments](#work-claims-sharing-issues-between-agents): if another agent holds an unexpired claim, pick other work or claim a separate scope. Otherwise post a `claim` before changing anything. 2. **Branch from the integration branch and push it at once.** Follow the repo's AGENTS.md / CONTRIBUTING.md: in cad-agent that is a `feat/` worktree from `origin/dev`; in a product repo, often `dev`. Push before you have much to push (`git push -u origin `), so the branch exists remotely from the start. 3. **Open a draft PR early** with `Closes #` in the body: `gh pr create --draft --base `. List in it what is still missing. CI on a draft may fail; say why in the PR. 4. **Commit and push in clean chunks.** One coherent change per commit (it builds, its message says what and why), pushed right away. Never batch an hour of work into one local commit. Generated files are committed only when they are regenerated by the committed sources (no hand-edited exports, no stale evidence). 5. **Checkpoint about every 30 minutes, and before anything long or risky.** A checkpoint is: - commit and push everything worth keeping (WIP commits on your branch are fine; say `WIP` in the subject); - tick the issue checklist and add a short comment: what changed, what is running, the exact next step; - for each run in flight, the host, command, log path and how to collect its results (see `cad-remote-runs`). Ask yourself: "if this session ended now, could someone else continue from GitHub without asking me?" If not, checkpoint. Post it as a work-claim `checkpoint` so it renews your lease (at least once a day). 6. **Close out as you go.** When a PR merges, its `Closes #N` closes the issue; check that it did, tick the tracking issue and comment with the merge commit. Close issues that turned out to be unnecessary, with the reason. Delete your own merged branches and remove your own worktrees (after checking no virtualenv imports from them); never touch another session's branch or worktree. End your claim with `done` (PR, commit, evidence), or `release` with resume notes if you stop before the end. ## Work claims: sharing issues between agents A claim is an issue comment whose first line the tools parse. No host names, user names or private infrastructure: agents are named by a stable pseudonymous tag. Always **post a new comment**; never edit an earlier one. ```markdown **Claimed** by agent `a-7kq2m4xw` until 2026-11-01T19:00Z UTC. Scope: Branch: ``` The first lines of the other actions: ```markdown ``` A checkpoint goes on with what was done and the next step, a release with resume notes, and done with the PR, commit or evidence. | Action | When | Lease | |---|---|---| | `claim` | before starting | until `lease=`, at most 24 h after the comment | | `checkpoint` | progress; at least once per lease | renews `lease=` | | `release` | stopping before the end, with resume notes | none: the claim ends | | `done` | PR merged or evidence posted | none: the claim ends | - **Format.** The tag is `a-` plus eight characters of `[a-z2-7]`. `lease=` is UTC to the minute, `YYYY-MM-DDTHH:MMZ` (no seconds), and is at most 24 h after the comment's own timestamp. - **A claim not refreshed within its lease has expired**: the issue is open for anyone. Daily checkpoints are the price of holding work. - **Who holds a contested issue.** Each agent's run of claims starts with its first `claim` and continues while every following `claim` or `checkpoint` is posted before the previous lease runs out. A gap ends the run; the next comment starts a new one. Of the agents whose latest comment is an unexpired `claim` or `checkpoint`, the one whose current run started first holds the issue. A later agent backs off, or claims a non-overlapping scope and says so. **Re-read the comments after posting a claim**: if an earlier run is still active, post `release`. - A claim coordinates; it is not a lock, a permission or proof of progress. Before continuing someone's expired claim, read its last checkpoint and re-check the branch, PR, runs and side effects. - **A tag names a harness on one machine, not a session.** Parallel sessions on the same machine and tool share a tag, so a claim with your own tag may be another session's. Read its latest checkpoint before acting on it. - Pick any stable tag of the right form and write the comments by hand. Valpatel agents have a helper that derives the tag and posts the comments. ## What must never live only locally - Decisions and their reasons: put them in the issue, the PR description or a doc in the repo. - Measurements and observations from the user (a photo, "the fan blows up", "it measured 68 mm"): record them in the repo (parameters with a note, or a doc) and in the issue, with the date. - Run configurations and results: the command line belongs in the issue; result files belong in the repo if the project tracks them, otherwise attached to the issue or the release. - Lessons: the repo's `lessons/` (cad-agent) or the product's notes folder, not only an agent's memory file. Large or licensed inputs (vendor CAD, datasheets) stay out of git; the repo records where they come from and how to fetch them (a sources list plus a fetch script) so a reformatted machine can rebuild. ## Resuming At the start of a session, or after a crash, rebuild the state from GitHub before touching anything: ```bash gh issue list --state open --assignee @me ; gh issue list --state open --limit 30 gh issue list --state open --search '"work v1" in:comments' # issues with claims: read who holds what gh pr list --state open --author @me git fetch --all --prune && git status -sb git log --oneline --branches --not --remotes # commits on no remote: push them first git stash list ``` Read the tracking issue's latest comment for the next step and the runs in flight; check each run's log on its host before starting it again (it may have finished). The `cad-resume` agent does this and reports a plan. If local commits exist that are not on the remote, push them first. ## Issue template ```markdown **Done** - [x] () **Next** - [ ] **Running** - : ``, log ``; done when ``; collect with `` **Resume:** branch ``, PR #. ``` ## Rules that go with it - Follow the repo's own branch rules: never push to protected branches, never force-push a branch you did not create, merge only when the repo's rules allow (CI green, review done, threads resolved, the user's go-ahead where the repo requires it). - Write issue and PR text for a reader who was not in the session: no "as discussed", no chat references. - Do not post private infrastructure details (host names, addresses, credentials) in public repos. Name a run host by role ("the CFD host") and keep its address in the private notes the team already uses. --- Source: https://cad-agent.valpatel.com/skills/design-process/SKILL.md # Design process: section by section A design that passes every per-part check can still be wrong as an assembly: contents outside the container that should hold them, subassemblies that only touch by coincidence, boxes where a mechanism should be, and nobody who looked. This process makes the design *declared* (what touches, holds, moves and fastens what), *measured* (the gates), *justified* (a decision with the alternatives rejected for each part) and *reviewed* (a separate reviewer with a checklist), one section at a time. References: [`docs/dossier.md`](https://cad-agent.valpatel.com/docs/dossier.md) (the YAML and the build-script declarations), [`docs/gates.md`](https://cad-agent.valpatel.com/docs/gates.md) (what each gate measures), [`docs/review-packet.md`](https://cad-agent.valpatel.com/docs/review-packet.md) (the packet and the checklist), [`docs/design-process-plan.md`](https://cad-agent.valpatel.com/docs/design-process-plan.md) §5 (why). ## Before you start - **Track it, as if the session could end any minute** (token budgets, crashes): one GitHub issue per section or piece of work, a `feat/` branch pushed at once, a draft PR with `Closes #N`. Commit and push at least every 30 minutes, WIP commits included, and leave a checkpoint comment on the issue: done, next, what is running and where. The `cad-work-tracking` skill of the cad-agent agent kit spells this out, where it is installed. - **Long runs go off the shared machine** (`cad-agent all`, the gates on a large assembly, FEA, CFD): run them detached on a compute host and record host role, command, log and done marker in the issue (the `cad-remote-runs` skill, where installed). Where agents share a workstation, one `nice`d job at a time. - **Set `CAD_AGENT_ACTOR`** to your session id. Builds record it; a reviewer's sign-off is refused when it equals the build's. ## The loop, per section ### 1. Frame the section: no geometry yet Write `dossier/sections/.yaml`: its `purpose`, the `requirements` it serves (in `project.yaml`, each with the user's words as `source.quote`), the `interfaces` to its neighbours with a `datum` and an `owner`, and the parts it needs. An interface is a contract: the owner section changes the datum, the other side rebuilds. Open questions become `status: open` decisions. If the model can answer one (a stack height, a screw length, a clearance), compute it and decide; ask the user only what the model cannot know. An open decision blocks sign-off. ### 2. Write each part's card with its geometry The minimum card is three things, in the same commit as the code that makes the part: ```yaml - id: hopper function: Hold the ball store and funnel balls to the feed throat under gravity interfaces: [I7, I8, I9] decisions: - id: D41 decision: Single-wall cone with a 55° floor why: balls roll at 35°; 55° clears the throat without a stirrer alternatives: [{option: stirrer paddle, rejected: adds a motor and a jam mode}] ``` Add the rest when it is known, not before: `material` with `wall_mm` when the process is chosen, `fasteners` when the joint is chosen, `contains` when the part holds something, `loads` when the analysis is set up, `checks` naming the gates and checks that verify it. Leave `status:` out: it is computed. The lint asks for what is missing. ### 3. Declare the geometry facts in the build script Only the code knows which solid is the cavity, where an axis is, where each screw sits: ```python from cadagent.dossier import Axis, part, cavity, joint, fastener, assembly, build @part("hopper", section="S3") def hopper(p): ... @cavity("hopper", "hopper_cavity") # always a declared solid; nothing is derived def hopper_void(p): ... joint("I9", "revolute", parts=("flap", "hopper"), axis=lambda p: Axis(p["pivot"], (0, 1, 0)), range_deg=(0, 70)) fastener("screw_m3x8", clamps="lid", into="insert_0", name="screw_0", via="I7", tool="hex 2.5", at=lambda p: [Axis(p["seat_0"], (0, 0, -1))], length_mm=8, thread_start_mm=3, min_engagement_mm=4) assembly.step(3, fit="lid", onto="hopper", direction="-Z", tool="hex 2.5", fasteners=["screw_0"]) ``` Every part goes in exactly one assembly step with a direction; every fastener in the step that drives it. Model the bought part (the McMaster STEP, the `mcmaster-parts` skill) and the mating connector, not a bounding box. Keep one parameter authority (`cad/parameters.json`); never copy a number into the YAML that the build computes. ### 4. Build, gate and lint, and fix in order ```bash cad-agent gates --build # every gate that applies, then the dossier lint; exit 1 on a failure cad-agent dossier # the per-section status table and the lint findings ``` Fix in this order, rerunning after each group (a fix early in the list often clears findings later in it): 1. **Loader and join errors** (`unknown-id`, `card-without-shape`, `shape-without-card`, `unknown-cavity`): the declarations must agree before any gate means anything. 2. **Connectivity**: floating components and unrealised interfaces first; they are usually a part in the wrong place. 3. **Containment**: contents outside their cavity, contents in the container's wall. 4. **Fasteners**: seat, stack, holes, engagement, tip, length, then tool access. 5. **Assembly access**: coverage, then blocked insertions and tools at their steps. 6. **Motion**: collisions over each joint's range. 7. **Wall**, then the **lint's status rules** (decisions, traces, checks). Then the standard part checks, which every project runs whether or not it has a dossier: `cad-agent holes` (every hole in every part clear of material: build features first and cut holes last) and the rest of `cad-agent all`. Walk [docs/common-issues.md](https://cad-agent.valpatel.com/docs/common-issues.md) for the issues no check covers yet. Never lower a threshold or widen a tolerance to pass. A finding that is not a defect becomes an explained exclusion: `gates.free_parts` (a loose item), `gates.intended_overlaps` (a heat-set insert, a press fit, a screw's thread envelope, which may name the fastener or an instance key) or `gates.ignore_pairs` (variants never fitted together), each with a `reason`. A verdict read as **stale** is not a pass: rerun the gates. A section is done with this step when `cad-agent dossier` shows it `gated`. ### 5. Render the section's review packet ```bash cad-agent review-packet S3 # exports/review/S3/*.png and manifest.json ``` The packet is solid, opposite iso, x-ray, cuts through each cavity (side and plan), joint axis and fastener group, exploded along the steps, access views along the screws, each joint's range ends, and the findings at their witness points, captioned with the section and content hashes. Open every image before you use it: current, lit, showing the feature. ### 6. Critique it yourself, measurements first Read the manifest's `measurements` and `findings` before the images. Then answer every question in `checklist.questions` (yes / no / unclear) from the views it names, and write the answers into the section's `review: {self: ...}` block with the view file each refers to. Do not answer a count, a gap, a wall or an engagement from an image: the gates measured those (`checklist.measured_only`). If an image and a measurement disagree, the measurement wins and the disagreement is a finding (a wrong declaration or a misleading view). Fix what you find and go back to step 4; do not hand over a section you would not sign. ### 7. Hand the section to a separate reviewer Do not set `reviewed` or `signed_off` yourself, and never write a reviewer record: the lint refuses records by a part's builder or a named author, but it cannot tell who wrote a file, so this rule is yours to keep. Write the reviewer's bundle and hand it to an agent or person that did not build or write the section (plan §5.3, `docs/dossier.md` "The reviewer pass"): ```bash cad-agent review-section S3 # exports/review-bundle/S3/: PROMPT.md, the section YAML, gates.json, status.json, # packet/ (manifest + images), checklist.json, diff.patch, record-template.json ``` Exit 1 means something would refuse a sign-off today (`blockers` in `bundle.json`: a stale or missing packet, the section below `gated`, an open decision, a part built by `unknown`); fix it first unless you only want changes requested. Give the reviewer the bundle directory, not your conclusions. The reviewer follows `PROMPT.md`: answers every question `yes`, `no` or `unclear` with evidence and marks image-only answers, judges each decision `justified`, `weak` or `missing_alternative`, traces each requirement, writes findings with a severity, and submits: ```bash CAD_AGENT_ACTOR= cad-agent review-section S3 --submit my-review.json # --check validates only ``` That refuses stale hashes (the geometry changed since the bundle), a reviewer who built a part of the section or is named as its author, and, for a sign-off, an open decision or a stale packet; on success it writes `dossier/reviews/
-.json`. Builds and reviews must run with `CAD_AGENT_ACTOR` set: each part records the actor that built its current geometry, so a reviewer who rebuilds unchanged geometry is still not its builder, but one who changes it is. Any change to a part's geometry voids the sign-off. Answer every reviewer finding: fix it and ask for a new review, or explain why it stands. ### 8. Report One line per requirement touched (its status from `exports/dossier-status.json`), per gate, per open finding, per section with its status. Say what is not verified: CAD and simulation do not prove physical fit, strength, print quality or that a mechanism works. ## For the agent that splits the work Plan the sections from the requirements first, agree the interfaces between them, then hand one section per implementer with this skill and its section file; sections whose interfaces are agreed can be built in parallel. Run the reviewer pass on each section as it comes in and the full gates on the integrated assembly. Nothing is done while a finding is open, a verdict is stale or a section lacks its sign-off; `cad-agent check` fails while any section is below `gated`. --- Source: https://cad-agent.valpatel.com/skills/mcmaster-parts/SKILL.md # McMaster-Carr parts Bought hardware is a real part: a part number, its manufacturer's model, its specification. McMaster-Carr publishes a STEP model and a full specification table (material, finish, strength class, thread, dimensions and tolerances, load and speed ratings, temperature range, compliance) for almost everything it sells. Use them. The tools: `cadagent.mcmaster` (Python) and `cad-agent mcmaster` (CLI). They read one product page at a time in the system Chrome (Playwright), with a pause between pages. That is the same traffic as an engineer choosing parts, and it must stay that way. ## Rules - **Download like an engineer, not a crawler.** One part at a time, a pause between pages (default 3 s; the tool never goes below 2 s), only the product pages of the parts you are choosing. The tool accepts only McMaster part numbers (`PN_RE`, e.g. 91290A242), never a category, search or other URL. No bulk downloads, no parallel sessions. A part already downloaded is reused offline (no browser); download it again only when you need to (`--force`). - **Keep the files local.** McMaster-Carr CAD is for design use; do not redistribute it. Gitignore the vendor folder and track only `SOURCE.txt`: references/vendor/** !references/vendor/**/ !references/vendor/**/SOURCE.txt - **Strong, but few parts.** Prefer one family per fastener type: for example, black-oxide alloy steel class 12.9 socket head screws (91290A) with a minimal set of lengths; 18-8 stainless where corrosion matters; class 10 nyloc nuts; one washer family per size. When a computed length is not stocked, move the seat or the pilot depth so a stocked length fits (lesson 04) before adding another SKU. Report the number of distinct SKUs and the total part count. - **Size from the CAD, then check against the page.** The stack comes from the CAD: grip, thread depth, insert pilot depth. Thread engagement is length minus grip. Check the chosen length against the depth actually available, and check the normalised model's bounding box against the catalogue dimensions. - **Report what McMaster does not stock**, with the closest stocked alternative (for example, a 61812-2RS is not stocked; 6812-2Z is), and any CAD dimension that should move to a stocked size. ## Process 1. **Pick the part like an engineer.** Open the category page (e.g. `https://www.mcmaster.com/products/ball-bearings/`), filter it (System of Measurement → Metric, then size, material, thread) and read the table. Choose by function (load, fit, material, strength class) and by the SKUs you already use. Only then open the product page `https://www.mcmaster.com//`. 2. **Download and normalise.** cad-agent mcmaster 91290A242 --kind screw --out references/vendor/mcmaster \ --catalog catalog/hardware.json --key screw_m5x20 This writes `OUT//.step` (the download, kept only if it is a STEP file), `.page.json` (title, price, pack quantity, every spec row, option choices), `.norm.step`, `.norm.json` (axis found, how it was seated, bbox, volume, mass) and an entry in `SOURCE.txt`. Kinds and their frames (`cadagent.mcmaster.FRAMES`) all put the axis on +Z, through the origin (the cylinders' axis line, not the centre of mass); which end goes down is decided from the part's shape, not from how the vendor modelled it: - `bearing`, `washer`, `nut`, `insert`, `standoff`, `spacer`, `pin`, `grommet`, `rod`: the larger end (flange, collar, head) at z = 0 and the part on +z; when both ends are the same size, the heavier end down (`info["end_down"]` says which rule decided). - `screw`: head on +z, shank towards -z. z = 0 is the underside of the head for socket, button, pan, hex and flanged heads, and the top face for a countersunk (flat) head (`info["head"]`). A part with no head wider than its shank (set screw, stud) is refused: use `pin` or `rod`. - `shoulder_screw`: the underside of the head at z = 0, head on +z, shoulder and thread towards -z. - `rod_end`: the ball centre at the origin, ball bore along +Y, threaded shank along -Z (without a modelled ball, the middle of the bore). A part with variants (a rod end's thread direction, a choice of finish) offers its CAD only after the choice. Give the option text after the part numbers, one `--choose` per option, prefixed with the part it is for: cad-agent mcmaster 59935K53 91290A242 --choose "59935K53:Right Hand" --out references/vendor/mcmaster (the `PN:` prefix may be left out when there is one part number). In Python: `Session.fetch(pn, out, choose=["Right Hand"])`. The page data records what was offered and what was chosen. Playwright: `pip install playwright` into the environment that runs it, plus Google Chrome (then nothing else is needed; without Chrome the command stops with an install hint). 3. **Record the catalogue entry.** `catalog_entry()` / `--catalog` gives: `mcmaster` (PN), `description`, `url`, `price_usd`, `pack_qty`, `dims_mm`, `material`, `mass_g` with `mass_basis` (McMaster's listed weight when the page gives one, otherwise CAD volume x density, which it says), `cad` (the normalised STEP path), `frame`, and `specs.raw` (label → value) with `specs.parsed` (mm, g, N, MPa, deg C). Add what only the project knows: `used_for`, the quantity, and notes (stack and engagement arithmetic, CAD changes needed). Fields you add are kept when the entry is refreshed. 4. **Check the model.** Compare `bbox_mm` with the page's dimensions (a bearing's OD and width, a screw's length below the head, a rod end's centre-to-end length). A mismatch means the wrong frame or the wrong part. 5. **Place it in the assembly.** Import `.norm.step` and move it to the seat: for a screw, origin on the seat face and +Z out of the hole; for a bearing, the bottom face on the shoulder. Run the clash and tool-access checks (lesson 04) with the real heads, nuts and washers in place, not envelopes. ## Python ```python import json from cadagent import mcmaster as mc with mc.Session(delay_s=3) as s: # one browser, one page at a time step = s.fetch("5972K82", "references/vendor/mcmaster/5972K82") norm, info = mc.normalise(step, "bearing", material="Steel") page = json.loads((step.parent / "5972K82.page.json").read_text()) mc.update_catalog("catalog/hardware.json", "bearing_6001", mc.catalog_entry(page, info, cad=str(norm)) | {"used_for": "knee", "qty": 4}) ```