feat: unify CAD workflows on DesignIR 3.0

This commit is contained in:
Jerry
2026-07-28 14:41:26 +08:00
parent 6e33bc0e11
commit b5738e9109
36 changed files with 9690 additions and 1305 deletions
+126 -7
View File
@@ -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)