wip: preserve local engine and agent workflow changes before ganjihong refactor integration
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
"""Strict model-facing Authoring CDSL contract.
|
||||
|
||||
This is intentionally separate from the runtime CDSL: model output contains
|
||||
only document-local names and declarative references. Runtime identities are
|
||||
allocated by :mod:`authoring_compiler`.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import re
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||
|
||||
_NAME = r"^[a-z][a-z0-9_]{0,63}$"
|
||||
_FORBIDDEN = {
|
||||
"id",
|
||||
"task_id", "revision_id", "candidate_id", "action_id", "requirement_id",
|
||||
"claim_id", "evidence_id", "feature_id", "sketch_id", "body_id",
|
||||
"stable_id", "snapshot_id", "owner_feature_id", "working_head",
|
||||
"selector_token", "selector_tokens", "selector token", "selector-token", "selectorToken",
|
||||
"host_face", "mirror_plane",
|
||||
}
|
||||
|
||||
|
||||
def validation_error_code(error: Exception) -> str:
|
||||
"""Map strict model validation failures to a stable public diagnostic."""
|
||||
return "AUTHOR_FORBIDDEN_FIELD" if "AUTHOR_FORBIDDEN_FIELD:" in str(error) else "AUTHOR_SCHEMA_INVALID"
|
||||
|
||||
|
||||
class AuthorModel(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid", strict=True)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def reject_internal_fields(cls, value: Any) -> Any:
|
||||
if isinstance(value, dict):
|
||||
found = sorted(
|
||||
key for key in value
|
||||
if isinstance(key, str)
|
||||
and (key in _FORBIDDEN or key.lower().replace("-", "_").replace(" ", "_") in _FORBIDDEN)
|
||||
)
|
||||
if found:
|
||||
raise ValueError(f"AUTHOR_FORBIDDEN_FIELD: {found[0]}")
|
||||
for nested in value.values():
|
||||
cls.reject_internal_fields(nested)
|
||||
return value
|
||||
if isinstance(value, list):
|
||||
for item in value:
|
||||
cls.reject_internal_fields(item)
|
||||
return value
|
||||
|
||||
|
||||
class AuthorWorkplane(AuthorModel):
|
||||
"""A fully explicit local sketch frame in world millimetres."""
|
||||
|
||||
origin_mm: list[float] = Field(min_length=3, max_length=3)
|
||||
x_dir: list[float] = Field(min_length=3, max_length=3)
|
||||
normal: list[float] = Field(min_length=3, max_length=3)
|
||||
|
||||
|
||||
class AuthorCircleProfile(AuthorModel):
|
||||
"""A declarative circle expressed with the user-facing diameter."""
|
||||
|
||||
type: Literal["circle"]
|
||||
diameter_mm: float = Field(gt=0)
|
||||
center_mm: list[float] = Field(default_factory=lambda: [0.0, 0.0], min_length=2, max_length=2)
|
||||
|
||||
|
||||
class AuthorPolygonProfile(AuthorModel):
|
||||
"""A closed polygon in the local sketch workplane."""
|
||||
|
||||
type: Literal["polygon"]
|
||||
vertices: list[list[float]] = Field(min_length=3)
|
||||
|
||||
@field_validator("vertices")
|
||||
@classmethod
|
||||
def require_planar_points(cls, value: list[list[float]]) -> list[list[float]]:
|
||||
if any(len(point) != 2 for point in value):
|
||||
raise ValueError("polygon vertices must have exactly two coordinates")
|
||||
return value
|
||||
|
||||
|
||||
class AuthorSketch(AuthorModel):
|
||||
"""The only authoring sketch form currently accepted by the compiler."""
|
||||
|
||||
workplane: AuthorWorkplane
|
||||
profile: AuthorCircleProfile | AuthorPolygonProfile
|
||||
|
||||
|
||||
class SelectorIntent(AuthorModel):
|
||||
"""A local feature-output reference, never a Runtime selector token."""
|
||||
|
||||
kind: Literal["face", "edge", "axis", "plane", "vertex", "body"]
|
||||
source: str = Field(
|
||||
min_length=3,
|
||||
max_length=160,
|
||||
description="A local feature output in the form <feature_name>.<output_role>.",
|
||||
)
|
||||
match: Literal["unique", "all"] = "unique"
|
||||
|
||||
@field_validator("source")
|
||||
@classmethod
|
||||
def require_feature_output_reference(cls, value: str) -> str:
|
||||
feature, separator, role = value.partition(".")
|
||||
if not separator or not re.fullmatch(_NAME, feature) or not re.fullmatch(r"[a-z][a-z0-9_.-]{0,80}", role):
|
||||
raise ValueError("selector source must be <feature_name>.<output_role>")
|
||||
return value
|
||||
|
||||
|
||||
class AuthorFeature(AuthorModel):
|
||||
name: str = Field(pattern=_NAME)
|
||||
operation: str = Field(pattern=r"^[a-z][a-z0-9_]{0,80}$")
|
||||
params: dict[str, Any] = Field(
|
||||
default_factory=dict,
|
||||
description="Only parameters from this feature operation's supplied params_schema.",
|
||||
)
|
||||
depends_on: list[str] = Field(default_factory=list, max_length=32)
|
||||
selectors: list[SelectorIntent] = Field(default_factory=list, max_length=32)
|
||||
sketch: AuthorSketch | None = Field(
|
||||
default=None,
|
||||
description="For sketch operations: exactly {workplane, profile}. Circle profiles use diameter_mm and center_mm.",
|
||||
)
|
||||
|
||||
|
||||
class AuthorBody(AuthorModel):
|
||||
name: str = Field(pattern=_NAME)
|
||||
features: list[AuthorFeature] = Field(min_length=1, max_length=256)
|
||||
|
||||
|
||||
class AuthoringDocument(AuthorModel):
|
||||
schema_version: str = Field(default="cad.author.v1", pattern=r"^cad\.author\.v1$")
|
||||
units: str = Field(default="mm", pattern=r"^mm$")
|
||||
coordinate_system: str = Field(default="right_handed", pattern=r"^[a-z][a-z0-9_-]{0,40}$")
|
||||
assumptions: list[str] = Field(default_factory=list, max_length=64)
|
||||
bodies: list[AuthorBody] = Field(min_length=1, max_length=32)
|
||||
acceptance_targets: list[dict[str, Any]] = Field(default_factory=list, max_length=128)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_symbols(self) -> "AuthoringDocument":
|
||||
validate_finite(self.model_dump(mode="python"))
|
||||
bodies = [b.name for b in self.bodies]
|
||||
if len(bodies) != len(set(bodies)):
|
||||
raise ValueError("duplicate body name")
|
||||
names: set[str] = set()
|
||||
for body in self.bodies:
|
||||
for feature in body.features:
|
||||
if feature.name in names:
|
||||
raise ValueError(f"duplicate feature name: {feature.name}")
|
||||
names.add(feature.name)
|
||||
for body in self.bodies:
|
||||
for feature in body.features:
|
||||
if len(feature.depends_on) != len(set(feature.depends_on)):
|
||||
raise ValueError(f"duplicate dependency: {feature.name}")
|
||||
if any(dep not in names for dep in feature.depends_on):
|
||||
missing = next(dep for dep in feature.depends_on if dep not in names)
|
||||
raise ValueError(f"unknown feature reference: {missing}")
|
||||
return self
|
||||
|
||||
|
||||
def validate_finite(value: Any, path: str = "$") -> None:
|
||||
if isinstance(value, float) and not math.isfinite(value):
|
||||
raise ValueError(f"non-finite number at {path}")
|
||||
if isinstance(value, dict):
|
||||
for key, item in value.items():
|
||||
validate_finite(item, f"{path}.{key}")
|
||||
elif isinstance(value, list):
|
||||
for index, item in enumerate(value):
|
||||
validate_finite(item, f"{path}[{index}]")
|
||||
Reference in New Issue
Block a user