1384 lines
80 KiB
Python
1384 lines
80 KiB
Python
"""Validation for the complete CDSL v1.1 semantic contract.
|
|
|
|
The current runtime accepts only a subset of this contract. Keeping this
|
|
validator separate lets import tooling preserve a SolidWorks feature history
|
|
without claiming that every feature can already be rebuilt locally.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import math
|
|
import re
|
|
from functools import lru_cache
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from jsonschema import Draft202012Validator
|
|
|
|
from .operation_contracts import materialized_feature_contracts
|
|
from .selector_capabilities import (
|
|
is_direct_blind_extrude_cap_output_role,
|
|
is_immediate_retained_source_prism_swept_face_extent,
|
|
is_direct_prism_shell_offset_edge_tdd,
|
|
is_direct_prism_shell_offset_edge_vertex,
|
|
is_initial_direct_loft_cap_output_role,
|
|
is_initial_direct_sweep_cap_edge,
|
|
is_initial_direct_sweep_cap_output_role,
|
|
is_initial_direct_sweep_swept_edge,
|
|
is_initial_direct_sweep_swept_face,
|
|
is_initial_two_sided_circle_shell_cap_output_role,
|
|
is_planar_imprint_extrude_cap_output_role,
|
|
is_primary_add_dressup_cap_output_role,
|
|
is_primary_add_shell_cap_output_role,
|
|
is_primary_add_up_to_surface_cap_output_role,
|
|
is_symmetric_direct_prism_two_sided_up_to_surface_cap_pair,
|
|
copy_selector_contract_error,
|
|
owner_body_selector_contract_error,
|
|
proven_operand_set_contract_error,
|
|
)
|
|
|
|
|
|
_ID = re.compile(r"^[A-Za-z0-9_-]{1,80}$")
|
|
|
|
|
|
def _mappings(value: Any):
|
|
"""Yield nested mapping values without treating selector-like data as text."""
|
|
if isinstance(value, dict):
|
|
yield value
|
|
for child in value.values():
|
|
yield from _mappings(child)
|
|
elif isinstance(value, list):
|
|
for child in value:
|
|
yield from _mappings(child)
|
|
|
|
|
|
def _selector_slot_descendants(selectors: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
"""Return selectors that inherit an operation's declared selector slot.
|
|
|
|
A proven ``QUERY_SET`` is one selector in the feature slot; its operands
|
|
are not unrelated nested mappings. They must therefore receive the same
|
|
output-role contract validation as their parent. Only ``query_operands``
|
|
inherit this position, so metadata such as ``output_role_source`` cannot
|
|
use the exception to become an executable selector.
|
|
"""
|
|
descendants: list[dict[str, Any]] = []
|
|
seen: set[int] = set()
|
|
|
|
def visit(selector: Any) -> None:
|
|
if not isinstance(selector, dict) or id(selector) in seen:
|
|
return
|
|
seen.add(id(selector))
|
|
descendants.append(selector)
|
|
operands = selector.get("query_operands")
|
|
if isinstance(operands, list):
|
|
for operand in operands:
|
|
visit(operand)
|
|
|
|
for selector in selectors:
|
|
visit(selector)
|
|
return descendants
|
|
|
|
|
|
def _direct_profile_source_entity_ids(sketch: dict[str, Any]) -> set[str]:
|
|
"""Return source labels represented by an unchanged direct profile.
|
|
|
|
A ``COPY(CAP_FACE)`` workplane can only inherit the cut tool's full
|
|
profile boundary. These labels are contract data, not a shape-matching
|
|
hint: a selected or split profile simply has a different source set.
|
|
"""
|
|
profile = sketch.get("profile") or {}
|
|
source_ids: set[str] = set()
|
|
direct_circle = profile.get("source_entity_id") if profile.get("type") == "circle" else None
|
|
if isinstance(direct_circle, str) and direct_circle:
|
|
source_ids.add(direct_circle)
|
|
for contour in profile.get("contours") or ():
|
|
if not isinstance(contour, dict):
|
|
continue
|
|
for segment in contour.get("segments") or ():
|
|
source_entity_id = segment.get("source_entity_id") if isinstance(segment, dict) else None
|
|
if isinstance(source_entity_id, str) and source_entity_id:
|
|
source_ids.add(source_entity_id)
|
|
return source_ids
|
|
|
|
|
|
def _validate_primary_cut_copy_cap_face_attachment(
|
|
sketch: dict[str, Any],
|
|
*,
|
|
preceding_features: dict[str, dict[str, Any]],
|
|
sketches_by_id: dict[str, dict[str, Any]],
|
|
) -> None:
|
|
"""Cross-check the narrow COPY(CAP_FACE) source set against its producer.
|
|
|
|
The generic selector contract proves typed query shape. This preflight
|
|
check additionally binds the claimed complete OSD set to the producer's
|
|
actual direct profile, so a hand-authored attachment cannot substitute a
|
|
partial or unrelated source set before runtime topology resolution.
|
|
"""
|
|
attachment = sketch.get("attachment")
|
|
intent = attachment.get("selector_intent") if isinstance(attachment, dict) else None
|
|
if not (
|
|
isinstance(intent, dict)
|
|
and intent.get("query_family") == "COPY"
|
|
and intent.get("copy_contract") == "primary_cut_cap_face_workplane"
|
|
):
|
|
return
|
|
owner = attachment.get("owner_feature_id")
|
|
producer = preceding_features.get(str(owner or ""))
|
|
params = (producer or {}).get("params") or {}
|
|
profile_sketch = sketches_by_id.get(str((producer or {}).get("sketch_id") or ""))
|
|
source_sketch_id = profile_sketch.get("source_sketch_id") if isinstance(profile_sketch, dict) else None
|
|
expected_ids = _direct_profile_source_entity_ids(profile_sketch) if isinstance(profile_sketch, dict) else set()
|
|
query_input = attachment.get("query_input") if isinstance(attachment, dict) else None
|
|
input_intent = query_input.get("selector_intent") if isinstance(query_input, dict) else None
|
|
source_entities = input_intent.get("source_entities") if isinstance(input_intent, dict) else None
|
|
actual_pairs = {
|
|
(item.get("sketch_id"), item.get("entity_id"))
|
|
for item in source_entities or ()
|
|
if isinstance(item, dict)
|
|
} if isinstance(source_entities, list) else set()
|
|
expected_pairs = {(source_sketch_id, entity_id) for entity_id in expected_ids}
|
|
if (
|
|
not isinstance(producer, dict)
|
|
or producer.get("atomic_id") != "extrude_cut_blind"
|
|
or params.get("result_mode") is not None
|
|
or (params.get("end_condition") or {}).get("type") != "blind"
|
|
or params.get("draft") is not None
|
|
or not isinstance(source_sketch_id, str)
|
|
or not expected_ids
|
|
or actual_pairs != expected_pairs
|
|
or len(actual_pairs) != len(source_entities or ())
|
|
):
|
|
raise ValueError(
|
|
f"Sketch {sketch.get('id')} COPY(CAP_FACE) attachment requires the complete direct primary-cut source-profile edge set"
|
|
)
|
|
|
|
|
|
def _validate_direct_prism_cap_face_attachment(
|
|
sketch: dict[str, Any],
|
|
*,
|
|
features: list[dict[str, Any]],
|
|
preceding_features: dict[str, dict[str, Any]],
|
|
sketches_by_id: dict[str, dict[str, Any]],
|
|
) -> None:
|
|
"""Require a CAP-face attachment to consume its immediate prism result."""
|
|
attachment = sketch.get("attachment")
|
|
intent = attachment.get("selector_intent") if isinstance(attachment, dict) else None
|
|
if not (
|
|
isinstance(intent, dict)
|
|
and intent.get("query_family") == "CAP_FACE"
|
|
and intent.get("consumer_contract") == "direct_prism_cap_face_workplane"
|
|
):
|
|
return
|
|
owner = attachment.get("owner_feature_id") if isinstance(attachment, dict) else None
|
|
producer = preceding_features.get(str(owner or ""))
|
|
sketch_map = {str(item.get("id") or ""): item for item in sketches_by_id.values()}
|
|
if not is_direct_blind_extrude_cap_output_role(attachment, producer, sketch_map):
|
|
raise ValueError(
|
|
f"Sketch {sketch.get('id')} CAP_FACE attachment requires one direct new-body blind prism cap"
|
|
)
|
|
consumers = [
|
|
index for index, feature in enumerate(features)
|
|
if feature.get("sketch_id") == sketch.get("id")
|
|
]
|
|
# A later source feature may be deferred during lowering, leaving its
|
|
# sketch as diagnostics-only data in an otherwise valid executable prefix.
|
|
# No consumer means no runtime attachment resolution; multiple or delayed
|
|
# consumers would be a materialized contract violation.
|
|
if not consumers:
|
|
return
|
|
if len(consumers) != 1 or consumers[0] == 0 or features[consumers[0] - 1].get("id") != owner:
|
|
raise ValueError(
|
|
f"Sketch {sketch.get('id')} CAP_FACE attachment must be consumed immediately after its producer"
|
|
)
|
|
|
|
|
|
def _validate_primary_cut_copy_swept_face_attachment(
|
|
sketch: dict[str, Any],
|
|
*,
|
|
preceding_features: dict[str, dict[str, Any]],
|
|
sketches_by_id: dict[str, dict[str, Any]],
|
|
) -> None:
|
|
"""Bind the sole COPY(SWEPT_FACE) anchor to an unchanged cut-tool profile."""
|
|
attachment = sketch.get("attachment")
|
|
intent = attachment.get("selector_intent") if isinstance(attachment, dict) else None
|
|
if not (
|
|
isinstance(intent, dict)
|
|
and intent.get("query_family") == "COPY"
|
|
and intent.get("copy_contract") == "primary_cut_swept_face_workplane"
|
|
):
|
|
return
|
|
owner = attachment.get("owner_feature_id")
|
|
producer = preceding_features.get(str(owner or ""))
|
|
params = (producer or {}).get("params") or {}
|
|
profile_sketch = sketches_by_id.get(str((producer or {}).get("sketch_id") or ""))
|
|
source_sketch_id = profile_sketch.get("source_sketch_id") if isinstance(profile_sketch, dict) else None
|
|
expected_ids = _direct_profile_source_entity_ids(profile_sketch) if isinstance(profile_sketch, dict) else set()
|
|
query_input = attachment.get("query_input") if isinstance(attachment, dict) else None
|
|
input_intent = query_input.get("selector_intent") if isinstance(query_input, dict) else None
|
|
source_entity = input_intent.get("source_entity") if isinstance(input_intent, dict) else None
|
|
if (
|
|
not isinstance(producer, dict)
|
|
or producer.get("atomic_id") != "extrude_cut_blind"
|
|
or params.get("result_mode") is not None
|
|
or (params.get("end_condition") or {}).get("type") != "blind"
|
|
or params.get("draft") is not None
|
|
or not isinstance(source_sketch_id, str)
|
|
or not expected_ids
|
|
or not isinstance(source_entity, dict)
|
|
or (source_entity.get("sketch_id"), source_entity.get("entity_id")) not in {
|
|
(source_sketch_id, entity_id) for entity_id in expected_ids
|
|
}
|
|
):
|
|
raise ValueError(
|
|
f"Sketch {sketch.get('id')} COPY(SWEPT_FACE) attachment requires one direct primary-cut source-profile edge"
|
|
)
|
|
|
|
|
|
def _validate_direct_prism_blend_face_attachment(
|
|
sketch: dict[str, Any],
|
|
*,
|
|
preceding_features: dict[str, dict[str, Any]],
|
|
sketches_by_id: dict[str, dict[str, Any]],
|
|
) -> None:
|
|
"""Cross-check the producer and native dress-up behind a BLEND_FACE host."""
|
|
attachment = sketch.get("attachment")
|
|
intent = attachment.get("selector_intent") if isinstance(attachment, dict) else None
|
|
if not (
|
|
isinstance(intent, dict)
|
|
and intent.get("query_family") == "BLEND_FACE"
|
|
and isinstance(intent.get("blend_face_source"), dict)
|
|
):
|
|
return
|
|
source = intent["blend_face_source"]
|
|
producer_id = source.get("owner_feature_id")
|
|
dressup_id = attachment.get("owner_feature_id") if isinstance(attachment, dict) else None
|
|
producer = preceding_features.get(str(producer_id or ""))
|
|
dressup = preceding_features.get(str(dressup_id or ""))
|
|
profile = sketches_by_id.get(str((producer or {}).get("sketch_id") or ""))
|
|
source_entity = source.get("source_entity") if isinstance(source, dict) else None
|
|
source_sketch_id = source_entity.get("sketch_id") if isinstance(source_entity, dict) else None
|
|
source_entity_id = source_entity.get("entity_id") if isinstance(source_entity, dict) else None
|
|
source_sketch = next(
|
|
(
|
|
item for item in sketches_by_id.values()
|
|
if item.get("source_sketch_id") == source_sketch_id and item.get("id") == f"sketch_{source_sketch_id}"
|
|
),
|
|
None,
|
|
)
|
|
params = (producer or {}).get("params") or {}
|
|
ordered_ids = list(preceding_features)
|
|
if (
|
|
not isinstance(producer, dict)
|
|
or not isinstance(dressup, dict)
|
|
or producer.get("atomic_id") != "extrude_add_blind"
|
|
or params.get("result_mode") != "new_body"
|
|
or (params.get("end_condition") or {}).get("type") != "blind"
|
|
or params.get("draft") is not None
|
|
or dressup.get("atomic_id") not in {"fillet", "chamfer"}
|
|
or dressup.get("depends_on") != [producer_id]
|
|
or producer_id not in ordered_ids
|
|
or dressup_id not in ordered_ids
|
|
or ordered_ids.index(str(dressup_id)) != ordered_ids.index(str(producer_id)) + 1
|
|
or not isinstance(profile, dict)
|
|
or not isinstance(source_sketch, dict)
|
|
or profile.get("source_sketch_id") != source_sketch_id
|
|
or profile.get("workplane") != source_sketch.get("workplane")
|
|
or profile.get("profile") != source_sketch.get("profile")
|
|
or not isinstance(source_entity_id, str)
|
|
or source_entity_id not in _direct_profile_source_entity_ids(profile)
|
|
):
|
|
raise ValueError(
|
|
f"Sketch {sketch.get('id')} BLEND_FACE attachment requires one immediate direct new-body blind prism and native dress-up"
|
|
)
|
|
|
|
|
|
def _contract_selectors(feature: dict[str, Any], contract: dict[str, Any]) -> list[dict[str, Any]]:
|
|
"""Return the selectors at the operation contract's declared slot."""
|
|
slot = str(contract.get("selector_slot") or "")
|
|
if slot == "feature.selectors":
|
|
values = feature.get("selectors") or []
|
|
elif slot.startswith("params.") and slot.count(".") == 1:
|
|
value = (feature.get("params") or {}).get(slot.removeprefix("params."))
|
|
values = value if isinstance(value, list) else [value]
|
|
else:
|
|
values = []
|
|
return [value for value in values if isinstance(value, dict)]
|
|
|
|
|
|
def _up_to_surface_output_role_reference(feature: dict[str, Any], contract: dict[str, Any]) -> dict[str, Any] | None:
|
|
"""Return the one nested output-role slot that an extrusion may consume."""
|
|
policy = (contract.get("nested_selector_policies") or {}).get("params.end_condition.reference")
|
|
if (
|
|
not isinstance(policy, dict)
|
|
or policy.get("end_condition_type") != "up_to_surface"
|
|
or policy.get("token_kind") != "face"
|
|
or policy.get("output_role_contract") != "direct_or_primary_add_blind_extrude_cap"
|
|
or policy.get("requires_immediate_owner") is not True
|
|
):
|
|
return None
|
|
end_condition = (feature.get("params") or {}).get("end_condition") or {}
|
|
reference = end_condition.get("reference") if isinstance(end_condition, dict) else None
|
|
if end_condition.get("type") == "up_to_surface" and isinstance(reference, dict) and reference.get("output_role") is not None:
|
|
return reference
|
|
return None
|
|
|
|
|
|
def _two_sided_up_to_surface_cap_pair_references(feature: dict[str, Any], contract: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]] | None:
|
|
"""Return the inseparable forward/reverse CAP-role extent contract."""
|
|
policies = contract.get("nested_selector_policies") or {}
|
|
required = "symmetric_direct_prism_two_sided_up_to_surface_cap_pair"
|
|
forward_policy = policies.get("params.end_condition.reference")
|
|
reverse_policy = policies.get("params.reverse_end_condition.reference")
|
|
if not (
|
|
isinstance(forward_policy, dict)
|
|
and isinstance(reverse_policy, dict)
|
|
and forward_policy.get("output_role_contract") == required
|
|
and reverse_policy.get("output_role_contract") == required
|
|
and forward_policy.get("requires_immediate_owner") is True
|
|
and reverse_policy.get("requires_immediate_owner") is True
|
|
):
|
|
return None
|
|
params = feature.get("params") or {}
|
|
forward = params.get("end_condition") or {}
|
|
reverse = params.get("reverse_end_condition") or {}
|
|
forward_reference = forward.get("reference") if isinstance(forward, dict) else None
|
|
reverse_reference = reverse.get("reference") if isinstance(reverse, dict) else None
|
|
if (
|
|
forward.get("type") == "up_to_surface"
|
|
and reverse.get("type") == "up_to_surface"
|
|
and isinstance(forward_reference, dict)
|
|
and isinstance(reverse_reference, dict)
|
|
and forward_reference.get("output_role") is not None
|
|
and reverse_reference.get("output_role") is not None
|
|
):
|
|
return forward_reference, reverse_reference
|
|
return None
|
|
|
|
|
|
def _validate_source_vertex_extent_references(feature: dict[str, Any], source_sketch_ids: set[str]) -> None:
|
|
"""Validate source-sketch vertex data separately from runtime selectors."""
|
|
params = feature.get("params") or {}
|
|
for parameter in ("end_condition", "reverse_end_condition"):
|
|
condition = params.get(parameter)
|
|
if not isinstance(condition, dict):
|
|
continue
|
|
reference = condition.get("reference")
|
|
if not isinstance(reference, dict) or reference.get("kind") != "source_vertex":
|
|
continue
|
|
point = reference.get("point_mm")
|
|
if condition.get("type") != "up_to_vertex":
|
|
raise ValueError(f"Feature {feature.get('id')} source_vertex datum is valid only for up_to_vertex")
|
|
if (
|
|
reference.get("source_sketch_id") not in source_sketch_ids
|
|
or not isinstance(reference.get("source_entity_id"), str)
|
|
or not isinstance(point, list)
|
|
or len(point) != 3
|
|
or not all(isinstance(value, (int, float)) and math.isfinite(float(value)) for value in point)
|
|
):
|
|
raise ValueError(f"Feature {feature.get('id')} has an invalid source_vertex extent datum")
|
|
|
|
|
|
def _validate_assign_variable(feature: dict[str, Any], assigned_names: set[str]) -> str:
|
|
"""Validate the intentionally scalar, declaration-only source-variable contract."""
|
|
params = feature.get("params") or {}
|
|
name = params.get("name")
|
|
value = params.get("value")
|
|
if not isinstance(name, str) or not name:
|
|
raise ValueError(f"Feature {feature.get('id')} assign_variable requires one non-empty name")
|
|
if name in assigned_names:
|
|
raise ValueError(f"Feature {feature.get('id')} assign_variable redeclares source variable {name}")
|
|
if (
|
|
not isinstance(value, (int, float))
|
|
or isinstance(value, bool)
|
|
or not math.isfinite(float(value))
|
|
or params.get("value_kind") not in {"any", "length"}
|
|
):
|
|
raise ValueError(f"Feature {feature.get('id')} assign_variable requires one finite scalar value")
|
|
return name
|
|
|
|
|
|
def _validate_selector_intent(selector: dict[str, Any], feature_id: str, index: int) -> None:
|
|
"""Enforce the provenance boundary before the runtime can bind a selector."""
|
|
intent = selector.get("selector_intent")
|
|
if intent is None:
|
|
if selector.get("selector_intent_version") is not None:
|
|
raise ValueError(f"Feature {feature_id} selector {index} declares an intent version without selector_intent")
|
|
return
|
|
if not isinstance(intent, dict) or intent.get("version") != "1.0":
|
|
raise ValueError(f"Feature {feature_id} selector {index} has an unsupported selector intent")
|
|
legacy_version = selector.get("selector_intent_version")
|
|
if legacy_version is not None and legacy_version != intent["version"]:
|
|
raise ValueError(f"Feature {feature_id} selector {index} has conflicting selector intent versions")
|
|
if intent.get("kind") not in {None, selector.get("kind")}:
|
|
raise ValueError(f"Feature {feature_id} selector {index} intent kind differs from selector kind")
|
|
family = intent.get("query_family")
|
|
derived = {"CAP_FACE", "CAP_EDGE", "CAP_VERTEX", "SWEPT_FACE", "SWEPT_EDGE", "SWEPT_BODY", "OFFSET_FACE", "OFFSET_EDGE", "INTERSECT", "BLEND_EDGE", "BLEND_FACE", "COPY"}
|
|
if family in derived and not selector.get("owner_feature_id"):
|
|
raise ValueError(f"Feature {feature_id} selector {index} derived intent requires owner_feature_id")
|
|
policy = intent.get("derivation_policy") or {}
|
|
allowed = policy.get("allowed") or []
|
|
if not allowed or any(value not in {"continuation", "fragment", "merge", "intersection", "boundary", "replacement"} for value in allowed):
|
|
raise ValueError(f"Feature {feature_id} selector {index} has an invalid lineage derivation policy")
|
|
if policy.get("multiplicity") not in {"one", "all_fragments", "source_qualified", "none"}:
|
|
raise ValueError(f"Feature {feature_id} selector {index} has an invalid lineage multiplicity")
|
|
if policy.get("multiplicity") == "all_fragments" and "fragment" not in allowed:
|
|
raise ValueError(f"Feature {feature_id} selector {index} all_fragments policy requires fragment lineage")
|
|
if family in derived and intent.get("evidence") == "geometry_hint":
|
|
raise ValueError(f"Feature {feature_id} selector {index} cannot use geometry_hint for FeatureScript provenance")
|
|
if selector.get("owner_match_required") and intent.get("evidence") == "geometry_hint":
|
|
raise ValueError(f"Feature {feature_id} selector {index} owner match cannot use geometry fallback")
|
|
source_query = intent.get("source_query") or {}
|
|
version = source_query.get("featurescript_version") if isinstance(source_query, dict) else None
|
|
# An absent source version is a valid preservation state. The resolver,
|
|
# which owns execution eligibility, returns selector_query_version_unknown
|
|
# instead of rewriting it as a fake numeric version during validation.
|
|
if version is not None and (not isinstance(version, str) or not re.fullmatch(r"[0-9]+(?:\.[0-9]+)*", version)):
|
|
raise ValueError(f"Feature {feature_id} selector {index} has an invalid FeatureScript query version")
|
|
query_expr = intent.get("query_expr")
|
|
if query_expr is not None and (
|
|
not isinstance(query_expr, dict)
|
|
or query_expr.get("version") != intent.get("version")
|
|
or not isinstance(query_expr.get("root"), dict)
|
|
):
|
|
raise ValueError(f"Feature {feature_id} selector {index} has an invalid versioned query expression")
|
|
query_set_error = proven_operand_set_contract_error(selector)
|
|
if query_set_error is not None:
|
|
raise ValueError(f"Feature {feature_id} selector {index} {query_set_error}")
|
|
owner_body_error = owner_body_selector_contract_error(selector)
|
|
if owner_body_error is not None:
|
|
raise ValueError(f"Feature {feature_id} selector {index} {owner_body_error}")
|
|
copy_error = copy_selector_contract_error(selector)
|
|
if copy_error is not None:
|
|
raise ValueError(f"Feature {feature_id} selector {index} {copy_error}")
|
|
if intent.get("output_role") is not None and intent.get("output_role") != selector.get("output_role"):
|
|
raise ValueError(f"Feature {feature_id} selector {index} intent output role differs from selector output role")
|
|
source_entity = intent.get("source_entity")
|
|
source_entities = intent.get("source_entities")
|
|
if source_entity is not None and source_entities is not None:
|
|
raise ValueError(f"Feature {feature_id} selector {index} intent cannot mix one source entity with a source-vertex set")
|
|
lineage_role = intent.get("lineage_role")
|
|
if lineage_role is not None:
|
|
valid_cap_edge = (
|
|
family in {"CAP_EDGE", "OFFSET_EDGE"}
|
|
and selector.get("kind") == "edge"
|
|
and source_entity is not None
|
|
and lineage_role in (
|
|
{"extrude.start", "extrude.end", "sweep.start", "sweep.end"}
|
|
if family == "CAP_EDGE" else {"extrude.start", "extrude.end"}
|
|
)
|
|
)
|
|
valid_cap_vertex = (
|
|
family == "CAP_VERTEX"
|
|
and selector.get("kind") == "vertex"
|
|
and source_entities is not None
|
|
and lineage_role in {"extrude.start", "extrude.end"}
|
|
)
|
|
valid_cap_face = (
|
|
family == "CAP_FACE"
|
|
and selector.get("kind") == "face"
|
|
and source_entities is not None
|
|
and lineage_role in {"extrude.start", "extrude.end"}
|
|
)
|
|
if not valid_cap_edge and not valid_cap_vertex and not valid_cap_face:
|
|
raise ValueError(f"Feature {feature_id} selector {index} has an invalid cap lineage role")
|
|
if source_entities is not None:
|
|
minimum = 1 if family == "CAP_FACE" else 2
|
|
if family not in {"SWEPT_EDGE", "CAP_VERTEX", "CAP_FACE", "OFFSET_EDGE"} or not isinstance(source_entities, list) or len(source_entities) < minimum:
|
|
raise ValueError(f"Feature {feature_id} selector {index} source entity set is invalid for {family}")
|
|
pairs = []
|
|
for source in source_entities:
|
|
if not isinstance(source, dict):
|
|
raise ValueError(f"Feature {feature_id} selector {index} source-vertex anchor is invalid")
|
|
sketch_id, entity_id = source.get("sketch_id"), source.get("entity_id")
|
|
if not isinstance(sketch_id, str) or not sketch_id or not isinstance(entity_id, str) or not entity_id:
|
|
raise ValueError(f"Feature {feature_id} selector {index} source-vertex anchor is incomplete")
|
|
pairs.append((sketch_id, entity_id))
|
|
if len(set(pairs)) != len(pairs):
|
|
raise ValueError(f"Feature {feature_id} selector {index} source-vertex anchor repeats an entity")
|
|
intersection_sources = intent.get("intersection_sources")
|
|
blend_sources = intent.get("blend_sources")
|
|
blend_face_source = intent.get("blend_face_source")
|
|
deferred_source_query = (
|
|
policy.get("multiplicity") == "none"
|
|
and intent.get("evidence") == "feature_script_query"
|
|
)
|
|
if family == "SWEPT_BODY" and not deferred_source_query:
|
|
if (
|
|
selector.get("kind") != "body"
|
|
or selector.get("source") != "runtime_snapshot"
|
|
or intent.get("evidence") != "active_body_member"
|
|
or intent.get("body_member_contract") != "direct_new_body"
|
|
or policy.get("allowed") != ["boundary"]
|
|
or policy.get("multiplicity") != "one"
|
|
or any(selector.get(key) is not None for key in ("stable_id", "snapshot_id", "geometry", "binding_feature_id", "output_role"))
|
|
):
|
|
raise ValueError(f"Feature {feature_id} selector {index} has an invalid SWEPT_BODY member contract")
|
|
if family == "INTERSECT":
|
|
if deferred_source_query:
|
|
# CADFS source preservation deliberately keeps unsupported outer
|
|
# query semantics in the candidate. The resolver rejects this
|
|
# non-executable state before any topology/geometry fallback;
|
|
# validation must not erase the preceding executable checkpoint.
|
|
if intersection_sources is not None:
|
|
raise ValueError(f"Feature {feature_id} selector {index} deferred INTERSECT cannot declare executable section sources")
|
|
else:
|
|
if (
|
|
selector.get("kind") != "edge"
|
|
or policy.get("allowed") != ["intersection"]
|
|
or policy.get("multiplicity") != "one"
|
|
or not isinstance(intersection_sources, list)
|
|
or len(intersection_sources) != 2
|
|
):
|
|
raise ValueError(f"Feature {feature_id} selector {index} has an invalid INTERSECT section contract")
|
|
owners = set()
|
|
for source in intersection_sources:
|
|
if not isinstance(source, dict):
|
|
raise ValueError(f"Feature {feature_id} selector {index} INTERSECT source is invalid")
|
|
source_family = source.get("query_family")
|
|
source_owner = source.get("owner_feature_id")
|
|
if source_family not in {"CAP_FACE", "SWEPT_FACE"} or not isinstance(source_owner, str) or not source_owner:
|
|
raise ValueError(f"Feature {feature_id} selector {index} INTERSECT source is incomplete")
|
|
owners.add(source_owner)
|
|
if source_family == "CAP_FACE":
|
|
if source.get("output_role") not in {"extrude.start", "extrude.end"} or source.get("source_entity") is not None:
|
|
raise ValueError(f"Feature {feature_id} selector {index} CAP_FACE INTERSECT source is invalid")
|
|
elif not isinstance(source.get("source_entity"), dict):
|
|
raise ValueError(f"Feature {feature_id} selector {index} SWEPT_FACE INTERSECT source is invalid")
|
|
if len(owners) != 2:
|
|
raise ValueError(f"Feature {feature_id} selector {index} INTERSECT sources must have distinct owners")
|
|
elif intersection_sources is not None:
|
|
raise ValueError(f"Feature {feature_id} selector {index} only INTERSECT may declare section sources")
|
|
if family == "BLEND_EDGE":
|
|
if deferred_source_query:
|
|
# Preserve an unsupported BLEND_EDGE query for diagnostics and
|
|
# bounded replay failure. It cannot carry executable transition
|
|
# witnesses, just as a deferred INTERSECT cannot carry section
|
|
# witnesses above.
|
|
if blend_sources is not None:
|
|
raise ValueError(f"Feature {feature_id} selector {index} deferred BLEND_EDGE cannot declare transition sources")
|
|
else:
|
|
edge_source = blend_sources.get("edge") if isinstance(blend_sources, dict) else None
|
|
face_source = blend_sources.get("face") if isinstance(blend_sources, dict) else None
|
|
if (
|
|
selector.get("kind") != "edge"
|
|
or selector.get("source") != "runtime_snapshot"
|
|
or intent.get("evidence") != "kernel_history"
|
|
or policy.get("allowed") != ["boundary"]
|
|
or policy.get("multiplicity") != "one"
|
|
or not isinstance(edge_source, dict)
|
|
or not isinstance(face_source, dict)
|
|
or edge_source.get("query_family") != "CAP_EDGE"
|
|
or face_source.get("query_family") not in {"CAP_FACE", "SWEPT_FACE"}
|
|
or edge_source.get("owner_feature_id") != face_source.get("owner_feature_id")
|
|
or not isinstance(edge_source.get("owner_feature_id"), str)
|
|
or not isinstance(edge_source.get("source_entity"), dict)
|
|
or edge_source.get("lineage_role") not in {"extrude.start", "extrude.end"}
|
|
or any(selector.get(key) is not None for key in ("stable_id", "snapshot_id", "geometry", "binding_feature_id", "output_role"))
|
|
):
|
|
raise ValueError(f"Feature {feature_id} selector {index} has an invalid BLEND_EDGE transition contract")
|
|
if face_source.get("query_family") == "CAP_FACE":
|
|
if face_source.get("output_role") not in {"extrude.start", "extrude.end"} or face_source.get("source_entity") is not None:
|
|
raise ValueError(f"Feature {feature_id} selector {index} has an invalid BLEND_EDGE cap-face source")
|
|
elif not isinstance(face_source.get("source_entity"), dict) or face_source.get("output_role") is not None:
|
|
raise ValueError(f"Feature {feature_id} selector {index} has an invalid BLEND_EDGE swept-face source")
|
|
elif face_source["source_entity"] != edge_source["source_entity"]:
|
|
raise ValueError(f"Feature {feature_id} selector {index} BLEND_EDGE swept-face source must match its cap edge")
|
|
elif blend_sources is not None:
|
|
raise ValueError(f"Feature {feature_id} selector {index} only BLEND_EDGE may declare blend sources")
|
|
if family == "BLEND_FACE":
|
|
if deferred_source_query:
|
|
if blend_face_source is not None:
|
|
raise ValueError(f"Feature {feature_id} selector {index} deferred BLEND_FACE cannot declare a patch source")
|
|
else:
|
|
if (
|
|
selector.get("kind") != "face"
|
|
or selector.get("source") != "runtime_snapshot"
|
|
or intent.get("evidence") != "kernel_history"
|
|
or policy.get("allowed") != ["boundary"]
|
|
or policy.get("multiplicity") != "one"
|
|
or not isinstance(blend_face_source, dict)
|
|
or blend_face_source.get("query_family") != "CAP_EDGE"
|
|
or not isinstance(blend_face_source.get("owner_feature_id"), str)
|
|
or not isinstance(blend_face_source.get("source_entity"), dict)
|
|
or blend_face_source.get("lineage_role") not in {"extrude.start", "extrude.end"}
|
|
or any(selector.get(key) is not None for key in ("stable_id", "snapshot_id", "geometry", "binding_feature_id", "output_role"))
|
|
):
|
|
raise ValueError(f"Feature {feature_id} selector {index} has an invalid BLEND_FACE patch contract")
|
|
source = blend_face_source["source_entity"]
|
|
if not all(isinstance(source.get(key), str) and source[key] for key in ("sketch_id", "entity_id")):
|
|
raise ValueError(f"Feature {feature_id} selector {index} BLEND_FACE source edge is incomplete")
|
|
elif blend_face_source is not None:
|
|
raise ValueError(f"Feature {feature_id} selector {index} only BLEND_FACE may declare a patch source")
|
|
forbidden = {"runtime_id", "record_id", "topology_record_id", "task_id", "revision_id"}
|
|
stack = [intent]
|
|
while stack:
|
|
value = stack.pop()
|
|
if isinstance(value, dict):
|
|
if forbidden.intersection(value):
|
|
raise ValueError(f"Feature {feature_id} selector {index} intent contains a runtime identifier")
|
|
stack.extend(value.values())
|
|
elif isinstance(value, list):
|
|
stack.extend(value)
|
|
|
|
|
|
@lru_cache(maxsize=1)
|
|
def _schema() -> dict[str, Any]:
|
|
path = Path(__file__).with_name("cdsl_schema.json")
|
|
schema = json.loads(path.read_text(encoding="utf-8"))
|
|
Draft202012Validator.check_schema(schema)
|
|
return schema
|
|
|
|
|
|
@lru_cache(maxsize=1)
|
|
def _validator() -> Draft202012Validator:
|
|
return Draft202012Validator(_schema())
|
|
|
|
|
|
@lru_cache(maxsize=1)
|
|
def _operation_contracts() -> dict[str, dict[str, Any]]:
|
|
path = Path(__file__).with_name("profile_schema.json")
|
|
return materialized_feature_contracts(json.loads(path.read_text(encoding="utf-8")))
|
|
|
|
|
|
def _schema_error(document: dict[str, Any]) -> str | None:
|
|
validator = _validator()
|
|
errors = sorted(validator.iter_errors(document), key=lambda error: (list(error.absolute_path), error.message))
|
|
if not errors:
|
|
return None
|
|
error = errors[0]
|
|
location = "$" + "".join(f"[{item}]" if isinstance(item, int) else f".{item}" for item in error.absolute_path)
|
|
return f"CDSL schema violation at {location}: {error.message}"
|
|
|
|
|
|
def validate_semantic_cdsl(cdsl: dict[str, Any]) -> dict[str, Any]:
|
|
"""Validate a CDSL document without invoking the rebuild compiler.
|
|
|
|
The return value is intentionally serializable so the batch converter can
|
|
write it unchanged into a per-model diagnostic file.
|
|
"""
|
|
if not isinstance(cdsl, dict):
|
|
raise ValueError("CDSL must be a JSON object")
|
|
if cdsl.get("schema") not in {"cad.cdsl.llm.v1", "cad.runtime.v1"}:
|
|
raise ValueError("Unsupported CDSL schema")
|
|
schema_error = _schema_error(cdsl)
|
|
if schema_error:
|
|
raise ValueError(schema_error)
|
|
|
|
version = str(cdsl.get("schema_version") or "1.0.0")
|
|
if not re.fullmatch(r"1\.[0-9]+\.[0-9]+", version):
|
|
raise ValueError("schema_version must be a 1.x.y version")
|
|
version_numbers = tuple(int(component) for component in version.split("."))
|
|
if version_numbers >= (1, 1, 0) and cdsl.get("meta", {}).get("unit") != "mm":
|
|
raise ValueError("CDSL v1.1 requires meta.unit = 'mm'")
|
|
|
|
sketches = (cdsl.get("geometry") or {}).get("sketches") or []
|
|
sketch_ids = {str(sketch.get("id") or "") for sketch in sketches}
|
|
source_sketch_ids = {str(sketch.get("source_sketch_id") or "") for sketch in sketches}
|
|
if len(sketch_ids) != len(sketches) or not all(_ID.fullmatch(item) for item in sketch_ids):
|
|
raise ValueError("Sketch ids must be unique valid CDSL identifiers")
|
|
for sketch in sketches:
|
|
profile = sketch.get("profile") or {}
|
|
if profile.get("type") == "planar_imprint":
|
|
entities = profile.get("source_entities") or []
|
|
entity_ids = [str(entity.get("id") or "") for entity in entities if isinstance(entity, dict)]
|
|
if len(entity_ids) != len(entities) or len(set(entity_ids)) != len(entity_ids) or not all(entity_ids):
|
|
raise ValueError(f"Sketch {sketch.get('id')} planar_imprint source entity ids must be unique")
|
|
known = set(entity_ids)
|
|
external_anchors = profile.get("external_anchors") or []
|
|
external_ids = [str(anchor.get("id") or "") for anchor in external_anchors if isinstance(anchor, dict)]
|
|
if len(external_ids) != len(external_anchors) or len(set(external_ids)) != len(external_ids) or not all(external_ids):
|
|
raise ValueError(f"Sketch {sketch.get('id')} planar_imprint external anchor ids must be unique")
|
|
if external_ids and not isinstance(sketch.get("attachment"), dict):
|
|
raise ValueError(f"Sketch {sketch.get('id')} planar_imprint external anchors require a runtime face attachment")
|
|
if len(entities) == 1 and not external_ids:
|
|
raise ValueError(f"Sketch {sketch.get('id')} planar_imprint needs two source entities without an external anchor")
|
|
for index, selection in enumerate(profile.get("selections") or []):
|
|
source = str(selection.get("source_entity_id") or "")
|
|
fragment = selection.get("fragment") or {}
|
|
anchor = str(fragment.get("anchor_entity_id") or "") if fragment else ""
|
|
external_anchor = str(fragment.get("external_anchor_id") or "") if fragment else ""
|
|
if source not in known:
|
|
raise ValueError(f"Sketch {sketch.get('id')} planar_imprint selection {index} has an unknown source entity")
|
|
if fragment and bool(anchor) == bool(external_anchor):
|
|
raise ValueError(f"Sketch {sketch.get('id')} planar_imprint selection {index} must name exactly one fragment anchor")
|
|
if anchor and (anchor not in known or anchor == source):
|
|
raise ValueError(f"Sketch {sketch.get('id')} planar_imprint selection {index} has an invalid fragment anchor")
|
|
if external_anchor and external_anchor not in external_ids:
|
|
raise ValueError(f"Sketch {sketch.get('id')} planar_imprint selection {index} has an unknown external anchor")
|
|
if len(entities) == 1 and not external_anchor:
|
|
raise ValueError(
|
|
f"Sketch {sketch.get('id')} planar_imprint single-source selection {index} requires an external anchor"
|
|
)
|
|
elif profile.get("type") == "multi_source_regions":
|
|
sources, children = profile.get("source_sketch_ids") or [], profile.get("profiles") or []
|
|
if len(sources) < 2 or len(sources) != len(children) or len(set(sources)) != len(sources):
|
|
raise ValueError(f"Sketch {sketch.get('id')} multi_source_regions must have one unique source id per profile")
|
|
if sketch.get("source_sketch_id") is not None or sketch.get("attachment") is not None:
|
|
raise ValueError(f"Sketch {sketch.get('id')} multi_source_regions cannot claim a single source or runtime attachment")
|
|
allowed = {"circle", "polygon", "analytic_contours"}
|
|
if any(not isinstance(child, dict) or child.get("type") not in allowed for child in children):
|
|
raise ValueError(f"Sketch {sketch.get('id')} multi_source_regions has an unsupported child profile")
|
|
|
|
feature_ids: set[str] = set()
|
|
preceding_features: dict[str, dict[str, Any]] = {}
|
|
previous_feature_id: str | None = None
|
|
assigned_variable_names: set[str] = set()
|
|
deferred: list[str] = []
|
|
unresolved: list[dict[str, Any]] = []
|
|
contracts = _operation_contracts()
|
|
features = cdsl.get("features") or []
|
|
for feature in features:
|
|
fid = str(feature.get("id") or "")
|
|
if not _ID.fullmatch(fid) or fid in feature_ids:
|
|
raise ValueError("Feature ids must be unique valid CDSL identifiers")
|
|
for dependency in feature.get("depends_on") or []:
|
|
if dependency not in feature_ids:
|
|
raise ValueError(f"Feature {fid} has a forward or missing dependency: {dependency}")
|
|
sketch_id = feature.get("sketch_id")
|
|
if sketch_id is not None and str(sketch_id) not in sketch_ids:
|
|
raise ValueError(f"Feature {fid} refers to a missing sketch: {sketch_id}")
|
|
if version_numbers >= (1, 1, 0) and feature.get("execution_status") not in {"supported", "deferred"}:
|
|
raise ValueError(f"Feature {fid} must declare execution_status")
|
|
if feature.get("execution_status") == "deferred":
|
|
deferred.append(fid)
|
|
contract = contracts.get(str(feature.get("atomic_id") or "")) or {}
|
|
assigned_variable_name = (
|
|
_validate_assign_variable(feature, assigned_variable_names)
|
|
if feature.get("atomic_id") == "assign_variable" else None
|
|
)
|
|
_validate_source_vertex_extent_references(feature, source_sketch_ids)
|
|
feature_selectors = _contract_selectors(feature, contract)
|
|
extent_selector = _up_to_surface_output_role_reference(feature, contract)
|
|
two_sided_extent_pair = _two_sided_up_to_surface_cap_pair_references(feature, contract)
|
|
output_role_selectors = [
|
|
*feature_selectors,
|
|
*([extent_selector] if extent_selector is not None else []),
|
|
*(list(two_sided_extent_pair) if two_sided_extent_pair is not None else []),
|
|
]
|
|
selector_slot_selectors = _selector_slot_descendants(output_role_selectors)
|
|
output_role_selector_ids = {id(selector) for selector in selector_slot_selectors}
|
|
selector_intent_ids = {
|
|
id(selector.get("selector_intent"))
|
|
for selector in selector_slot_selectors
|
|
if isinstance(selector, dict) and isinstance(selector.get("selector_intent"), dict)
|
|
}
|
|
validated_selector_ids: set[int] = set()
|
|
for index, selector in enumerate(selector_slot_selectors):
|
|
_validate_selector_intent(selector, fid, index)
|
|
validated_selector_ids.add(id(selector))
|
|
owner = selector.get("owner_feature_id")
|
|
binding_owner = selector.get("binding_feature_id")
|
|
if owner is not None and owner not in feature_ids and binding_owner not in feature_ids:
|
|
raise ValueError(f"Feature {fid} selector {index} has a forward or missing owner_feature_id")
|
|
if selector.get("output_role") is not None:
|
|
if selector.get("kind") != "face" or not owner or owner not in feature_ids:
|
|
raise ValueError(f"Feature {fid} selector {index} output role requires a preceding face owner_feature_id")
|
|
if selector.get("source") != "runtime_snapshot":
|
|
raise ValueError(f"Feature {fid} selector {index} output role must use runtime_snapshot evidence")
|
|
if any(selector.get(key) is not None for key in ("stable_id", "snapshot_id", "geometry", "binding_feature_id")):
|
|
raise ValueError(f"Feature {fid} selector {index} output role cannot mix stable or geometry evidence")
|
|
role_source = selector.get("output_role_source")
|
|
selector_intent = selector.get("selector_intent")
|
|
is_retained_shell_cap_offset_profile = (
|
|
feature.get("atomic_id") == "extrude_from_face"
|
|
and isinstance(selector_intent, dict)
|
|
and selector_intent.get("query_family") == "OFFSET_FACE"
|
|
and selector_intent.get("consumer_contract")
|
|
== "shell_retained_direct_prism_cap_offset_face_profile"
|
|
and selector.get("output_role") == "shell.offset_face"
|
|
and owner == previous_feature_id
|
|
)
|
|
is_shell_cap_face_output_role = (
|
|
feature.get("atomic_id") == "shell"
|
|
and isinstance(selector_intent, dict)
|
|
and selector_intent.get("query_family") == "CAP_FACE"
|
|
and selector.get("output_role") in {
|
|
"extrude.start", "extrude.end", "loft.start", "loft.end", "sweep.start", "sweep.end",
|
|
}
|
|
)
|
|
is_imprint_dressup_cap_output_role = (
|
|
feature.get("atomic_id") in {"fillet", "chamfer"}
|
|
and isinstance(selector_intent, dict)
|
|
and selector_intent.get("query_family") == "CAP_FACE"
|
|
and selector.get("output_role") in {"extrude.start", "extrude.end"}
|
|
and selector_intent.get("consumer_contract") != "primary_add_dressup_union_continuation"
|
|
)
|
|
is_primary_add_dressup_cap_role = (
|
|
feature.get("atomic_id") in {"fillet", "chamfer"}
|
|
and isinstance(selector_intent, dict)
|
|
and selector_intent.get("query_family") == "CAP_FACE"
|
|
and selector.get("output_role") in {"extrude.start", "extrude.end"}
|
|
and is_primary_add_dressup_cap_output_role(
|
|
selector, preceding_features.get(str(owner)),
|
|
{str(sketch.get("id") or ""): sketch for sketch in sketches},
|
|
)
|
|
)
|
|
if two_sided_extent_pair is not None and any(selector is reference for reference in two_sided_extent_pair):
|
|
if owner != previous_feature_id or not is_symmetric_direct_prism_two_sided_up_to_surface_cap_pair(
|
|
two_sided_extent_pair[0], two_sided_extent_pair[1],
|
|
preceding_features.get(str(owner)), {str(sketch.get("id") or ""): sketch for sketch in sketches},
|
|
):
|
|
raise ValueError(
|
|
f"Feature {fid} two-sided up_to_surface CAP roles require the immediately preceding direct symmetric new_body prism pair"
|
|
)
|
|
elif selector is extent_selector:
|
|
retained_source_swept_face = is_immediate_retained_source_prism_swept_face_extent(
|
|
selector,
|
|
preceding_features.get(str(owner)),
|
|
{str(sketch.get("id") or ""): sketch for sketch in sketches},
|
|
)
|
|
if owner != previous_feature_id or not (
|
|
is_direct_blind_extrude_cap_output_role(
|
|
selector, preceding_features.get(str(owner)), {str(sketch.get("id") or ""): sketch for sketch in sketches},
|
|
)
|
|
or is_primary_add_up_to_surface_cap_output_role(
|
|
selector, preceding_features.get(str(owner)), {str(sketch.get("id") or ""): sketch for sketch in sketches},
|
|
)
|
|
or retained_source_swept_face
|
|
):
|
|
raise ValueError(
|
|
f"Feature {fid} up_to_surface reference requires an immediately preceding direct new_body or primary ADD blind extrusion cap, or one exact retained source wall"
|
|
)
|
|
elif is_shell_cap_face_output_role:
|
|
producer = preceding_features.get(str(owner))
|
|
is_valid = (
|
|
is_direct_blind_extrude_cap_output_role(
|
|
selector, producer, {str(sketch.get("id") or ""): sketch for sketch in sketches},
|
|
)
|
|
or is_primary_add_shell_cap_output_role(
|
|
selector, producer, {str(sketch.get("id") or ""): sketch for sketch in sketches},
|
|
)
|
|
or is_initial_direct_loft_cap_output_role(
|
|
selector, producer, {str(sketch.get("id") or ""): sketch for sketch in sketches},
|
|
)
|
|
or is_initial_direct_sweep_cap_output_role(
|
|
selector, producer, {str(sketch.get("id") or ""): sketch for sketch in sketches},
|
|
)
|
|
or is_initial_two_sided_circle_shell_cap_output_role(
|
|
selector, producer, {str(sketch.get("id") or ""): sketch for sketch in sketches},
|
|
)
|
|
)
|
|
if owner != previous_feature_id or not is_valid:
|
|
raise ValueError(
|
|
f"Feature {fid} CAP_FACE output role requires the immediately preceding direct new_body blind extrusion cap"
|
|
)
|
|
elif is_primary_add_dressup_cap_role:
|
|
if owner != previous_feature_id:
|
|
raise ValueError(
|
|
f"Feature {fid} primary ADD CAP_FACE output role requires the immediately preceding blind extrusion"
|
|
)
|
|
elif is_imprint_dressup_cap_output_role:
|
|
producer = preceding_features.get(str(owner))
|
|
sketch_map = {str(sketch.get("id") or ""): sketch for sketch in sketches}
|
|
if owner != previous_feature_id or not (
|
|
is_planar_imprint_extrude_cap_output_role(selector, producer, sketch_map)
|
|
or is_direct_blind_extrude_cap_output_role(selector, producer, sketch_map)
|
|
):
|
|
raise ValueError(
|
|
f"Feature {fid} CAP_FACE dress-up role requires an immediately preceding complete direct or planar-IMPRINT new_body blind prism"
|
|
)
|
|
elif not contract.get("selector_slot") or contract.get("selector_token_kind") != "face":
|
|
raise ValueError(f"Feature {fid} selector {index} cannot consume a feature output role")
|
|
if role_source is not None:
|
|
source_owner = role_source.get("owner_feature_id") if isinstance(role_source, dict) else None
|
|
source_role = role_source.get("output_role") if isinstance(role_source, dict) else None
|
|
source_feature = preceding_features.get(str(source_owner or ""))
|
|
source_params = (source_feature or {}).get("params") or {}
|
|
if not source_owner or source_owner not in feature_ids:
|
|
raise ValueError(f"Feature {fid} selector {index} output role source requires a preceding owner_feature_id")
|
|
if (
|
|
selector.get("output_role") != "shell.offset_face"
|
|
or not (feature.get("atomic_id") == "shell" or is_retained_shell_cap_offset_profile)
|
|
):
|
|
raise ValueError(
|
|
f"Feature {fid} selector {index} output role source is only supported for a shell offset-face contract"
|
|
)
|
|
if source_role not in {"extrude.start", "extrude.end"}:
|
|
raise ValueError(
|
|
f"Feature {fid} selector {index} shell.offset_face source must be an extrude cap role"
|
|
)
|
|
if (
|
|
source_feature is None
|
|
or source_feature.get("atomic_id") != "extrude_add_blind"
|
|
or source_params.get("result_mode") != "new_body"
|
|
or (source_params.get("end_condition") or {}).get("type") != "blind"
|
|
):
|
|
raise ValueError(
|
|
f"Feature {fid} selector {index} shell.offset_face source requires a preceding direct new_body blind extrusion"
|
|
)
|
|
if is_retained_shell_cap_offset_profile:
|
|
shell_feature = preceding_features.get(str(owner)) or {}
|
|
shell_selectors = shell_feature.get("selectors") or []
|
|
shell_params = shell_feature.get("params") or {}
|
|
feature_params = feature.get("params") or {}
|
|
removed = shell_selectors[0] if len(shell_selectors) == 1 else {}
|
|
removed_intent = removed.get("selector_intent") if isinstance(removed, dict) else {}
|
|
disambiguation = selector_intent.get("disambiguation") or {}
|
|
source_ids = disambiguation.get("source_profile_entity_ids") if isinstance(disambiguation, dict) else None
|
|
expected_retained = "extrude.end" if removed.get("output_role") == "extrude.start" else "extrude.start"
|
|
if (
|
|
shell_feature.get("atomic_id") != "shell"
|
|
or shell_feature.get("depends_on") != [source_owner]
|
|
or not isinstance(removed, dict)
|
|
or removed.get("owner_feature_id") != source_owner
|
|
or removed.get("output_role") not in {"extrude.start", "extrude.end"}
|
|
or not isinstance(removed_intent, dict)
|
|
or removed_intent.get("query_family") != "CAP_FACE"
|
|
or source_role != expected_retained
|
|
or not isinstance(source_ids, list)
|
|
or not source_ids
|
|
or len(set(source_ids)) != len(source_ids)
|
|
or disambiguation.get("removed_cap_role") != removed.get("output_role")
|
|
or feature_params.get("operation") != "add"
|
|
or feature_params.get("result_mode") != "new_body"
|
|
or (feature_params.get("end_condition") or {}).get("type") != "blind"
|
|
or feature_params.get("two_sided")
|
|
or feature_params.get("draft") is not None
|
|
):
|
|
raise ValueError(
|
|
f"Feature {fid} selector {index} retained shell cap profile requires one immediate opposite direct-prism cap removal"
|
|
)
|
|
elif selector.get("output_role_source") is not None:
|
|
raise ValueError(f"Feature {fid} selector {index} output role source requires output_role")
|
|
if selector.get("query_operands") is not None and feature.get("atomic_id") not in {"fillet", "chamfer"}:
|
|
raise ValueError(f"Feature {fid} qUnion selectors are supported only for fillet or chamfer")
|
|
selector_intent = selector.get("selector_intent") if isinstance(selector, dict) else None
|
|
if (
|
|
isinstance(selector_intent, dict)
|
|
and selector_intent.get("consumer_contract") == "immediate_retained_source_prism_swept_face_up_to_surface"
|
|
and selector is not extent_selector
|
|
):
|
|
raise ValueError(
|
|
f"Feature {fid} retained source prism SWEPT_FACE is valid only as its up_to_surface reference"
|
|
)
|
|
if (
|
|
isinstance(selector_intent, dict)
|
|
and selector_intent.get("query_family") == "OFFSET_EDGE"
|
|
and selector_intent.get("consumer_contract") == "direct_prism_shell_offset_edge_tdd"
|
|
and (
|
|
feature.get("atomic_id") not in {"fillet", "chamfer"}
|
|
or not isinstance(selector_intent.get("disambiguation"), dict)
|
|
or selector_intent["disambiguation"].get("shell_feature_id") != previous_feature_id
|
|
or not is_direct_prism_shell_offset_edge_tdd(
|
|
selector,
|
|
preceding_features.get(str(previous_feature_id or "")),
|
|
preceding_features.get(str(selector.get("owner_feature_id") or "")),
|
|
{str(sketch.get("id") or ""): sketch for sketch in sketches},
|
|
)
|
|
)
|
|
):
|
|
raise ValueError(
|
|
f"Feature {fid} OFFSET_EDGE TDD requires the immediately preceding direct-prism shell retained-cap continuation"
|
|
)
|
|
if (
|
|
isinstance(selector_intent, dict)
|
|
and selector_intent.get("query_family") == "OFFSET_EDGE"
|
|
and selector_intent.get("consumer_contract") == "direct_prism_shell_offset_edge_vertex"
|
|
and (
|
|
feature.get("atomic_id") not in {"fillet", "chamfer"}
|
|
or not isinstance(selector_intent.get("disambiguation"), dict)
|
|
or selector_intent["disambiguation"].get("shell_feature_id") != previous_feature_id
|
|
or not is_direct_prism_shell_offset_edge_vertex(
|
|
selector,
|
|
preceding_features.get(str(previous_feature_id or "")),
|
|
preceding_features.get(str(selector.get("owner_feature_id") or "")),
|
|
{str(sketch.get("id") or ""): sketch for sketch in sketches},
|
|
)
|
|
)
|
|
):
|
|
raise ValueError(
|
|
f"Feature {fid} OFFSET_EDGE vertex requires the immediately preceding direct-prism shell continuation"
|
|
)
|
|
if (
|
|
isinstance(selector_intent, dict)
|
|
and selector_intent.get("query_family") == "CAP_EDGE"
|
|
and selector_intent.get("lineage_role") in {"sweep.start", "sweep.end"}
|
|
and (
|
|
feature.get("atomic_id") not in {"fillet", "chamfer"}
|
|
or selector.get("owner_feature_id") != previous_feature_id
|
|
or not is_initial_direct_sweep_cap_edge(
|
|
selector,
|
|
preceding_features.get(str(selector.get("owner_feature_id") or "")),
|
|
{str(sketch.get("id") or ""): sketch for sketch in sketches},
|
|
)
|
|
)
|
|
):
|
|
raise ValueError(
|
|
f"Feature {fid} CAP_EDGE sweep role requires the immediately preceding direct new_body one-edge sweep"
|
|
)
|
|
if (
|
|
isinstance(selector_intent, dict)
|
|
and selector_intent.get("query_family") == "SWEPT_FACE"
|
|
and (selector_intent.get("disambiguation") or {}).get("type") == "sweep_profile_path"
|
|
and (
|
|
feature.get("atomic_id") not in {"fillet", "chamfer"}
|
|
or selector.get("owner_feature_id") != previous_feature_id
|
|
or not is_initial_direct_sweep_swept_face(
|
|
selector,
|
|
preceding_features.get(str(selector.get("owner_feature_id") or "")),
|
|
{str(sketch.get("id") or ""): sketch for sketch in sketches},
|
|
)
|
|
)
|
|
):
|
|
raise ValueError(
|
|
f"Feature {fid} SWEPT_FACE sweep relation requires the immediately preceding direct new_body analytic-profile sweep"
|
|
)
|
|
if (
|
|
isinstance(selector_intent, dict)
|
|
and selector_intent.get("query_family") == "SWEPT_EDGE"
|
|
and (selector_intent.get("disambiguation") or {}).get("type") == "sweep_profile_vertex_path"
|
|
and (
|
|
feature.get("atomic_id") not in {"fillet", "chamfer"}
|
|
or selector.get("owner_feature_id") != previous_feature_id
|
|
or not is_initial_direct_sweep_swept_edge(
|
|
selector,
|
|
preceding_features.get(str(selector.get("owner_feature_id") or "")),
|
|
{str(sketch.get("id") or ""): sketch for sketch in sketches},
|
|
)
|
|
)
|
|
):
|
|
raise ValueError(
|
|
f"Feature {fid} SWEPT_EDGE sweep relation requires the immediately preceding direct new_body analytic-profile sweep"
|
|
)
|
|
for selector in _mappings(feature):
|
|
if (
|
|
id(selector) not in validated_selector_ids
|
|
and (selector.get("selector_intent") is not None or selector.get("selector_intent_version") is not None)
|
|
):
|
|
_validate_selector_intent(selector, fid, len(validated_selector_ids))
|
|
validated_selector_ids.add(id(selector))
|
|
owner = selector.get("owner_feature_id")
|
|
binding_owner = selector.get("binding_feature_id")
|
|
if owner is not None and owner not in feature_ids and binding_owner not in feature_ids:
|
|
raise ValueError(
|
|
f"Feature {fid} nested selector has a forward or missing owner_feature_id"
|
|
)
|
|
selector_intent = selector.get("selector_intent") if isinstance(selector, dict) else None
|
|
if (
|
|
isinstance(selector_intent, dict)
|
|
and selector_intent.get("consumer_contract") == "immediate_retained_source_prism_swept_face_up_to_surface"
|
|
):
|
|
end_condition = (feature.get("params") or {}).get("end_condition") or {}
|
|
extent_reference = end_condition.get("reference") if isinstance(end_condition, dict) else None
|
|
if (
|
|
feature.get("atomic_id") not in {"extrude_add_blind", "extrude_cut_blind"}
|
|
or end_condition.get("type") != "up_to_surface"
|
|
or selector is not extent_reference
|
|
or selector.get("owner_feature_id") != previous_feature_id
|
|
or not is_immediate_retained_source_prism_swept_face_extent(
|
|
selector,
|
|
preceding_features.get(str(previous_feature_id or "")),
|
|
{str(sketch.get("id") or ""): sketch for sketch in sketches},
|
|
)
|
|
):
|
|
raise ValueError(
|
|
f"Feature {fid} retained source prism SWEPT_FACE requires the immediately preceding direct new-body prism up_to_surface extent"
|
|
)
|
|
if (
|
|
isinstance(selector_intent, dict)
|
|
and selector_intent.get("query_family") == "OFFSET_EDGE"
|
|
and selector_intent.get("consumer_contract") == "direct_prism_shell_offset_edge_tdd"
|
|
and (
|
|
feature.get("atomic_id") not in {"fillet", "chamfer"}
|
|
or not isinstance(selector_intent.get("disambiguation"), dict)
|
|
or selector_intent["disambiguation"].get("shell_feature_id") != previous_feature_id
|
|
or not is_direct_prism_shell_offset_edge_tdd(
|
|
selector,
|
|
preceding_features.get(str(previous_feature_id or "")),
|
|
preceding_features.get(str(selector.get("owner_feature_id") or "")),
|
|
{str(sketch.get("id") or ""): sketch for sketch in sketches},
|
|
)
|
|
)
|
|
):
|
|
raise ValueError(
|
|
f"Feature {fid} OFFSET_EDGE TDD requires the immediately preceding direct-prism shell retained-cap continuation"
|
|
)
|
|
if (
|
|
isinstance(selector_intent, dict)
|
|
and selector_intent.get("query_family") == "OFFSET_EDGE"
|
|
and selector_intent.get("consumer_contract") == "direct_prism_shell_offset_edge_vertex"
|
|
and (
|
|
feature.get("atomic_id") not in {"fillet", "chamfer"}
|
|
or not isinstance(selector_intent.get("disambiguation"), dict)
|
|
or selector_intent["disambiguation"].get("shell_feature_id") != previous_feature_id
|
|
or not is_direct_prism_shell_offset_edge_vertex(
|
|
selector,
|
|
preceding_features.get(str(previous_feature_id or "")),
|
|
preceding_features.get(str(selector.get("owner_feature_id") or "")),
|
|
{str(sketch.get("id") or ""): sketch for sketch in sketches},
|
|
)
|
|
)
|
|
):
|
|
raise ValueError(
|
|
f"Feature {fid} OFFSET_EDGE vertex requires the immediately preceding direct-prism shell continuation"
|
|
)
|
|
# ``output_role_source`` is provenance metadata nested inside a
|
|
# feature selector, not a selector on its own.
|
|
if (
|
|
selector.get("output_role") is not None
|
|
and selector.get("kind") is not None
|
|
and id(selector) not in output_role_selector_ids
|
|
and id(selector) not in selector_intent_ids
|
|
):
|
|
if contract.get("selector_slot") == "feature.selectors":
|
|
raise ValueError(
|
|
f"Feature {fid} output role selectors are only supported in feature.selectors"
|
|
)
|
|
raise ValueError(f"Feature {fid} output role selector is outside its operation contract slot")
|
|
if feature.get("atomic_id") == "shell":
|
|
target_feature_id = (feature.get("params") or {}).get("target_feature_id")
|
|
if target_feature_id is not None and target_feature_id not in feature_ids:
|
|
raise ValueError(f"Feature {fid} shell target_feature_id requires a preceding body feature")
|
|
if feature.get("atomic_id") in {"hole_blind", "hole_countersink", "hole_counterbore", "hole_wizard"}:
|
|
scope_feature_id = (feature.get("params") or {}).get("scope_feature_id")
|
|
if scope_feature_id is not None and scope_feature_id not in feature_ids:
|
|
raise ValueError(f"Feature {fid} hole scope_feature_id requires a preceding body feature")
|
|
if feature.get("atomic_id") == "transform_bodies":
|
|
params = feature.get("params") or {}
|
|
source_member_aliases = params.get("source_member_aliases") or []
|
|
if source_member_aliases:
|
|
direct_sources = params.get("source_feature_ids") or []
|
|
if (
|
|
not bool(params.get("make_copy"))
|
|
or not isinstance(direct_sources, list)
|
|
or len(direct_sources) != 1
|
|
):
|
|
raise ValueError(
|
|
f"Feature {fid} source member aliases require a single-source make_copy transform"
|
|
)
|
|
active_member = str(direct_sources[0])
|
|
if active_member not in preceding_features:
|
|
raise ValueError(
|
|
f"Feature {fid} source member aliases require a preceding active member"
|
|
)
|
|
seen_alias_sources: set[str] = set()
|
|
for index, alias in enumerate(source_member_aliases):
|
|
if not isinstance(alias, dict):
|
|
raise ValueError(f"Feature {fid} source member alias {index} is invalid")
|
|
source_id = str(alias.get("source_feature_id") or "")
|
|
alias_member = str(alias.get("active_member_feature_id") or "")
|
|
if (
|
|
not source_id
|
|
or source_id == active_member
|
|
or alias_member != active_member
|
|
or source_id not in preceding_features
|
|
or source_id in seen_alias_sources
|
|
):
|
|
raise ValueError(
|
|
f"Feature {fid} source member alias {index} does not bind one preceding semantic source to its selected member"
|
|
)
|
|
seen_alias_sources.add(source_id)
|
|
references = params.get("pattern_instance_refs") or []
|
|
for index, reference in enumerate(references):
|
|
if not isinstance(reference, dict):
|
|
raise ValueError(f"Feature {fid} pattern instance reference {index} is invalid")
|
|
pattern_id = str(reference.get("pattern_feature_id") or "")
|
|
source_id = str(reference.get("source_feature_id") or "")
|
|
pattern = preceding_features.get(pattern_id)
|
|
pattern_atomic_id = pattern.get("atomic_id") if pattern is not None else None
|
|
if pattern is None or pattern_atomic_id not in {"pattern_circular", "pattern_mirror"}:
|
|
raise ValueError(f"Feature {fid} pattern instance reference {index} has a forward or unsupported pattern owner")
|
|
if source_id not in {str(value) for value in (pattern.get("params") or {}).get("source_feature_ids") or ()}:
|
|
raise ValueError(f"Feature {fid} pattern instance reference {index} names a source outside its pattern")
|
|
source = preceding_features.get(source_id)
|
|
if (
|
|
pattern_atomic_id == "pattern_mirror"
|
|
and (source is None or (source.get("params") or {}).get("result_mode") != "new_body")
|
|
):
|
|
raise ValueError(
|
|
f"Feature {fid} pattern instance reference {index} requires a preceding new_body mirror source"
|
|
)
|
|
instance = reference.get("instance_index")
|
|
count = int((pattern.get("params") or {}).get("pattern_count") or 0)
|
|
excluded = {int(value) for value in (pattern.get("params") or {}).get("excluded_instance_indices") or ()}
|
|
if not isinstance(instance, int) or (
|
|
pattern_atomic_id == "pattern_mirror" and instance != 1
|
|
) or (
|
|
pattern_atomic_id == "pattern_circular" and (instance < 1 or instance >= count or instance in excluded)
|
|
):
|
|
raise ValueError(f"Feature {fid} pattern instance reference {index} is not a surviving copy")
|
|
copy_references = params.get("transform_copy_refs") or []
|
|
for index, reference in enumerate(copy_references):
|
|
if not isinstance(reference, dict):
|
|
raise ValueError(f"Feature {fid} transform COPY reference {index} is invalid")
|
|
transform_id = str(reference.get("transform_feature_id") or "")
|
|
source_id = str(reference.get("source_feature_id") or "")
|
|
transform = preceding_features.get(transform_id)
|
|
transform_params = (transform or {}).get("params") or {}
|
|
direct_sources = transform_params.get("source_feature_ids") or []
|
|
if (
|
|
transform is None
|
|
or transform.get("atomic_id") != "transform_bodies"
|
|
or not bool(transform_params.get("make_copy"))
|
|
or not isinstance(direct_sources, list)
|
|
or len(direct_sources) < 2
|
|
):
|
|
raise ValueError(
|
|
f"Feature {fid} transform COPY reference {index} requires a preceding multi-source make_copy transform"
|
|
)
|
|
if source_id not in {str(value) for value in direct_sources}:
|
|
raise ValueError(
|
|
f"Feature {fid} transform COPY reference {index} names a source outside its transform"
|
|
)
|
|
for source_id in params.get("source_feature_ids") or []:
|
|
source = preceding_features.get(str(source_id))
|
|
source_params = (source or {}).get("params") or {}
|
|
source_direct_ids = source_params.get("source_feature_ids") or []
|
|
if (
|
|
source is not None
|
|
and source.get("atomic_id") == "transform_bodies"
|
|
and bool(source_params.get("make_copy"))
|
|
and isinstance(source_direct_ids, list)
|
|
and len(source_direct_ids) > 1
|
|
):
|
|
raise ValueError(
|
|
f"Feature {fid} must use transform_copy_refs to select a source of multi-source COPY {source_id}"
|
|
)
|
|
if feature.get("atomic_id") == "boolean_bodies":
|
|
params = feature.get("params") or {}
|
|
if params.get("targetless_body_set"):
|
|
targets = params.get("target_feature_ids") or []
|
|
target_patterns = params.get("target_pattern_instance_refs") or []
|
|
target_transforms = params.get("target_transform_copy_refs") or []
|
|
tools = params.get("tool_feature_ids") or []
|
|
tool_patterns = params.get("tool_pattern_instance_refs") or []
|
|
tool_transforms = params.get("tool_transform_copy_refs") or []
|
|
if (
|
|
params.get("operation") not in {"union", "intersect"}
|
|
or not isinstance(targets, list)
|
|
or not isinstance(target_patterns, list)
|
|
or not isinstance(target_transforms, list)
|
|
or len(targets) + len(target_patterns) + len(target_transforms) != 1
|
|
or not isinstance(tools, list)
|
|
or not isinstance(tool_patterns, list)
|
|
or not isinstance(tool_transforms, list)
|
|
or not (len(tools) + len(tool_patterns) + len(tool_transforms))
|
|
):
|
|
raise ValueError(
|
|
f"Feature {fid} targetless_body_set requires one qualified body and UNION/INTERSECTION tools"
|
|
)
|
|
for parameter in ("target_pattern_instance_refs", "tool_pattern_instance_refs"):
|
|
for index, reference in enumerate(params.get(parameter) or []):
|
|
if not isinstance(reference, dict):
|
|
raise ValueError(f"Feature {fid} {parameter} entry {index} is invalid")
|
|
pattern_id = str(reference.get("pattern_feature_id") or "")
|
|
source_id = str(reference.get("source_feature_id") or "")
|
|
pattern = preceding_features.get(pattern_id)
|
|
pattern_atomic_id = pattern.get("atomic_id") if pattern is not None else None
|
|
if pattern is None or pattern_atomic_id not in {"pattern_circular", "pattern_mirror"}:
|
|
raise ValueError(f"Feature {fid} {parameter} entry {index} has a forward or unsupported pattern owner")
|
|
if source_id not in {str(value) for value in (pattern.get("params") or {}).get("source_feature_ids") or ()}:
|
|
raise ValueError(f"Feature {fid} {parameter} entry {index} names a source outside its pattern")
|
|
source = preceding_features.get(source_id)
|
|
if (
|
|
pattern_atomic_id == "pattern_mirror"
|
|
and (source is None or (source.get("params") or {}).get("result_mode") != "new_body")
|
|
):
|
|
raise ValueError(
|
|
f"Feature {fid} {parameter} entry {index} requires a preceding new_body mirror source"
|
|
)
|
|
instance = reference.get("instance_index")
|
|
count = int((pattern.get("params") or {}).get("pattern_count") or 0)
|
|
excluded = {int(value) for value in (pattern.get("params") or {}).get("excluded_instance_indices") or []}
|
|
if not isinstance(instance, int) or (
|
|
pattern_atomic_id == "pattern_mirror" and instance != 1
|
|
) or (
|
|
pattern_atomic_id == "pattern_circular" and (instance < 1 or instance >= count or instance in excluded)
|
|
):
|
|
raise ValueError(f"Feature {fid} {parameter} entry {index} is not a surviving copy")
|
|
for parameter in ("target_transform_copy_refs", "tool_transform_copy_refs"):
|
|
for index, reference in enumerate(params.get(parameter) or []):
|
|
if not isinstance(reference, dict):
|
|
raise ValueError(f"Feature {fid} {parameter} entry {index} is invalid")
|
|
transform_id = str(reference.get("transform_feature_id") or "")
|
|
source_id = str(reference.get("source_feature_id") or "")
|
|
transform = preceding_features.get(transform_id)
|
|
transform_params = (transform or {}).get("params") or {}
|
|
direct_sources = transform_params.get("source_feature_ids") or []
|
|
if (
|
|
transform is None
|
|
or transform.get("atomic_id") != "transform_bodies"
|
|
or not bool(transform_params.get("make_copy"))
|
|
or not isinstance(direct_sources, list)
|
|
or len(direct_sources) < 2
|
|
):
|
|
raise ValueError(
|
|
f"Feature {fid} {parameter} entry {index} requires a preceding multi-source make_copy transform"
|
|
)
|
|
if source_id not in {str(value) for value in direct_sources}:
|
|
raise ValueError(
|
|
f"Feature {fid} {parameter} entry {index} names a source outside its transform"
|
|
)
|
|
for parameter in ("target_feature_ids", "tool_feature_ids"):
|
|
for source_id in params.get(parameter) or []:
|
|
source = preceding_features.get(str(source_id))
|
|
source_params = (source or {}).get("params") or {}
|
|
source_direct_ids = source_params.get("source_feature_ids") or []
|
|
if (
|
|
source is not None
|
|
and source.get("atomic_id") == "transform_bodies"
|
|
and bool(source_params.get("make_copy"))
|
|
and isinstance(source_direct_ids, list)
|
|
and len(source_direct_ids) > 1
|
|
):
|
|
raise ValueError(
|
|
f"Feature {fid} must use transform COPY references to select a source of multi-source COPY {source_id}"
|
|
)
|
|
if feature.get("unresolved"):
|
|
unresolved.append({"feature_id": fid, "reasons": list(feature["unresolved"])})
|
|
feature_ids.add(fid)
|
|
preceding_features[fid] = feature
|
|
if assigned_variable_name is not None:
|
|
assigned_variable_names.add(assigned_variable_name)
|
|
else:
|
|
previous_feature_id = fid
|
|
|
|
sketches_by_id = {str(sketch.get("id") or ""): sketch for sketch in sketches}
|
|
for sketch in sketches:
|
|
attachment = sketch.get("attachment") if isinstance(sketch, dict) else None
|
|
intent = attachment.get("selector_intent") if isinstance(attachment, dict) else None
|
|
profile = sketch.get("profile") if isinstance(sketch, dict) else None
|
|
if isinstance(profile, dict) and profile.get("type") == "planar_imprint":
|
|
for index, anchor in enumerate(profile.get("external_anchors") or []):
|
|
selector = anchor.get("selector") if isinstance(anchor, dict) else None
|
|
if not isinstance(selector, dict) or selector.get("kind") != "edge":
|
|
raise ValueError(f"Sketch {sketch.get('id')} planar_imprint external anchor {index} requires an edge selector")
|
|
_validate_selector_intent(selector, f"sketch {sketch.get('id')} external anchor", index)
|
|
if not isinstance(intent, dict):
|
|
continue
|
|
_validate_selector_intent(attachment, f"sketch {sketch.get('id')}", 0)
|
|
_validate_direct_prism_cap_face_attachment(
|
|
sketch,
|
|
features=features,
|
|
preceding_features=preceding_features,
|
|
sketches_by_id=sketches_by_id,
|
|
)
|
|
if intent.get("query_family") == "COPY" and intent.get("copy_contract") == "primary_cut_cap_face_workplane":
|
|
_validate_primary_cut_copy_cap_face_attachment(
|
|
sketch,
|
|
preceding_features=preceding_features,
|
|
sketches_by_id=sketches_by_id,
|
|
)
|
|
if intent.get("query_family") == "COPY" and intent.get("copy_contract") == "primary_cut_swept_face_workplane":
|
|
_validate_primary_cut_copy_swept_face_attachment(
|
|
sketch,
|
|
preceding_features=preceding_features,
|
|
sketches_by_id=sketches_by_id,
|
|
)
|
|
if intent.get("query_family") == "BLEND_FACE":
|
|
_validate_direct_prism_blend_face_attachment(
|
|
sketch,
|
|
preceding_features=preceding_features,
|
|
sketches_by_id=sketches_by_id,
|
|
)
|
|
|
|
return {
|
|
"schema_version": version,
|
|
"feature_count": len(feature_ids),
|
|
"sketch_count": len(sketches),
|
|
"deferred_feature_ids": deferred,
|
|
"unresolved": unresolved,
|
|
"future_rebuild_ready": not unresolved,
|
|
}
|