feat(selector): 增加离线候选遍历与严格回放验证 Demo

- 新增 selector_candidate_demo,移除 provenance intent 后枚举候选 selector
- 对候选分支执行有界重建与严格 STEP 比较
- 仅在候选遍历完整且唯一 strict 通过时生成 selector 映射记录
- 增加 selector 候选搜索、预算限制和记录生成的测试
- 保持生产 selector resolver 不受 Demo 逻辑影响
- 更新 CADFS 能力台账,记录 IMPRINT 派生 profile 的 lineage selector 缺口
This commit is contained in:
2026-09-10 15:12:57 +08:00
parent d8fc2a9207
commit 994d06aaea
36 changed files with 9268 additions and 753 deletions
+156 -10
View File
@@ -16,6 +16,7 @@ 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
_ID = re.compile(r"^[A-Za-z0-9_-]{1,80}$")
@@ -45,6 +46,24 @@ def _contract_selectors(feature: dict[str, Any], contract: dict[str, Any]) -> li
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_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 _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")
@@ -52,14 +71,15 @@ def _validate_selector_intent(selector: dict[str, Any], feature_id: str, index:
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 selector.get("selector_intent_version") not in {None, "1.0"}:
raise ValueError(f"Feature {feature_id} selector {index} has an unsupported selector intent version")
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", "SWEPT_FACE", "SWEPT_EDGE", "OFFSET_FACE", "INTERSECT", "COPY"}
derived = {"CAP_FACE", "CAP_EDGE", "SWEPT_FACE", "SWEPT_EDGE", "SWEPT_BODY", "OFFSET_FACE", "INTERSECT", "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 {}
@@ -76,10 +96,90 @@ def _validate_selector_intent(selector: dict[str, Any], feature_id: str, index:
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
if not isinstance(version, str) or not re.fullmatch(r"[0-9]+(?:\.[0-9]+)*", version):
raise ValueError(f"Feature {feature_id} selector {index} has an unknown FeatureScript query version")
# 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")
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:
if (
family != "CAP_EDGE"
or selector.get("kind") != "edge"
or source_entity is None
or lineage_role not in {"extrude.start", "extrude.end"}
):
raise ValueError(f"Feature {feature_id} selector {index} has an invalid CAP_EDGE lineage role")
if source_entities is not None:
if family != "SWEPT_EDGE" or not isinstance(source_entities, list) or len(source_entities) < 2:
raise ValueError(f"Feature {feature_id} selector {index} source-vertex anchor requires SWEPT_EDGE and two source entities")
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")
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")
forbidden = {"runtime_id", "record_id", "topology_record_id", "task_id", "revision_id"}
stack = [intent]
while stack:
@@ -166,6 +266,7 @@ def validate_semantic_cdsl(cdsl: dict[str, Any]) -> dict[str, Any]:
feature_ids: set[str] = set()
preceding_features: dict[str, dict[str, Any]] = {}
previous_feature_id: str | None = None
deferred: list[str] = []
unresolved: list[dict[str, Any]] = []
contracts = _operation_contracts()
@@ -185,21 +286,26 @@ def validate_semantic_cdsl(cdsl: dict[str, Any]) -> dict[str, Any]:
deferred.append(fid)
contract = contracts.get(str(feature.get("atomic_id") or "")) or {}
feature_selectors = _contract_selectors(feature, contract)
output_role_selector_ids = {id(selector) for selector in feature_selectors if isinstance(selector, dict)}
extent_selector = _up_to_surface_output_role_reference(feature, contract)
output_role_selectors = [
*feature_selectors,
*([extent_selector] if extent_selector is not None else []),
]
output_role_selector_ids = {id(selector) for selector in output_role_selectors}
selector_intent_ids = {
id(selector.get("selector_intent"))
for selector in feature_selectors
for selector in output_role_selectors
if isinstance(selector, dict) and isinstance(selector.get("selector_intent"), dict)
}
for index, selector in enumerate(feature_selectors):
validated_selector_ids: set[int] = set()
for index, selector in enumerate(output_role_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 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 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":
@@ -207,6 +313,29 @@ def validate_semantic_cdsl(cdsl: dict[str, Any]) -> dict[str, Any]:
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_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"}
)
if selector is extent_selector:
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},
):
raise ValueError(
f"Feature {fid} up_to_surface output role requires the immediately preceding direct new_body blind extrusion cap"
)
elif is_shell_cap_face_output_role:
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},
):
raise ValueError(
f"Feature {fid} CAP_FACE output role requires the immediately preceding direct new_body blind extrusion cap"
)
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
@@ -234,6 +363,18 @@ def validate_semantic_cdsl(cdsl: dict[str, Any]) -> dict[str, Any]:
elif selector.get("output_role_source") is not None:
raise ValueError(f"Feature {fid} selector {index} output role source requires output_role")
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"
)
# ``output_role_source`` is provenance metadata nested inside a
# feature selector, not a selector on its own.
if (
@@ -251,6 +392,10 @@ def validate_semantic_cdsl(cdsl: dict[str, Any]) -> dict[str, Any]:
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 {}
references = params.get("pattern_instance_refs") or []
@@ -354,6 +499,7 @@ def validate_semantic_cdsl(cdsl: dict[str, Any]) -> dict[str, Any]:
unresolved.append({"feature_id": fid, "reasons": list(feature["unresolved"])})
feature_ids.add(fid)
preceding_features[fid] = feature
previous_feature_id = fid
return {
"schema_version": version,