Import CAD projects and cad-router v1

This commit is contained in:
Jerry
2026-07-22 19:38:36 +08:00
parent e4e0199bd5
commit af9f630c11
2253 changed files with 1994207 additions and 0 deletions
+50
View File
@@ -0,0 +1,50 @@
---
name: cad-router
description: Route natural-language CAD, mechanical-part, assembly, 3D-printing, and robot-description requests across text-to-cad, SimpleCADAPI, CADAM, and the CADDesigner workflow, then enforce shared generation, validation, and CAD Viewer handoff. Use for new or modified STEP/STP, SCAD, STL, 3MF, DXF, URDF, SRDF, SDF, or G-code work when the agent must choose the best modeling backend and preserve editable sources.
---
# CAD Router
Choose a backend before modeling, keep its source as the editable authority, and normalize every result into one handoff contract. Use all four projects by role: text-to-cad owns orchestration, general CAD, validation, and viewing; SimpleCADAPI owns specialized mechanical families; CADAM owns SCAD execution; CADDesigner supplies the ECIP refinement and repair workflow without starting its separate LLM service.
## Required workflow
1. Convert the request into a brief: part type, dimensions, units, manufacturing intent, required outputs, assembly relationships, editability, and validation targets.
2. Run the deterministic router from this skill directory:
```bash
python3 scripts/route.py "<request>" --explain
```
Add explicit signals when known: `--output step`, `--output stl`, `--manufacturing machining|printing|laser-cutting`, `--existing-model`, `--assembly`, or `--browser-controls`.
3. Follow the selected route unless a hard environmental constraint makes it unavailable. If overriding it, record the reason in the task manifest.
4. Load only the selected backend reference from `references/capabilities.md` and the matching installed Skill:
- `build123d`: use `$cad`; this is the default for general STEP-first mechanical parts, assemblies, and existing STEP modification.
- `simplecadapi`: use an installed SimpleCADAPI Skill/runtime; prefer it for gears, racks, ring gears, bearings, cycloidal parts, reducers, replayable graphs, semantic tags, and FreeCAD interchange.
- `cadam`: generate editable `.scad`, then execute `node scripts/cadam_compile.mjs <source.scad> <output.stl|output.dxf>`. Set `CADAM_ROOT` or pass `--cadam-root` when the CADAM checkout is not beside text-to-cad. Prefer it for fast parametric printable models, repeated patterns, decorative CSG, and parameter controls.
5. Always apply CADDesigner's `caddesigner_ecip` profile. When the route also reports `requirement_refinement` or `visual_repair`, refine the brief, generate, execute, inspect multiple views, and repair no more than three visual mismatch rounds before asking the user.
6. Generate into a task-owned directory and start CAD Viewer with that task directory as `--dir`; do not expose the repository-wide fixture library for a task review. Never change backend during an edit unless conversion is explicitly requested; modify the recorded source of truth.
7. Validate against the brief. STEP-first routes must run geometry inspection and snapshot review. Mesh routes must check compilation, bounds, solidity/manifold facts when available, and snapshot review.
8. Write a `cad-task.json` manifest using the contract in `references/backend-contract.md`. Hand supported artifacts to `$cad-viewer` and return its live links.
## Routing rules
- Prefer an explicit user backend when it can produce the required primary format.
- Prefer SimpleCADAPI when a supported standard mechanical factory or replay/semantic requirement materially reduces custom modeling.
- Prefer build123d for general machined parts such as brackets, connecting rods, housings, shafts, flanges, fixtures, and source-driven assemblies.
- Prefer CADAM for SCAD-native, fast printable, pattern-heavy, or interactive-parameter models. Do not choose it when editable STEP is mandatory.
- Route DXF, URDF, SRDF, SDF, G-code, printer, or fabrication handoffs to their focused installed Skills after primary geometry generation.
- Prefer source quality and future modification over the shortest first generation.
## V1 boundaries
- Do not promise lossless conversion of feature history between backends.
- Do not treat STL as an editable CAD source when a parametric source or STEP exists.
- Do not claim engineering certification, tolerance analysis, FEA, CAM toolpaths, or manufacturability without separate checks.
- Keep GPL/AGPL backends independently installed and invoked; do not copy their implementation into an MIT-distributed Skill.
## References
- Read `references/capabilities.md` when comparing or invoking backends.
- Read `references/backend-contract.md` before writing a task manifest or adding another backend.
- `scripts/capabilities.json` is the machine-readable V1 registry used by `scripts/route.py`.
@@ -0,0 +1,4 @@
interface:
display_name: "CAD Router"
short_description: "Route CAD work across four complementary projects."
default_prompt: "Use $cad-router to select and run the best CAD backend, validate the result, and open only the task outputs in CAD Viewer."
@@ -0,0 +1,74 @@
# Backend and artifact contract
## Router input
Capture these fields when known:
- Natural-language request and normalized part family.
- Units and dimensions.
- Manufacturing intent: machining, printing, laser cutting, simulation, or concept.
- Required primary and secondary output formats.
- New model versus modification, and existing source/artifact paths.
- Part versus assembly, standard components, mating requirements, and motion relationships.
- Required parameter controls, replay, semantic tags, or FreeCAD interchange.
- Geometry and visual validation targets.
## Route decision
Record:
- `selected_backend`: `build123d`, `simplecadapi`, or `cadam`.
- Origin project and optional executable adapter.
- `runner_skill`: installed Skill responsible for generation.
- Ranked backend scores and human-readable reasons.
- Fallback order.
- Workflow profiles such as `requirement_refinement` and `visual_repair`.
- Focused downstream Skills.
- Any explicit override and its reason.
## Task manifest
Write `cad-task.json` beside the task artifacts with this minimum shape:
```json
{
"schema_version": "1.0",
"request": "Create a 120 mm connecting rod",
"route": {
"selected_backend": "build123d",
"runner_skill": "cad",
"project": "text-to-cad",
"fallback_order": ["simplecadapi", "cadam"],
"workflow_profiles": ["caddesigner_ecip"]
},
"source": {
"path": "connecting_rod.py",
"format": "python",
"backend": "build123d"
},
"artifacts": [
{"path": "connecting_rod.step", "role": "primary"},
{"path": "connecting_rod.stl", "role": "secondary"}
],
"parameters": {},
"assumptions": [],
"validation": [],
"viewer_links": []
}
```
Paths are relative to the task directory unless an external input must remain absolute.
## Backend adapter requirements
Any future executable adapter must:
1. Accept an explicit task directory and explicit source/output paths.
2. Never scan or generate an entire workspace implicitly.
3. Preserve a human-editable source of truth.
4. Report backend name and version.
5. Return nonzero on generation failure.
6. Emit machine-readable artifact paths and validation facts.
7. Avoid importing code from sibling Skill directories; call installed tools or self-contained runtimes.
8. Keep permanent CAD artifacts under the repository `models/` root when working inside this repository.
9. Start CAD Viewer at the task directory, not the repository-wide examples directory.
@@ -0,0 +1,59 @@
# Backend capabilities
## build123d through CAD Skill
Use as the default STEP-first backend.
Best fit:
- Brackets, connecting rods, cranks, levers, shafts, bushings, flanges, plates, housings, covers, enclosures, fixtures, and machined features.
- Parametric Python sources with holes, pockets, slots, bosses, ribs, shells, fillets, chamfers, lofts, sweeps, and revolves.
- Source-level assemblies, named datums, joints, imported components, measurements, alignment checks, and existing STEP inspection.
- STEP primary output with STL, 3MF, or GLB sidecars.
Weak signals: a specialized mechanical family already implemented by SimpleCADAPI, SCAD-native output, browser-only execution, or decorative repeated CSG where OpenSCAD is substantially shorter.
## SimpleCADAPI
Use for its standard mechanical factories and replayable modeling graph.
Best fit:
- Spur, helical, and herringbone gears; internal ring gears; racks; cycloidal discs; ball bearings.
- Planetary and cycloidal reducers, joint actuators, and mechanism-heavy assemblies.
- Parameter expressions, operation-graph JSON, replay, semantic tags, topology queries, and FreeCAD translation.
- Reusable mechanical product families whose formulas should become deterministic library functions.
Before invocation, inspect the installed package and Skill versions together. Do not mix the current single-`Solid` boolean API with older `*rsolidlist` documentation. If the installed runtime and docs disagree, stop that route and use build123d or repair the installation.
## CADAM
Use CADAM's separately installed OpenSCAD WASM runtime for fast CSG authoring and printable parameterized models. Execute generated SCAD through `scripts/cadam_compile.mjs`; do not copy CADAM's GPL runtime into this Skill.
Best fit:
- Vases, containers, knobs, adapters, printable threads, lattice/honeycomb patterns, repeated details, decorative models, and quick concept geometry.
- SCAD source, STL output, simple projected DXF, and UI-facing parameter controls.
- BOSL/BOSL2/MCAD-backed printable features.
Do not use as the primary route for editable STEP, stable face/edge references, fit-critical source-level assemblies, or modification of imported STEP.
## CADDesigner workflow profile
CADDesigner is not a fourth geometry engine. Apply its ECIP workflow to every route, then enable requirement expansion and visual repair when requirements are vague, reference images carry important intent, or visual complexity makes one-pass generation unreliable. Do not launch CADDesigner's separate LLM/Redis stack:
1. Refine the request into a measurable brief.
2. Confirm important fit- or safety-critical unknowns.
3. Generate and execute backend source.
4. Validate deterministic geometry facts.
5. Inspect a multi-view render against the brief.
6. Repair visual mismatches, capped at three automatic visual rounds.
## Focused downstream Skills
- `$dxf`: 2D profiles, flat patterns, laser/plasma/waterjet drawings.
- `$urdf`, `$srdf`, `$sdf`: robot structure, MoveIt semantics, and simulation models/worlds.
- `$gcode`: slice validated meshes with a real slicer.
- `$sendcutsend`: vendor-specific manufacturing preflight.
- `$bambu-labs`: cautious local printer handoff.
- `$cad-viewer`: visual review of supported generated artifacts.
@@ -0,0 +1,107 @@
#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
function usage() {
return "Usage: node cadam_compile.mjs <input.scad> <output.stl|output.dxf> [--cadam-root <path>]";
}
function parseArgs(argv) {
const positional = [];
let cadamRoot = String(process.env.CADAM_ROOT || "").trim();
for (let index = 0; index < argv.length; index += 1) {
const value = argv[index];
if (value === "--cadam-root") {
cadamRoot = String(argv[index + 1] || "").trim();
index += 1;
} else {
positional.push(value);
}
}
if (positional.length !== 2) {
throw new Error(usage());
}
return { inputPath: path.resolve(positional[0]), outputPath: path.resolve(positional[1]), cadamRoot };
}
function defaultCadamRoot() {
const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
return path.resolve(scriptDirectory, "../../../../CADAM");
}
function runtimePaths(configuredRoot) {
const root = path.resolve(configuredRoot || defaultCadamRoot());
const runtimeDirectory = path.join(root, "src", "vendor", "openscad-wasm");
const modulePath = path.join(runtimeDirectory, "openscad.js");
const wasmPath = path.join(runtimeDirectory, "openscad.wasm");
if (!fs.existsSync(modulePath) || !fs.existsSync(wasmPath)) {
throw new Error(
`CADAM OpenSCAD WASM runtime not found under ${runtimeDirectory}. ` +
"Set CADAM_ROOT or pass --cadam-root to an installed CADAM checkout."
);
}
return { root, modulePath, wasmPath };
}
async function compile({ inputPath, outputPath, cadamRoot }) {
if (path.extname(inputPath).toLowerCase() !== ".scad") {
throw new Error("CADAM input must be an OpenSCAD .scad source file");
}
const extension = path.extname(outputPath).toLowerCase();
if (!new Set([".stl", ".dxf"]).has(extension)) {
throw new Error("CADAM output must be .stl or .dxf");
}
if (!fs.existsSync(inputPath) || !fs.statSync(inputPath).isFile()) {
throw new Error(`CADAM input file not found: ${inputPath}`);
}
const runtime = runtimePaths(cadamRoot);
const logs = { stdout: [], stderr: [] };
const runtimeModule = await import(pathToFileURL(runtime.modulePath).href);
const instance = await runtimeModule.default({
noInitialRun: true,
wasmBinary: fs.readFileSync(runtime.wasmPath),
print: (line) => logs.stdout.push(String(line)),
printErr: (line) => logs.stderr.push(String(line))
});
const virtualInput = "/input.scad";
const virtualOutput = `/out${extension}`;
instance.FS.writeFile(virtualInput, fs.readFileSync(inputPath, "utf8"));
const format = extension === ".stl" ? "binstl" : "dxf";
const exitCode = instance.callMain([
virtualInput,
"-o",
virtualOutput,
`--export-format=${format}`,
"--backend=manifold",
"--enable=lazy-union"
]);
if (exitCode !== 0) {
throw new Error(`CADAM OpenSCAD compilation failed with exit code ${exitCode}: ${logs.stderr.join("\n")}`);
}
const output = instance.FS.readFile(virtualOutput, { encoding: "binary" });
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, output);
return {
backend: "cadam",
runtime: "CADAM OpenSCAD WASM",
cadam_root: runtime.root,
input: inputPath,
output: outputPath,
bytes: output.length,
exit_code: exitCode,
stderr_tail: logs.stderr.slice(-8)
};
}
try {
const result = await compile(parseArgs(process.argv.slice(2)));
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
} catch (error) {
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
process.exitCode = 1;
}
@@ -0,0 +1,56 @@
{
"schema_version": "1.0",
"backends": {
"build123d": {
"project": "text-to-cad",
"runner_skill": "cad",
"source_formats": ["py"],
"primary_formats": ["step", "stp"],
"secondary_formats": ["stl", "3mf", "glb"],
"strengths": [
"general mechanical parts",
"machined features",
"source-level assemblies",
"existing STEP inspection and modification",
"geometry measurement and validation"
],
"source_of_truth": "build123d Python generator"
},
"simplecadapi": {
"project": "SimpleCADAPI",
"runner_skill": "simplecadapi",
"source_formats": ["py", "json"],
"primary_formats": ["step", "stp", "fcstd"],
"secondary_formats": ["stl", "json"],
"strengths": [
"gears, ring gears, racks, and cycloidal discs",
"bearings, reducers, and joint actuators",
"replayable operation graphs",
"semantic tags and topology queries",
"FreeCAD interchange"
],
"source_of_truth": "SimpleCADAPI Python generator and model JSON"
},
"cadam": {
"project": "CADAM",
"runner_skill": "cad-router",
"adapter": "scripts/cadam_compile.mjs",
"source_formats": ["scad"],
"primary_formats": ["scad", "stl"],
"secondary_formats": ["dxf"],
"strengths": [
"fast printable CSG models",
"patterns, lattices, and decorative geometry",
"printable threads and BOSL/BOSL2 libraries",
"browser-friendly parameter controls"
],
"source_of_truth": "CADAM-compatible OpenSCAD source"
}
},
"workflow_projects": {
"CADDesigner-Code": {
"profiles": ["caddesigner_ecip", "requirement_refinement", "visual_repair"],
"role": "Explicit context, requirement expansion, execution feedback, and bounded visual repair without launching a second LLM agent"
}
}
}
+283
View File
@@ -0,0 +1,283 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import importlib.util
import json
import os
import sys
from dataclasses import dataclass, field
from pathlib import Path
SKILL_ROOT = Path(__file__).resolve().parents[1]
REGISTRY_PATH = Path(__file__).with_name("capabilities.json")
CADAM_RUNTIME_FILES = (
Path("src/vendor/openscad-wasm/openscad.js"),
Path("src/vendor/openscad-wasm/openscad.wasm"),
)
@dataclass
class BackendScore:
name: str
score: int
reasons: list[str] = field(default_factory=list)
def add(self, points: int, reason: str) -> None:
self.score += points
self.reasons.append(f"{points:+d} {reason}")
KEYWORD_GROUPS: dict[str, tuple[str, ...]] = {
"special_mechanical": (
"gear", "gears", "齿轮", "齿条", "rack", "ring gear", "内齿圈",
"cycloidal", "摆线", "bearing", "轴承", "planetary", "行星",
"reducer", "gearbox", "减速器", "减速箱", "joint actuator", "关节执行器",
),
"general_mechanical": (
"bracket", "支架", "connecting rod", "连杆", "crank", "曲柄",
"lever", "摇臂", "shaft", "轴", "bushing", "轴套", "flange", "法兰",
"housing", "壳体", "enclosure", "外壳", "fixture", "夹具", "plate", "安装板",
),
"printable": (
"3d print", "3d-print", "打印", "printable", "vase", "花瓶", "knob", "旋钮",
"container", "容器", "adapter", "适配器", "honeycomb", "蜂窝", "lattice", "晶格",
"pattern", "阵列", "decorative", "装饰", "openscad", "scad", "bosl",
),
"replay": (
"replay", "重放", "operation graph", "model json", "模型图", "semantic tag",
"语义标签", "freecad", "fcstd",
),
"visual_complexity": (
"image", "photo", "sketch", "图片", "照片", "草图", "复杂", "complex",
"像", "参考图", "reference",
),
}
FOCUSED_SKILLS: tuple[tuple[str, tuple[str, ...]], ...] = (
("dxf", ("dxf", "laser cut", "激光切割", "waterjet", "水刀", "flat pattern", "展开")),
("urdf", ("urdf", "robot description", "机器人描述")),
("srdf", ("srdf", "moveit", "planning group", "规划组")),
("sdf", ("sdformat", "gazebo", "simulation world", "仿真世界", ".sdf")),
("gcode", ("g-code", "gcode", "slice", "切片")),
("sendcutsend", ("sendcutsend",)),
("bambu-labs", ("bambu", "拓竹")),
)
def contains_any(text: str, terms: tuple[str, ...]) -> bool:
return any(term in text for term in terms)
def normalize_formats(formats: list[str]) -> list[str]:
result: list[str] = []
for value in formats:
normalized = value.lower().lstrip(".")
if normalized not in result:
result.append(normalized)
return result
def cadam_root() -> Path:
configured = os.environ.get("CADAM_ROOT", "").strip()
if configured:
return Path(configured).expanduser().resolve()
return (SKILL_ROOT.parents[2] / "CADAM").resolve()
def cadam_runtime_available(root: Path | None = None) -> bool:
candidate = root or cadam_root()
return all((candidate / relative_path).is_file() for relative_path in CADAM_RUNTIME_FILES)
def probe_backends() -> dict[str, dict[str, object]]:
return {
"build123d": {
"available": importlib.util.find_spec("build123d") is not None,
"probe": f"python={sys.executable}",
},
"simplecadapi": {
"available": importlib.util.find_spec("simplecadapi") is not None,
"probe": f"python={sys.executable}",
},
"cadam": {
"available": cadam_runtime_available(),
"probe": str(cadam_root()),
},
}
def route(args: argparse.Namespace) -> dict[str, object]:
request = args.request.strip()
text = request.lower()
outputs = normalize_formats(args.output)
scores = {
"build123d": BackendScore("build123d", 35, ["+35 general STEP-first default"]),
"simplecadapi": BackendScore("simplecadapi", 20, ["+20 specialist mechanical baseline"]),
"cadam": BackendScore("cadam", 10, ["+10 CADAM printable CSG baseline"]),
}
if contains_any(text, KEYWORD_GROUPS["special_mechanical"]):
scores["simplecadapi"].add(65, "supported standard mechanical family")
scores["build123d"].add(8, "can model the family generically")
if contains_any(text, KEYWORD_GROUPS["general_mechanical"]):
scores["build123d"].add(35, "general mechanical part with direct parametric features")
scores["simplecadapi"].add(10, "general OCP modeling is available")
if contains_any(text, KEYWORD_GROUPS["printable"]):
scores["cadam"].add(55, "printable, pattern-heavy, or SCAD-native request")
if contains_any(text, KEYWORD_GROUPS["replay"]):
scores["simplecadapi"].add(60, "replay, semantic graph, or FreeCAD requirement")
if args.existing_model:
scores["build123d"].add(55, "existing model inspection or source modification")
scores["cadam"].add(-50, "poor fit for imported editable STEP")
if args.assembly:
scores["build123d"].add(15, "source-level assembly workflow")
scores["simplecadapi"].add(18, "mechanism and constraint workflow")
scores["cadam"].add(-15, "weak fit for engineering assembly relationships")
if args.browser_controls:
scores["cadam"].add(45, "CADAM-style parameter controls requested")
if args.manufacturing == "machining":
scores["build123d"].add(25, "machining favors STEP-first B-Rep")
scores["simplecadapi"].add(18, "OCP STEP geometry suits machining")
scores["cadam"].add(-25, "mesh/CSG route is not preferred for machining")
elif args.manufacturing == "printing":
scores["cadam"].add(30, "printing favors fast parametric CSG")
scores["build123d"].add(8, "can export validated printable meshes")
elif args.manufacturing == "laser-cutting":
scores["build123d"].add(12, "can own projected source geometry")
if any(fmt in {"step", "stp"} for fmt in outputs):
scores["build123d"].add(35, "editable STEP requested")
scores["simplecadapi"].add(30, "OCP STEP export requested")
scores["cadam"].add(-70, "no native editable STEP output")
if "fcstd" in outputs:
scores["simplecadapi"].add(70, "FreeCAD output requested")
scores["cadam"].add(-40, "FreeCAD output is unsupported")
if "scad" in outputs:
scores["cadam"].add(80, "SCAD source requested")
if any(fmt in {"stl", "3mf"} for fmt in outputs):
scores["cadam"].add(12, "print mesh requested")
scores["build123d"].add(5, "secondary mesh export is available")
forced_backend = args.backend if args.backend != "auto" else None
ranked = sorted(scores.values(), key=lambda item: (-item.score, item.name))
if forced_backend:
selected = scores[forced_backend]
selected.reasons.append("explicit backend override")
fallback = [item.name for item in ranked if item.name != forced_backend]
else:
selected = ranked[0]
fallback = [item.name for item in ranked[1:]]
profiles: list[str] = ["caddesigner_ecip"]
if contains_any(text, KEYWORD_GROUPS["visual_complexity"]):
profiles.extend(["requirement_refinement", "visual_repair"])
if not any(char.isdigit() for char in request) and contains_any(
text,
KEYWORD_GROUPS["general_mechanical"] + KEYWORD_GROUPS["special_mechanical"],
):
profiles.append("requirement_refinement")
profiles = list(dict.fromkeys(profiles))
downstream = [name for name, terms in FOCUSED_SKILLS if contains_any(text, terms)]
if args.manufacturing == "laser-cutting" and "dxf" not in downstream:
downstream.append("dxf")
if any(fmt in {"stl", "3mf", "gcode"} for fmt in outputs) and "cad-viewer" not in downstream:
downstream.append("cad-viewer")
elif selected.name in {"build123d", "simplecadapi"}:
downstream.append("cad-viewer")
registry = json.loads(REGISTRY_PATH.read_text(encoding="utf-8"))
backend_info = registry["backends"][selected.name]
gap = selected.score - max((scores[name].score for name in scores if name != selected.name), default=0)
confidence = "high" if gap >= 40 else "medium" if gap >= 15 else "low"
result: dict[str, object] = {
"schema_version": "1.0",
"request": request,
"selected_backend": selected.name,
"runner_skill": backend_info["runner_skill"],
"project": backend_info["project"],
"adapter": backend_info.get("adapter"),
"confidence": confidence,
"backend_scores": [
{"backend": item.name, "score": item.score, "reasons": item.reasons}
for item in sorted(scores.values(), key=lambda item: (-item.score, item.name))
],
"fallback_order": fallback,
"workflow_profiles": profiles,
"downstream_skills": downstream,
"requested_outputs": outputs,
"source_of_truth": backend_info["source_of_truth"],
"project_contributions": {
"text-to-cad": "core Skill host, build123d CAD backend, validation, artifacts, and CAD Viewer handoff",
"SimpleCADAPI": "specialized mechanical backend with replayable graphs and semantic modeling",
"CADAM": "OpenSCAD-WASM execution for printable and pattern-heavy SCAD models",
"CADDesigner-Code": "ECIP brief expansion, execution feedback, and bounded visual repair workflow",
},
"availability": probe_backends() if args.probe else None,
}
return result
def explain(result: dict[str, object]) -> str:
lines = [
f"Selected backend: {result['selected_backend']}",
f"Runner skill: ${result['runner_skill']}",
f"Project: {result['project']}",
f"Confidence: {result['confidence']}",
f"Source of truth: {result['source_of_truth']}",
]
profiles = result["workflow_profiles"]
if profiles:
lines.append(f"Workflow profiles: {', '.join(profiles)}")
downstream = result["downstream_skills"]
if downstream:
lines.append(f"Downstream skills: {', '.join('$' + name for name in downstream)}")
lines.append("Ranking:")
for item in result["backend_scores"]:
lines.append(f" {item['backend']}: {item['score']}")
for reason in item["reasons"]:
lines.append(f" - {reason}")
return "\n".join(lines)
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Route a CAD request to a V1 modeling backend.")
parser.add_argument("request", help="Natural-language CAD request.")
parser.add_argument(
"--backend",
choices=("auto", "build123d", "simplecadapi", "cadam"),
default="auto",
help="Override automatic routing.",
)
parser.add_argument("--output", action="append", default=[], help="Required output format; repeatable.")
parser.add_argument(
"--manufacturing",
choices=("unspecified", "machining", "printing", "laser-cutting", "concept"),
default="unspecified",
)
parser.add_argument("--existing-model", action="store_true", help="The task modifies or inspects an existing model.")
parser.add_argument("--assembly", action="store_true", help="The requested result is an assembly.")
parser.add_argument("--browser-controls", action="store_true", help="Interactive browser parameter controls are required.")
parser.add_argument("--probe", action="store_true", help="Probe current-interpreter and OpenSCAD availability.")
parser.add_argument("--explain", action="store_true", help="Print a human-readable decision instead of JSON.")
parser.add_argument("--manifest", type=Path, help="Also write the route decision as JSON to this explicit path.")
return parser
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
result = route(args)
payload = json.dumps(result, ensure_ascii=False, indent=2) + "\n"
if args.manifest:
args.manifest.parent.mkdir(parents=True, exist_ok=True)
args.manifest.write_text(payload, encoding="utf-8")
print(explain(result) if args.explain else payload, end="\n" if args.explain else "")
return 0
if __name__ == "__main__":
raise SystemExit(main())