#!/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 math 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" ) 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", "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: 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", ) 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 _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", "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 = read_designir(designir_path) 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, semantic_backend: str = "build123d", ) -> 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" 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" 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() preferred_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 = read_designir(designir_path) contract_checks = _validate_designir_contract(payload) 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) 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, }, ) _write_json(geometry_report_path, geometry_report) geometry_pass = geometry_report.get("geometry_pass_count") == 1 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}" ) 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": 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" if geometry_pass else "unresolved" ), } _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) 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", "request": request, "operation": "uploaded_step_reconstruction", "route": { "router": "cad-router", "runtime": "DesignIR 3.0 / SurfaceIR", "selected_backend": semantic_backend, "execution_adapter": "surfaceir_occt", "validation_adapters": [ "surfaceir_geometry_acceptance", "simplecadapi_brep_compare", ], "source_of_truth": designir_path.name, }, "source": { "format": source.suffix.lower().lstrip("."), "role": "teacher_and_acceptance_truth_only", "embedded_in_output": False, }, "artifacts": published_artifacts, "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": 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), }, "edit_command_template": [ str(runtime), str(SURFACEIR_PIPELINE), "edit", designir_path.name, "--parameter", "", "--value", "", "--output-designir", "", "--output-step", "", ], } _write_json(task_manifest_path, task_manifest) manifest_path.unlink(missing_ok=True) return { "schema_version": "1.0", "status": ( ( "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" ), "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": 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": [ 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, "brep_validation": { "engine": "SimpleCADAPI 2.0.2", "status": brep_comparison.get("status"), }, }, } 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) parser.add_argument( "--semantic-backend", choices=("build123d", "simplecadapi"), default="build123d", help="Preferred editable semantic backend after SurfaceIR reconstruction.", ) 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, semantic_backend=args.semantic_backend, ) 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())