Files
cdsl-cad/backend/app/cad_agent/adapters/runtime.py
T
2026-09-02 13:51:35 +08:00

759 lines
48 KiB
Python

"""CDSL runtime adapter driven exclusively by profile operation contracts."""
from __future__ import annotations
from copy import deepcopy
from hashlib import sha256
import json
import math
from pathlib import Path
from typing import Any
from app.cad_agent.domain.operation_contract import (
SEMANTIC_PREFLIGHT_NAMES,
OperationContractError,
canonical_hash,
validate_fragment,
validate_operation_contract,
)
from app.cad_agent.domain.verifier_registry import default_registry
from app.services.engine_service import load_engine, topology_snapshot, validate_cdsl
from app.services.review_renderer import ReviewRenderError, render_checkpoint
from app.settings import Settings
from vendor.cdsl_preview_runtime import step_to_glb
class RuntimeAdapterError(RuntimeError):
pass
class ProfileCadRuntime:
"""Adapter that owns engine imports; application code sees only its port."""
def __init__(self, settings: Settings) -> None:
self.settings = settings
self.engine = load_engine(settings)
self._semantic_preflight_handlers = {
"sketch_workplane": self._preflight_sketch_workplane,
"profile_non_self_intersecting": self._preflight_profile_non_self_intersecting,
"host_face_exists": self._preflight_host_face_exists,
"hole_positions_on_host_plane": self._preflight_hole_positions_on_host_plane,
"cut_exit_distance": self._preflight_cut_exit_distance,
"requires_active_solid": self._preflight_requires_active_solid,
"revolve_axis_on_sketch": self._preflight_revolve_axis_on_sketch,
"reference_plane_nonzero_normal": self._preflight_reference_plane_nonzero_normal,
"reference_axis_nonzero_direction": self._preflight_reference_axis_nonzero_direction,
"selected_edges_exist": self._preflight_selected_edges_exist,
"source_features_exist": self._preflight_source_features_exist,
"mirror_plane_exists": self._preflight_mirror_plane_exists,
}
schema_path = Path(str(self.engine.__file__)).with_name("profile_schema.json")
try:
self._profile = json.loads(schema_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as error:
raise RuntimeAdapterError("RUNTIME_CONTRACT_INVALID: profile schema is unavailable") from error
self._contracts = self._profile.get("operation_contracts")
if not isinstance(self._contracts, dict):
raise RuntimeAdapterError("RUNTIME_CONTRACT_INVALID: profile has no v3 operation contracts")
declared = {str(item) for item in self._contracts}
registered = {str(item) for item in getattr(self.engine, "SUPPORTED_ATOMIC_IDS", ())}
if declared != registered:
raise RuntimeAdapterError("RUNTIME_CONTRACT_INVALID: runtime and profile operation registries disagree")
verifier_kinds = set(default_registry().claim_kinds)
for contract in self._contracts.values():
try:
validate_operation_contract(contract)
except OperationContractError as error:
raise RuntimeAdapterError(f"RUNTIME_CONTRACT_INVALID: {error}") from error
unknown_preflights = set(contract["semantic_preflight"]) - set(self._semantic_preflight_handlers)
unknown_verifiers = set(contract["candidate_verifiers"]) - verifier_kinds
if unknown_preflights or unknown_verifiers:
raise RuntimeAdapterError(
"RUNTIME_CONTRACT_INVALID: operation contract references an unavailable "
f"{'preflight' if unknown_preflights else 'candidate verifier'}"
)
if set(self._semantic_preflight_handlers) != SEMANTIC_PREFLIGHT_NAMES:
raise RuntimeAdapterError("RUNTIME_CONTRACT_INVALID: runtime preflight registry is incomplete")
def supported_atomic_ids(self) -> tuple[str, ...]:
return tuple(sorted(self._contracts))
def operation_contract(self, atomic_id: str) -> dict[str, Any]:
contract = self._contracts.get(atomic_id)
if not isinstance(contract, dict):
raise RuntimeAdapterError(f"RUNTIME_CONTRACT_INVALID: unknown operation {atomic_id}")
result = deepcopy(contract)
result["contract_hash"] = canonical_hash(contract)
result["registry_revision"] = str(self._profile.get("schema_version") or "")
return result
def selector_tokens(self, topology: dict[str, Any] | None) -> dict[str, dict[str, Any]]:
if not isinstance(topology, dict):
return {}
snapshot_id = str(topology.get("snapshot_id") or "")
if not snapshot_id:
return {}
result: dict[str, dict[str, Any]] = {}
for record in topology.get("records") or ():
if not isinstance(record, dict) or not record.get("executable"):
continue
record_id = str(record.get("record_id") or "")
kind = str(record.get("kind") or "")
if not record_id or kind not in {"face", "edge", "plane", "axis", "body", "vertex"}:
continue
token = "sel_" + sha256(f"{snapshot_id}|{record_id}".encode("utf-8")).hexdigest()[:16]
geometry = deepcopy(record.get("geometry") or {})
owners = record.get("owner_feature_ids") or [record.get("feature_id") or ""]
result[token] = {
"token": token,
"kind": kind,
"snapshot_id": snapshot_id,
"selector": {"kind": kind, "stable_id": record_id, "owner_feature_id": str(owners[0] or ""), "geometry": geometry, "source": "runtime_snapshot", "snapshot_id": snapshot_id, "confidence": 1.0},
"geometry": geometry,
}
return result
def reference_tokens(self, cdsl: dict[str, Any] | None) -> dict[str, str]:
"""Return opaque, head-scoped feature references for pattern contracts."""
result: dict[str, str] = {}
document_hash = canonical_hash(cdsl) if isinstance(cdsl, dict) else "root"
for feature in (cdsl or {}).get("features") or ():
if not isinstance(feature, dict) or not isinstance(feature.get("id"), str) or not feature["id"]:
continue
feature_id = feature["id"]
result["ref_" + sha256(f"{document_hash}|{feature_id}".encode("utf-8")).hexdigest()[:16]] = feature_id
return result
def materialize_fragment(self, base_cdsl: dict[str, Any] | None, fragment: dict[str, Any], contract: dict[str, Any], selector_tokens: dict[str, dict[str, Any]], reference_tokens: dict[str, str], *, require_through: bool = False) -> tuple[dict[str, Any], dict[str, Any]]:
# ActionCommandHandler validates the exposed schema first, but this
# adapter is also used during crash recovery. Keep the runtime boundary
# self-contained so a corrupted/replayed staged payload cannot produce
# a candidate merely by bypassing that handler-level validation.
try:
validate_operation_contract(contract)
except OperationContractError as error:
raise RuntimeAdapterError(f"RUNTIME_CONTRACT_INVALID: {error}") from error
current = self.operation_contract(str(contract.get("atomic_id") or ""))
if (
contract.get("contract_hash") != current["contract_hash"]
or contract.get("registry_revision") != current["registry_revision"]
):
raise RuntimeAdapterError("RUNTIME_CONTRACT_INVALID: operation contract is not the current verified registry entry")
selector_shape = str((contract.get("fragment_shape") or {}).get("selector_tokens") or "forbidden")
selector_kind = str((contract.get("selector_policy") or {}).get("token_kind") or "")
allowed_selectors = [
token
for token, value in selector_tokens.items()
if selector_shape == "required"
and isinstance(value, dict)
and value.get("kind") == selector_kind
]
errors = validate_fragment(
contract,
fragment,
selector_tokens=allowed_selectors,
reference_tokens=list(reference_tokens),
root_xy_datum=not bool((base_cdsl or {}).get("features")),
)
if errors:
raise RuntimeAdapterError(
"RUNTIME_PRECONDITION_FAILED: fragment no longer matches the active operation schema: "
+ errors[0]["message"]
)
materialized = deepcopy(fragment)
reference = contract["reference_policy"]
if reference["mode"] == "snapshot_bound":
slot = str(reference["slot"]).removeprefix("params.")
supplied = materialized.get("feature", {}).get("params", {}).get(slot, [])
if not isinstance(supplied, list) or not all(token in reference_tokens for token in supplied):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: reference token is absent or stale")
materialized["feature"]["params"][slot] = [reference_tokens[token] for token in supplied]
self._semantic_preflight(materialized, contract, selector_tokens, base_cdsl, require_through=require_through)
document = deepcopy(base_cdsl) if isinstance(base_cdsl, dict) else {
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "agent_preflight", "geometry": {"sketches": []}, "features": [],
}
geometry = document.setdefault("geometry", {})
sketches = geometry.setdefault("sketches", []) if isinstance(geometry, dict) else None
features = document.setdefault("features", [])
if not isinstance(sketches, list) or not isinstance(features, list):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: base CDSL collections are invalid")
index = len(features) + 1
feature = materialized["feature"]
output = {"id": f"feature_{index:03d}", "atomic_id": contract["atomic_id"], "params": deepcopy(feature["params"]), "depends_on": [str(features[-1].get("id"))] if features else []}
if contract["fragment_shape"]["sketch"] == "required":
sketch_id = f"sketch_{len(sketches) + 1:03d}"
sketch = materialized["sketch"]
sketches.append({"id": sketch_id, "workplane": deepcopy(sketch["workplane"]), "profile": deepcopy(sketch["profile"])})
output["sketch_id"] = sketch_id
selected = [selector_tokens[token]["selector"] for token in feature.get("selector_tokens", [])]
slot = contract["selector_policy"]["slot"]
if slot == "params.host_face":
# A topology selector is authoritative while accepting the
# action, but a later pattern replays this feature after its own
# cut may have split the selected B-rep face. Lower the accepted
# planar selector to a concrete, world-aligned host frame and
# transform the author-supplied world coordinates into that
# frame. The token remains in the fragment audit for provenance;
# the materialized CDSL is stable under replay.
output["params"]["host_face"], output["params"]["positions"] = self._materialized_hole_host_frame(
selector_tokens[str(feature["selector_tokens"][0])],
output["params"].get("positions"),
)
elif slot == "params.mirror_plane":
output["params"]["mirror_plane"] = selected[0]
elif slot == "feature.selectors":
output["selectors"] = selected
features.append(output)
try:
self._validate_finite_tree(document)
self._validate_materialized_runtime_types(output)
validate_cdsl(document, self.engine)
except Exception as error:
message = str(error)
# A JSON-schema failure after server materialization is a registry
# drift: the author could not have supplied the missing server
# field. Engine capability analysis, however, can reject a
# schema-valid author sketch (for example an unresolvable analytic
# contour). That is a recoverable action precondition failure,
# not a deployment defect.
if "CDSL engine runtime preflight failed:" in message:
raise RuntimeAdapterError(
"RUNTIME_PRECONDITION_FAILED: materialized fragment is not executable by the engine: "
+ message
) from error
raise RuntimeAdapterError(
"RUNTIME_CONTRACT_INVALID: materialized fragment violates the engine CDSL schema: "
+ message
) from error
return document, {"schema_version": "cad.v3.fragment-audit.v1", "atomic_id": contract["atomic_id"], "fragment_hash": canonical_hash(fragment), "contract_hash": contract["contract_hash"], "assigned_feature_ids": [output["id"]], "assigned_sketch_ids": [output["sketch_id"]] if output.get("sketch_id") else [], "selector_snapshot_id": next(iter(selector_tokens.values()), {}).get("snapshot_id", ""), "selector_tokens": list(feature.get("selector_tokens", [])), "reference_snapshot_id": canonical_hash(reference_tokens), "reference_tokens": list(fragment.get("feature", {}).get("params", {}).get(str(reference.get("slot") or "").removeprefix("params."), [])) if reference["mode"] == "snapshot_bound" else []}
def rebuild(self, cdsl: dict[str, Any], output_dir: str, task_id: str, revision_id: str) -> dict[str, Any]:
root = Path(output_dir)
root.mkdir(parents=True, exist_ok=True)
cdsl_copy = deepcopy(cdsl)
cdsl_copy["part_id"] = task_id
step_path = root / "model.step"
glb_path = root / "model.glb"
self._write_json(root / "model.cdsl.json", cdsl_copy)
try:
self._validate_finite_tree(cdsl_copy)
validate_cdsl(cdsl_copy, self.engine)
except RuntimeAdapterError:
raise
except Exception as error:
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: persisted CDSL failed rebuild preflight: {error}") from error
try:
engine_result = self.engine.run_cdsl_only(cdsl_copy, step_path)
if str(engine_result.get("engine") or "") != "cdsl_only":
raise RuntimeAdapterError("engine did not execute CDSL-only rebuild")
preview = step_to_glb(step_path, glb_path)
health = self._health(engine_result, step_path, glb_path)
topology = topology_snapshot(engine_result, task_id=task_id, revision_id=revision_id, preview=preview)
self._write_json(root / "model.topology.json", topology)
manifest = render_checkpoint(self.settings, step_path=step_path, output_dir=root / "renders")
report = {"engine_result": engine_result, "preview": preview, "health": health, "render_manifest": manifest}
self._write_json(root / "rebuild-report.json", report)
return {"health": health, "topology": topology, "report": report, "render_manifest": manifest, "paths": {"cdsl": "model.cdsl.json", "step": "model.step", "glb": "model.glb", "topology": "model.topology.json", "report": "rebuild-report.json", "render_manifest": "renders/render-manifest.json"}}
except OSError:
# Artifact writes are a recoverable infrastructure outage. Let the
# application handler park the same candidate stage for replay.
raise
except ReviewRenderError as error:
raise RuntimeAdapterError(f"RENDER_SERVICE_UNAVAILABLE: {error}") from error
except Exception as error:
raise RuntimeAdapterError(f"RUNTIME_EXECUTION_FAILURE: {error}") from error
def rebuild_best_effort(
self,
cdsl: dict[str, Any],
output_dir: str,
task_id: str,
revision_id: str,
) -> tuple[dict[str, Any], list[dict[str, Any]]]:
"""Build independent feature prefixes without discarding prior geometry.
A full CDSL document can contain several features even though the
action protocol normally appends one at a time. Engine validation is
all-or-nothing, so replay features in source order and retain each
executable feature. A failed feature is reported as a diagnostic;
it does not erase already executable geometry or prevent later,
independent features from being tried.
"""
features = cdsl.get("features") if isinstance(cdsl.get("features"), list) else []
accepted: list[dict[str, Any]] = []
accepted_ids: set[str] = set()
failures: list[dict[str, Any]] = []
latest: dict[str, Any] | None = None
for index, feature in enumerate(features):
if not isinstance(feature, dict):
failures.append({"feature_index": index, "feature_id": "", "message": "Feature must be an object."})
continue
feature_id = str(feature.get("id") or f"feature_{index + 1:03d}")
dependencies = [str(value) for value in feature.get("depends_on") or () if isinstance(value, str)]
unavailable = [value for value in dependencies if value not in accepted_ids]
if unavailable:
failures.append({
"feature_index": index,
"feature_id": feature_id,
"message": "Feature was skipped because an earlier dependency did not execute.",
"dependencies": unavailable,
})
continue
candidate = self._feature_subset(cdsl, [*accepted, feature])
try:
latest = self.rebuild(candidate, output_dir, task_id, revision_id)
except Exception as error:
failures.append({"feature_index": index, "feature_id": feature_id, "message": str(error)[:1000]})
continue
accepted.append(deepcopy(feature))
accepted_ids.add(feature_id)
if not accepted:
if failures:
raise RuntimeAdapterError(str(failures[0].get("message") or "RUNTIME_EXECUTION_FAILURE: no feature could be rebuilt"))
raise RuntimeAdapterError("RUNTIME_EXECUTION_FAILURE: CDSL document has no executable features")
# A failed later attempt may have left partial files in the stage.
# Rebuild the retained feature set once so all published artifacts are
# guaranteed to describe the same successful checkpoint.
latest = self.rebuild(self._feature_subset(cdsl, accepted), output_dir, task_id, revision_id)
return {**latest, "executed_feature_ids": sorted(accepted_ids)}, failures
@staticmethod
def _feature_subset(cdsl: dict[str, Any], features: list[dict[str, Any]]) -> dict[str, Any]:
document = deepcopy(cdsl)
document["features"] = deepcopy(features)
geometry = document.get("geometry") if isinstance(document.get("geometry"), dict) else {}
sketches = geometry.get("sketches") if isinstance(geometry.get("sketches"), list) else []
sketch_ids = {str(feature.get("sketch_id") or "") for feature in features}
document["geometry"] = {
**geometry,
"sketches": [deepcopy(sketch) for sketch in sketches if isinstance(sketch, dict) and str(sketch.get("id") or "") in sketch_ids],
}
return document
def _semantic_preflight(self, fragment: dict[str, Any], contract: dict[str, Any], selector_tokens: dict[str, dict[str, Any]], base_cdsl: dict[str, Any] | None, *, require_through: bool) -> None:
if fragment.get("feature", {}).get("atomic_id") != contract.get("atomic_id"):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: atomic_id does not match active contract")
policy = contract["selector_policy"]
supplied = fragment.get("feature", {}).get("selector_tokens", [])
if contract["fragment_shape"]["selector_tokens"] == "required":
if not all(token in selector_tokens and selector_tokens[token]["kind"] == policy["token_kind"] for token in supplied):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: selector token is absent, stale, or has the wrong kind")
for name in contract["semantic_preflight"]:
self._semantic_preflight_handlers[name](fragment, selector_tokens, base_cdsl, require_through)
def _preflight_sketch_workplane(self, fragment: dict[str, Any], _selectors: dict[str, dict[str, Any]], _base: dict[str, Any] | None, _require_through: bool) -> None:
sketch = fragment.get("sketch")
workplane = sketch.get("workplane") if isinstance(sketch, dict) else None
if not isinstance(workplane, dict):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: sketch workplane is unavailable")
normal, x_dir = workplane.get("normal"), workplane.get("x_dir")
if not isinstance(normal, list) or not isinstance(x_dir, list) or self._norm(normal) <= 1e-9 or self._norm(x_dir) <= 1e-9 or self._norm(self._cross(normal, x_dir)) <= 1e-9:
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: sketch workplane vectors are degenerate")
def _preflight_profile_non_self_intersecting(self, fragment: dict[str, Any], _selectors: dict[str, dict[str, Any]], _base: dict[str, Any] | None, _require_through: bool) -> None:
sketch = fragment.get("sketch")
profile = sketch.get("profile") if isinstance(sketch, dict) else None
if not isinstance(profile, dict):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: sketch profile is unavailable")
if profile.get("type") == "polygon":
vertices = profile.get("vertices")
if not isinstance(vertices, list) or len(vertices) < 3 or self._polygon_self_intersects(vertices):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: polygon profile self-intersects")
elif profile.get("type") == "analytic_contours":
self._preflight_analytic_contours(profile)
def _preflight_host_face_exists(self, fragment: dict[str, Any], selectors: dict[str, dict[str, Any]], _base: dict[str, Any] | None, _require_through: bool) -> None:
supplied = fragment.get("feature", {}).get("selector_tokens", [])
if len(supplied) != 1 or not isinstance(selectors.get(supplied[0]), dict) or selectors[supplied[0]].get("kind") != "face":
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: host face is absent or stale")
def _materialized_hole_host_frame(
self,
selected: dict[str, Any],
positions: Any,
) -> tuple[dict[str, Any], list[dict[str, list[float]]]]:
"""Lower a verified planar host selector into replay-stable CDSL data.
Hole position inputs are world coordinates at the author boundary.
The engine's frame form uses local coordinates, so both values must
be converted together. Keeping only the selector makes a later
pattern replay depend on a face that the source cut has already
subdivided, which is neither stable nor geometrically meaningful.
"""
geometry = selected.get("geometry") if isinstance(selected, dict) else None
center = geometry.get("center_mm") if isinstance(geometry, dict) else None
normal = geometry.get("normal") if isinstance(geometry, dict) else None
if not self._valid_vector3(center) or not self._valid_vector3(normal, require_nonzero=True):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: selected host face has no usable plane frame")
if not isinstance(positions, list) or not positions:
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: hole positions are unavailable for host-frame materialization")
origin = [float(value) for value in center]
unit_normal = [float(value) / self._norm(normal) for value in normal]
seed = [1.0, 0.0, 0.0] if abs(unit_normal[0]) < 0.9 else [0.0, 1.0, 0.0]
x_raw = [seed[index] - self._dot(seed, unit_normal) * unit_normal[index] for index in range(3)]
x_length = self._norm(x_raw)
if x_length <= 1e-9:
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: selected host face cannot define a stable x direction")
x_dir = [value / x_length for value in x_raw]
y_dir = self._cross(unit_normal, x_dir)
local_positions: list[dict[str, list[float]]] = []
for position in positions:
point = position.get("mm") if isinstance(position, dict) else None
if not self._valid_vector3(point):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: hole position is not a finite 3D point")
offset = [float(point[index]) - origin[index] for index in range(3)]
local_positions.append({"mm": [
self._dot(offset, x_dir),
self._dot(offset, y_dir),
self._dot(offset, unit_normal),
]})
return (
{"frame": {"origin_mm": origin, "x_dir": x_dir, "y_dir": y_dir, "normal": unit_normal}},
local_positions,
)
def _preflight_hole_positions_on_host_plane(self, fragment: dict[str, Any], selectors: dict[str, dict[str, Any]], _base: dict[str, Any] | None, _require_through: bool) -> None:
positions = fragment.get("feature", {}).get("params", {}).get("positions")
if not isinstance(positions, list) or not positions or not all(isinstance(item, dict) and isinstance(item.get("mm"), list) and len(item["mm"]) == 3 for item in positions):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: hole positions are invalid for the host plane")
supplied = fragment.get("feature", {}).get("selector_tokens", [])
host = selectors.get(supplied[0]) if len(supplied) == 1 else None
geometry = host.get("geometry") if isinstance(host, dict) else None
normal = geometry.get("normal") if isinstance(geometry, dict) else None
center = geometry.get("center_mm") if isinstance(geometry, dict) else None
bbox = geometry.get("bbox_mm") if isinstance(geometry, dict) else None
if not isinstance(geometry, dict) or geometry.get("surface_type") != "plane" or not self._valid_vector3(normal, require_nonzero=True) or not self._valid_vector3(center):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: hole host must be an observable planar face")
unit_normal = [float(component) / self._norm(normal) for component in normal]
tolerance = 1e-5
if not self._valid_bbox(bbox):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: host face has no usable topology bounds")
for position in positions:
point = position["mm"]
if not self._valid_vector3(point):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: hole position is not a finite 3D point")
offset = [float(point[index]) - float(center[index]) for index in range(3)]
if abs(self._dot(offset, unit_normal)) > tolerance:
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: hole position is not on the selected host plane")
if any(float(point[index]) < float(bbox[index]) - tolerance or float(point[index]) > float(bbox[index + 3]) + tolerance for index in range(3)):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: hole position is outside the selected host-face bounds")
boundary_loops = geometry.get("boundary_loops_mm")
if isinstance(boundary_loops, list) and boundary_loops and not self._point_in_planar_face(point, boundary_loops, unit_normal, tolerance):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: hole position is outside the selected host face's effective boundary")
def _preflight_analytic_contours(self, profile: dict[str, Any]) -> None:
contours = profile.get("contours")
if not isinstance(contours, list) or not contours:
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: analytic profile requires at least one contour")
tolerance = 1e-7
for contour_index, contour in enumerate(contours):
segments = contour.get("segments") if isinstance(contour, dict) else None
if not isinstance(segments, list) or not segments:
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: analytic contour {contour_index} is empty")
if any(segment.get("type") == "circle" for segment in segments if isinstance(segment, dict)):
if len(segments) != 1 or segments[0].get("type") != "circle":
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: analytic contour {contour_index} cannot mix a full circle with other segments")
continue
endpoints: list[tuple[tuple[float, float], tuple[float, float]]] = []
for segment_index, segment in enumerate(segments):
if not isinstance(segment, dict) or segment.get("type") not in {"line", "arc"}:
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: analytic contour {contour_index} has an unsupported segment")
start = self._point2(segment.get("start"))
end = self._point2(segment.get("end"))
if start is None or end is None:
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: analytic contour {contour_index} segment {segment_index} has non-finite endpoints")
if self._distance2(start, end) <= tolerance:
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: analytic contour {contour_index} segment {segment_index} is degenerate")
if segment.get("type") == "arc":
center = self._point2(segment.get("center"))
radius = segment.get("radius_mm")
if center is None or not isinstance(radius, (int, float)) or isinstance(radius, bool) or not math.isfinite(float(radius)) or float(radius) <= 0:
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: analytic contour {contour_index} arc {segment_index} has an invalid circle")
arc_tolerance = max(tolerance, float(radius) * 1e-7)
if abs(self._distance2(start, center) - float(radius)) > arc_tolerance or abs(self._distance2(end, center) - float(radius)) > arc_tolerance:
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: analytic contour {contour_index} arc {segment_index} endpoints are not on the declared circle")
endpoints.append((start, end))
for segment_index in range(1, len(endpoints)):
if self._distance2(endpoints[segment_index - 1][1], endpoints[segment_index][0]) > tolerance:
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: analytic contour {contour_index} is discontinuous before segment {segment_index}")
if contour.get("closed") is True and self._distance2(endpoints[-1][1], endpoints[0][0]) > tolerance:
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: analytic contour {contour_index} is not closed")
line_segments = [segment for segment, raw in zip(endpoints, segments) if raw.get("type") == "line"]
for first, (a, b) in enumerate(line_segments):
for second, (c, d) in enumerate(line_segments):
if second <= first + 1 or (first == 0 and second == len(line_segments) - 1 and contour.get("closed") is True):
continue
if self._segments_intersect(a, b, c, d):
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: analytic contour {contour_index} has an obvious self-intersection")
@classmethod
def _validate_materialized_runtime_types(cls, feature: dict[str, Any]) -> None:
atomic_id = str(feature.get("atomic_id") or "")
if not atomic_id.startswith("hole_"):
return
params = feature.get("params") if isinstance(feature.get("params"), dict) else {}
host = params.get("host_face") if isinstance(params.get("host_face"), dict) else None
frame = host.get("frame") if isinstance(host, dict) and isinstance(host.get("frame"), dict) else None
if not isinstance(frame, dict):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: materialized hole host_face.frame is unavailable")
for field in ("origin_mm", "x_dir", "y_dir", "normal"):
if not cls._valid_vector3(frame.get(field), require_nonzero=field != "origin_mm"):
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: materialized hole host_face.frame.{field} is invalid")
x_dir, y_dir, normal = frame["x_dir"], frame["y_dir"], frame["normal"]
if abs(cls._dot(x_dir, y_dir)) > 1e-6 or abs(cls._dot(x_dir, normal)) > 1e-6 or abs(cls._dot(y_dir, normal)) > 1e-6:
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: materialized hole host frame is not orthogonal")
positions = params.get("positions")
if not isinstance(positions, list) or not positions:
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: materialized hole positions are unavailable")
for index, position in enumerate(positions):
point = position.get("mm") if isinstance(position, dict) else None
if not cls._valid_vector3(point) or abs(float(point[2])) > 1e-5:
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: materialized hole position {index} is invalid in the host frame")
@staticmethod
def _validate_finite_tree(value: Any, path: str = "$") -> None:
if value is None:
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: {path} must not be null")
if isinstance(value, float) and not math.isfinite(value):
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: {path} must be finite")
if isinstance(value, dict):
for key, child in value.items():
ProfileCadRuntime._validate_finite_tree(child, f"{path}.{key}")
elif isinstance(value, list):
for index, child in enumerate(value):
ProfileCadRuntime._validate_finite_tree(child, f"{path}[{index}]")
@staticmethod
def _point2(value: Any) -> tuple[float, float] | None:
if not isinstance(value, list) or len(value) != 2:
return None
if not all(isinstance(item, (int, float)) and not isinstance(item, bool) and math.isfinite(float(item)) for item in value):
return None
return float(value[0]), float(value[1])
@staticmethod
def _distance2(left: tuple[float, float], right: tuple[float, float]) -> float:
return math.hypot(left[0] - right[0], left[1] - right[1])
@classmethod
def _point_in_planar_face(cls, point: list[float], loops: list[Any], normal: list[float], tolerance: float) -> bool:
drop_axis = max(range(3), key=lambda index: abs(float(normal[index])))
def project(value: Any) -> tuple[float, float] | None:
if not cls._valid_vector3(value):
return None
coordinates = [float(value[index]) for index in range(3) if index != drop_axis]
return coordinates[0], coordinates[1]
projected_point = project(point)
if projected_point is None:
return False
polygons: list[list[tuple[float, float]]] = []
for loop in loops:
polygon = [project(vertex) for vertex in loop] if isinstance(loop, list) else []
if len(polygon) >= 3 and all(vertex is not None for vertex in polygon):
polygons.append([vertex for vertex in polygon if vertex is not None])
if not polygons:
return False
def area(polygon: list[tuple[float, float]]) -> float:
return abs(sum(
polygon[index][0] * polygon[(index + 1) % len(polygon)][1]
- polygon[(index + 1) % len(polygon)][0] * polygon[index][1]
for index in range(len(polygon))
)) / 2
def contains(polygon: list[tuple[float, float]]) -> bool:
x, y = projected_point
inside = False
for index, first in enumerate(polygon):
second = polygon[(index + 1) % len(polygon)]
if cls._distance_to_segment_2d(projected_point, first, second) <= tolerance:
return True
if (first[1] > y) != (second[1] > y):
crossing_x = (second[0] - first[0]) * (y - first[1]) / (second[1] - first[1]) + first[0]
if x < crossing_x:
inside = not inside
return inside
outer = max(polygons, key=area)
return contains(outer) and not any(contains(inner) for inner in polygons if inner is not outer)
@staticmethod
def _distance_to_segment_2d(point: tuple[float, float], start: tuple[float, float], end: tuple[float, float]) -> float:
dx, dy = end[0] - start[0], end[1] - start[1]
length_squared = dx * dx + dy * dy
if length_squared <= 1e-18:
return math.hypot(point[0] - start[0], point[1] - start[1])
fraction = max(0.0, min(1.0, ((point[0] - start[0]) * dx + (point[1] - start[1]) * dy) / length_squared))
return math.hypot(point[0] - (start[0] + fraction * dx), point[1] - (start[1] + fraction * dy))
def _preflight_cut_exit_distance(self, fragment: dict[str, Any], selectors: dict[str, dict[str, Any]], _base: dict[str, Any] | None, require_through: bool) -> None:
if not require_through:
return
params = fragment.get("feature", {}).get("params", {})
depth = params.get("depth_mm", params.get("distance_mm"))
supplied = fragment.get("feature", {}).get("selector_tokens", [])
if not isinstance(depth, (int, float)):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: cut depth is unavailable")
if supplied:
host = selectors[supplied[0]]["geometry"]
bbox = host.get("bbox_mm") if isinstance(host, dict) else None
normal = host.get("normal") or host.get("plane_normal") if isinstance(host, dict) else None
thickness = self._span_from_bbox(bbox, normal)
else:
bbox = self._active_body_bbox(selectors)
normal = (fragment.get("sketch") or {}).get("workplane", {}).get("normal")
thickness = self._span_from_bbox(bbox, normal)
if thickness is None:
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: cannot prove through-cut exit distance from the active topology")
if float(depth) <= thickness + 1e-6:
raise RuntimeAdapterError(
"RUNTIME_PRECONDITION_FAILED: "
f"cut depth {float(depth):g} mm must exceed measured host-body thickness {thickness:g} mm"
)
def _preflight_requires_active_solid(self, _fragment: dict[str, Any], _selectors: dict[str, dict[str, Any]], base: dict[str, Any] | None, _require_through: bool) -> None:
# A committed active CDSL document exists only after a candidate with
# a solid has been accepted. Reference and subtraction operations
# therefore cannot be the first feature of a part.
if not isinstance(base, dict) or not base.get("features"):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: operation requires an active solid checkpoint")
def _preflight_revolve_axis_on_sketch(self, fragment: dict[str, Any], _selectors: dict[str, dict[str, Any]], _base: dict[str, Any] | None, _require_through: bool) -> None:
axis = fragment.get("feature", {}).get("params", {}).get("axis")
direction = axis.get("direction") if isinstance(axis, dict) else None
origin = axis.get("origin_mm") if isinstance(axis, dict) else None
workplane = fragment.get("sketch", {}).get("workplane") if isinstance(fragment.get("sketch"), dict) else None
plane_origin = workplane.get("origin_mm") if isinstance(workplane, dict) else None
plane_normal = workplane.get("normal") if isinstance(workplane, dict) else None
if not self._valid_vector3(direction, require_nonzero=True) or not self._valid_vector3(origin) or not self._valid_vector3(plane_origin) or not self._valid_vector3(plane_normal, require_nonzero=True):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: revolve axis or sketch plane is invalid")
unit_direction = [float(component) / self._norm(direction) for component in direction]
unit_normal = [float(component) / self._norm(plane_normal) for component in plane_normal]
if abs(self._dot(unit_direction, unit_normal)) > 1e-6:
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: revolve axis is not parallel to the sketch plane")
axis_offset = [float(origin[index]) - float(plane_origin[index]) for index in range(3)]
if abs(self._dot(axis_offset, unit_normal)) > 1e-5:
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: revolve axis is not on the sketch plane")
def _preflight_reference_plane_nonzero_normal(self, fragment: dict[str, Any], _selectors: dict[str, dict[str, Any]], _base: dict[str, Any] | None, _require_through: bool) -> None:
plane = fragment.get("feature", {}).get("params", {}).get("plane")
normal = plane.get("normal") if isinstance(plane, dict) else None
if not isinstance(normal, list) or self._norm(normal) <= 1e-9:
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: reference plane normal is degenerate")
def _preflight_reference_axis_nonzero_direction(self, fragment: dict[str, Any], _selectors: dict[str, dict[str, Any]], _base: dict[str, Any] | None, _require_through: bool) -> None:
axis = fragment.get("feature", {}).get("params", {}).get("axis")
direction = axis.get("direction") if isinstance(axis, dict) else None
if not isinstance(direction, list) or self._norm(direction) <= 1e-9:
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: reference axis direction is degenerate")
def _preflight_selected_edges_exist(self, fragment: dict[str, Any], selectors: dict[str, dict[str, Any]], _base: dict[str, Any] | None, _require_through: bool) -> None:
supplied = fragment.get("feature", {}).get("selector_tokens", [])
if not supplied or not all(isinstance(selectors.get(token), dict) and selectors[token].get("kind") == "edge" for token in supplied):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: selected edge is absent or stale")
def _preflight_source_features_exist(self, fragment: dict[str, Any], _selectors: dict[str, dict[str, Any]], base: dict[str, Any] | None, _require_through: bool) -> None:
params = fragment.get("feature", {}).get("params", {})
known = {str(feature.get("id") or "") for feature in ((base or {}).get("features") or ()) if isinstance(feature, dict)}
if not set(params.get("source_feature_ids") or ()).issubset(known):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: source feature is not part of current head")
def _preflight_mirror_plane_exists(self, fragment: dict[str, Any], selectors: dict[str, dict[str, Any]], _base: dict[str, Any] | None, _require_through: bool) -> None:
supplied = fragment.get("feature", {}).get("selector_tokens", [])
if len(supplied) != 1 or not isinstance(selectors.get(supplied[0]), dict) or selectors[supplied[0]].get("kind") != "plane":
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: mirror plane is absent or stale")
@staticmethod
def _polygon_self_intersects(vertices: list[Any]) -> bool:
points = [(float(point[0]), float(point[1])) for point in vertices if isinstance(point, list) and len(point) == 2]
if len(points) != len(vertices):
return True
segments = list(zip(points, [*points[1:], points[0]]))
for first, (a, b) in enumerate(segments):
for second, (c, d) in enumerate(segments):
if second <= first + 1 or (first == 0 and second == len(segments) - 1):
continue
if ProfileCadRuntime._segments_intersect(a, b, c, d):
return True
return False
@staticmethod
def _segments_intersect(a: tuple[float, float], b: tuple[float, float], c: tuple[float, float], d: tuple[float, float]) -> bool:
def orientation(p: tuple[float, float], q: tuple[float, float], r: tuple[float, float]) -> float:
return (q[0] - p[0]) * (r[1] - p[1]) - (q[1] - p[1]) * (r[0] - p[0])
left = orientation(a, b, c)
right = orientation(a, b, d)
low = orientation(c, d, a)
high = orientation(c, d, b)
return (left > 0) != (right > 0) and (low > 0) != (high > 0)
@staticmethod
def _active_body_bbox(selector_tokens: dict[str, dict[str, Any]]) -> list[float] | None:
for token in selector_tokens.values():
if token.get("kind") != "body":
continue
geometry = token.get("geometry")
bbox = geometry.get("bbox_mm") if isinstance(geometry, dict) else None
if isinstance(bbox, list) and len(bbox) == 6 and all(isinstance(value, (int, float)) for value in bbox):
return [float(value) for value in bbox]
return None
@staticmethod
def _span_from_bbox(bbox: Any, normal: Any) -> float | None:
if not isinstance(bbox, list) or len(bbox) != 6 or not isinstance(normal, list) or len(normal) != 3:
return None
if not all(isinstance(value, (int, float)) for value in [*bbox, *normal]):
return None
length = math.sqrt(sum(float(value) ** 2 for value in normal))
if length <= 1e-9:
return None
return sum(
abs(float(normal[index]) / length) * abs(float(bbox[index + 3]) - float(bbox[index]))
for index in range(3)
)
@staticmethod
def _norm(vector: list[float]) -> float:
return math.sqrt(sum(float(item) ** 2 for item in vector))
@staticmethod
def _dot(left: list[float], right: list[float]) -> float:
return sum(float(left[index]) * float(right[index]) for index in range(3))
@staticmethod
def _valid_vector3(value: Any, *, require_nonzero: bool = False) -> bool:
valid = (
isinstance(value, list)
and len(value) == 3
and all(isinstance(item, (int, float)) and not isinstance(item, bool) and math.isfinite(float(item)) for item in value)
)
return valid and (not require_nonzero or ProfileCadRuntime._norm(value) > 1e-9)
@staticmethod
def _valid_bbox(value: Any) -> bool:
return (
isinstance(value, list)
and len(value) == 6
and all(isinstance(item, (int, float)) and not isinstance(item, bool) and math.isfinite(float(item)) for item in value)
and all(float(value[index]) <= float(value[index + 3]) for index in range(3))
)
@staticmethod
def _cross(a: list[float], b: list[float]) -> list[float]:
return [float(a[1]) * float(b[2]) - float(a[2]) * float(b[1]), float(a[2]) * float(b[0]) - float(a[0]) * float(b[2]), float(a[0]) * float(b[1]) - float(a[1]) * float(b[0])]
@staticmethod
def _write_json(path: Path, payload: dict[str, Any]) -> None:
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
@staticmethod
def _health(engine_result: dict[str, Any], step_path: Path, glb_path: Path) -> dict[str, Any]:
bbox = engine_result.get("bbox_mm") if isinstance(engine_result.get("bbox_mm"), dict) else {}
minimum, maximum = bbox.get("min"), bbox.get("max")
if not isinstance(minimum, list) or not isinstance(maximum, list) or len(minimum) != 3 or len(maximum) != 3 or not step_path.is_file() or not glb_path.is_file():
raise RuntimeAdapterError("rebuild did not produce complete deterministic artifacts")
return {"bbox_mm": {"min": [float(item) for item in minimum], "max": [float(item) for item in maximum], "dimensions": [float(maximum[index]) - float(minimum[index]) for index in range(3)]}, "volume_mm3": float(engine_result["volume_mm3"]), "solid_count": int(engine_result["solid_count"]), "feature_count": len(engine_result.get("feature_results") or [])}