feat: integrate SimpleCADAPI 2.0.2 CAD workflows
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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)}")
|
||||
|
||||
Reference in New Issue
Block a user