"""Controlled CDSL fragments; the backend, never string concatenation, materialises a model.""" from __future__ import annotations from copy import deepcopy from hashlib import sha256 import json from typing import Any from app.services.generation_plan import GenerationPlanError class CdslFragmentError(ValueError): """A node fragment cannot be applied safely to its declared base revision.""" def cdsl_sha256(cdsl: dict[str, Any] | None) -> str: value = cdsl or {"geometry": {"sketches": []}, "features": []} return sha256(json.dumps(value, ensure_ascii=True, sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest() def _items(value: Any, field: str) -> list[dict[str, Any]]: if value is None: return [] if not isinstance(value, list) or not all(isinstance(item, dict) for item in value): raise CdslFragmentError(f"{field} must be an array of objects") return deepcopy(value) def _ids(items: list[dict[str, Any]], field: str) -> list[str]: values = [str(item.get("id") or "").strip() for item in items] if not all(values) or len(values) != len(set(values)): raise CdslFragmentError(f"{field} must have unique non-empty ids") return values def _node(plan: dict[str, Any], node_id: str) -> dict[str, Any]: node = next((item for item in plan.get("nodes") or () if isinstance(item, dict) and item.get("id") == node_id), None) if node is None: raise CdslFragmentError(f"Fragment references an unknown node: {node_id}") return node def _single_output_id(node: dict[str, Any], field: str) -> str: values = [str(item) for item in node.get(field) or () if str(item)] if len(values) != 1: raise CdslFragmentError(f"Node {node.get('id')} must declare exactly one {field} output") return values[0] def _node_feature_id(plan: dict[str, Any], node_id: str) -> str: return _single_output_id(_node(plan, node_id), "cdsl_feature_ids") def _materialize_node_references(value: Any, plan: dict[str, Any]) -> Any: """Translate fragment-only node references into CDSL feature references.""" if isinstance(value, list): return [_materialize_node_references(item, plan) for item in value] if not isinstance(value, dict): return value materialized = {key: _materialize_node_references(item, plan) for key, item in value.items()} owner_node_id = materialized.pop("owner_node_id", None) if owner_node_id is not None: if not isinstance(owner_node_id, str) or not owner_node_id.strip(): raise CdslFragmentError("owner_node_id must be a non-empty plan node id") materialized["owner_feature_id"] = _node_feature_id(plan, owner_node_id.strip()) source_node_ids = materialized.pop("source_node_ids", None) if source_node_ids is not None: if not isinstance(source_node_ids, list) or not source_node_ids or not all(isinstance(item, str) and item.strip() for item in source_node_ids): raise CdslFragmentError("source_node_ids must be a non-empty array of plan node ids") materialized["source_feature_ids"] = [_node_feature_id(plan, item.strip()) for item in source_node_ids] return materialized def _selector_references(value: Any) -> list[dict[str, Any]]: """Collect selector-shaped objects from feature selectors and params.""" found: list[dict[str, Any]] = [] if isinstance(value, dict): if "kind" in value and ("stable_id" in value or "snapshot_id" in value or "owner_feature_id" in value or "owner_node_id" in value): found.append(value) for child in value.values(): found.extend(_selector_references(child)) elif isinstance(value, list): for child in value: found.extend(_selector_references(child)) return found def validate_fragment( fragment: dict[str, Any], *, plan: dict[str, Any], node_id: str, base_revision_id: str, base_cdsl: dict[str, Any] | None, required_snapshot_id: str = "", ) -> dict[str, Any]: if not isinstance(fragment, dict): raise CdslFragmentError("CDSL fragment must be an object") version = str(fragment.get("schema_version") or "cad.cdsl-fragment.v1") if version != "cad.cdsl-fragment.v1": raise CdslFragmentError(f"Unsupported CDSL fragment schema: {version}") declared_node_id = str(fragment.get("node_id") or "") if declared_node_id != node_id: raise CdslFragmentError("Fragment node_id does not match the active node") if str(fragment.get("base_revision_id") or "") != base_revision_id: raise CdslFragmentError("Fragment base_revision_id does not match the active revision") if str(fragment.get("base_cdsl_sha256") or "") != cdsl_sha256(base_cdsl): raise CdslFragmentError("Fragment base_cdsl_sha256 does not match the active CDSL") snapshot_id = str(fragment.get("required_snapshot_id") or "") if required_snapshot_id and snapshot_id != required_snapshot_id: raise CdslFragmentError("Fragment required_snapshot_id does not match the active topology snapshot") if not required_snapshot_id and snapshot_id: raise CdslFragmentError("Fragment cannot use a topology snapshot before one exists") sketches = _items(fragment.get("add_sketches"), "add_sketches") features = _items(fragment.get("add_features"), "add_features") node = _node(plan, node_id) expected = {str(item) for item in node.get("cdsl_feature_ids") or ()} expected_sketches = {str(item) for item in node.get("cdsl_sketch_ids") or ()} if len(expected) != 1: # Compatibility path for pre-incremental plans which could own more # than one CDSL feature in a single node. feature_ids = _ids(features, "add_features") if set(feature_ids) != expected: raise CdslFragmentError( f"Fragment features must exactly match node outputs: expected {sorted(expected)}, got {sorted(feature_ids)}" ) elif len(features) != 1: raise CdslFragmentError("An atomic plan node must generate exactly one feature") if len(expected_sketches) > 1: sketch_ids = _ids(sketches, "add_sketches") if set(sketch_ids) != expected_sketches: raise CdslFragmentError( f"Fragment sketches must exactly match node outputs: expected {sorted(expected_sketches)}, got {sorted(sketch_ids)}" ) elif len(sketches) != len(expected_sketches): expected_description = "one" if expected_sketches else "no" raise CdslFragmentError(f"This node requires {expected_description} new sketch") # The planner and fragment author never own CDSL object identities. The # merger writes node-derived IDs and dependencies after it has validated # the current immutable plan. if len(expected) == 1: features[0]["id"] = next(iter(expected)) if len(expected_sketches) == 1: sketch_id = next(iter(expected_sketches)) sketches[0]["id"] = sketch_id features[0]["sketch_id"] = sketch_id elif not expected_sketches and len(features) == 1: features[0].pop("sketch_id", None) features = _materialize_node_references(features, plan) sketches = _materialize_node_references(sketches, plan) sketch_ids = _ids(sketches, "add_sketches") feature_ids = _ids(features, "add_features") atomic_id = str(node.get("atomic_id") or "") if len(expected) == 1: features[0]["atomic_id"] = atomic_id elif any(str(feature.get("atomic_id") or "") != atomic_id for feature in features): raise CdslFragmentError(f"Fragment features must use plan atomic_id {atomic_id}") base_sketch_ids = { str(item.get("id")) for item in ((base_cdsl or {}).get("geometry") or {}).get("sketches") or () if isinstance(item, dict) } base_feature_ids = { str(item.get("id")) for item in (base_cdsl or {}).get("features") or () if isinstance(item, dict) } if base_sketch_ids & set(sketch_ids): raise CdslFragmentError("Fragment attempts to overwrite an existing sketch") if base_feature_ids & set(feature_ids): raise CdslFragmentError("Fragment attempts to overwrite a frozen feature") predecessor_features = { feature_id for dependency in node.get("depends_on") or () for feature_id in (_node(plan, str(dependency)).get("cdsl_feature_ids") or ()) } if len(expected) == 1: features[0]["depends_on"] = sorted(predecessor_features) available_features = base_feature_ids | set(feature_ids) available_sketches = base_sketch_ids | set(sketch_ids) for feature in features: feature_id = str(feature["id"]) dependencies = feature.get("depends_on") or [] if not isinstance(dependencies, list) or any(str(item) not in available_features for item in dependencies): raise CdslFragmentError(f"Feature {feature_id} has a missing dependency") sketch_id = str(feature.get("sketch_id") or "") if sketch_id and sketch_id not in available_sketches: raise CdslFragmentError(f"Feature {feature_id} references an unknown sketch") if required_snapshot_id: selectors = [selector for feature in features for selector in _selector_references(feature)] if not selectors: raise CdslFragmentError("Topology-dependent fragment must provide runtime snapshot selectors") for selector in selectors: if str(selector.get("snapshot_id") or "") != required_snapshot_id: raise CdslFragmentError("Topology selector must reference the active snapshot") if not str(selector.get("owner_feature_id") or ""): raise CdslFragmentError("Topology selector must declare owner_feature_id") if not isinstance(selector.get("geometry"), dict) or not selector["geometry"]: raise CdslFragmentError("Topology selector must declare a non-empty geometry signature") declared_dependencies = { str(dependency) for feature in features for dependency in feature.get("depends_on") or () } if not predecessor_features.issubset(declared_dependencies): raise CdslFragmentError("Fragment does not preserve all plan-node dependencies") rules = fragment.get("verification_rules") or [] if not isinstance(rules, list) or not all(isinstance(item, dict) for item in rules): raise CdslFragmentError("verification_rules must be an array of objects") assumptions = fragment.get("assumptions") or [] if not isinstance(assumptions, list) or not all(isinstance(item, str) for item in assumptions): raise CdslFragmentError("assumptions must be an array of strings") return { "schema_version": "cad.cdsl-fragment.v1", "node_id": node_id, "base_revision_id": base_revision_id, "base_cdsl_sha256": cdsl_sha256(base_cdsl), "required_snapshot_id": snapshot_id, "add_sketches": sketches, "add_features": features, "expected_feature_ids": sorted(expected), "verification_rules": deepcopy(rules), "assumptions": [item.strip() for item in assumptions if item.strip()], } def materialize_fragment(base_cdsl: dict[str, Any] | None, fragment: dict[str, Any]) -> dict[str, Any]: """Return the complete, append-only document that the runtime must rebuild.""" if base_cdsl is None: document: dict[str, Any] = { "schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "agent_preflight", "geometry": {"sketches": []}, "features": [], } else: document = deepcopy(base_cdsl) geometry = document.setdefault("geometry", {}) if not isinstance(geometry, dict): raise CdslFragmentError("Base CDSL geometry must be an object") sketches = geometry.setdefault("sketches", []) features = document.setdefault("features", []) if not isinstance(sketches, list) or not isinstance(features, list): raise CdslFragmentError("Base CDSL has invalid collections") sketches.extend(deepcopy(fragment["add_sketches"])) features.extend(deepcopy(fragment["add_features"])) return document def selector_bindings(engine_result: dict[str, Any], *, node_id: str, snapshot_id: str) -> dict[str, Any]: """Persist the runtime's actual selector choices as auditable node evidence.""" values = [] for resolution in engine_result.get("selector_resolution") or (): if not isinstance(resolution, dict): continue selector = resolution.get("selector") if isinstance(resolution.get("selector"), dict) else {} candidates = resolution.get("candidates") if isinstance(resolution.get("candidates"), (list, tuple)) else [] values.append({ "consumer_node_id": node_id, "consumer_feature_id": str(resolution.get("feature_id") or ""), "source_snapshot_id": str(selector.get("snapshot_id") or snapshot_id), "kind": str(selector.get("kind") or ""), "owner_feature_id": str(selector.get("owner_feature_id") or ""), "stable_id": str(selector.get("stable_id") or ""), "geometry": deepcopy(selector.get("geometry") or {}), "status": str(resolution.get("status") or ""), "candidates": deepcopy(list(candidates)), "score": resolution.get("score"), "selected": deepcopy(resolution.get("selected") or resolution.get("record") or {}), }) return {"schema_version": "cad.selector-bindings.v1", "node_id": node_id, "bindings": values}