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:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user