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