feat: unify CAD workflows on DesignIR 3.0
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: cad-router
|
||||
description: Route natural-language or image-guided CAD generation and modification, plus uploaded STEP/STP reconstruction and modification, across text-to-cad and SimpleCADAPI. Create one executable DesignIR 2.0 model per part, produce STEP geometry without teacher geometry dependencies, and enforce independent validation, visual repair, and CAD Viewer review.
|
||||
description: Route natural-language or image-guided CAD generation and modification, plus uploaded STEP/STP reconstruction and modification, across text-to-cad and SimpleCADAPI. Use unified executable DesignIR 3.0 in semantic-feature mode for authored models and SurfaceIR mode for uploaded STEP reconstruction, without teacher geometry dependencies, with independent validation and CAD Viewer review.
|
||||
---
|
||||
|
||||
# CAD Router
|
||||
@@ -9,7 +9,7 @@ Choose a backend before modeling, keep its source as the editable authority, and
|
||||
|
||||
## Required workflow
|
||||
|
||||
1. Convert the natural-language or image-guided request into a brief: part type, dimensions, units, assembly relationships, editability, and validation targets. The artifact contract is fixed to executable DesignIR 2.0 plus STEP geometry.
|
||||
1. Convert the natural-language or image-guided request into a brief: part type, dimensions, units, assembly relationships, editability, and validation targets. Every model uses DesignIR 3.0 plus STEP geometry. New text/image models use `fully_semantic_parametric`; uploaded STEP reconstruction uses `surface_parametric`. A generated semantic model may include a compiled SurfaceIR snapshot and then uses `hybrid_semantic_surface_parametric`, while its semantic layer remains authoritative.
|
||||
Never treat a teacher STEP or a single-case DesignIR as experience. Batch case
|
||||
JSON belongs to the separate CAD Experience plugin. Generation may consume
|
||||
only its promoted schema `2.0` generalized library.
|
||||
@@ -31,22 +31,38 @@ Choose a backend before modeling, keep its source as the editable authority, and
|
||||
dimensionless distributions second. Use `--experience-library`,
|
||||
`--experience-family`, or repeated `--experience-feature` only to override
|
||||
those defaults.
|
||||
For one uploaded STEP, execute the route rather than stopping after the
|
||||
routing decision:
|
||||
|
||||
```bash
|
||||
../../.venv/bin/python scripts/route.py \
|
||||
"重建上传的 STEP,并返回可修改参数" \
|
||||
--edit-source /absolute/path/to/upload.step \
|
||||
--task-dir /absolute/path/to/task \
|
||||
--execute
|
||||
```
|
||||
|
||||
This command must finish extraction, source-independent reconstruction,
|
||||
geometry acceptance, parameter perturbation acceptance, and artifact
|
||||
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.
|
||||
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.json` per part and treat it as the editable geometry contract; keep `cad-task.json` as the execution record. Read `references/designir-2.0.md` before creating, reconstructing, or modifying a model. Never change backend during an edit unless conversion is explicitly requested; modify the recorded DesignIR source of truth.
|
||||
For a supplied STEP/STP, the deterministic ingestion service may extract
|
||||
private teacher evidence. The reconstruction agent receives that evidence,
|
||||
never the STEP itself, and must author complete executable DesignIR. Compile
|
||||
it in an isolated environment where the teacher STEP is absent.
|
||||
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
|
||||
approximate the model when the executable adapter is available. The
|
||||
deterministic ingestion service extracts private teacher evidence into
|
||||
DesignIR 3.0. The independent rebuild subprocess receives the DesignIR and
|
||||
output path, never the STEP path. The acceptance stage alone receives both
|
||||
teacher and reconstruction.
|
||||
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.
|
||||
If the request does not name the source part family, inspect the explicit
|
||||
source first and rerun the experience query with `--experience-family` and
|
||||
`--experience-feature`; do not guess across family-scoped experience.
|
||||
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.
|
||||
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
|
||||
@@ -70,6 +86,6 @@ Choose a backend before modeling, keep its source as the editable authority, and
|
||||
|
||||
- Read `references/capabilities.md` when comparing or invoking backends.
|
||||
- Read `references/backend-contract.md` before writing a task manifest or adding another backend.
|
||||
- Read `references/designir-2.0.md` when generating, reconstructing, or
|
||||
modifying a per-part executable design.
|
||||
- Read `references/designir-3.0.md` for semantic authoring, uploaded STEP reconstruction, and edits.
|
||||
- Read `references/designir-2.0.md` only when migrating legacy artifacts.
|
||||
- `scripts/capabilities.json` is the machine-readable V1 registry used by `scripts/route.py`.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
interface:
|
||||
display_name: "CAD Router"
|
||||
short_description: "Route CAD and manage per-part JSON model specs."
|
||||
default_prompt: "Use $cad-router to select the best CAD backend, create or modify the part through DesignIR 2.0, validate the result independently, and open the STEP in CAD Viewer."
|
||||
default_prompt: "Use $cad-router to select the best CAD backend, create or modify the part through unified DesignIR 3.0, validate the result independently, and open the STEP in CAD Viewer."
|
||||
|
||||
@@ -70,14 +70,17 @@ Write `cad-task.json` beside the task artifacts with this minimum shape:
|
||||
|
||||
Paths are relative to the task directory unless an external input must remain absolute.
|
||||
Do not store authoritative model parameters in `cad-task.json`. Every part must
|
||||
have a sibling `*.designir.json` that owns its coordinate system, datums,
|
||||
parameters, expressions, features, constraints, edit interface, and validation
|
||||
contract. See `designir-2.0.md`.
|
||||
have a sibling `*.designir.json` as its editable source of truth. Authored
|
||||
feature models use semantic DesignIR 3.0; uploaded STEP reconstruction uses
|
||||
DesignIR 3.0 / SurfaceIR. See `designir-3.0.md`; `designir-2.0.md` is retained
|
||||
only for legacy migration.
|
||||
|
||||
For an uploaded STEP, keep teacher geometry in `designir-pipeline/input/` and
|
||||
all private evidence, DesignIR candidates, rebuilds, and acceptance reports in
|
||||
`designir-pipeline/runs/`. The reconstruction task receives evidence but no
|
||||
teacher STEP or imported B-Rep.
|
||||
For an uploaded STEP, use `scripts/route.py --execute` with an explicit source
|
||||
and task directory. Do not move an ordinary upload into or scan the
|
||||
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.
|
||||
|
||||
## Backend adapter requirements
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ Best fit:
|
||||
- Brackets, connecting rods, cranks, levers, shafts, bushings, flanges, plates, housings, covers, enclosures, fixtures, and machined features.
|
||||
- Parametric Python sources with holes, pockets, slots, bosses, ribs, shells, fillets, chamfers, lofts, sweeps, and revolves.
|
||||
- Source-level assemblies, named datums, joints, imported components, measurements, alignment checks, and existing STEP inspection.
|
||||
- STEP output compiled from executable DesignIR 2.0.
|
||||
- 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.
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# DesignIR 2.0 contract
|
||||
|
||||
> Legacy migration reference only. New text, image, and STEP workflows must
|
||||
> emit DesignIR 3.0. Use `designir_pipeline.py migrate` to preserve an existing
|
||||
> 2.0 semantic feature program inside the DesignIR 3.0 envelope.
|
||||
|
||||
Every generated, reconstructed, or modified part owns one executable
|
||||
`*.designir.json`. It records design intent rather than source faces or a copied
|
||||
solid.
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
# DesignIR 3.0 unified contract
|
||||
|
||||
DesignIR 3.0 is the only editable part contract. It has three explicit modes:
|
||||
|
||||
- `fully_semantic_parametric`: authored from text or images. `semantic_layer`
|
||||
contains design intent, named parameters, constraints and executable
|
||||
features.
|
||||
- `surface_parametric`: inferred from an uploaded STEP. `surface_layer`
|
||||
contains analytic/B-spline surfaces, boundary curves, pcurves, vertices,
|
||||
tolerances, shells and solids in CadSet's vocabulary.
|
||||
- `hybrid_semantic_surface_parametric`: a semantic feature program plus a
|
||||
compiled SurfaceIR snapshot extracted from its generated STEP. The semantic
|
||||
layer remains authoritative.
|
||||
|
||||
The two authoring paths share one envelope and edit vocabulary, but they do not
|
||||
claim equivalent evidence. Text/image generation authors design intent. STEP
|
||||
ingestion infers semantics from final B-Rep and never claims to recover the
|
||||
source CAD application's sketch or feature history.
|
||||
|
||||
## Text and image authoring
|
||||
|
||||
Author these top-level values:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": "3.0",
|
||||
"designir_kind": "independent_parametric_cad",
|
||||
"model_id": "part_id",
|
||||
"family": "semantic_part_family",
|
||||
"units": "mm",
|
||||
"document_status": "geometry_present",
|
||||
"reconstruction_mode": "fully_semantic_parametric",
|
||||
"authoring_mode": "semantic_feature_program",
|
||||
"backend_hint": "build123d",
|
||||
"semantic_layer": {
|
||||
"reconstruction_status": "ready",
|
||||
"coordinate_system": {
|
||||
"origin": [0, 0, 0],
|
||||
"x_axis": [1, 0, 0],
|
||||
"y_axis": [0, 1, 0],
|
||||
"z_axis": [0, 0, 1]
|
||||
},
|
||||
"datums": {},
|
||||
"parameters": {},
|
||||
"expressions": {},
|
||||
"sketches": [],
|
||||
"constraints": [],
|
||||
"features": [],
|
||||
"patterns": [],
|
||||
"attachments": [],
|
||||
"construction_stages": []
|
||||
},
|
||||
"edit_interface": {
|
||||
"semantic_parameters": [],
|
||||
"surface_parameter_groups": [],
|
||||
"modification_levels": ["semantic_feature"],
|
||||
"preserved_interfaces": []
|
||||
},
|
||||
"validation_contract": {
|
||||
"source_independence": true,
|
||||
"geometry_checks": ["solid_count", "bounding_box", "volume"],
|
||||
"edit_checks": ["parameter_perturbation"],
|
||||
"invariants": [],
|
||||
"perturbations": [],
|
||||
"thresholds": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Every name in `semantic_parameters` must identify an editable parameter, drive
|
||||
at least one feature, and have a perturbation test. Prefer intent names such as
|
||||
`flange_thickness`, `shelf_depth`, `hole_spacing`, `rib_count`,
|
||||
`bore_diameter`, and `bolt_circle_diameter`.
|
||||
|
||||
After isolated compilation, attach SurfaceIR only from the generated STEP and
|
||||
switch the mode to `hybrid_semantic_surface_parametric`. Never use a teacher
|
||||
STEP to fill the compiled snapshot.
|
||||
|
||||
## Uploaded STEP execution
|
||||
|
||||
Run an uploaded file through the router's executable adapter:
|
||||
|
||||
```bash
|
||||
../../.venv/bin/python scripts/route.py \
|
||||
"重建上传的 STEP,并返回可修改参数" \
|
||||
--edit-source /absolute/path/to/part.step \
|
||||
--task-dir /absolute/path/to/task \
|
||||
--execute
|
||||
```
|
||||
|
||||
The task directory contains:
|
||||
|
||||
- `<part>.designir.json`: independent editable source of truth;
|
||||
- `<part>.reconstructed.step`: reconstructed geometry;
|
||||
- `parameters.json`: only parameters that passed perturbation acceptance;
|
||||
- `validation/geometry.json`: teacher-versus-reconstruction measurements;
|
||||
- `validation/edits.json`: parameter acceptance outcomes;
|
||||
- `cad-task.json`: route, artifacts, isolation and validation state.
|
||||
|
||||
## Isolation boundary
|
||||
|
||||
Extraction and acceptance may read the uploaded STEP. The reconstruction
|
||||
subprocess receives only DesignIR and an output path. DesignIR must not contain
|
||||
a teacher path, an import operation, STEP text, embedded B-Rep, or a mesh
|
||||
substitute.
|
||||
|
||||
The runtime performs an anti-cheat scan before rebuilding. Geometry acceptance
|
||||
requires valid reconstructed topology and the thresholds recorded in
|
||||
`validation/geometry.json`.
|
||||
|
||||
The adapter loads only formally promoted experiences whose scope is
|
||||
`surfaceir_reconstruction` or `surfaceir_semantic_editing`. These methods are
|
||||
applied conditionally by the runtime; `cad-task.json` records both the available
|
||||
method count and the exact method IDs used for the task.
|
||||
|
||||
## Parameter boundary
|
||||
|
||||
STEP does not contain authoritative feature names or modeling history. The
|
||||
semantic layer therefore records inferred canonical parameters separately from
|
||||
validated editable parameters.
|
||||
|
||||
Only parameters in `parameters.json` are public edit controls. Each was changed
|
||||
in isolation, rebuilt, and checked for target response, preserved validity and
|
||||
non-target stability. Use the `edit_command_template` in `cad-task.json` to
|
||||
produce an edited DesignIR and STEP.
|
||||
|
||||
An accepted baseline reconstruction does not imply that every inferred
|
||||
parameter is editable. An empty `parameters.json` is valid for an empty source
|
||||
document and must not be presented as editable geometry.
|
||||
@@ -0,0 +1,661 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Execute one uploaded STEP -> DesignIR 3.0 reconstruction task.
|
||||
|
||||
The teacher STEP is passed only to deterministic extraction and acceptance
|
||||
commands. The independent rebuild subprocess receives the DesignIR path and
|
||||
output path only.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
SCRIPT_PATH = Path(__file__).resolve()
|
||||
SKILL_ROOT = SCRIPT_PATH.parents[1]
|
||||
WORKSPACE_ROOT = SKILL_ROOT.parents[2]
|
||||
SURFACEIR_PIPELINE = (
|
||||
WORKSPACE_ROOT / "designir-pipeline" / "scripts" / "surfaceir_pipeline.py"
|
||||
)
|
||||
EXPERIENCE_LIBRARY = (
|
||||
WORKSPACE_ROOT / "designir-pipeline" / "output" / "library.json"
|
||||
)
|
||||
DESIGNIR_SCHEMA = (
|
||||
WORKSPACE_ROOT
|
||||
/ "designir-pipeline"
|
||||
/ "contracts"
|
||||
/ "designir-3.0.schema.json"
|
||||
)
|
||||
|
||||
EXECUTABLE_EDIT_STATES = {
|
||||
"executable_enlarge_cut_binding",
|
||||
"executable_axis_affine_binding",
|
||||
"executable_uniform_scale_binding",
|
||||
"validated_executable_binding",
|
||||
}
|
||||
FORBIDDEN_DESIGNIR_TOKENS = (
|
||||
'"source_path"',
|
||||
'"teacher_step"',
|
||||
"teacher.step",
|
||||
"import_step",
|
||||
"stepcontrol_reader",
|
||||
'"brep"',
|
||||
'"mesh"',
|
||||
)
|
||||
|
||||
|
||||
class ReconstructionError(RuntimeError):
|
||||
"""Raised when a reconstruction stage cannot produce an accepted result."""
|
||||
|
||||
|
||||
def _runtime_python() -> Path:
|
||||
configured = os.environ.get("CAD_ROUTER_PYTHON", "").strip()
|
||||
candidates = [
|
||||
Path(configured).expanduser() if configured else None,
|
||||
WORKSPACE_ROOT / "text-to-cad" / ".venv" / "bin" / "python",
|
||||
Path(sys.executable),
|
||||
]
|
||||
for candidate in candidates:
|
||||
if candidate is not None and candidate.is_file():
|
||||
# Preserve a virtualenv launcher symlink. Resolving it to the base
|
||||
# interpreter discards the virtualenv's package search path.
|
||||
return candidate.absolute()
|
||||
raise ReconstructionError(
|
||||
"No CAD runtime Python found; set CAD_ROUTER_PYTHON or create "
|
||||
"text-to-cad/.venv."
|
||||
)
|
||||
|
||||
|
||||
def _safe_stem(source: Path) -> str:
|
||||
stem = re.sub(r"[^a-zA-Z0-9_.-]+", "_", source.stem).strip("._")
|
||||
return stem or "uploaded_part"
|
||||
|
||||
|
||||
def _write_json(path: Path, payload: dict[str, Any]) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(
|
||||
json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def _run_json(
|
||||
command: list[str],
|
||||
*,
|
||||
timeout_seconds: int,
|
||||
stage: str,
|
||||
) -> dict[str, Any]:
|
||||
completed = subprocess.run(
|
||||
command,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout_seconds,
|
||||
check=False,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
detail = completed.stderr.strip() or completed.stdout.strip()
|
||||
raise ReconstructionError(
|
||||
f"{stage} failed with exit code {completed.returncode}: "
|
||||
f"{detail[-2000:]}"
|
||||
)
|
||||
lines = [line for line in completed.stdout.splitlines() if line.strip()]
|
||||
if not lines:
|
||||
return {}
|
||||
try:
|
||||
return json.loads(lines[-1])
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ReconstructionError(
|
||||
f"{stage} returned non-JSON output: {lines[-1][-1000:]}"
|
||||
) from exc
|
||||
|
||||
|
||||
def _validate_designir_contract(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
required = {
|
||||
"schema_version",
|
||||
"designir_kind",
|
||||
"model_id",
|
||||
"units",
|
||||
"document_status",
|
||||
"reconstruction_mode",
|
||||
"semantic_layer",
|
||||
"surface_layer",
|
||||
"edit_interface",
|
||||
"validation_contract",
|
||||
}
|
||||
missing = sorted(required - set(payload))
|
||||
if missing:
|
||||
raise ReconstructionError(
|
||||
f"DesignIR 3.0 is missing required fields: {', '.join(missing)}"
|
||||
)
|
||||
if payload["schema_version"] != "3.0":
|
||||
raise ReconstructionError("Uploaded STEP did not produce DesignIR 3.0")
|
||||
if payload["designir_kind"] != "independent_parametric_cad":
|
||||
raise ReconstructionError("DesignIR is not source-independent")
|
||||
if payload["validation_contract"].get("source_independence") is not True:
|
||||
raise ReconstructionError("DesignIR source-independence contract is false")
|
||||
serialized = json.dumps(payload, ensure_ascii=False).lower()
|
||||
violations = [
|
||||
token for token in FORBIDDEN_DESIGNIR_TOKENS if token in serialized
|
||||
]
|
||||
if violations:
|
||||
raise ReconstructionError(
|
||||
"DesignIR anti-cheat scan failed: " + ", ".join(violations)
|
||||
)
|
||||
return {
|
||||
"schema_contract": "designir-3.0",
|
||||
"schema_path": str(DESIGNIR_SCHEMA),
|
||||
"required_fields_present": True,
|
||||
"source_independence_declared": True,
|
||||
"anti_cheat_pass": True,
|
||||
"forbidden_token_matches": 0,
|
||||
}
|
||||
|
||||
|
||||
def _candidate_parameters(
|
||||
payload: dict[str, Any], maximum: int
|
||||
) -> list[tuple[str, float]]:
|
||||
parameters = payload.get("semantic_layer", {}).get("parameters", {})
|
||||
names = payload.get("edit_interface", {}).get("semantic_parameters", [])
|
||||
feature_candidates: list[tuple[str, float]] = []
|
||||
fallback: tuple[str, float] | None = None
|
||||
for name in names:
|
||||
parameter = parameters.get(name)
|
||||
if not isinstance(parameter, dict):
|
||||
continue
|
||||
if parameter.get("edit_state") not in EXECUTABLE_EDIT_STATES:
|
||||
continue
|
||||
try:
|
||||
old_value = float(parameter["value"])
|
||||
except (KeyError, TypeError, ValueError):
|
||||
continue
|
||||
candidate = (name, old_value * 1.1)
|
||||
if name == "overall_scale":
|
||||
fallback = candidate
|
||||
else:
|
||||
feature_candidates.append(candidate)
|
||||
selected = feature_candidates[: max(0, maximum)]
|
||||
if fallback is not None:
|
||||
selected.append(fallback)
|
||||
return selected
|
||||
|
||||
|
||||
def _surfaceir_experience_ids() -> set[str]:
|
||||
if not EXPERIENCE_LIBRARY.is_file():
|
||||
return set()
|
||||
try:
|
||||
payload = json.loads(EXPERIENCE_LIBRARY.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return set()
|
||||
policy = payload.get("policy", {})
|
||||
if (
|
||||
payload.get("library_kind") != "generalized_cad_experience"
|
||||
or policy.get("surfaceir_consumable") is not True
|
||||
or policy.get("instance_parameters_allowed") is not False
|
||||
):
|
||||
return set()
|
||||
return {
|
||||
item["id"]
|
||||
for item in payload.get("experiences", [])
|
||||
if isinstance(item, dict)
|
||||
and isinstance(item.get("id"), str)
|
||||
and set(item.get("scope", [])).intersection(
|
||||
{"surfaceir_reconstruction", "surfaceir_semantic_editing"}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def _applied_experience_ids(
|
||||
available: set[str], accepted_parameters: list[dict[str, Any]]
|
||||
) -> list[str]:
|
||||
applied = {
|
||||
experience_id
|
||||
for experience_id in (
|
||||
"failure_repair.surface_topology.bind_face_pcurves_and_group_shells",
|
||||
"failure_repair.surface_topology.preserve_vertex_tolerance",
|
||||
)
|
||||
if experience_id in available
|
||||
}
|
||||
names = {parameter["name"] for parameter in accepted_parameters}
|
||||
if any("hole_diameter" in name or "bore_diameter" in name for name in names):
|
||||
applied.update(
|
||||
experience_id
|
||||
for experience_id in (
|
||||
"edit_strategy.semantic_cylindrical_cut.enlarge_diameter",
|
||||
"failure_repair.semantic_edit.merge_duplicate_cylindrical_targets",
|
||||
"edit_strategy.semantic_edit.preserve_validated_geometry_strategy",
|
||||
)
|
||||
if experience_id in available
|
||||
)
|
||||
if "overall_scale" in names:
|
||||
experience_id = (
|
||||
"edit_strategy.semantic_parameter.source_independent_overall_scale"
|
||||
)
|
||||
if experience_id in available:
|
||||
applied.add(experience_id)
|
||||
if names.intersection(
|
||||
{"outer_diameter", "body_length", "plate_thickness"}
|
||||
):
|
||||
experience_id = (
|
||||
"edit_strategy.semantic_parameter.primary_axis_affine_dimensions"
|
||||
)
|
||||
if experience_id in available:
|
||||
applied.add(experience_id)
|
||||
if any(name.startswith("overall_size_") for name in names):
|
||||
experience_id = (
|
||||
"edit_strategy.semantic_parameter.canonical_directional_envelope"
|
||||
)
|
||||
if experience_id in available:
|
||||
applied.add(experience_id)
|
||||
return sorted(applied)
|
||||
|
||||
|
||||
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"))
|
||||
parameters = payload.get("semantic_layer", {}).get("parameters", {})
|
||||
accepted_parameters: list[dict[str, Any]] = []
|
||||
attempted_names = set()
|
||||
for attempt in attempts:
|
||||
name = attempt["parameter"]
|
||||
attempted_names.add(name)
|
||||
parameter = parameters[name]
|
||||
old_value = float(parameter["value"])
|
||||
requested_value = float(attempt["requested_value"])
|
||||
if attempt["accepted"]:
|
||||
parameter["editable"] = True
|
||||
parameter["edit_state"] = "validated_executable_binding"
|
||||
parameter["validated_range"] = {
|
||||
"direction": "increase",
|
||||
"minimum_tested_inclusive": old_value,
|
||||
"maximum_tested_inclusive": requested_value,
|
||||
"acceptance_contract": "designir_3_semantic_edit_acceptance",
|
||||
}
|
||||
accepted_parameters.append(
|
||||
{
|
||||
"name": name,
|
||||
"semantic_role": parameter.get("semantic_role", name),
|
||||
"value": parameter["value"],
|
||||
"unit": parameter.get("unit", "mm"),
|
||||
"validated_range": parameter["validated_range"],
|
||||
"confidence": parameter.get("confidence"),
|
||||
}
|
||||
)
|
||||
else:
|
||||
parameter["editable"] = False
|
||||
parameter["edit_state"] = "rejected_by_runtime_validation"
|
||||
parameter.pop("validated_range", None)
|
||||
payload["edit_interface"]["semantic_parameters"] = sorted(
|
||||
parameter["name"] for parameter in accepted_parameters
|
||||
)
|
||||
payload["reconstruction_strategy"] = {
|
||||
"boundary_strategy": "exact_3d_pcurve",
|
||||
"selection_status": "geometry_validated",
|
||||
}
|
||||
_write_json(designir_path, payload)
|
||||
return payload, accepted_parameters
|
||||
|
||||
|
||||
def reconstruct_uploaded_step(
|
||||
source: Path,
|
||||
task_dir: Path,
|
||||
*,
|
||||
request: str,
|
||||
maximum_parameters: int = 8,
|
||||
timeout_seconds: int = 180,
|
||||
) -> dict[str, Any]:
|
||||
source = source.expanduser().resolve()
|
||||
task_dir = task_dir.expanduser().resolve()
|
||||
if not source.is_file():
|
||||
raise ReconstructionError(f"Uploaded STEP does not exist: {source}")
|
||||
if source.suffix.lower() not in {".step", ".stp"}:
|
||||
raise ReconstructionError("Uploaded reconstruction source must be STEP/STP")
|
||||
if source == task_dir or task_dir == source.parent:
|
||||
raise ReconstructionError(
|
||||
"Task directory must be separate from the uploaded STEP directory"
|
||||
)
|
||||
if source == task_dir or task_dir in source.parents:
|
||||
raise ReconstructionError(
|
||||
"Task directory cannot contain the uploaded teacher STEP"
|
||||
)
|
||||
if not SURFACEIR_PIPELINE.is_file():
|
||||
raise ReconstructionError(
|
||||
f"DesignIR 3.0 runtime is missing: {SURFACEIR_PIPELINE}"
|
||||
)
|
||||
|
||||
task_dir.mkdir(parents=True, exist_ok=True)
|
||||
stem = _safe_stem(source)
|
||||
designir_path = task_dir / f"{stem}.designir.json"
|
||||
rebuilt_path = task_dir / f"{stem}.reconstructed.step"
|
||||
validation_dir = task_dir / "validation"
|
||||
geometry_report_path = validation_dir / "geometry.json"
|
||||
edit_report_path = validation_dir / "edits.json"
|
||||
parameters_path = task_dir / "parameters.json"
|
||||
task_manifest_path = task_dir / "cad-task.json"
|
||||
collisions = [
|
||||
path
|
||||
for path in (
|
||||
designir_path,
|
||||
rebuilt_path,
|
||||
parameters_path,
|
||||
task_manifest_path,
|
||||
)
|
||||
if path.exists()
|
||||
]
|
||||
if collisions:
|
||||
raise ReconstructionError(
|
||||
"Refusing to overwrite existing task artifacts: "
|
||||
+ ", ".join(str(path) for path in collisions)
|
||||
)
|
||||
|
||||
runtime = _runtime_python()
|
||||
common = [str(runtime), str(SURFACEIR_PIPELINE)]
|
||||
available_experience_ids = _surfaceir_experience_ids()
|
||||
boundary_strategy = (
|
||||
"exact_3d_pcurve"
|
||||
if (
|
||||
"failure_repair.surface_topology.bind_face_pcurves_and_group_shells"
|
||||
in available_experience_ids
|
||||
)
|
||||
else "exact_3d"
|
||||
)
|
||||
|
||||
extraction = _run_json(
|
||||
common
|
||||
+ [
|
||||
"extract",
|
||||
str(source),
|
||||
"--output",
|
||||
str(designir_path),
|
||||
],
|
||||
timeout_seconds=timeout_seconds,
|
||||
stage="DesignIR 3.0 extraction",
|
||||
)
|
||||
payload = json.loads(designir_path.read_text(encoding="utf-8"))
|
||||
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,
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"input_count": 1,
|
||||
"rows": [
|
||||
{
|
||||
"source_name": source.name,
|
||||
"output_name": designir_path.name,
|
||||
"state": "extracted",
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
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",
|
||||
)
|
||||
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:
|
||||
raise ReconstructionError(
|
||||
"Independent reconstruction did not satisfy the geometry contract; "
|
||||
f"see {geometry_report_path}"
|
||||
)
|
||||
shutil.rmtree(geometry_work_dir)
|
||||
|
||||
attempts: list[dict[str, Any]] = []
|
||||
if payload["document_status"] == "geometry_present":
|
||||
for name, requested_value in _candidate_parameters(
|
||||
payload, maximum_parameters
|
||||
):
|
||||
safe_name = _safe_stem(Path(name))
|
||||
attempt_dir = validation_dir / "parameter-tests" / safe_name
|
||||
try:
|
||||
result = _run_json(
|
||||
common
|
||||
+ [
|
||||
"validate-edit",
|
||||
str(source),
|
||||
str(designir_path),
|
||||
"--parameter",
|
||||
name,
|
||||
"--value",
|
||||
str(requested_value),
|
||||
"--output-dir",
|
||||
str(attempt_dir),
|
||||
],
|
||||
timeout_seconds=timeout_seconds,
|
||||
stage=f"parameter validation ({name})",
|
||||
)
|
||||
report = json.loads(
|
||||
(attempt_dir / "acceptance-report.json").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
)
|
||||
attempts.append(
|
||||
{
|
||||
"parameter": name,
|
||||
"requested_value": requested_value,
|
||||
"accepted": bool(report.get("accepted")),
|
||||
"state": report.get("state"),
|
||||
"report": str(
|
||||
attempt_dir / "acceptance-report.json"
|
||||
),
|
||||
"worker": result,
|
||||
}
|
||||
)
|
||||
except (ReconstructionError, subprocess.TimeoutExpired) as exc:
|
||||
attempts.append(
|
||||
{
|
||||
"parameter": name,
|
||||
"requested_value": requested_value,
|
||||
"accepted": False,
|
||||
"state": "worker_failed",
|
||||
"error": str(exc),
|
||||
}
|
||||
)
|
||||
|
||||
payload, accepted_parameters = _persist_edit_results(
|
||||
designir_path, attempts
|
||||
)
|
||||
payload["reconstruction_strategy"] = {
|
||||
"boundary_strategy": boundary_strategy,
|
||||
"selection_status": "geometry_validated",
|
||||
}
|
||||
_write_json(designir_path, payload)
|
||||
applied_experience_ids = _applied_experience_ids(
|
||||
available_experience_ids, accepted_parameters
|
||||
)
|
||||
edit_report = {
|
||||
"schema_version": "1.0",
|
||||
"report_kind": "cad_router_runtime_parameter_acceptance",
|
||||
"attempt_count": len(attempts),
|
||||
"accepted_count": sum(row["accepted"] for row in attempts),
|
||||
"attempts": attempts,
|
||||
}
|
||||
_write_json(edit_report_path, edit_report)
|
||||
parameter_catalog = {
|
||||
"schema_version": "1.0",
|
||||
"model_id": payload["model_id"],
|
||||
"document_status": payload["document_status"],
|
||||
"parameter_count": len(accepted_parameters),
|
||||
"parameters": accepted_parameters,
|
||||
"policy": "Only parameters that passed an isolated perturbation test are exposed.",
|
||||
}
|
||||
_write_json(parameters_path, parameter_catalog)
|
||||
|
||||
task_manifest = {
|
||||
"schema_version": "1.0",
|
||||
"request": request,
|
||||
"operation": "uploaded_step_reconstruction",
|
||||
"route": {
|
||||
"router": "cad-router",
|
||||
"runtime": "DesignIR 3.0 / SurfaceIR",
|
||||
"selected_backend": "build123d",
|
||||
"execution_adapter": "surfaceir_occt",
|
||||
"source_of_truth": designir_path.name,
|
||||
},
|
||||
"source": {
|
||||
"format": source.suffix.lower().lstrip("."),
|
||||
"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",
|
||||
},
|
||||
],
|
||||
"experience": {
|
||||
"library": (
|
||||
str(EXPERIENCE_LIBRARY)
|
||||
if EXPERIENCE_LIBRARY.is_file()
|
||||
else None
|
||||
),
|
||||
"runtime_policy": "promoted_generalized_experience_only",
|
||||
"available_method_count": len(available_experience_ids),
|
||||
"methods_applied": applied_experience_ids,
|
||||
},
|
||||
"validation": {
|
||||
"geometry_accepted": True,
|
||||
"source_independent_rebuild": True,
|
||||
"anti_cheat_pass": True,
|
||||
"validated_parameter_count": len(accepted_parameters),
|
||||
},
|
||||
"edit_command_template": [
|
||||
str(runtime),
|
||||
str(SURFACEIR_PIPELINE),
|
||||
"edit",
|
||||
designir_path.name,
|
||||
"--parameter",
|
||||
"<validated-parameter-name>",
|
||||
"--value",
|
||||
"<value-within-validated-range>",
|
||||
"--output-designir",
|
||||
"<edited.designir.json>",
|
||||
"--output-step",
|
||||
"<edited.step>",
|
||||
],
|
||||
}
|
||||
_write_json(task_manifest_path, task_manifest)
|
||||
manifest_path.unlink(missing_ok=True)
|
||||
|
||||
return {
|
||||
"schema_version": "1.0",
|
||||
"status": (
|
||||
"accepted"
|
||||
if payload["document_status"] == "geometry_present"
|
||||
else "accepted_empty_source"
|
||||
),
|
||||
"task_dir": str(task_dir),
|
||||
"designir": str(designir_path),
|
||||
"rebuilt_step": str(rebuilt_path),
|
||||
"parameters": str(parameters_path),
|
||||
"task_manifest": str(task_manifest_path),
|
||||
"geometry_report": str(geometry_report_path),
|
||||
"edit_report": str(edit_report_path),
|
||||
"geometry_accepted": True,
|
||||
"document_status": payload["document_status"],
|
||||
"validated_parameter_count": len(accepted_parameters),
|
||||
"validated_parameter_names": [
|
||||
parameter["name"] for parameter in accepted_parameters
|
||||
],
|
||||
"experience": {
|
||||
"available_method_count": len(available_experience_ids),
|
||||
"methods_applied": applied_experience_ids,
|
||||
},
|
||||
"source_independence": {
|
||||
"rebuild_command_received_teacher_path": False,
|
||||
**contract_checks,
|
||||
},
|
||||
"stages": {
|
||||
"extraction": extraction,
|
||||
"geometry_validation": geometry_result,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--source", type=Path, required=True)
|
||||
parser.add_argument("--task-dir", type=Path, required=True)
|
||||
parser.add_argument("--request", default="重建上传的 STEP")
|
||||
parser.add_argument("--maximum-parameters", type=int, default=8)
|
||||
parser.add_argument("--timeout-seconds", type=int, default=180)
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = build_parser().parse_args(argv)
|
||||
try:
|
||||
result = reconstruct_uploaded_step(
|
||||
args.source,
|
||||
args.task_dir,
|
||||
request=args.request,
|
||||
maximum_parameters=args.maximum_parameters,
|
||||
timeout_seconds=args.timeout_seconds,
|
||||
)
|
||||
except (ReconstructionError, subprocess.TimeoutExpired) as exc:
|
||||
print(
|
||||
json.dumps(
|
||||
{"status": "failed", "error": str(exc)},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
print(json.dumps(result, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -5,6 +5,7 @@ import argparse
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
@@ -12,6 +13,7 @@ from pathlib import Path
|
||||
|
||||
SKILL_ROOT = Path(__file__).resolve().parents[1]
|
||||
REGISTRY_PATH = Path(__file__).with_name("capabilities.json")
|
||||
RECONSTRUCTION_ADAPTER = Path(__file__).with_name("reconstruct_step.py")
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -335,8 +337,10 @@ def load_experience_library(
|
||||
path: Path | None,
|
||||
family: str | None = None,
|
||||
features: list[str] | None = None,
|
||||
consumer: str = "router",
|
||||
) -> dict[str, object] | None:
|
||||
"""Load promoted generalized methods; reject case parameters and coordinates."""
|
||||
explicit_library = path is not None
|
||||
candidate = path or default_experience_library()
|
||||
if candidate is None:
|
||||
return None
|
||||
@@ -358,15 +362,26 @@ def load_experience_library(
|
||||
or policy.get("single_case_promotion_allowed") is not False
|
||||
):
|
||||
raise ValueError("CAD experience library policy permits instance-answer leakage")
|
||||
if (
|
||||
policy.get("llm_semantic_review_required") is not True
|
||||
or policy.get("draft_evidence_verified") is not True
|
||||
or policy.get("router_consumable") is not True
|
||||
):
|
||||
reviewed = (
|
||||
policy.get("llm_semantic_review_required") is True
|
||||
and policy.get("draft_evidence_verified") is True
|
||||
)
|
||||
consumer_flag = (
|
||||
"surfaceir_consumable"
|
||||
if consumer == "surfaceir"
|
||||
else "router_consumable"
|
||||
)
|
||||
if not reviewed:
|
||||
raise ValueError(
|
||||
"CAD experience library was not produced by LLM semantic review "
|
||||
"plus deterministic evidence verification"
|
||||
)
|
||||
if policy.get(consumer_flag) is not True:
|
||||
if not explicit_library:
|
||||
return None
|
||||
raise ValueError(
|
||||
f"CAD experience library is not approved for {consumer}"
|
||||
)
|
||||
forbidden = _find_forbidden_experience_key(payload)
|
||||
if forbidden:
|
||||
raise ValueError(f"CAD experience library contains forbidden instance key: {forbidden}")
|
||||
@@ -388,6 +403,12 @@ def load_experience_library(
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
scope = set(item.get("scope", []))
|
||||
if consumer == "surfaceir":
|
||||
if scope.intersection(
|
||||
{"surfaceir_reconstruction", "surfaceir_semantic_editing"}
|
||||
):
|
||||
selected.append(item)
|
||||
continue
|
||||
if "global" not in scope:
|
||||
if not compatible_scopes or scope.isdisjoint(compatible_scopes):
|
||||
continue
|
||||
@@ -402,6 +423,11 @@ def load_experience_library(
|
||||
"compatible_scopes": sorted(compatible_scopes),
|
||||
"requested_features": sorted(requested_features),
|
||||
"experiences": selected,
|
||||
"selection_policy": (
|
||||
"surfaceir_runtime_conditioned"
|
||||
if consumer == "surfaceir"
|
||||
else "request_family_and_feature_match"
|
||||
),
|
||||
"policy": {
|
||||
"contains_instance_parameters": False,
|
||||
"contains_absolute_coordinates": False,
|
||||
@@ -429,6 +455,7 @@ def route(args: argparse.Namespace) -> dict[str, object]:
|
||||
args.experience_library,
|
||||
experience_family,
|
||||
experience_features,
|
||||
"surfaceir" if teacher_step_reconstruction else "router",
|
||||
)
|
||||
needs_source_classification = bool(edit_context and not experience_family)
|
||||
scores = {
|
||||
@@ -480,7 +507,12 @@ def route(args: argparse.Namespace) -> dict[str, object]:
|
||||
|
||||
registry = json.loads(REGISTRY_PATH.read_text(encoding="utf-8"))
|
||||
backend_info = registry["backends"][selected.name]
|
||||
source_of_truth = "per-part DesignIR 2.0 with an isolated backend compiler"
|
||||
editable_contract = "DesignIR 3.0"
|
||||
source_of_truth = (
|
||||
"per-part DesignIR 3.0 with an isolated SurfaceIR compiler"
|
||||
if teacher_step_reconstruction
|
||||
else "per-part semantic DesignIR 3.0 with an isolated backend compiler"
|
||||
)
|
||||
gap = selected.score - max((scores[name].score for name in scores if name != selected.name), default=0)
|
||||
confidence = "high" if gap >= 40 else "medium" if gap >= 15 else "low"
|
||||
result: dict[str, object] = {
|
||||
@@ -499,7 +531,7 @@ def route(args: argparse.Namespace) -> dict[str, object]:
|
||||
"workflow_profiles": profiles,
|
||||
"artifact_contract": {
|
||||
"primary_format": "step",
|
||||
"editable_contract": "DesignIR 2.0",
|
||||
"editable_contract": editable_contract,
|
||||
},
|
||||
"source_of_truth": source_of_truth,
|
||||
"experience_context": experience_context,
|
||||
@@ -604,9 +636,72 @@ def route(args: argparse.Namespace) -> dict[str, object]:
|
||||
},
|
||||
"availability": probe_backends() if args.probe else None,
|
||||
}
|
||||
if teacher_step_reconstruction:
|
||||
result["execution_adapter"] = {
|
||||
"path": str(RECONSTRUCTION_ADAPTER),
|
||||
"runtime": "DesignIR 3.0 / SurfaceIR",
|
||||
"automatic_geometry_acceptance": True,
|
||||
"automatic_parameter_acceptance": True,
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
def execute_reconstruction(
|
||||
args: argparse.Namespace, result: dict[str, object]
|
||||
) -> dict[str, object]:
|
||||
edit_context = result.get("design_plan", {}).get("edit_context")
|
||||
if not isinstance(edit_context, dict) or edit_context.get(
|
||||
"modification_mode"
|
||||
) != "private_evidence_to_isolated_designir_reconstruction":
|
||||
raise ValueError(
|
||||
"--execute currently requires --edit-source with one STEP/STP file"
|
||||
)
|
||||
if args.edit_source is None or args.task_dir is None:
|
||||
raise ValueError(
|
||||
"--execute requires both --edit-source and --task-dir"
|
||||
)
|
||||
runtime = os.environ.get("CAD_ROUTER_PYTHON", "").strip()
|
||||
workspace_python = (
|
||||
SKILL_ROOT.parents[2] / "text-to-cad" / ".venv" / "bin" / "python"
|
||||
)
|
||||
interpreter = (
|
||||
Path(runtime).expanduser()
|
||||
if runtime
|
||||
else workspace_python
|
||||
if workspace_python.is_file()
|
||||
else Path(sys.executable)
|
||||
)
|
||||
command = [
|
||||
str(interpreter),
|
||||
str(RECONSTRUCTION_ADAPTER),
|
||||
"--source",
|
||||
str(args.edit_source),
|
||||
"--task-dir",
|
||||
str(args.task_dir),
|
||||
"--request",
|
||||
args.request,
|
||||
"--maximum-parameters",
|
||||
str(args.maximum_parameters),
|
||||
"--timeout-seconds",
|
||||
str(args.timeout_seconds),
|
||||
]
|
||||
completed = subprocess.run(
|
||||
command,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
detail = completed.stderr.strip() or completed.stdout.strip()
|
||||
raise RuntimeError(
|
||||
"cad-router STEP reconstruction failed: " + detail[-3000:]
|
||||
)
|
||||
lines = [line for line in completed.stdout.splitlines() if line.strip()]
|
||||
if not lines:
|
||||
raise RuntimeError("cad-router reconstruction returned no result")
|
||||
return json.loads(lines[-1])
|
||||
|
||||
|
||||
def explain(result: dict[str, object]) -> str:
|
||||
lines = [
|
||||
f"Selected backend: {result['selected_backend']}",
|
||||
@@ -665,12 +760,36 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
)
|
||||
parser.add_argument("--explain", action="store_true", help="Print a human-readable decision instead of JSON.")
|
||||
parser.add_argument("--manifest", type=Path, help="Also write the route decision as JSON to this explicit path.")
|
||||
parser.add_argument(
|
||||
"--execute",
|
||||
action="store_true",
|
||||
help="Execute an uploaded STEP reconstruction after routing.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--task-dir",
|
||||
type=Path,
|
||||
help="Explicit output directory for --execute.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--maximum-parameters",
|
||||
type=int,
|
||||
default=8,
|
||||
help="Maximum feature-specific edit candidates to validate; overall_scale is also tested as fallback.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--timeout-seconds",
|
||||
type=int,
|
||||
default=180,
|
||||
help="Timeout for each reconstruction or acceptance worker.",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = build_parser().parse_args(argv)
|
||||
result = route(args)
|
||||
if args.execute:
|
||||
result["execution"] = execute_reconstruction(args, result)
|
||||
payload = json.dumps(result, ensure_ascii=False, indent=2) + "\n"
|
||||
if args.manifest:
|
||||
args.manifest.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
Reference in New Issue
Block a user