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
+23 -5
View File
@@ -5,7 +5,7 @@ description: Route natural-language or image-guided CAD generation and modificat
# CAD Router
Choose a backend before modeling, keep its source as the editable authority, and normalize every result into one handoff contract. Use two projects by role: text-to-cad owns orchestration, general CAD, refinement, validation, repair, and viewing; SimpleCADAPI owns specialized mechanical families.
Choose a backend before modeling, keep its source as the editable authority, and normalize every result into one handoff contract. `text-to-cad` is the backend-neutral orchestrator and DesignIR owner, not a backend that must win by default. `build123d` and SimpleCADAPI 2.0.2 are equal-priority authoring backends selected from evidence. Uploaded STEP reconstruction is executed by SurfaceIR and independently checked by both the existing geometry validator and SimpleCADAPI B-Rep comparison.
## Required workflow
@@ -47,8 +47,8 @@ Choose a backend before modeling, keep its source as the editable authority, and
publication. A route decision alone is not a completed reconstruction.
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, and semantic tags.
- `build123d`: use `$cad`; prefer it for custom brackets, housings, shafts, flanges, fixtures, source-level assemblies, and freeform/custom surface work.
- `simplecadapi`: use an installed SimpleCADAPI 2.0.2 Skill/runtime; prefer it when a standard factory exists, or when replayable ModelResult graphs, semantic lineage, physical units, tolerance chains, scene packages, or CAD translators materially improve the result. Its standard library includes gears, racks, ring and bevel gears, bearings, bolts, nuts, roller-chain sprockets, cycloidal parts, and reducer families.
5. When the route 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. Write one DesignIR 3.0 `*.designir.json` per part and treat it as the editable geometry contract; keep `cad-task.json` as the execution record. Read `references/designir-3.0.md` for both semantic authoring and uploaded STEP reconstruction. `references/designir-2.0.md` is migration-only. Never change backend during an edit unless conversion is explicitly requested; modify the recorded authoritative layer.
For a supplied STEP/STP, use `scripts/route.py --execute`; do not manually
@@ -60,21 +60,39 @@ Choose a backend before modeling, keep its source as the editable authority, and
The DesignIR and generator must not import STEP, embed B-Rep or meshes, retain
face references, or use the source model as a base feature. The acceptance
agent alone compares the teacher and reconstructed STEP.
Dense SurfaceIR may be stored losslessly in the DesignIR 3.0
`gzip+base64+json` envelope. CAD runtimes verify and decode it; LLM prompts
receive only the semantic layer, edit interface, and validated parameter
catalog, never the encoded geometry payload.
Only expose parameters listed by `parameters.json`; every listed parameter
has passed an isolated perturbation test. Other inferred names remain
evidence, not a promised edit interface.
The acceptance zone also runs the installed SimpleCADAPI 2.0.2 strict B-Rep
comparator. `brep_exact` requires both the bidirectional geometric-set check
and the geometry-labelled incidence graph check. A geometry-equal but
topology-different result is returned as `review_required`, not silently
described as an exact reconstruction. Failure or absence of the supplemental
comparator must be recorded separately and must not be confused with a pass.
7. Validate against the brief. Run STEP geometry inspection and snapshot review.
For experience-informed generation, record which generalized methods were
applied, including the reconstruction grammar ID and instantiated symbolic
roles. Validate against the user's brief and held-out targets, never against
parameters exposed from the same source case.
8. Write a `cad-task.json` manifest using the contract in `references/backend-contract.md`. Hand the STEP artifact to `$cad-viewer` and return its live link.
9. Export URDF or MJCF only on explicit request. Run the deterministic
DesignIR robot-description exporter lazily and return its portable ZIP with
XML, STL visual/collision geometry, and export manifest. When authoritative
assembly/joint/material evidence is absent, export one fixed `base_link`;
never invent joints, limits, actuators, mass, or inertia.
## Routing rules
- Prefer an explicit user backend when it can produce the editable STEP contract.
- 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.
- Preserve the backend of an explicitly supplied DesignIR or Python generator unless conversion is requested.
- Do not assign a permanent default backend. Score native-source continuity, factory coverage, semantic/replay needs, geometry vocabulary, assembly needs, and validation needs.
- Prefer SimpleCADAPI when a supported standard mechanical factory or its model graph, semantic topology, unit/tolerance, scene, or translator capabilities materially improve editability and verification.
- Prefer build123d for custom machined parts, source-driven assemblies, and freeform or surface-heavy work not covered by a SimpleCADAPI factory or tested operation vocabulary.
- For uploaded STEP, report SurfaceIR as the effective reconstruction engine. The selected authoring backend describes future semantic repair/edit work; it is not allowed to read the teacher STEP during reconstruction.
- Prefer source quality and future modification over the shortest first generation.
## V1 boundaries
@@ -23,6 +23,10 @@ Capture these fields when known:
Record:
- `selected_backend`: `build123d` or `simplecadapi`.
- `routing_policy.effective_execution_engine`: the actual engine. It is
`surfaceir` for an uploaded STEP even though `selected_backend` records the
preferred semantic authoring backend for later edits.
- `routing_policy.validation_engines`: validators required by the chosen mode.
- Origin project and optional executable adapter.
- `runner_skill`: installed Skill responsible for generation.
- Ranked backend scores and human-readable reasons.
@@ -81,6 +85,11 @@ distillation corpus. The task publishes the independent DesignIR, reconstructed
STEP, validated parameter catalog, geometry report, edit report, and
`cad-task.json`. The reconstruction subprocess receives no teacher STEP path;
only extraction and acceptance may read the explicit uploaded source.
The acceptance stage records both SurfaceIR geometry validation and the
SimpleCADAPI B-Rep comparator. A B-Rep report may be `passed`, `mismatch`, or
`unavailable`; only `passed` earns the `brep_exact` quality tier. Geometry
equality with a topology mismatch remains usable for review but is not an exact
reconstruction.
## Backend adapter requirements
@@ -1,8 +1,25 @@
# Backend capabilities
## build123d through CAD Skill
## Selection principle
Use as the default STEP-first backend.
There is no permanent default authoring backend. `text-to-cad` owns routing,
DesignIR, validation, repair, artifacts, and Viewer handoff. It does not receive
extra score simply for being the orchestrator.
The router scores:
1. continuity with an explicitly supplied editable source;
2. deterministic standard-factory coverage;
3. semantic graph, replay, units, tolerances, and translation requirements;
4. custom profile and surface vocabulary;
5. assembly and mechanism requirements;
6. validation requirements.
Uploaded STEP reconstruction is a separate mode: SurfaceIR is the effective
rebuild engine, while SimpleCADAPI B-Rep comparison supplements the independent
acceptance stage.
## build123d through CAD Skill
Best fit:
@@ -11,20 +28,36 @@ Best fit:
- Source-level assemblies, named datums, joints, imported components, measurements, alignment checks, and existing STEP inspection.
- STEP output compiled from executable semantic DesignIR 3.0.
Weak signals: a specialized mechanical family already implemented by SimpleCADAPI or a browser-only execution requirement.
Weak signals: a standard mechanical family already implemented by
SimpleCADAPI, or a primary requirement for replayable operation graphs, typed
semantic topology, unit checking, tolerance chains, or CAD translation.
## SimpleCADAPI
Use for its standard mechanical factories and replayable modeling graph.
Best fit:
Best fit in version 2.0.2:
- Spur, helical, and herringbone gears; internal ring gears; racks; cycloidal discs; ball bearings.
- Spur, helical, herringbone, ring, and straight bevel gears; racks; cycloidal
discs; ball bearings; bolts; nuts; and roller-chain sprockets.
- Planetary and cycloidal reducers, joint actuators, and mechanism-heavy assemblies.
- Parameter expressions, operation-graph JSON, replay, semantic tags, and topology queries.
- `@model` / `ModelResult`, parameter expressions, operation-graph JSON, replay,
scene packages, semantic tags, topology lineage, source mapping, and QL.
- Physical units, dimension checking, and manufacturing tolerance chains.
- FreeCAD, Fusion 360, and SolidWorks translation.
- STEP B-Rep inspection, strict target/candidate comparison, multi-view
diagnostics, and slice XOR diagnostics.
- 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.
Weak signals: freeform or multi-rail surface construction outside the tested SDK
vocabulary, or an existing build123d source whose feature history must remain
authoritative.
Before invocation, inspect the installed package and Skill versions together.
Require version `2.0.2` or newer for the capabilities listed above. 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.
## Router refinement and repair
@@ -113,6 +113,49 @@ The adapter loads only formally promoted experiences whose scope is
applied conditionally by the runtime; `cad-task.json` records both the available
method count and the exact method IDs used for the task.
## Compact single-file storage
A DesignIR remains one source-independent JSON file. For surface-parametric and
hybrid documents, the dense `surface_layer` may use the lossless
`gzip+base64+json` storage envelope:
```json
{
"surface_layer": {
"format": "surfaceir-3.0",
"encoding": "gzip+base64+json",
"uncompressed_bytes": 10286354,
"compressed_bytes": 2358852,
"sha256": "<digest>",
"data": "<base64>"
}
}
```
The runtime verifies the byte counts and SHA-256 digest before rebuilding.
Decoding restores the same SurfaceIR object; no coordinate, topology, pcurve,
tolerance, parameter binding, or edit information is discarded. Legacy
documents containing an expanded `surface_layer` remain supported.
Agents must read the ordinary top-level `semantic_layer`, `edit_interface`, and
validated `parameters.json`. They must not place the encoded `surface_layer.data`
string in an LLM prompt. Deterministic CAD runtimes alone decode that field.
## Lazy URDF and MJCF export
URDF and MJCF are derived artifacts, not additional DesignIR authorities.
Generate them only after an explicit download/export request. The deterministic
exporter rebuilds from DesignIR alone and packages:
- the `.urdf` or `.mjcf.xml` description;
- an STL mesh used for visual and collision geometry;
- `robot-export.json` with source hash, scale, reconstruction strategy,
structural assumptions, and dynamics status.
STEP-derived geometry without authoritative assembly, joint, or material
evidence exports as one fixed `base_link`. Do not infer articulated joints,
limits, actuators, density, mass, or inertia merely from disconnected solids.
## Parameter boundary
STEP does not contain authoritative feature names or modeling history. The
@@ -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)}")