903 lines
30 KiB
Python
903 lines
30 KiB
Python
#!/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",
|
|
"<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 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())
|