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
+116 -6
View File
@@ -17,6 +17,7 @@ from .runtime_types import (
pattern_instance_member_id, transform_copy_member_id,
)
from .operation_contracts import materialized_feature_contracts
from .selector_capabilities import is_direct_blind_extrude_cap_output_role
_SKETCH_ATOM_PREFIXES = ("extrude_", "revolve_", "sweep_")
@@ -97,6 +98,24 @@ def _contract_selectors(node: FeaturePlanNode, contract: dict[str, Any] | None)
return [value for value in values if isinstance(value, dict)]
def _up_to_surface_output_role_reference(node: FeaturePlanNode, contract: dict[str, Any] | None) -> dict[str, Any] | None:
"""Return the one nested output-role slot supported by extrusion extents."""
policy = ((contract or {}).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 = node.params.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 _transform_member_sources(params: dict[str, Any]) -> set[str]:
"""Return internal body-member keys named by a transform contract."""
source_ids = {str(value) for value in params.get("source_feature_ids") or ()}
@@ -232,6 +251,8 @@ def _next_body_graph(
return members, has_active_body
if atomic_id in {"extrude_cut_blind", "extrude_cut_two_sided", "extrude_cut_through", "revolve_cut"} or (
atomic_id in _HOLE_ATOMICS and node.params.get("scope_feature_id") is not None
) or (
atomic_id == "extrude_from_face" and node.params.get("operation") == "cut"
):
# Primary cuts execute per explicit member in the runtime so a later
@@ -448,6 +469,7 @@ class CapabilityAnalyzer:
completed: set[str] = set()
body_available = False
body_members: set[str] = set()
previous_node: FeaturePlanNode | None = None
for node in plan:
blockers: list[RuntimeDiagnostic] = []
contract = self.contracts.get(node.atomic_id)
@@ -559,19 +581,38 @@ class CapabilityAnalyzer:
sketch_id=sketch_id,
))
contract_selectors = _contract_selectors(node, contract)
contract_selector_ids = {id(selector) for selector in contract_selectors}
extent_selector = _up_to_surface_output_role_reference(node, contract)
output_role_selectors = [
*contract_selectors,
*([extent_selector] if extent_selector is not None else []),
]
contract_selector_ids = {id(selector) for selector in output_role_selectors}
selector_intent_ids = {
id(selector.get("selector_intent"))
for selector in output_role_selectors
if isinstance(selector.get("selector_intent"), dict)
}
for selector in _mappings(params):
if selector.get("output_role") is not None and id(selector) not in contract_selector_ids:
if (
selector.get("output_role") is not None
and selector.get("kind") is not None
and id(selector) not in contract_selector_ids
and id(selector) not in selector_intent_ids
):
blockers.append(self._blocker(
node.feature_id,
"unsupported_output_role_selector_context",
"Feature output role selector is outside the operation contract slot",
))
for selector_index, selector in enumerate(contract_selectors):
for selector_index, selector in enumerate(output_role_selectors):
if selector.get("output_role") is None:
continue
required.append("selector:feature_output_role")
if contract is None or not contract.get("selector_slot") or contract.get("selector_token_kind") != "face":
is_extent_reference = selector is extent_selector
if (
not is_extent_reference
and (contract is None or not contract.get("selector_slot") or contract.get("selector_token_kind") != "face")
):
blockers.append(self._blocker(
node.feature_id,
"unsupported_output_role_selector",
@@ -600,6 +641,39 @@ class CapabilityAnalyzer:
selector_index=selector_index,
))
role_source = selector.get("output_role_source")
selector_intent = selector.get("selector_intent")
is_shell_cap_face_output_role = (
node.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 is_extent_reference and (
previous_node is None
or selector.get("owner_feature_id") != previous_node.feature_id
or not is_direct_blind_extrude_cap_output_role(
selector, previous_node.source_feature, sketches,
)
):
blockers.append(self._blocker(
node.feature_id,
"unsupported_extent_output_role_selector",
"up_to_surface output roles require the immediately preceding direct new_body blind extrusion cap",
selector_index=selector_index,
))
elif is_shell_cap_face_output_role and (
previous_node is None
or selector.get("owner_feature_id") != previous_node.feature_id
or not is_direct_blind_extrude_cap_output_role(
selector, previous_node.source_feature, sketches,
)
):
blockers.append(self._blocker(
node.feature_id,
"unsupported_cap_face_output_role_selector",
"CAP_FACE output roles require the immediately preceding direct new_body blind extrusion cap",
selector_index=selector_index,
))
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
@@ -647,11 +721,25 @@ class CapabilityAnalyzer:
))
elif kind == "bspline" and (
not isinstance(segment.get("points"), list)
or len(segment.get("points") or []) < 3
or len(segment.get("points") or []) < 2
):
blockers.append(self._blocker(
node.feature_id, "invalid_sweep_path",
"Sweep B-spline path requires at least three interpolation points",
"Sweep B-spline path requires at least two interpolation points",
))
elif (
kind == "bspline"
and len(segment.get("points") or []) == 2
and not (
isinstance(segment.get("start_tangent"), list)
and len(segment["start_tangent"]) == 2
and isinstance(segment.get("end_tangent"), list)
and len(segment["end_tangent"]) == 2
)
):
blockers.append(self._blocker(
node.feature_id, "invalid_sweep_path",
"A two-point B-spline sweep path requires both endpoint tangents",
))
if node.atomic_id == "boolean_bodies":
target_ids = params.get("target_feature_ids")
@@ -762,6 +850,27 @@ class CapabilityAnalyzer:
"shell target no longer has an independently selectable body output",
target_feature_id=target_feature_id,
))
if node.atomic_id in _HOLE_ATOMICS and params.get("scope_feature_id") is not None:
scope_feature_id = params.get("scope_feature_id")
required.append("hole:explicit_scope_body")
scope = nodes_by_id.get(str(scope_feature_id or ""))
if not isinstance(scope_feature_id, str) or not scope_feature_id:
blockers.append(self._blocker(
node.feature_id, "invalid_hole_scope_body",
"hole scope_feature_id must name one preceding body member",
))
elif scope is None or scope.feature_id not in completed or scope.feature_id not in body_members:
blockers.append(self._blocker(
node.feature_id, "hole_scope_body_unavailable",
"hole scope body is no longer an independently selectable body output",
scope_feature_id=scope_feature_id,
))
elif len(body_members) != 1:
blockers.append(self._blocker(
node.feature_id, "hole_scope_body_ambiguous",
"hole scope body must be the sole active member",
scope_feature_id=scope_feature_id,
))
if node.atomic_id in {"transform_bodies", "delete_bodies"}:
parameter = "source_feature_ids" if node.atomic_id == "transform_bodies" else "target_feature_ids"
source_ids = params.get(parameter)
@@ -1086,6 +1195,7 @@ class CapabilityAnalyzer:
if status == "executable":
completed.add(node.feature_id)
body_members, body_available = _next_body_graph(node, body_members, body_available, nodes_by_id)
previous_node = node
body_producers = {
"extrude_add_blind", "extrude_add_blind_with_hole", "extrude_add_two_sided", "extrude_cut_blind", "extrude_cut_two_sided", "extrude_from_face",
"extrude_cut_through", "loft_add", "loft_add_with_cap_face", "sweep_add", "boolean_bodies",