feat: integrate SimpleCADAPI 2.0.2 CAD workflows

This commit is contained in:
Jerry
2026-08-03 11:17:05 +08:00
parent b5738e9109
commit c3a0f269b7
481 changed files with 110229 additions and 12826 deletions
@@ -1,5 +1,21 @@
{
"schema_version": "1.0",
"schema_version": "2.0",
"routing_policy": {
"default_backend": null,
"selection_basis": [
"native source continuity",
"standard factory coverage",
"semantic and replay requirements",
"geometry vocabulary coverage",
"assembly requirements",
"validation requirements"
],
"uploaded_step_execution_engine": "surfaceir",
"uploaded_step_validation_engines": [
"surfaceir_geometry_acceptance",
"simplecadapi_brep_compare"
]
},
"backends": {
"build123d": {
"project": "text-to-cad",
@@ -10,21 +26,43 @@
"general mechanical parts",
"machined features",
"source-level assemblies",
"existing STEP inspection and modification",
"freeform profiles, lofts, sweeps, and mixed surface workflows",
"geometry measurement and validation"
],
"preferred_when": [
"the part is a custom bracket, housing, shaft, flange, fixture, or enclosure",
"source-level assembly mates or existing build123d Python must be preserved",
"the geometry needs custom profiles or surface-heavy construction not covered by a SimpleCADAPI factory"
],
"weak_when": [
"a deterministic SimpleCADAPI standard factory covers the requested part",
"operation-graph replay, typed semantic tags, units, or tolerance chains are primary requirements"
],
"source_of_truth": "build123d Python generator"
},
"simplecadapi": {
"project": "SimpleCADAPI",
"runner_skill": "simplecadapi",
"minimum_version": "2.0.2",
"source_formats": ["py", "json"],
"primary_formats": ["step", "stp"],
"strengths": [
"gears, ring gears, racks, and cycloidal discs",
"bearings, reducers, and joint actuators",
"replayable operation graphs",
"semantic tags and topology queries"
"gears, ring gears, racks, bevel gears, and cycloidal discs",
"bearings, bolts, nuts, roller-chain sprockets, reducers, and joint actuators",
"replayable ModelResult operation graphs and scene packages",
"typed semantic tags, topology lineage, and source mapping",
"physical units and tolerance-chain validation",
"STEP B-Rep inspection, strict comparison, and slice diagnostics",
"FreeCAD, Fusion 360, and SolidWorks translation"
],
"preferred_when": [
"a standard mechanical factory directly covers the requested family",
"replayable model JSON, semantic edit anchors, units, tolerances, or CAD translation are required",
"a reusable mechanical product family should become a deterministic library function"
],
"weak_when": [
"the request depends on complex freeform surfaces outside the supported operation vocabulary",
"an existing build123d generator or source-level build123d assembly must be preserved"
],
"source_of_truth": "SimpleCADAPI Python generator and model JSON"
}
@@ -10,6 +10,7 @@ from __future__ import annotations
import argparse
import json
import math
import os
import re
import shutil
@@ -34,6 +35,11 @@ DESIGNIR_SCHEMA = (
/ "contracts"
/ "designir-3.0.schema.json"
)
DESIGNIR_SCRIPTS = WORKSPACE_ROOT / "designir-pipeline" / "scripts"
if str(DESIGNIR_SCRIPTS) not in sys.path:
sys.path.insert(0, str(DESIGNIR_SCRIPTS))
from designir_codec import read_designir, write_designir
EXECUTABLE_EDIT_STATES = {
"executable_enlarge_cut_binding",
@@ -80,10 +86,14 @@ def _safe_stem(source: Path) -> str:
def _write_json(path: Path, payload: dict[str, Any]) -> None:
if path.name.endswith(".designir.json"):
write_designir(path, payload)
return
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(
json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
errors="replace",
)
@@ -117,6 +127,66 @@ def _run_json(
) from exc
def _run_simplecad_brep_compare(
runtime: Path,
source: Path,
rebuilt: Path,
report_path: Path,
*,
timeout_seconds: int,
) -> dict[str, Any]:
"""Run the installed SimpleCADAPI comparator in the acceptance zone.
A topology mismatch is a valid diagnostic result, not a worker failure.
The teacher path is provided only to this acceptance subprocess.
"""
command = [
str(runtime),
"-m",
"simplecadapi.inverse_engineer.brep.cli",
"compare",
str(source),
str(rebuilt),
"--output",
str(report_path),
]
try:
completed = subprocess.run(
command,
capture_output=True,
text=True,
timeout=timeout_seconds,
check=False,
)
except subprocess.TimeoutExpired as exc:
return {
"status": "unavailable",
"reason": "timeout",
"error": str(exc),
}
if report_path.is_file():
try:
payload = json.loads(report_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
return {
"status": "unavailable",
"reason": "invalid_report",
"error": str(exc),
}
payload["status"] = (
"passed" if payload.get("hard_gate_passed") else "mismatch"
)
payload["engine"] = "simplecadapi.inverse_engineer.brep"
return payload
detail = completed.stderr.strip() or completed.stdout.strip()
return {
"status": "unavailable",
"reason": "worker_failed",
"return_code": completed.returncode,
"error": detail[-2000:],
}
def _validate_designir_contract(payload: dict[str, Any]) -> dict[str, Any]:
required = {
"schema_version",
@@ -261,7 +331,7 @@ def _persist_edit_results(
designir_path: Path,
attempts: list[dict[str, Any]],
) -> tuple[dict[str, Any], list[dict[str, Any]]]:
payload = json.loads(designir_path.read_text(encoding="utf-8"))
payload = read_designir(designir_path)
parameters = payload.get("semantic_layer", {}).get("parameters", {})
accepted_parameters: list[dict[str, Any]] = []
attempted_names = set()
@@ -312,6 +382,7 @@ def reconstruct_uploaded_step(
request: str,
maximum_parameters: int = 8,
timeout_seconds: int = 180,
semantic_backend: str = "build123d",
) -> dict[str, Any]:
source = source.expanduser().resolve()
task_dir = task_dir.expanduser().resolve()
@@ -338,6 +409,7 @@ def reconstruct_uploaded_step(
rebuilt_path = task_dir / f"{stem}.reconstructed.step"
validation_dir = task_dir / "validation"
geometry_report_path = validation_dir / "geometry.json"
brep_report_path = validation_dir / "simplecad-brep.json"
edit_report_path = validation_dir / "edits.json"
parameters_path = task_dir / "parameters.json"
task_manifest_path = task_dir / "cad-task.json"
@@ -360,7 +432,7 @@ def reconstruct_uploaded_step(
runtime = _runtime_python()
common = [str(runtime), str(SURFACEIR_PIPELINE)]
available_experience_ids = _surfaceir_experience_ids()
boundary_strategy = (
preferred_boundary_strategy = (
"exact_3d_pcurve"
if (
"failure_repair.surface_topology.bind_face_pcurves_and_group_shells"
@@ -380,25 +452,9 @@ def reconstruct_uploaded_step(
timeout_seconds=timeout_seconds,
stage="DesignIR 3.0 extraction",
)
payload = json.loads(designir_path.read_text(encoding="utf-8"))
payload = read_designir(designir_path)
contract_checks = _validate_designir_contract(payload)
# The rebuild worker receives no teacher path. It can regenerate the model
# from the DesignIR artifact alone.
_run_json(
common
+ [
"rebuild",
str(designir_path),
"--output",
str(rebuilt_path),
"--boundary-strategy",
boundary_strategy,
],
timeout_seconds=timeout_seconds,
stage="source-independent SurfaceIR rebuild",
)
manifest_path = task_dir / "manifest.json"
_write_json(
manifest_path,
@@ -415,35 +471,162 @@ def reconstruct_uploaded_step(
},
)
validation_dir.mkdir(parents=True, exist_ok=True)
geometry_work_dir = validation_dir / ".geometry-work"
geometry_result = _run_json(
common
+ [
"validate-folder",
str(source.parent),
"--designir-dir",
str(task_dir),
"--rebuilt-dir",
str(geometry_work_dir),
"--limit",
"1",
"--boolean",
"--boundary-strategy",
boundary_strategy,
],
timeout_seconds=timeout_seconds,
stage="Acceptance-Agent geometry validation",
strategy_root = validation_dir / ".strategy-work"
strategies = [preferred_boundary_strategy]
if (
"failure_repair.semantic_edit.boundary_strategy_fallback"
in available_experience_ids
):
strategies.extend(
strategy
for strategy in (
"exact_3d",
"first_pcurve",
"analytic_uv_rect",
"uv_rect_all",
)
if strategy not in strategies
)
strategy_attempts: list[dict[str, Any]] = []
selected: tuple[
tuple[float, ...],
str,
dict[str, Any],
dict[str, Any],
Path,
] | None = None
for strategy in strategies:
attempt_dir = strategy_root / strategy
try:
candidate_result = _run_json(
common
+ [
"validate-folder",
str(source.parent),
"--designir-dir",
str(task_dir),
"--rebuilt-dir",
str(attempt_dir),
"--limit",
"1",
"--boolean",
"--boundary-strategy",
strategy,
],
timeout_seconds=timeout_seconds,
stage=f"Acceptance-Agent geometry validation ({strategy})",
)
report = json.loads(
(attempt_dir / "validation-report.json").read_text(
encoding="utf-8"
)
)
row = (report.get("rows") or [{}])[0]
candidate_steps = sorted(attempt_dir.glob("*.step"))
if not candidate_steps:
raise ReconstructionError(
f"Boundary strategy {strategy} produced no rebuilt STEP"
)
def finite_metric(name: str) -> float:
value = row.get(name)
if isinstance(value, (int, float)) and math.isfinite(value):
return float(value)
return math.inf
score = (
0.0
if row.get("geometry_pass")
else 1.0
if row.get("usable_reconstruction")
else 2.0,
0.0 if row.get("topology_match") else 1.0,
finite_metric("relative_volume_error"),
finite_metric("relative_surface_area_error"),
finite_metric("center_of_mass_error"),
finite_metric("bounds_error"),
finite_metric("bidirectional_sample_distance"),
)
strategy_attempts.append(
{
"strategy": strategy,
"status": "evaluated",
"score": list(score),
"geometry_pass": bool(row.get("geometry_pass")),
"usable_reconstruction": bool(
row.get("usable_reconstruction")
),
"topology_match": bool(row.get("topology_match")),
"relative_volume_error": row.get("relative_volume_error"),
"relative_surface_area_error": row.get(
"relative_surface_area_error"
),
"center_of_mass_error": row.get("center_of_mass_error"),
"bounds_error": row.get("bounds_error"),
"bidirectional_sample_distance": row.get(
"bidirectional_sample_distance"
),
}
)
candidate = (
score,
strategy,
report,
candidate_result,
candidate_steps[0],
)
if selected is None or candidate[0] < selected[0]:
selected = candidate
except (ReconstructionError, subprocess.TimeoutExpired) as exc:
strategy_attempts.append(
{
"strategy": strategy,
"status": "worker_failed",
"error": str(exc),
}
)
strategy_report_path = validation_dir / "strategies.json"
if selected is None:
_write_json(
strategy_report_path,
{
"schema_version": "1.0",
"selected_strategy": None,
"attempts": strategy_attempts,
},
)
raise ReconstructionError("Every SurfaceIR boundary strategy failed")
_, boundary_strategy, geometry_report, geometry_result, selected_step = selected
shutil.copy2(selected_step, rebuilt_path)
_write_json(
strategy_report_path,
{
"schema_version": "1.0",
"selected_strategy": boundary_strategy,
"attempts": strategy_attempts,
},
)
raw_geometry_report = geometry_work_dir / "validation-report.json"
geometry_report = json.loads(raw_geometry_report.read_text(encoding="utf-8"))
_write_json(geometry_report_path, geometry_report)
geometry_pass = geometry_report.get("geometry_pass_count") == 1
if not geometry_pass:
usable_reconstruction = (
geometry_report.get("usable_reconstruction_count") == 1
)
if not geometry_pass and not usable_reconstruction:
raise ReconstructionError(
"Independent reconstruction did not satisfy the geometry contract; "
f"see {geometry_report_path}"
)
shutil.rmtree(geometry_work_dir)
brep_comparison = _run_simplecad_brep_compare(
runtime,
source,
rebuilt_path,
brep_report_path,
timeout_seconds=timeout_seconds,
)
shutil.rmtree(strategy_root)
attempts: list[dict[str, Any]] = []
if payload["document_status"] == "geometry_present":
@@ -502,7 +685,9 @@ def reconstruct_uploaded_step(
)
payload["reconstruction_strategy"] = {
"boundary_strategy": boundary_strategy,
"selection_status": "geometry_validated",
"selection_status": (
"geometry_validated" if geometry_pass else "unresolved"
),
}
_write_json(designir_path, payload)
applied_experience_ids = _applied_experience_ids(
@@ -525,6 +710,41 @@ def reconstruct_uploaded_step(
"policy": "Only parameters that passed an isolated perturbation test are exposed.",
}
_write_json(parameters_path, parameter_catalog)
brep_hard_gate_passed = brep_comparison.get("hard_gate_passed") is True
brep_available = brep_comparison.get("status") in {"passed", "mismatch"}
quality_tier = (
"brep_exact"
if geometry_pass and brep_hard_gate_passed
else "geometry_summary_accepted_brep_mismatch"
if geometry_pass and brep_available
else "geometry_accepted_brep_unavailable"
if geometry_pass
else "review_required"
)
published_artifacts = [
{"path": rebuilt_path.name, "role": "primary_step"},
{"path": designir_path.name, "role": "editable_designir"},
{"path": parameters_path.name, "role": "validated_parameters"},
{
"path": str(geometry_report_path.relative_to(task_dir)),
"role": "geometry_acceptance",
},
{
"path": str(edit_report_path.relative_to(task_dir)),
"role": "parameter_acceptance",
},
{
"path": str(strategy_report_path.relative_to(task_dir)),
"role": "boundary_strategy_acceptance",
},
]
if brep_report_path.is_file():
published_artifacts.append(
{
"path": str(brep_report_path.relative_to(task_dir)),
"role": "strict_brep_acceptance",
}
)
task_manifest = {
"schema_version": "1.0",
@@ -533,8 +753,12 @@ def reconstruct_uploaded_step(
"route": {
"router": "cad-router",
"runtime": "DesignIR 3.0 / SurfaceIR",
"selected_backend": "build123d",
"selected_backend": semantic_backend,
"execution_adapter": "surfaceir_occt",
"validation_adapters": [
"surfaceir_geometry_acceptance",
"simplecadapi_brep_compare",
],
"source_of_truth": designir_path.name,
},
"source": {
@@ -542,19 +766,7 @@ def reconstruct_uploaded_step(
"role": "teacher_and_acceptance_truth_only",
"embedded_in_output": False,
},
"artifacts": [
{"path": rebuilt_path.name, "role": "primary_step"},
{"path": designir_path.name, "role": "editable_designir"},
{"path": parameters_path.name, "role": "validated_parameters"},
{
"path": str(geometry_report_path.relative_to(task_dir)),
"role": "geometry_acceptance",
},
{
"path": str(edit_report_path.relative_to(task_dir)),
"role": "parameter_acceptance",
},
],
"artifacts": published_artifacts,
"experience": {
"library": (
str(EXPERIENCE_LIBRARY)
@@ -566,7 +778,11 @@ def reconstruct_uploaded_step(
"methods_applied": applied_experience_ids,
},
"validation": {
"geometry_accepted": True,
"geometry_accepted": geometry_pass,
"geometry_usable": usable_reconstruction,
"brep_comparison_status": brep_comparison.get("status"),
"brep_hard_gate_passed": brep_hard_gate_passed,
"quality_tier": quality_tier,
"source_independent_rebuild": True,
"anti_cheat_pass": True,
"validated_parameter_count": len(accepted_parameters),
@@ -592,7 +808,15 @@ def reconstruct_uploaded_step(
return {
"schema_version": "1.0",
"status": (
"accepted"
(
"accepted"
if geometry_pass
and (
brep_hard_gate_passed
or brep_comparison.get("status") == "unavailable"
)
else "review_required"
)
if payload["document_status"] == "geometry_present"
else "accepted_empty_source"
),
@@ -603,7 +827,13 @@ def reconstruct_uploaded_step(
"task_manifest": str(task_manifest_path),
"geometry_report": str(geometry_report_path),
"edit_report": str(edit_report_path),
"geometry_accepted": True,
"geometry_accepted": geometry_pass,
"geometry_usable": usable_reconstruction,
"brep_comparison": brep_comparison,
"brep_report": (
str(brep_report_path) if brep_report_path.is_file() else None
),
"quality_tier": quality_tier,
"document_status": payload["document_status"],
"validated_parameter_count": len(accepted_parameters),
"validated_parameter_names": [
@@ -620,6 +850,10 @@ def reconstruct_uploaded_step(
"stages": {
"extraction": extraction,
"geometry_validation": geometry_result,
"brep_validation": {
"engine": "SimpleCADAPI 2.0.2",
"status": brep_comparison.get("status"),
},
},
}
@@ -631,6 +865,12 @@ def build_parser() -> argparse.ArgumentParser:
parser.add_argument("--request", default="重建上传的 STEP")
parser.add_argument("--maximum-parameters", type=int, default=8)
parser.add_argument("--timeout-seconds", type=int, default=180)
parser.add_argument(
"--semantic-backend",
choices=("build123d", "simplecadapi"),
default="build123d",
help="Preferred editable semantic backend after SurfaceIR reconstruction.",
)
return parser
@@ -643,6 +883,7 @@ def main(argv: list[str] | None = None) -> int:
request=args.request,
maximum_parameters=args.maximum_parameters,
timeout_seconds=args.timeout_seconds,
semantic_backend=args.semantic_backend,
)
except (ReconstructionError, subprocess.TimeoutExpired) as exc:
print(
+244 -24
View File
@@ -5,9 +5,11 @@ import argparse
import importlib.util
import json
import os
import re
import subprocess
import sys
from dataclasses import dataclass, field
from importlib.metadata import PackageNotFoundError, version
from pathlib import Path
@@ -28,19 +30,31 @@ class BackendScore:
KEYWORD_GROUPS: dict[str, tuple[str, ...]] = {
"special_mechanical": (
"standard_factory": (
"gear", "gears", "齿轮", "齿条", "rack", "ring gear", "内齿圈",
"cycloidal", "摆线", "bearing", "轴承", "planetary", "行星",
"reducer", "gearbox", "减速器", "减速箱", "joint actuator", "关节执行器",
"bevel gear", "锥齿轮", "sprocket", "链轮", "roller chain", "滚子链",
"bolt", "螺栓", "nut", "螺母", "fastener", "紧固件",
),
"general_mechanical": (
"bracket", "支架", "connecting rod", "连杆", "crank", "曲柄",
"lever", "摇臂", "shaft", "轴", "bushing", "轴套", "flange", "法兰",
"housing", "壳体", "enclosure", "外壳", "fixture", "夹具", "plate", "安装板",
),
"replay": (
"semantic_modeling": (
"replay", "重放", "operation graph", "model json", "模型图", "semantic tag",
"语义标签",
"语义标签", "topology", "拓扑追踪", "source mapping", "来源映射",
"unit", "单位", "tolerance", "公差", "公差链", "scene.zip",
"freecad", "fusion 360", "solidworks",
),
"custom_surface": (
"freeform", "自由曲面", "organic", "有机曲面", "nurbs", "复杂曲面",
"surface patch", "曲面补片", "multi rail", "多轨扫掠",
),
"mechanism": (
"mechanism", "机构", "motion", "运动关系", "gear constraint", "齿轮约束",
"belt constraint", "皮带约束", "revolute", "转动副", "prismatic", "移动副",
),
"visual_complexity": (
"image", "photo", "sketch", "图片", "照片", "草图", "复杂", "complex",
@@ -186,18 +200,98 @@ def infer_experience_query(request: str) -> tuple[str | None, list[str]]:
def probe_backends() -> dict[str, dict[str, object]]:
def distribution_version(name: str) -> str | None:
try:
return version(name)
except PackageNotFoundError:
return None
simplecad_version = distribution_version("simplecadapi")
return {
"build123d": {
"available": importlib.util.find_spec("build123d") is not None,
"version": distribution_version("build123d"),
"probe": f"python={sys.executable}",
},
"simplecadapi": {
"available": importlib.util.find_spec("simplecadapi") is not None,
"version": simplecad_version,
"minimum_version": "2.0.2",
"compatible": bool(
simplecad_version
and tuple(
int(part)
for part in re.findall(r"\d+", simplecad_version)[:3]
)
>= (2, 0, 2)
),
"probe": f"python={sys.executable}",
},
}
def _source_backend_hint(edit_source: Path | None) -> str | None:
"""Read only an explicitly supplied editable source to preserve its backend."""
if edit_source is None:
return None
source = edit_source.expanduser()
if not source.is_file():
return None
if source.name.endswith(".designir.json"):
try:
payload = json.loads(source.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return None
hint = payload.get("backend_hint")
if hint in {"build123d", "simplecadapi"}:
return hint
compiler = payload.get("compiler")
if isinstance(compiler, dict) and compiler.get("backend") in {
"build123d",
"simplecadapi",
}:
return str(compiler["backend"])
if source.suffix.lower() == ".py":
try:
prefix = source.read_text(encoding="utf-8")[:100_000].lower()
except OSError:
return None
if "import simplecadapi" in prefix or "from simplecadapi" in prefix:
return "simplecadapi"
if "import build123d" in prefix or "from build123d" in prefix:
return "build123d"
return None
def _routing_requirements(
text: str,
*,
edit_context: dict[str, object] | None,
assembly: bool,
source_backend: str | None,
) -> list[str]:
requirements = ["step_export", "editable_designir"]
if contains_any(text, KEYWORD_GROUPS["standard_factory"]):
requirements.append("standard_mechanical_factory")
if contains_any(text, KEYWORD_GROUPS["semantic_modeling"]):
requirements.append("semantic_graph_and_replay")
if contains_any(text, KEYWORD_GROUPS["custom_surface"]):
requirements.append("custom_surface_vocabulary")
if contains_any(text, KEYWORD_GROUPS["mechanism"]):
requirements.append("mechanism_constraints")
if assembly:
requirements.append("assembly")
if source_backend:
requirements.append(f"preserve_{source_backend}_source")
if edit_context and edit_context.get("modification_mode") == (
"private_evidence_to_isolated_designir_reconstruction"
):
requirements.extend(
["source_independent_surfaceir_rebuild", "strict_brep_validation"]
)
return list(dict.fromkeys(requirements))
EXPERIENCE_FORBIDDEN_KEYS = {
"parameters",
"parameter_examples",
@@ -458,34 +552,125 @@ def route(args: argparse.Namespace) -> dict[str, object]:
"surfaceir" if teacher_step_reconstruction else "router",
)
needs_source_classification = bool(edit_context and not experience_family)
source_backend = _source_backend_hint(args.edit_source)
requirements = _routing_requirements(
text,
edit_context=edit_context,
assembly=args.assembly,
source_backend=source_backend,
)
scores = {
"build123d": BackendScore("build123d", 35, ["+35 general STEP-first default"]),
"simplecadapi": BackendScore("simplecadapi", 20, ["+20 specialist mechanical baseline"]),
"build123d": BackendScore(
"build123d",
30,
["+30 broad custom mechanical geometry coverage"],
),
"simplecadapi": BackendScore(
"simplecadapi",
30,
["+30 replayable OCP-native parametric coverage"],
),
}
availability = probe_backends() if args.probe else None
if availability:
for backend_name, facts in availability.items():
if facts.get("available") is not True:
scores[backend_name].add(
-10_000, "backend is unavailable in the probed runtime"
)
if availability["simplecadapi"].get("compatible") is not True:
scores["simplecadapi"].add(
-10_000,
"installed SimpleCADAPI does not satisfy the 2.0.2 capability contract",
)
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["standard_factory"]):
scores["simplecadapi"].add(
85, "a deterministic standard mechanical factory is available"
)
scores["build123d"].add(10, "the family can be modeled 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["replay"]):
scores["simplecadapi"].add(60, "replay or semantic graph requirement")
scores["build123d"].add(
32, "custom mechanical part benefits from broad feature construction"
)
scores["simplecadapi"].add(
24, "general OCP feature modeling and semantic tagging are available"
)
if contains_any(text, KEYWORD_GROUPS["semantic_modeling"]):
scores["simplecadapi"].add(
70,
"model replay, semantic lineage, units, tolerances, or translation is required",
)
scores["build123d"].add(
8, "editable Python can preserve parameters but has no equivalent unified graph"
)
if contains_any(text, KEYWORD_GROUPS["custom_surface"]):
scores["build123d"].add(
65, "custom/freeform surface vocabulary favors the general CAD backend"
)
scores["simplecadapi"].add(
-20, "requested surface vocabulary may exceed deterministic SDK coverage"
)
if contains_any(text, KEYWORD_GROUPS["mechanism"]):
scores["simplecadapi"].add(
38, "typed mechanism constraints and replayable products are available"
)
scores["build123d"].add(
24, "source-level joints and assemblies are available"
)
if edit_context:
scores["build123d"].add(55, "existing model inspection or source modification")
if source_backend:
scores[source_backend].add(
120, f"preserve the explicitly supplied {source_backend} source of truth"
)
other = "simplecadapi" if source_backend == "build123d" else "build123d"
scores[other].add(
-35, "backend conversion would risk feature-history and edit-contract loss"
)
elif edit_context and not teacher_step_reconstruction:
scores["build123d"].add(
20, "general existing-source inspection and local feature repair"
)
scores["simplecadapi"].add(
20, "semantic graph editing is available when the source supports it"
)
if teacher_step_reconstruction:
# Uploaded STEP reconstruction is executed by SurfaceIR. Backend scores
# describe the preferred semantic authoring backend after reconstruction,
# not the source-independent rebuild engine itself.
scores["build123d"].add(
18, "broad semantic feature vocabulary can author future repairs"
)
scores["simplecadapi"].add(
18, "strict B-Rep comparison and semantic lineage strengthen acceptance"
)
if args.assembly:
scores["build123d"].add(15, "source-level assembly workflow")
scores["simplecadapi"].add(18, "mechanism and constraint workflow")
scores["build123d"].add(35, "editable STEP is the fixed artifact contract")
scores["build123d"].add(25, "source-level assembly and datum workflow")
scores["simplecadapi"].add(
25, "replayable product, connector, and constraint workflow"
)
scores["build123d"].add(15, "STEP export and editable Python satisfy the contract")
scores["simplecadapi"].add(
30, "OCP STEP export satisfies the fixed artifact contract"
15, "STEP export plus model JSON satisfy the contract"
)
if experience_context and experience_context["experiences"]:
scores["build123d"].add(18, "generalized experience can guide parametric feature composition")
scores["simplecadapi"].add(18, "generalized experience can guide semantic feature composition")
scores["build123d"].add(
18, "generalized experience can guide parametric feature composition"
)
scores["simplecadapi"].add(
24, "generalized experience can bind to semantic roles and operation graphs"
)
forced_backend = args.backend if args.backend != "auto" else None
if (
forced_backend
and availability
and scores[forced_backend].score < -5_000
):
raise ValueError(
f"Explicit backend '{forced_backend}' is unavailable or incompatible "
"in the probed runtime"
)
ranked = sorted(scores.values(), key=lambda item: (-item.score, item.name))
if forced_backend:
selected = scores[forced_backend]
@@ -500,7 +685,7 @@ def route(args: argparse.Namespace) -> dict[str, object]:
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"],
KEYWORD_GROUPS["general_mechanical"] + KEYWORD_GROUPS["standard_factory"],
):
profiles.append("requirement_refinement")
profiles = list(dict.fromkeys(profiles))
@@ -523,6 +708,29 @@ def route(args: argparse.Namespace) -> dict[str, object]:
"project": backend_info["project"],
"adapter": backend_info.get("adapter"),
"confidence": confidence,
"routing_policy": {
"version": "2.0",
"default_backend": None,
"selection_scope": (
"post_reconstruction_semantic_backend"
if teacher_step_reconstruction
else "authoring_backend"
),
"requirements": requirements,
"native_source_backend": source_backend,
"score_margin": gap,
"effective_execution_engine": (
"surfaceir" if teacher_step_reconstruction else selected.name
),
"validation_engines": (
[
"surfaceir_geometry_acceptance",
"simplecadapi_brep_compare",
]
if teacher_step_reconstruction
else ["backend_geometry_validation"]
),
},
"backend_scores": [
{"backend": item.name, "score": item.score, "reasons": item.reasons}
for item in sorted(scores.values(), key=lambda item: (-item.score, item.name))
@@ -631,10 +839,10 @@ def route(args: argparse.Namespace) -> dict[str, object]:
),
},
"project_contributions": {
"text-to-cad": "core Skill host, build123d CAD backend, refinement, visual repair, validation, artifacts, and CAD Viewer handoff",
"SimpleCADAPI": "specialized mechanical backend with replayable graphs and semantic modeling",
"text-to-cad": "backend-neutral orchestrator, DesignIR owner, SurfaceIR reconstruction, refinement, repair, validation, artifacts, and Viewer handoff",
"SimpleCADAPI": "equal-priority OCP backend for standard factories, replayable graphs, semantic lineage, units, tolerances, translators, and B-Rep diagnostics",
},
"availability": probe_backends() if args.probe else None,
"availability": availability,
}
if teacher_step_reconstruction:
result["execution_adapter"] = {
@@ -642,6 +850,7 @@ def route(args: argparse.Namespace) -> dict[str, object]:
"runtime": "DesignIR 3.0 / SurfaceIR",
"automatic_geometry_acceptance": True,
"automatic_parameter_acceptance": True,
"supplemental_brep_acceptance": "SimpleCADAPI 2.0.2",
}
return result
@@ -684,6 +893,8 @@ def execute_reconstruction(
str(args.maximum_parameters),
"--timeout-seconds",
str(args.timeout_seconds),
"--semantic-backend",
str(result["selected_backend"]),
]
completed = subprocess.run(
command,
@@ -703,13 +914,22 @@ def execute_reconstruction(
def explain(result: dict[str, object]) -> str:
policy = result.get("routing_policy", {})
lines = [
f"Selected backend: {result['selected_backend']}",
f"Runner skill: ${result['runner_skill']}",
f"Project: {result['project']}",
f"Confidence: {result['confidence']}",
"Effective execution engine: "
f"{policy.get('effective_execution_engine', result['selected_backend'])}",
f"Source of truth: {result['source_of_truth']}",
]
requirements = policy.get("requirements", [])
if requirements:
lines.append(f"Requirements: {', '.join(requirements)}")
validators = policy.get("validation_engines", [])
if validators:
lines.append(f"Validation engines: {', '.join(validators)}")
profiles = result["workflow_profiles"]
if profiles:
lines.append(f"Workflow profiles: {', '.join(profiles)}")