from __future__ import annotations import copy import json import math import re import sys from pathlib import Path from typing import Any from jsonschema import Draft202012Validator from jsonschema.exceptions import SchemaError from vendor.cdsl_preview_runtime import step_to_glb from app.services.storage import WorkspaceStore, now_iso, write_json from app.settings import Settings def load_engine(settings: Settings) -> Any: parent = str(settings.engine_root.parent) if parent not in sys.path: sys.path.insert(0, parent) import cdsl_engine return cdsl_engine def _walk(value: Any) -> list[tuple[str, Any]]: result: list[tuple[str, Any]] = [] if isinstance(value, dict): for key, child in value.items(): result.append((str(key), child)) result.extend(_walk(child)) elif isinstance(value, list): for child in value: result.extend(_walk(child)) return result def _engine_schema(engine: Any) -> dict[str, Any]: schema_path = Path(str(engine.__file__)).with_name("profile_schema.json") try: schema = json.loads(schema_path.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError) as error: raise RuntimeError("The local engine schema document is unavailable or invalid") from error if not isinstance(schema, dict) or not isinstance(schema.get("feature_atomic_ids"), dict): raise RuntimeError("The local engine schema has no feature_atomic_ids contract") return schema def load_cdsl_json_schema(engine: Any) -> dict[str, Any]: document = _engine_schema(engine) schema_name = str(document.get("cdsl_json_schema_file") or "") if not schema_name or Path(schema_name).name != schema_name: raise RuntimeError("The local engine schema has an invalid CDSL JSON Schema path") schema_path = Path(str(engine.__file__)).with_name(schema_name) try: schema = json.loads(schema_path.read_text(encoding="utf-8")) Draft202012Validator.check_schema(schema) except (OSError, json.JSONDecodeError, SchemaError) as error: raise RuntimeError("The local CDSL JSON Schema is unavailable or invalid") from error return schema def _validate_cdsl_json_schema(cdsl: dict[str, Any], engine: Any) -> None: validator = Draft202012Validator(load_cdsl_json_schema(engine)) errors = sorted(validator.iter_errors(cdsl), key=lambda error: (list(error.absolute_path), error.message)) if not errors: return error = errors[0] location = "$" + "".join( f"[{item}]" if isinstance(item, int) else f".{item}" for item in error.absolute_path ) raise ValueError(f"CDSL schema violation at {location}: {error.message}") def validate_cdsl(cdsl: dict[str, Any], engine: Any) -> None: if not isinstance(cdsl, dict): raise ValueError("CDSL must be a JSON object") if cdsl.get("schema") != "cad.cdsl.llm.v1": raise ValueError("Unsupported CDSL schema") _validate_cdsl_json_schema(cdsl, engine) part_id = str(cdsl.get("part_id") or "") if not re.fullmatch(r"[a-zA-Z0-9_-]{3,80}", part_id): raise ValueError("part_id must use letters, numbers, underscores, or hyphens") forbidden = {"compiler_context", "unknown_shape", "complex_arc_shape", "contour_edges_mm", "contour_regions_mm", "entities"} for key, value in _walk(cdsl): if key in forbidden or (isinstance(value, str) and value in {"unknown_shape", "complex_arc_shape"}): raise ValueError(f"Training-unsafe CDSL field: {key}") features = cdsl.get("features") sketches = cdsl.get("geometry", {}).get("sketches") if not isinstance(features, list) or not features or not isinstance(sketches, list) or not sketches: raise ValueError("CDSL requires features and parameterized sketches") sketch_ids = {str(sketch.get("id")) for sketch in sketches} semantic_contract = _engine_schema(engine) atomic_contracts = semantic_contract["feature_atomic_ids"] supported_atomic_ids = sorted( str(atomic_id) for atomic_id in semantic_contract.get("runtime_supported_atomic_ids", atomic_contracts) ) feature_ids: set[str] = set() for feature in features: fid = str(feature.get("id") or "") if not fid or fid in feature_ids: raise ValueError("Feature ids must be unique") feature_ids.add(fid) atomic_id = str(feature.get("atomic_id") or "") if not atomic_id: raise ValueError(f"Feature {fid} has no atomic_id") contract = atomic_contracts.get(atomic_id) if atomic_id not in supported_atomic_ids or not isinstance(contract, dict): if feature.get("execution_status") == "deferred": raise ValueError(f"Feature {fid} is deferred and cannot be rebuilt by the current engine") raise ValueError( f"Unsupported CDSL atomic_id: {atomic_id}. Supported: {', '.join(supported_atomic_ids)}" ) params = feature.get("params") if not isinstance(params, dict): raise ValueError(f"Feature {fid} params must be an object") for parameter_name in contract.get("required_params") or []: if params.get(parameter_name) is None: raise ValueError(f"Feature {fid} ({atomic_id}) is missing required parameter: {parameter_name}") if atomic_id.startswith("extrude_") and float(params.get("distance_mm") or 0) <= 0: raise ValueError(f"Feature {fid} ({atomic_id}) requires distance_mm > 0 for runtime rebuild") if atomic_id.startswith("revolve_") and float(params.get("angle_deg") or 0) <= 0: raise ValueError(f"Feature {fid} ({atomic_id}) requires angle_deg > 0 for runtime rebuild") if contract.get("requires_sketch") and str(feature.get("sketch_id") or "") not in sketch_ids: raise ValueError(f"Feature {fid} ({atomic_id}) requires a valid sketch_id") for dependency in feature.get("depends_on") or []: if dependency not in feature_ids: raise ValueError(f"Feature {fid} has a forward or missing dependency") for sketch in sketches: profile = sketch.get("profile") if sketch.get("profile_from"): continue if not isinstance(profile, dict): raise ValueError(f"Sketch {sketch.get('id')} has no self-contained profile") profile_type = str(profile.get("type") or "") if profile_type == "polygon": if not profile.get("vertices"): raise ValueError("Polygon profiles require vertices") elif profile_type not in engine.SHAPE_GENERATORS: raise ValueError(f"Unsupported CDSL profile: {profile_type}") try: analysis = engine.analyze_cdsl(copy.deepcopy(cdsl)) except Exception as error: raise ValueError(f"CDSL engine runtime preflight failed: {error}") from error if not analysis.runtime_eligible: first = next((result for result in analysis.feature_results if not result.executable), None) if first is None: raise ValueError(f"CDSL engine runtime preflight failed: {analysis.document_blockers[0].code}") blockers = ", ".join(blocker.code for blocker in first.blockers) raise ValueError(f"CDSL engine runtime preflight failed: feature {first.feature_id}: {blockers}") def _parameter_id(path: list[str]) -> str: return "param_" + "_".join(re.sub(r"[^a-zA-Z0-9]+", "_", item).strip("_") for item in path) def _parameter_label(path: list[str]) -> str: return " / ".join(path[-2:]).replace("_mm", " (mm)").replace("_", " ") def _derived_parameters(cdsl: dict[str, Any]) -> list[dict[str, Any]]: parameters: list[dict[str, Any]] = [] def add(path: list[str], value: Any, group: str) -> None: if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(float(value)): return number = float(value) magnitude = max(abs(number), 1.0) parameters.append({ "id": _parameter_id(path), "name": ".".join(path), "display_name": _parameter_label(path), "path": path, "value": number, "default_value": number, "minimum": 0.01 if number >= 0 else -magnitude * 10, "maximum": magnitude * 10, "step": 0.1 if abs(number) < 100 else 1.0, "precision": 2, "unit": "mm" if path[-1].endswith("_mm") else "", "group": group, "editable": True, }) for feature_index, feature in enumerate(cdsl.get("features") or []): for key, value in (feature.get("params") or {}).items(): add(["features", str(feature_index), "params", str(key)], value, "Features") for sketch_index, sketch in enumerate(cdsl.get("geometry", {}).get("sketches") or []): profile = sketch.get("profile") or {} def walk_profile(value: Any, path: list[str]) -> None: if isinstance(value, dict): for key, child in value.items(): walk_profile(child, [*path, str(key)]) elif isinstance(value, list): # Coordinates are topology anchors, not user-facing dimensions. return else: add(path, value, "Sketches") walk_profile(profile, ["geometry", "sketches", str(sketch_index), "profile"]) return parameters def parameter_contract(cdsl: dict[str, Any]) -> dict[str, Any]: declared = cdsl.get("meta", {}).get("editable_parameters") if isinstance(declared, list) and declared: values = [item for item in declared if isinstance(item, dict) and isinstance(item.get("path"), list)] if values: return {"schema_version": "1.0", "parameters": values, "source": "declared"} return {"schema_version": "1.0", "parameters": _derived_parameters(cdsl), "source": "derived"} def topology_sidecars(engine_result: dict[str, Any], preview: dict[str, Any] | None = None) -> tuple[dict[str, Any], dict[str, Any]]: topology_faces = (preview or {}).get("topology_faces") if isinstance(topology_faces, list) and topology_faces: references = [] for face in topology_faces: if not isinstance(face, dict): continue frame = face.get("frame") center = face.get("center") normal = face.get("normal") if not isinstance(frame, dict) or not isinstance(center, list) or not isinstance(normal, list): continue references.append({ "id": str(face.get("id") or f"face_{len(references):03d}"), "selectorType": "face", "label": str(face.get("surface_type") or "face"), "center": center, "normal": normal, "frame": frame, "bbox": face.get("bbox") or {}, "surface_type": str(face.get("surface_type") or "unknown"), "triangle_start": int(face.get("triangle_start") or 0), "triangle_count": int(face.get("triangle_count") or 0), }) if references: return ({"schema_version": "1.1", "references": references}, {"schema_version": "1.0", "edges": []}) bbox = engine_result.get("bbox_mm") or {} minimum = [float(value) for value in bbox.get("min") or [0, 0, 0]] maximum = [float(value) for value in bbox.get("max") or [0, 0, 0]] if len(minimum) != 3 or len(maximum) != 3: raise ValueError("Engine result is missing a valid bounding box") center = [(minimum[index] + maximum[index]) / 2 for index in range(3)] definitions = [ ("top", [center[0], center[1], maximum[2]], [0, 0, 1], [1, 0, 0], [0, 1, 0]), ("bottom", [center[0], center[1], minimum[2]], [0, 0, -1], [1, 0, 0], [0, -1, 0]), ("right", [maximum[0], center[1], center[2]], [1, 0, 0], [0, 1, 0], [0, 0, 1]), ("left", [minimum[0], center[1], center[2]], [-1, 0, 0], [0, 1, 0], [0, 0, -1]), ("front", [center[0], maximum[1], center[2]], [0, 1, 0], [1, 0, 0], [0, 0, -1]), ("back", [center[0], minimum[1], center[2]], [0, -1, 0], [1, 0, 0], [0, 0, 1]), ] references = [ { "id": f"face_{name}", "selectorType": "face", "label": name, "center": point, "normal": normal, "frame": {"origin_mm": point, "normal": normal, "x_dir": x_dir, "y_dir": y_dir}, "bbox": {"min": minimum, "max": maximum}, } for name, point, normal, x_dir, y_dir in definitions ] return ({"schema_version": "1.0", "references": references}, {"schema_version": "1.0", "edges": []}) def _set_parameter_value(document: dict[str, Any], path: list[str], value: float) -> None: target: Any = document for index, key in enumerate(path): final = index == len(path) - 1 if isinstance(target, list): item_index = int(key) if item_index < 0 or item_index >= len(target): raise ValueError("Parameter path is no longer valid") if final: target[item_index] = value else: target = target[item_index] elif isinstance(target, dict): if key not in target: raise ValueError("Parameter path is no longer valid") if final: target[key] = value else: target = target[key] else: raise ValueError("Parameter path is no longer valid") def apply_parameter_updates(cdsl: dict[str, Any], values: dict[str, float]) -> tuple[dict[str, Any], dict[str, Any]]: contract = parameter_contract(cdsl) entries = {str(item.get("id")): item for item in contract["parameters"]} updated = copy.deepcopy(cdsl) for parameter_id, raw_value in values.items(): entry = entries.get(parameter_id) value = float(raw_value) if entry is None or not entry.get("editable", False): raise ValueError(f"Unknown editable parameter: {parameter_id}") if not math.isfinite(value): raise ValueError("Parameter values must be finite") minimum, maximum = entry.get("minimum"), entry.get("maximum") if isinstance(minimum, (int, float)) and value < float(minimum): raise ValueError(f"{parameter_id} is below its minimum") if isinstance(maximum, (int, float)) and value > float(maximum): raise ValueError(f"{parameter_id} is above its maximum") path = entry.get("path") if not isinstance(path, list) or not all(isinstance(item, str) for item in path): raise ValueError(f"{parameter_id} has an invalid path") _set_parameter_value(updated, path, value) declared = updated.get("meta", {}).get("editable_parameters") if isinstance(declared, list): for declared_entry in declared: if isinstance(declared_entry, dict) and str(declared_entry.get("id")) == parameter_id: declared_entry["value"] = value return updated, parameter_contract(updated) def _part_skill_audit( part_skills: dict[str, Any] | None, request: str, generation_assumptions: list[str] | None, ) -> dict[str, Any]: """Normalize the planning audit persisted beside a product revision.""" audit = copy.deepcopy(part_skills) if isinstance(part_skills, dict) else {} skills = [item for item in audit.get("skills") or [] if isinstance(item, dict)] skill_ids = [str(item) for item in audit.get("skill_ids") or [] if str(item)] if not skill_ids: skill_ids = [str(item.get("id")) for item in skills if item.get("id")] evidence = audit.get("evidence") if isinstance(audit.get("evidence"), dict) else {} audit.update({ "schema_version": str(audit.get("schema_version") or "1.0"), "request": str(audit.get("request") or request), "structural_intent": str(audit.get("structural_intent") or request), "skill_ids": skill_ids, "skills": skills, "inherited_skill_ids": [str(item) for item in audit.get("inherited_skill_ids") or [] if str(item)], "assumptions": [str(item) for item in generation_assumptions or []], "evidence": evidence, "capability_translations": [ item for item in audit.get("capability_translations") or [] if isinstance(item, dict) ], }) return audit def _generation_context( reference_ids: list[str], part_skill_audit: dict[str, Any], design_intent: dict[str, Any] | None = None, design_intent_path: str = "", design_intent_id: str = "", ) -> dict[str, Any]: skills = [item for item in part_skill_audit.get("skills") or [] if isinstance(item, dict)] intent = design_intent if isinstance(design_intent, dict) else {} return { "design_intent_id": str(intent.get("intent_id") or design_intent_id or ""), "design_intent_path": design_intent_path, "design_intent_structures": [ str(item.get("id") or "") for item in intent.get("structures") or [] if isinstance(item, dict) and item.get("id") ], "cdsl_reference_ids": list(reference_ids), "part_skill_ids": list(part_skill_audit.get("skill_ids") or []), "part_skill_paths": [ { "id": str(item.get("id") or ""), "bridge": str(item.get("bridge") or ""), "source": str(item.get("source") or ""), } for item in skills ], "generation_assumptions": list(part_skill_audit.get("assumptions") or []), "design_intent_assumptions": list(part_skill_audit.get("design_intent_assumptions") or []), "design_intent_capability_gaps": list(part_skill_audit.get("design_intent_capability_gaps") or []), "capability_translations": list(part_skill_audit.get("capability_translations") or []), } def build_revision( *, settings: Settings, store: WorkspaceStore, task_id: str | None, request: str, cdsl: dict[str, Any], reference_ids: list[str], summary: str, parent_revision_id: str | None = None, operation: dict[str, Any] | None = None, attachments: list[dict[str, Any]] | None = None, part_skills: dict[str, Any] | None = None, generation_assumptions: list[str] | None = None, design_intent: dict[str, Any] | None = None, design_intent_path: str = "", design_intent_id: str = "", design_intent_status: str = "", ) -> dict[str, Any]: engine = load_engine(settings) task = store.ensure_task(task_id, request) revision_id, revision_dir = store.next_revision(task["task_id"]) cdsl_path = revision_dir / "model.cdsl.json" step_path = revision_dir / "model.step" glb_path = revision_dir / "model.glb" report_path = revision_dir / "rebuild-report.json" request_path = revision_dir / "request.json" references_path = revision_dir / "references.json" parameters_path = revision_dir / "parameters.json" selector_path = revision_dir / "model.selector.json" edges_path = revision_dir / "model.edges.json" part_skills_path = revision_dir / "part-skills.json" intent = copy.deepcopy(design_intent) if isinstance(design_intent, dict) else {} intent_assumptions = [str(item) for item in intent.get("assumptions") or [] if str(item)] merged_assumptions = list(dict.fromkeys([ *intent_assumptions, *(str(item) for item in generation_assumptions or [] if str(item)), ])) part_skill_audit = _part_skill_audit(part_skills, request, merged_assumptions) intent_id = str(intent.get("intent_id") or design_intent_id or "") if design_intent_path: design_intent_path = str(design_intent_path) part_skill_audit.update({ "design_intent_id": intent_id, "design_intent_path": design_intent_path, "intent_status": str(intent.get("status") or design_intent_status or ""), "design_intent_assumptions": intent_assumptions, "design_intent_capability_gaps": [ item for item in intent.get("capability_gaps") or [] if isinstance(item, dict) ], }) generation_context = _generation_context(reference_ids, part_skill_audit, intent, design_intent_path, intent_id) write_json(request_path, {"request": request, "created_at": now_iso()}) write_json(references_path, {"reference_ids": reference_ids}) write_json(part_skills_path, part_skill_audit) def revision_record(status: str, *, error: str = "", engine_name: str = "") -> dict[str, Any]: record = { "revision_id": revision_id, "status": status, "created_at": now_iso(), "request_path": request_path.relative_to(store.task_dir(task["task_id"])).as_posix(), "cdsl_path": cdsl_path.relative_to(store.task_dir(task["task_id"])).as_posix(), "report_path": report_path.relative_to(store.task_dir(task["task_id"])).as_posix(), "parameters_path": parameters_path.relative_to(store.task_dir(task["task_id"])).as_posix(), "part_skills_path": part_skills_path.relative_to(store.task_dir(task["task_id"])).as_posix(), "design_intent_id": intent_id, "design_intent_path": design_intent_path, "part_skill_ids": list(part_skill_audit.get("skill_ids") or []), "generation_assumptions": list(part_skill_audit.get("assumptions") or []), "reference_ids": reference_ids, "summary": summary, "parent_revision_id": parent_revision_id or "", "operation": operation or {}, "attachments": attachments or [], } if status == "success": record.update({ "step_path": step_path.relative_to(store.task_dir(task["task_id"])).as_posix(), "glb_path": glb_path.relative_to(store.task_dir(task["task_id"])).as_posix(), "selector_path": selector_path.relative_to(store.task_dir(task["task_id"])).as_posix(), "edges_path": edges_path.relative_to(store.task_dir(task["task_id"])).as_posix(), "engine": engine_name, }) else: record["error"] = error return record try: cdsl_copy = copy.deepcopy(cdsl) if not isinstance(cdsl_copy, dict): raise ValueError("CDSL must be a JSON object") cdsl_copy["part_id"] = task["task_id"] meta = cdsl_copy.setdefault("meta", {}) if not isinstance(meta, dict): raise ValueError("CDSL meta must be an object when present") if not isinstance(meta.get("editable_parameters"), list) or not meta["editable_parameters"]: meta["editable_parameters"] = _derived_parameters(cdsl_copy) write_json(cdsl_path, cdsl_copy) write_json(parameters_path, parameter_contract(cdsl_copy)) validate_cdsl(cdsl_copy, engine) # Product revisions are semantic CDSL artifacts. Do not route them # through the legacy rebuild entry point, which is allowed to use # compiler_context/translator compatibility fallbacks. engine_result = engine.run_cdsl_only(cdsl_copy, step_path) if engine_result.get("engine") != "cdsl_only" or not step_path.is_file() or step_path.stat().st_size == 0: raise RuntimeError("Engine did not produce a CDSL-only STEP artifact") preview = step_to_glb(step_path, glb_path) selector, edges = topology_sidecars(engine_result, preview) write_json(selector_path, selector) write_json(edges_path, edges) report = { "engine_result": engine_result, "preview": preview, "generation_context": generation_context, "validated_at": now_iso(), } write_json(report_path, report) revision = revision_record("success", engine_name=str(engine_result["engine"])) except Exception as error: if not cdsl_path.is_file() and isinstance(cdsl, dict): write_json(cdsl_path, copy.deepcopy(cdsl)) write_json(report_path, { "error": str(error), "generation_context": generation_context, "validated_at": now_iso(), }) revision = revision_record("failed", error=str(error)) store.update_task(task["task_id"], revision) raise store.update_task(task["task_id"], revision) return {"task_id": task["task_id"], **revision}