#!/usr/bin/env python3 """Normalize backend-native CAD generation artifacts into DesignIR 3.0. This script intentionally treats backend-native source or model graph files as the editable authority. DesignIR records the contract, parameters, validation evidence, and a non-authoritative SurfaceIR snapshot compiled from the generated STEP. """ from __future__ import annotations import argparse import hashlib import json import re import sys from pathlib import Path from typing import Any SCRIPT_DIR = Path(__file__).resolve().parent if str(SCRIPT_DIR) not in sys.path: sys.path.insert(0, str(SCRIPT_DIR)) from designir_codec import write_designir import surfaceir_pipeline def _safe_id(value: str, fallback: str) -> str: token = re.sub(r"[^a-zA-Z0-9_.-]+", "_", value.strip()).strip("._-") return token or fallback def _read_json(path: Path | None) -> dict[str, Any]: if path is None: return {} try: payload = json.loads(path.read_text(encoding="utf-8")) except FileNotFoundError: return {} if not isinstance(payload, dict): return {} return payload def _sha256(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as handle: for chunk in iter(lambda: handle.read(1024 * 1024), b""): digest.update(chunk) return digest.hexdigest() def _parameter_record( item: dict[str, Any], *, backend: str, source_path: str | None, graph_path: str | None, ) -> tuple[str, dict[str, Any]] | None: name = str(item.get("name") or item.get("id") or "").strip() if not name: return None try: value = float(item.get("value")) except (TypeError, ValueError): return None binding_kind = str(item.get("binding_kind") or "python_constant") binding_source = str(item.get("source_path") or source_path or "") if binding_kind == "model_graph_parameter" and graph_path: binding_source = str(item.get("source_path") or graph_path) minimum = item.get("min", item.get("minimum")) maximum = item.get("max", item.get("maximum")) record: dict[str, Any] = { "value": value, "unit": str(item.get("unit") or "mm"), "editable": bool(item.get("editable", True)), "semantic_role": str(item.get("semantic_role") or name), "display_name": item.get("display_name") or item.get("label") or name, "description": item.get("description") or "", "edit_state": str(item.get("edit_state") or "backend_bound_parameter"), "backend_binding": { "backend": backend, "source_path": binding_source, "binding_kind": binding_kind, "parameter_path": str(item.get("parameter_path") or f"PARAMETERS.{name}"), "regenerate_adapter": str(item.get("regenerate_adapter") or backend), }, } if minimum is not None: record["min"] = minimum if maximum is not None: record["max"] = maximum for key in ("step", "precision", "group", "validated_range"): if key in item: record[key] = item[key] return name, record def normalize_backend_result( *, request: str, backend: str, source_of_truth: str, step_path: Path, output_path: Path, model_id: str, family: str, native_source_path: str | None, backend_graph_path: str | None, metadata_path: Path | None, validation_paths: list[str], ) -> dict[str, Any]: metadata = _read_json(metadata_path) parameters: dict[str, Any] = {} for item in metadata.get("parameters", []): if isinstance(item, dict): parsed = _parameter_record( item, backend=backend, source_path=native_source_path, graph_path=backend_graph_path, ) if parsed: name, record = parsed parameters[name] = record features = [] for index, item in enumerate(metadata.get("features", []), start=1): if not isinstance(item, dict): continue feature_id = str(item.get("id") or f"backend_feature_{index}") features.append( { **item, "id": feature_id, "operation": str(item.get("operation") or "backend_native_feature"), "backend": backend, "original_history_recovered": False, "source_of_truth": source_of_truth, } ) if not features: features.append( { "id": "backend_native_model", "operation": "backend_native_feature", "backend": backend, "source_of_truth": source_of_truth, "original_history_recovered": False, "description": "Model was authored by the selected backend's native generation flow.", } ) surface_payload: dict[str, Any] | None = None surface_snapshot: dict[str, Any] = { "status": "materialized", "source": "generated_step", } try: surface_payload = surfaceir_pipeline.extract_surfaceir(step_path) except Exception as exc: # SurfaceIR is a non-authoritative snapshot for backend-native models. # Unsupported STEP surface vocabulary must not discard a valid STEP, # native source, or replayable model graph. surface_snapshot = { "status": "unavailable", "source": "generated_step", "error": str(exc), } has_surface_snapshot = surface_payload is not None reconstruction_mode = ( "hybrid_semantic_surface_parametric" if has_surface_snapshot else "fully_semantic_parametric" ) designir: dict[str, Any] = { "schema_version": "3.0", "designir_kind": "independent_parametric_cad", "model_id": model_id, "family": family, "units": str(metadata.get("units") or "mm"), "document_status": ( surface_payload.get("document_status", "geometry_present") if surface_payload else "geometry_present" ), "reconstruction_mode": reconstruction_mode, "authoring_mode": "semantic_feature_program", "backend_hint": backend, "source_of_truth": { "kind": source_of_truth, "backend": backend, "native_source_path": native_source_path, "backend_graph_path": backend_graph_path, "primary_step_path": str(step_path), "primary_step_sha256": _sha256(step_path), }, "semantic_layer": { "reconstruction_status": "ready" if has_surface_snapshot else "partial", "coordinate_system": { "origin": [0, 0, 0], "x_axis": [1, 0, 0], "y_axis": [0, 1, 0], "z_axis": [0, 0, 1], }, "datums": { "primary_axis": {"kind": "axis", "axis": "z"}, }, "parameters": parameters, "expressions": {}, "sketches": [], "constraints": [], "features": features, "patterns": [], "attachments": [ { "id": "backend_native_authority", "kind": "source_of_truth", "backend": backend, "source_of_truth": source_of_truth, "native_source_path": native_source_path, "backend_graph_path": backend_graph_path, } ], "construction_stages": [ { "id": "backend_native_generation", "kind": "backend_generation", "backend": backend, "validation_artifacts": validation_paths, }, { "id": "surfaceir_snapshot", "kind": "non_authoritative_surface_snapshot", **surface_snapshot, }, ], }, "edit_interface": { "semantic_parameters": [ name for name, record in parameters.items() if record.get("editable") is True and record.get("backend_binding") ], "surface_parameter_groups": ( surface_payload.get("edit_interface", {}).get( "surface_parameter_groups", [] ) if surface_payload else [] ), "modification_levels": ["backend_native_parameter", "semantic_feature"], "preserved_interfaces": [], }, "validation_contract": { "source_independence": True, "geometry_checks": ( ["backend_native_generation", "step_surfaceir_materialization"] if has_surface_snapshot else ["backend_native_generation"] ), "edit_checks": ["backend_bound_parameter_regeneration"], "invariants": [], "perturbations": [], "thresholds": {}, "backend_validation_artifacts": validation_paths, "surface_snapshot": surface_snapshot, }, } if surface_payload: designir["surface_layer"] = surface_payload["surface_layer"] designir["reconstruction_strategy"] = surface_payload.get( "reconstruction_strategy", {"boundary_strategy": "exact_3d", "selection_status": "default"}, ) designir["compiled_surface_provenance"] = { "source": "generated_step", "teacher_geometry_used": False, "semantic_layer_authoritative": True, } write_designir(output_path, designir) return { "valid": True, "output": str(output_path.expanduser().resolve()), "schema_version": "3.0", "reconstruction_mode": designir["reconstruction_mode"], "backend": backend, "source_of_truth": source_of_truth, "parameter_count": len(parameters), "feature_count": len(features), "surface_snapshot": surface_snapshot, } def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--request", required=True) parser.add_argument("--backend", choices=("build123d", "simplecadapi", "surfaceir"), required=True) parser.add_argument( "--source-of-truth", choices=("native_source", "model_graph", "surfaceir_designir"), required=True, ) parser.add_argument("--step", type=Path, required=True) parser.add_argument("--output", type=Path, required=True) parser.add_argument("--model-id", required=True) parser.add_argument("--family", default="backend_native_model") parser.add_argument("--native-source-path") parser.add_argument("--backend-graph-path") parser.add_argument("--metadata", type=Path) parser.add_argument("--validation-artifact", action="append", default=[]) return parser def main(argv: list[str] | None = None) -> int: args = build_parser().parse_args(argv) try: result = normalize_backend_result( request=args.request, backend=args.backend, source_of_truth=args.source_of_truth, step_path=args.step.expanduser().resolve(), output_path=args.output.expanduser().resolve(), model_id=_safe_id(args.model_id, "backend_native_model"), family=args.family, native_source_path=args.native_source_path, backend_graph_path=args.backend_graph_path, metadata_path=args.metadata, validation_paths=args.validation_artifact, ) except Exception as exc: print(json.dumps({"valid": False, "error": str(exc)}, ensure_ascii=False, indent=2)) return 2 print(json.dumps(result, ensure_ascii=False, indent=2)) return 0 if __name__ == "__main__": raise SystemExit(main())