"""Generic, model-family-independent verification for direct CDSL revisions.""" from __future__ import annotations import math from typing import Any QUALITY_RULE_TYPES = frozenset({ "bbox", "solid_count", "feature_count", "hole_count", "hole_diameter", "hole_center", "overall_length", "overall_width", "overall_height", "overall_diameter", "through_condition", }) FEATURE_RULE_TYPES = frozenset({"hole_count", "hole_diameter", "hole_center", "through_condition"}) _SEVERITIES = {"blocking", "warning", "informational"} def _bbox_bounds(engine_result: dict[str, Any], feature_id: str | None = None) -> tuple[list[float], list[float]] | None: """Return a whole-model or feature-owned runtime bounding box.""" candidates: list[Any] = [] if feature_id: for record in engine_result.get("topology_records") or []: if not isinstance(record, dict): continue owners = record.get("owner_feature_ids") or [] # Runtime records may carry the current operation in feature_id # while owner_feature_ids identify the actual geometry owner. Use # ownership when present so inherited faces do not pollute a # feature-local measurement. matches = feature_id in owners if owners else record.get("feature_id") == feature_id if matches: geometry = record.get("geometry") if isinstance(geometry, dict): candidates.append(geometry.get("bbox_mm")) else: bbox = engine_result.get("bbox_mm") if isinstance(bbox, dict): candidates.append([ *(bbox.get("min") or []), *(bbox.get("max") or []), ]) boxes = [ value for value in candidates if isinstance(value, list) and len(value) == 6 and all(isinstance(item, (int, float)) and math.isfinite(float(item)) for item in value) ] if not boxes: return None minimum = [min(box[index] for box in boxes) for index in (0, 1, 2)] maximum = [max(box[index] for box in boxes) for index in (3, 4, 5)] return [float(item) for item in minimum], [float(item) for item in maximum] def _bbox_value(engine_result: dict[str, Any], feature_id: str | None = None) -> dict[str, Any] | None: bounds = _bbox_bounds(engine_result, feature_id) if bounds is None: return None minimum, maximum = bounds return { "min": minimum, "max": maximum, "dimensions": [maximum[index] - minimum[index] for index in range(3)], } def _dimensions(engine_result: dict[str, Any]) -> list[float] | None: value = _bbox_value(engine_result) return value["dimensions"] if value else None def _feature(cdsl: dict[str, Any], feature_id: str | None) -> dict[str, Any] | None: return next((item for item in cdsl.get("features") or [] if isinstance(item, dict) and str(item.get("id")) == feature_id), None) def _sketch(cdsl: dict[str, Any], sketch_id: str | None) -> dict[str, Any] | None: return next((item for item in (cdsl.get("geometry") or {}).get("sketches") or [] if isinstance(item, dict) and str(item.get("id")) == sketch_id), None) def _circles(profile: dict[str, Any]) -> list[dict[str, Any]]: if profile.get("type") == "circle": center, radius = profile.get("center"), profile.get("radius_mm") if isinstance(center, list) and len(center) >= 2 and isinstance(radius, (int, float)): return [{"center": [float(center[0]), float(center[1])], "radius_mm": float(radius)}] if profile.get("type") != "analytic_contours": return [] return [ {"center": [float(segment["center"][0]), float(segment["center"][1])], "radius_mm": float(segment["radius_mm"])} for contour in profile.get("contours") or [] if isinstance(contour, dict) for segment in contour.get("segments") or [] if isinstance(segment, dict) and segment.get("type") == "circle" and isinstance(segment.get("center"), list) and len(segment["center"]) >= 2 and isinstance(segment.get("radius_mm"), (int, float)) ] def _circle_feature_bbox(feature: dict[str, Any] | None, sketch: dict[str, Any] | None) -> dict[str, Any] | None: """Derive a stable world-space bbox for a circular extrude feature.""" if not isinstance(feature, dict) or not isinstance(sketch, dict): return None if str(feature.get("atomic_id") or "") != "extrude_add_blind": return None profile = sketch.get("profile") workplane = sketch.get("workplane") params = feature.get("params") if not isinstance(profile, dict) or profile.get("type") != "circle" or not isinstance(workplane, dict): return None center_local = profile.get("center") radius = profile.get("radius_mm") origin = workplane.get("origin_mm") x_dir = workplane.get("x_dir") y_dir = workplane.get("y_dir") normal = workplane.get("normal") distance = (params or {}).get("distance_mm") if isinstance(params, dict) else None if not isinstance(center_local, list) or len(center_local) < 2 or not _finite_number(radius): return None if not all(isinstance(value, list) and len(value) >= 3 for value in (origin, x_dir, y_dir, normal)) or not _finite_number(distance): return None center = [ float(origin[index]) + float(center_local[0]) * float(x_dir[index]) + float(center_local[1]) * float(y_dir[index]) for index in range(3) ] points = [] for axis_x in (-1.0, 1.0): for axis_y in (-1.0, 1.0): cross_section = [ center[index] + axis_x * float(radius) * float(x_dir[index]) + axis_y * float(radius) * float(y_dir[index]) for index in range(3) ] points.append(cross_section) points.append([ cross_section[index] + float(distance) * float(normal[index]) for index in range(3) ]) minimum = [min(point[index] for point in points) for index in range(3)] maximum = [max(point[index] for point in points) for index in range(3)] return { "min": minimum, "max": maximum, "dimensions": [maximum[index] - minimum[index] for index in range(3)], } def _finite_number(value: Any) -> bool: return isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(float(value)) def _validate_expected(kind: str, expected: Any, index: int) -> None: scalar_types = { "solid_count", "feature_count", "hole_count", "hole_diameter", "overall_length", "overall_width", "overall_height", "overall_diameter", } if kind in scalar_types and not _finite_number(expected): raise ValueError(f"verification.rules[{index}].expected must be a finite number for {kind}") if kind == "bbox": valid_dimensions = isinstance(expected, list) and len(expected) == 3 and all(_finite_number(value) for value in expected) valid_ranges = isinstance(expected, dict) and set(expected) in ({"min", "max"}, {"x_min", "x_max", "y_min", "y_max", "z_min", "z_max"}) if valid_ranges and set(expected) == {"min", "max"}: valid_ranges = all( isinstance(expected[key], list) and len(expected[key]) == 3 and all(_finite_number(value) for value in expected[key]) for key in ("min", "max") ) elif valid_ranges: valid_ranges = all(_finite_number(expected[key]) for key in expected) if not valid_dimensions and not valid_ranges: raise ValueError( f"verification.rules[{index}].expected for bbox must be a three-number bbox [dx, dy, dz], " "{min, max}, or {x_min, x_max, y_min, y_max, z_min, z_max}" ) if kind == "hole_center" and ( not isinstance(expected, list) or len(expected) != 2 or not all(_finite_number(value) for value in expected) ): raise ValueError(f"verification.rules[{index}].expected must be a two-number hole center") if kind == "through_condition" and not isinstance(expected, bool): raise ValueError(f"verification.rules[{index}].expected must be boolean for through_condition") def validate_verification(verification: Any, cdsl: dict[str, Any]) -> list[dict[str, Any]]: if verification is None: return [] if not isinstance(verification, dict) or set(verification) - {"rules"}: raise ValueError("verification must be an object containing only rules") rules = verification.get("rules", []) if not isinstance(rules, list) or len(rules) > 32: raise ValueError("verification.rules must be an array with at most 32 rules") feature_ids = {str(item.get("id")) for item in cdsl.get("features") or [] if isinstance(item, dict) and item.get("id")} normalized: list[dict[str, Any]] = [] ids: set[str] = set() for index, raw in enumerate(rules): if not isinstance(raw, dict): raise ValueError(f"verification.rules[{index}] must be an object") allowed = {"id", "type", "feature", "expected", "tolerance", "severity"} if set(raw) - allowed: raise ValueError(f"verification.rules[{index}] has unsupported fields") rule_id = str(raw.get("id") or "").strip() kind = str(raw.get("type") or "").strip() if not rule_id or rule_id in ids: raise ValueError(f"verification.rules[{index}].id must be unique and non-empty") if kind not in QUALITY_RULE_TYPES: raise ValueError(f"verification.rules[{index}].type is unsupported: {kind}") if "expected" not in raw: raise ValueError(f"verification.rules[{index}].expected is required") _validate_expected(kind, raw["expected"], index) severity = str(raw.get("severity") or "blocking") if severity not in _SEVERITIES: raise ValueError(f"verification.rules[{index}].severity is unsupported") try: tolerance = float(raw.get("tolerance") or 0.0) except (TypeError, ValueError) as error: raise ValueError(f"verification.rules[{index}].tolerance must be numeric") from error if not math.isfinite(tolerance) or tolerance < 0: raise ValueError(f"verification.rules[{index}].tolerance must be finite and non-negative") feature = str(raw.get("feature") or "").strip() if kind in FEATURE_RULE_TYPES and not feature: raise ValueError(f"verification.rules[{index}].feature is required for {kind}") if feature and feature not in feature_ids: raise ValueError(f"verification.rules[{index}].feature must reference a CDSL feature ID") ids.add(rule_id) normalized.append({"id": rule_id, "type": kind, "feature": feature, "expected": raw["expected"], "tolerance": tolerance, "severity": severity}) return normalized def _actual(rule: dict[str, Any], cdsl: dict[str, Any], engine_result: dict[str, Any]) -> tuple[Any, str]: kind, target = rule["type"], rule.get("feature") or None feature = _feature(cdsl, target) params = (feature or {}).get("params") if isinstance((feature or {}).get("params"), dict) else {} sketch = _sketch(cdsl, str((feature or {}).get("sketch_id") or "")) profile = (sketch or {}).get("profile") if isinstance(sketch, dict) else {} circles = _circles(profile) if isinstance(profile, dict) else [] dimensions = _dimensions(engine_result) if kind == "bbox": derived = _circle_feature_bbox(feature, sketch) if target and derived is not None: return derived, f"cdsl.features.{target}.sketch + params" value = _bbox_value(engine_result, target) source = "runtime.bbox_mm" if not target else f"runtime.topology_records[{target}].bbox_mm" return value, source if kind == "solid_count": return float(engine_result.get("solid_count", 1)), "runtime.solid_count" if kind == "feature_count": return float(len(cdsl.get("features") or [])), "cdsl.features" if kind == "hole_count": return float(len(circles)), f"cdsl.features.{target}.sketch" if kind == "hole_diameter": value = params.get("diameter_mm", params.get("hole_diameter_mm")) if isinstance(value, (int, float)): return float(value), f"cdsl.features.{target}.params" return (circles[0]["radius_mm"] * 2 if circles else None), f"cdsl.features.{target}.sketch" if kind == "hole_center": centers = [circle["center"] for circle in circles] return (centers[0] if len(centers) == 1 else centers), f"cdsl.features.{target}.sketch" if kind == "overall_length": return (max(dimensions) if dimensions else None), "runtime.bbox_mm" if kind == "overall_width": return (dimensions[1] if dimensions and len(dimensions) > 1 else None), "runtime.bbox_mm[y]" if kind == "overall_height": return (dimensions[2] if dimensions and len(dimensions) > 2 else None), "runtime.bbox_mm[z]" if kind == "overall_diameter": if target and circles: return max(circle["radius_mm"] for circle in circles) * 2.0, f"cdsl.features.{target}.sketch" return (min(dimensions) if dimensions else None), "runtime.bbox_mm" if kind == "through_condition": end = params.get("end_condition") if isinstance(params.get("end_condition"), dict) else {} if end.get("type") in {"through_all", "through_all_both", "through_all_and_blind"}: return True, f"cdsl.features.{target}.params.end_condition" distance = params.get("distance_mm") return bool(isinstance(distance, (int, float)) and dimensions and float(distance) >= min(dimensions) - 1e-6), "cdsl.params + runtime.bbox_mm" return None, "unsupported verification type" def _matches(expected: Any, actual: Any, tolerance: float) -> bool: if actual is None: return False if isinstance(expected, list): return isinstance(actual, list) and len(expected) == len(actual) and all( _matches(expected_item, actual_item, tolerance) for expected_item, actual_item in zip(expected, actual) ) if isinstance(expected, bool): return bool(actual) is expected if isinstance(expected, (int, float)) and isinstance(actual, (int, float)): return math.isclose(float(expected), float(actual), abs_tol=tolerance, rel_tol=0.0) return expected == actual def _matches_bbox(expected: Any, actual: dict[str, Any] | None, tolerance: float) -> bool: if actual is None: return False if isinstance(expected, list): return _matches(expected, actual["dimensions"], tolerance) if not isinstance(expected, dict): return False if set(expected) == {"min", "max"}: return _matches(expected["min"], actual["min"], tolerance) and _matches(expected["max"], actual["max"], tolerance) if set(expected) == {"x_min", "x_max", "y_min", "y_max", "z_min", "z_max"}: actual_ranges = { "x_min": actual["min"][0], "x_max": actual["max"][0], "y_min": actual["min"][1], "y_max": actual["max"][1], "z_min": actual["min"][2], "z_max": actual["max"][2], } return all(_matches(expected[key], actual_ranges[key], tolerance) for key in actual_ranges) return False def evaluate_quality(rules: list[dict[str, Any]], cdsl: dict[str, Any], engine_result: dict[str, Any]) -> dict[str, Any]: results = [] for rule in rules: actual, source = _actual(rule, cdsl, engine_result) passed = ( _matches_bbox(rule["expected"], actual, rule["tolerance"]) if rule["type"] == "bbox" else _matches(rule["expected"], actual, rule["tolerance"]) ) results.append({**rule, "status": "passed" if passed else ("failed" if actual is not None else "unavailable"), "actual": actual, "source": source}) blocking = [result for result in results if result["severity"] == "blocking" and result["status"] != "passed"] warnings = [result for result in results if result["severity"] != "blocking" and result["status"] != "passed"] return { "schema": "cad.quality-report.v1", "schema_version": "1.0", "status": "passed" if not blocking else "failed", "verification_requested": bool(rules), "results": results, "blocking_failures": blocking, "warnings": warnings, "measurements": {"bbox_mm": engine_result.get("bbox_mm"), "solid_count": engine_result.get("solid_count", 1)}, }