Files
cdsl-cad/backend/app/services/cdsl_fragment.py
T
2026-08-26 14:13:11 +08:00

273 lines
13 KiB
Python

"""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}