"""Deterministic feature-plan validation and readiness calculations.""" from __future__ import annotations from collections import defaultdict, deque from copy import deepcopy from typing import Any, Iterable PLAN_SCHEMA_VERSION = "cad.feature-plan.v1" NODE_STATUSES = { "planned", "ready", "waiting_for_topology", "waiting_for_selection", "blocked", "executing", "executed", "failed", "completed", } _TOPOLOGY_REQUIRED_ATOMICS = { "fillet", "chamfer", "hole_blind", "hole_countersink", "hole_counterbore", "pattern_mirror", } class FeaturePlanError(ValueError): """A feature plan is not a valid acyclic executable plan.""" def _text(value: Any, field: str, *, required: bool = True) -> str: result = str(value or "").strip() if required and not result: raise FeaturePlanError(f"{field} is required") return result def _bool(value: Any) -> bool: return value is True def _normalise_node(raw: Any, index: int) -> dict[str, Any]: if not isinstance(raw, dict): raise FeaturePlanError(f"nodes[{index}] must be an object") node_id = _text(raw.get("id"), f"nodes[{index}].id") atomic_id = _text(raw.get("atomic_id"), f"nodes[{index}].atomic_id") depends_on = raw.get("depends_on") or [] if not isinstance(depends_on, list) or not all(isinstance(item, str) and item.strip() for item in depends_on): raise FeaturePlanError(f"nodes[{index}].depends_on must be an array of non-empty strings") feature_ids = raw.get("cdsl_feature_ids") if feature_ids is None: feature_ids = [node_id] if not isinstance(feature_ids, list) or not feature_ids or not all(isinstance(item, str) and item.strip() for item in feature_ids): raise FeaturePlanError(f"nodes[{index}].cdsl_feature_ids must be a non-empty string array") query = raw.get("topology_query") if query is not None and not isinstance(query, dict): raise FeaturePlanError(f"nodes[{index}].topology_query must be an object") requires_topology = _bool(raw.get("requires_topology")) or atomic_id in _TOPOLOGY_REQUIRED_ATOMICS status = str(raw.get("status") or "planned") if status not in NODE_STATUSES: raise FeaturePlanError(f"nodes[{index}].status is unsupported: {status}") node = { "id": node_id, "intent": _text(raw.get("intent"), f"nodes[{index}].intent", required=False), "atomic_id": atomic_id, "depends_on": list(dict.fromkeys(item.strip() for item in depends_on)), "requires_topology": requires_topology, "topology_query": deepcopy(query) if query is not None else None, "status": status, "cdsl_feature_ids": list(dict.fromkeys(item.strip() for item in feature_ids)), } if raw.get("selector_required") is not None: node["selector_required"] = _bool(raw.get("selector_required")) if isinstance(raw.get("failure"), dict): node["failure"] = deepcopy(raw["failure"]) return node def validate_feature_plan(plan: dict[str, Any], *, supported_atomic_ids: Iterable[str] = ()) -> dict[str, Any]: if not isinstance(plan, dict): raise FeaturePlanError("Feature plan must be an object") version = str(plan.get("schema_version") or PLAN_SCHEMA_VERSION) if version != PLAN_SCHEMA_VERSION: raise FeaturePlanError(f"Unsupported feature plan schema: {version}") plan_id = _text(plan.get("plan_id"), "plan_id") task_id = _text(plan.get("task_id"), "task_id", required=False) topology_snapshot_id = _text(plan.get("topology_snapshot_id"), "topology_snapshot_id", required=False) raw_nodes = plan.get("nodes") if not isinstance(raw_nodes, list) or not raw_nodes: raise FeaturePlanError("Feature plan requires a non-empty nodes array") nodes = [_normalise_node(item, index) for index, item in enumerate(raw_nodes)] by_id: dict[str, dict[str, Any]] = {} node_index: dict[str, int] = {} feature_owner: dict[str, str] = {} supported = {str(item) for item in supported_atomic_ids if str(item)} for index, node in enumerate(nodes): if node["id"] in by_id: raise FeaturePlanError(f"Duplicate feature plan node: {node['id']}") if supported and node["atomic_id"] not in supported: raise FeaturePlanError(f"Unsupported feature plan atomic_id: {node['atomic_id']}") by_id[node["id"]] = node node_index[node["id"]] = index for feature_id in node["cdsl_feature_ids"]: if feature_id in feature_owner: raise FeaturePlanError(f"CDSL feature belongs to multiple plan nodes: {feature_id}") feature_owner[feature_id] = node["id"] indegree = {node_id: 0 for node_id in by_id} children: dict[str, list[str]] = defaultdict(list) for node in nodes: for dependency in node["depends_on"]: if dependency not in by_id: raise FeaturePlanError(f"Node {node['id']} has missing dependency: {dependency}") if node_index[dependency] >= node_index[node["id"]]: raise FeaturePlanError(f"Node {node['id']} must appear after dependency: {dependency}") indegree[node["id"]] += 1 children[dependency].append(node["id"]) queue = deque(node_id for node_id, degree in indegree.items() if degree == 0) visited: list[str] = [] while queue: node_id = queue.popleft() visited.append(node_id) for child in children[node_id]: indegree[child] -= 1 if indegree[child] == 0: queue.append(child) if len(visited) != len(nodes): raise FeaturePlanError("Feature plan contains a dependency cycle") return { "schema_version": PLAN_SCHEMA_VERSION, "plan_id": plan_id, "task_id": task_id, "topology_snapshot_id": topology_snapshot_id, "nodes": nodes, "feature_owner": feature_owner, } def _topology_available(topology: dict[str, Any] | None) -> bool: if not isinstance(topology, dict): return False return any( isinstance(record, dict) and record.get("executable", True) is not False and record.get("kind") in {"face", "edge", "vertex", "body", "plane", "axis"} for record in topology.get("records") or () ) def compute_node_statuses( plan: dict[str, Any], *, cdsl: dict[str, Any] | None = None, topology: dict[str, Any] | None = None, ) -> dict[str, Any]: """Return a copy with deterministic status and readiness information.""" checked = validate_feature_plan(plan) present_features = { str(feature.get("id")) for feature in (cdsl or {}).get("features") or () if isinstance(feature, dict) and feature.get("id") } has_topology = _topology_available(topology) topology_snapshot_id = str((topology or {}).get("snapshot_id") or "") selection_ready = bool(topology_snapshot_id and checked.get("topology_snapshot_id") == topology_snapshot_id) by_id = {node["id"]: node for node in checked["nodes"]} result_nodes: list[dict[str, Any]] = [] for original in checked["nodes"]: node = deepcopy(original) if node["status"] in {"failed", "blocked"}: result_nodes.append(node) continue if all(feature_id in present_features for feature_id in node["cdsl_feature_ids"]): node["status"] = "completed" result_nodes.append(node) continue dependencies_done = all(by_id[item]["status"] in {"executed", "completed"} or all( feature_id in present_features for feature_id in by_id[item]["cdsl_feature_ids"] ) for item in node["depends_on"]) if not dependencies_done: node["status"] = "planned" elif node["requires_topology"] and not has_topology: node["status"] = "waiting_for_topology" elif node["requires_topology"] and not selection_ready: node["status"] = "waiting_for_selection" else: node["status"] = "ready" result_nodes.append(node) ready = [node["id"] for node in result_nodes if node["status"] == "ready"] waiting = [node["id"] for node in result_nodes if node["status"] in {"waiting_for_topology", "waiting_for_selection"}] blocked = [node["id"] for node in result_nodes if node["status"] == "blocked"] completed = [node["id"] for node in result_nodes if node["status"] == "completed"] return { **checked, "nodes": result_nodes, "ready_nodes": ready, "waiting_nodes": waiting, "blocked_nodes": blocked, "completed_nodes": completed, "complete": len(completed) == len(result_nodes) and not blocked, } def plan_feature_ids(plan: dict[str, Any], node_ids: Iterable[str]) -> set[str]: checked = validate_feature_plan(plan) wanted = set(node_ids) return { feature_id for node in checked["nodes"] if node["id"] in wanted for feature_id in node["cdsl_feature_ids"] } def node_for_feature(plan: dict[str, Any], feature_id: str) -> dict[str, Any] | None: checked = validate_feature_plan(plan) return next((node for node in checked["nodes"] if feature_id in node["cdsl_feature_ids"]), None)