Files
cdsl-cad/backend/engine/cdsl_engine/design_intent.py
T
2026-08-24 14:52:08 +08:00

332 lines
15 KiB
Python

"""Validation for the semantic planning artifact between a request and CDSL."""
from __future__ import annotations
import json
from copy import deepcopy
from pathlib import Path
from typing import Any
from jsonschema import Draft202012Validator
from jsonschema.exceptions import SchemaError
class DesignIntentError(ValueError):
"""A plan-contract violation with a stable error code for the Agent."""
def __init__(self, code: str, message: str) -> None:
super().__init__(message)
self.code = code
def _schema_path(engine: Any) -> Path:
return Path(str(engine.__file__)).with_name("design_intent_schema.json")
def load_design_intent_schema(engine: Any) -> dict[str, Any]:
path = _schema_path(engine)
try:
schema = json.loads(path.read_text(encoding="utf-8"))
Draft202012Validator.check_schema(schema)
except (OSError, json.JSONDecodeError, SchemaError) as error:
raise RuntimeError("The local DesignIntent JSON Schema is unavailable or invalid") from error
return schema
def _location(error: Any) -> str:
return "$" + "".join(
f"[{item}]" if isinstance(item, int) else f".{item}"
for item in error.absolute_path
)
def _schema_validate(intent: dict[str, Any], engine: Any) -> None:
errors = sorted(
Draft202012Validator(load_design_intent_schema(engine)).iter_errors(intent),
key=lambda error: (list(error.absolute_path), error.message),
)
if errors:
error = errors[0]
raise DesignIntentError(
"INVALID_DESIGN_INTENT",
f"DesignIntent schema violation at {_location(error)}: {error.message}",
)
def _runtime_capabilities(engine: Any) -> tuple[set[str], set[str], dict[str, dict[str, Any]]]:
try:
profile_schema = json.loads(Path(str(engine.__file__)).with_name("profile_schema.json").read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as error:
raise RuntimeError("The local CDSL profile schema is unavailable or invalid") from error
atomic_ids = set(profile_schema.get("runtime_supported_atomic_ids") or getattr(engine, "SUPPORTED_ATOMIC_IDS", ()))
profiles = {
name
for name, contract in (profile_schema.get("profiles") or {}).items()
if isinstance(contract, dict) and contract.get("agent_allowed") is True
}
atomic_contracts = {
str(atomic_id): contract
for atomic_id, contract in (profile_schema.get("feature_atomic_ids") or {}).items()
if isinstance(contract, dict)
}
return {str(item) for item in atomic_ids}, {str(item) for item in profiles}, atomic_contracts
def _blocking(intent: dict[str, Any]) -> bool:
questions = intent.get("open_questions") or []
gaps = intent.get("capability_gaps") or []
return any(isinstance(item, dict) and item.get("blocking") is True for item in [*questions, *gaps])
def validate_design_intent(
intent: dict[str, Any],
engine: Any,
*,
current_revision_id: str = "",
) -> dict[str, Any]:
"""Validate DesignIntent syntax and semantic ordering against runtime capability."""
if not isinstance(intent, dict):
raise DesignIntentError("INVALID_DESIGN_INTENT", "DesignIntent must be a JSON object")
_schema_validate(intent, engine)
normalized = deepcopy(intent)
mode = str(normalized["mode"])
base_revision_id = str(normalized["base_revision_id"] or "")
if mode == "create" and base_revision_id:
raise DesignIntentError("INVALID_DESIGN_INTENT", "create DesignIntent must not set base_revision_id")
if mode == "revise":
if not base_revision_id:
raise DesignIntentError("INVALID_DESIGN_INTENT", "revise DesignIntent requires base_revision_id")
if current_revision_id and base_revision_id != current_revision_id:
raise DesignIntentError(
"INVALID_DESIGN_INTENT",
"revise DesignIntent base_revision_id must be the current successful revision",
)
structures = normalized["structures"]
ids = [str(item["id"]) for item in structures]
feature_ids = [str(item["cdsl_feature_id"]) for item in structures]
if len(set(ids)) != len(ids):
raise DesignIntentError("INVALID_DESIGN_INTENT", "DesignIntent structure ids must be unique")
if len(set(feature_ids)) != len(feature_ids):
raise DesignIntentError("INVALID_DESIGN_INTENT", "DesignIntent cdsl_feature_id values must be unique")
feature_order = [str(item) for item in normalized["feature_order"]]
if len(set(feature_order)) != len(feature_order) or set(feature_order) != set(ids):
raise DesignIntentError(
"INVALID_DESIGN_INTENT",
"feature_order must list every DesignIntent structure exactly once",
)
order = {structure_id: index for index, structure_id in enumerate(feature_order)}
atomic_ids, profile_types, atomic_contracts = _runtime_capabilities(engine)
for structure in structures:
structure_id = str(structure["id"])
for dependency in structure["depends_on"]:
dependency_id = str(dependency)
if dependency_id not in order:
raise DesignIntentError(
"INVALID_DESIGN_INTENT",
f"Structure {structure_id} depends on an unknown structure {dependency_id}",
)
if order[dependency_id] >= order[structure_id]:
raise DesignIntentError(
"INVALID_DESIGN_INTENT",
f"Structure {structure_id} must depend only on earlier feature_order entries",
)
strategy = structure["cdsl_strategy"]
atomic_id = str(strategy["atomic_id"])
if atomic_id not in atomic_ids:
raise DesignIntentError(
"INVALID_DESIGN_INTENT",
f"Structure {structure_id} uses unsupported atomic_id {atomic_id}",
)
profile_type = str(strategy.get("profile_type") or "")
if atomic_contracts.get(atomic_id, {}).get("requires_sketch") and not profile_type:
raise DesignIntentError(
"INVALID_DESIGN_INTENT",
f"Structure {structure_id} uses {atomic_id} and requires a profile_type",
)
if profile_type and profile_type not in profile_types:
raise DesignIntentError(
"INVALID_DESIGN_INTENT",
f"Structure {structure_id} uses unsupported or agent-disallowed profile_type {profile_type}",
)
if normalized["status"] == "ready" and _blocking(normalized):
raise DesignIntentError(
"DESIGN_INTENT_BLOCKED",
"A DesignIntent with blocking questions or capability gaps must need clarification",
)
if normalized["status"] == "needs_clarification" and not _blocking(normalized):
raise DesignIntentError(
"INVALID_DESIGN_INTENT",
"needs_clarification requires a blocking question or capability gap",
)
if normalized["status"] == "ready" and not structures:
raise DesignIntentError("INVALID_DESIGN_INTENT", "A ready DesignIntent needs at least one structure")
if normalized["status"] == "ready" and mode == "create" and not any(item["role"] == "base" for item in structures):
raise DesignIntentError("INVALID_DESIGN_INTENT", "A ready create DesignIntent needs a base structure")
nonblocking_gaps = [item for item in normalized["capability_gaps"] if not item["blocking"]]
if nonblocking_gaps and not normalized["assumptions"]:
raise DesignIntentError(
"INVALID_DESIGN_INTENT",
"Non-blocking capability gaps require an explicit approximation assumption",
)
for gap in normalized["capability_gaps"]:
if str(gap["structure_id"]) not in set(ids):
raise DesignIntentError(
"INVALID_DESIGN_INTENT",
f"Capability gap references an unknown structure {gap['structure_id']}",
)
valid_expectation_ids = set(ids) | set(feature_ids)
for expectation in normalized["verification_expectations"]:
if expectation["type"] == "feature_count" and str(expectation["feature_id"]) not in valid_expectation_ids:
raise DesignIntentError(
"INVALID_DESIGN_INTENT",
f"feature_count expectation references an unknown structure or CDSL feature {expectation['feature_id']}",
)
return normalized
def _has_selector_evidence(value: Any, role: str) -> bool:
if isinstance(value, dict):
for key, child in value.items():
if role in {"selector", "selectors"} and key == "selectors" and isinstance(child, list) and child:
return True
if key == role and child not in (None, "", [], {}):
if not (isinstance(child, dict) and set(child) == {"unresolved"}):
return True
if _has_selector_evidence(child, role):
return True
elif isinstance(value, list):
return any(_has_selector_evidence(item, role) for item in value)
return False
def validate_intent_cdsl(
intent: dict[str, Any],
cdsl: dict[str, Any],
engine: Any,
*,
current_cdsl: dict[str, Any] | None = None,
) -> None:
"""Ensure an executable CDSL document faithfully realizes one accepted plan."""
normalized = validate_design_intent(intent, engine)
if normalized["status"] != "ready":
raise DesignIntentError("DESIGN_INTENT_BLOCKED", "CDSL generation is blocked until the DesignIntent is ready")
if not isinstance(cdsl, dict):
raise DesignIntentError("INTENT_CDSL_MISMATCH", "CDSL must be a JSON object")
features = cdsl.get("features")
sketches = (cdsl.get("geometry") or {}).get("sketches")
if not isinstance(features, list) or not isinstance(sketches, list):
raise DesignIntentError("INTENT_CDSL_MISMATCH", "CDSL must contain features and sketches")
feature_by_id = {str(feature.get("id") or ""): feature for feature in features if isinstance(feature, dict)}
if len(feature_by_id) != len(features):
raise DesignIntentError("INTENT_CDSL_MISMATCH", "CDSL features must have unique ids")
structure_by_id = {str(item["id"]): item for item in normalized["structures"]}
expected_feature_ids = [str(structure_by_id[item]["cdsl_feature_id"]) for item in normalized["feature_order"]]
if normalized["mode"] == "revise" and isinstance(current_cdsl, dict):
base_feature_ids = {
str(feature.get("id") or "")
for feature in current_cdsl.get("features") or []
if isinstance(feature, dict) and feature.get("id")
}
if not base_feature_ids.issubset(set(expected_feature_ids)):
missing = ", ".join(sorted(base_feature_ids - set(expected_feature_ids)))
raise DesignIntentError(
"INTENT_CDSL_MISMATCH",
f"revise DesignIntent must explicitly preserve current features: {missing}",
)
actual_feature_ids = [str(feature.get("id") or "") for feature in features]
if actual_feature_ids != expected_feature_ids:
raise DesignIntentError(
"INTENT_CDSL_MISMATCH",
"CDSL feature ids and order must exactly match DesignIntent feature_order",
)
sketch_by_id = {str(sketch.get("id") or ""): sketch for sketch in sketches if isinstance(sketch, dict)}
feature_for_structure = {
str(structure["id"]): feature_by_id[str(structure["cdsl_feature_id"])]
for structure in normalized["structures"]
}
for structure in normalized["structures"]:
structure_id = str(structure["id"])
feature = feature_for_structure[structure_id]
strategy = structure["cdsl_strategy"]
if feature.get("atomic_id") != strategy["atomic_id"]:
raise DesignIntentError(
"INTENT_CDSL_MISMATCH",
f"CDSL feature {feature.get('id')} atomic_id does not match structure {structure_id}",
)
expected_dependencies = {
str(structure_by_id[dependency]["cdsl_feature_id"])
for dependency in structure["depends_on"]
}
actual_dependencies = {str(item) for item in feature.get("depends_on") or []}
if not expected_dependencies.issubset(actual_dependencies):
raise DesignIntentError(
"INTENT_CDSL_MISMATCH",
f"CDSL feature {feature.get('id')} is missing planned dependencies",
)
profile_type = str(strategy.get("profile_type") or "")
if profile_type:
sketch_id = str(feature.get("sketch_id") or "")
sketch = sketch_by_id.get(sketch_id)
actual_profile_type = str(((sketch or {}).get("profile") or {}).get("type") or "")
if actual_profile_type != profile_type:
raise DesignIntentError(
"INTENT_CDSL_MISMATCH",
f"CDSL feature {feature.get('id')} must use profile_type {profile_type}",
)
for selector_role in strategy["selector_roles"]:
if not _has_selector_evidence(feature, str(selector_role)):
raise DesignIntentError(
"INTENT_CDSL_MISMATCH",
f"CDSL feature {feature.get('id')} is missing selector evidence for {selector_role}",
)
def design_intent_from_cdsl(cdsl: dict[str, Any], *, request: str, mode: str = "create", base_revision_id: str = "") -> dict[str, Any]:
"""Create a conservative audit-only plan for legacy revisions when needed."""
sketches = (cdsl.get("geometry") or {}).get("sketches") or []
profile_by_sketch_id = {
str(sketch.get("id") or ""): str((sketch.get("profile") or {}).get("type") or "")
for sketch in sketches
if isinstance(sketch, dict)
}
structures: list[dict[str, Any]] = []
for index, feature in enumerate(cdsl.get("features") or []):
if not isinstance(feature, dict):
continue
feature_id = str(feature.get("id") or f"feature_{index + 1}")
atomic_id = str(feature.get("atomic_id") or "")
profile_type = profile_by_sketch_id.get(str(feature.get("sketch_id") or ""), "")
strategy: dict[str, Any] = {
"atomic_id": atomic_id,
"parameter_roles": sorted(str(key) for key in (feature.get("params") or {}).keys()),
"selector_roles": [],
}
if profile_type:
strategy["profile_type"] = profile_type
structures.append({
"id": feature_id,
"cdsl_feature_id": feature_id,
"role": "base" if index == 0 else "subtractive" if "cut" in atomic_id or atomic_id.startswith("hole") else "additive",
"purpose": str(feature.get("name") or feature_id),
"depends_on": [str(item) for item in feature.get("depends_on") or []],
"cdsl_strategy": strategy,
})
return {
"schema": "cad.cdsl.design-intent.v1",
"schema_version": "1.0",
"mode": mode,
"request": request,
"base_revision_id": base_revision_id,
"structures": structures,
"feature_order": [item["id"] for item in structures],
"assumptions": ["Synthesized from a legacy CDSL revision."],
"open_questions": [],
"capability_gaps": [],
"verification_expectations": [],
"status": "ready",
}