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:
+230
-140
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import math
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
from typing import Any
|
||||
@@ -95,6 +96,14 @@ def _binding_targets(selector: dict[str, Any]):
|
||||
yield selector
|
||||
|
||||
|
||||
def _runtime_selector(placeholder: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Convert a legacy prefix placeholder to the runtime selector contract."""
|
||||
selector = dict(placeholder)
|
||||
selector.setdefault("source", "runtime_snapshot")
|
||||
selector.setdefault("confidence", 1.0)
|
||||
return selector
|
||||
|
||||
|
||||
def _bound_selector(placeholder: dict[str, Any], records: list[dict[str, Any]]) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
||||
bound = []
|
||||
for record in records:
|
||||
@@ -109,160 +118,241 @@ def _bound_selector(placeholder: dict[str, Any], records: list[dict[str, Any]])
|
||||
return bound[0], bound
|
||||
|
||||
|
||||
def _selector_roots(feature: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
roots = list(feature.get("selectors") or [])
|
||||
for name in ("end_condition", "reverse_end_condition"):
|
||||
condition = (feature.get("params") or {}).get(name)
|
||||
reference = condition.get("reference") if isinstance(condition, dict) else None
|
||||
if isinstance(reference, dict):
|
||||
roots.append(reference)
|
||||
return roots
|
||||
|
||||
|
||||
def _selector_key(selector: dict[str, Any]) -> tuple[Any, ...]:
|
||||
stable_id = selector.get("stable_id")
|
||||
if stable_id is not None:
|
||||
return ("stable_id", str(stable_id))
|
||||
source = selector.get("output_role_source")
|
||||
return (
|
||||
"output_role",
|
||||
selector.get("owner_feature_id"),
|
||||
selector.get("kind"),
|
||||
selector.get("output_role"),
|
||||
source.get("owner_feature_id") if isinstance(source, dict) else None,
|
||||
source.get("output_role") if isinstance(source, dict) else None,
|
||||
)
|
||||
|
||||
|
||||
def _bind_feature_selectors(
|
||||
feature: dict[str, Any],
|
||||
*,
|
||||
registry: Any,
|
||||
body_id_for_feature: dict[str, str | None],
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Bind one feature immediately before it runs in the shared session."""
|
||||
targets = [target for root in _selector_roots(feature) for target in _binding_targets(root)]
|
||||
resolved: list[dict[str, Any]] = []
|
||||
for placeholder in targets:
|
||||
binding_feature_id = placeholder.get("binding_feature_id")
|
||||
if binding_feature_id is None:
|
||||
active_body_id = body_id_for_feature.get("__current__")
|
||||
else:
|
||||
if not isinstance(binding_feature_id, str) or binding_feature_id not in body_id_for_feature:
|
||||
raise ValueError(f"{feature['id']}: selector binding feature is missing or forward")
|
||||
active_body_id = body_id_for_feature[binding_feature_id]
|
||||
intent = placeholder.get("selector_intent")
|
||||
runtime_selector = _runtime_selector(placeholder)
|
||||
resolution = registry.resolve(runtime_selector, active_body_id=active_body_id)
|
||||
# Legacy owner-qualified, geometry-free context selectors predate the
|
||||
# runtime's canonical evidence fields. Their compatibility contract
|
||||
# permits a unique active context object, but never relaxes a
|
||||
# provenance, output-role, or instance-locked selector.
|
||||
if (
|
||||
resolution.status != "resolved"
|
||||
and not isinstance(intent, dict)
|
||||
and not runtime_selector.get("owner_match_required")
|
||||
and not runtime_selector.get("output_role")
|
||||
):
|
||||
fallback = dict(runtime_selector)
|
||||
fallback.pop("owner_feature_id", None)
|
||||
resolution = registry.resolve(fallback, active_body_id=active_body_id)
|
||||
if resolution.status != "resolved":
|
||||
code = resolution.diagnostic.code if resolution.diagnostic is not None else f"selector_{resolution.status}"
|
||||
raise ValueError(f"{feature['id']}: {code} during incremental replay")
|
||||
selected = list(resolution.records or ((resolution.record,) if resolution.record is not None else ()))
|
||||
if not selected:
|
||||
raise ValueError(f"{feature['id']}: selector_not_found during incremental replay")
|
||||
public_records = [record.public_dict() for record in selected]
|
||||
# Operation-role and provenance selectors stay declarative in bound
|
||||
# CDSL. A runtime record ID is execution evidence, never their durable
|
||||
# semantic replacement.
|
||||
if (isinstance(intent, dict) and intent.get("query_family") != "GEOMETRIC") or placeholder.get("output_role"):
|
||||
resolved.extend(public_records)
|
||||
continue
|
||||
selector, bound_selectors = _bound_selector(placeholder, public_records)
|
||||
placeholder.clear()
|
||||
placeholder.update(selector)
|
||||
resolved.extend(bound_selectors)
|
||||
selectors = feature.get("selectors") or []
|
||||
feature["selectors"] = list({_selector_key(selector): selector for selector in selectors}.values())
|
||||
if feature.get("atomic_id") == "pattern_mirror":
|
||||
planes = [selector for selector in feature["selectors"] if selector.get("kind") == "plane"]
|
||||
if len(planes) != 1:
|
||||
raise ValueError(f"{feature['id']}: mirror plane binding is not unique")
|
||||
feature.setdefault("params", {})["mirror_plane"] = planes[0]
|
||||
return resolved
|
||||
|
||||
|
||||
@dataclass
|
||||
class IncrementalBindingReplay:
|
||||
"""Bound candidate plus the one session that produced its evidence."""
|
||||
|
||||
bound_cdsl: dict[str, Any]
|
||||
evidence: list[dict[str, Any]]
|
||||
execution: Any
|
||||
|
||||
|
||||
def bind_and_execute_candidate_selectors(cdsl: dict[str, Any]) -> IncrementalBindingReplay:
|
||||
"""Bind and execute a CADFS candidate in one ordered kernel replay.
|
||||
|
||||
A feature is bound only against topology facts registered by earlier
|
||||
features in this session. Historical ``binding_feature_id`` values select
|
||||
a retained snapshot ID, so the binder never needs to re-run a prefix or
|
||||
recreate an OCC body. Resolver failures retain the live session for the
|
||||
caller to export its last executable checkpoint.
|
||||
"""
|
||||
from engine.cdsl_engine.runtime import prepare_cdsl_execution
|
||||
|
||||
bound = deepcopy(cdsl)
|
||||
execution = prepare_cdsl_execution(bound)
|
||||
if not execution.analysis.runtime_eligible:
|
||||
first = next((result for result in execution.analysis.feature_results if not result.executable), None)
|
||||
code = first.blockers[0].code if first and first.blockers else "runtime_ineligible"
|
||||
raise ValueError(code)
|
||||
bound_features = {str(feature.get("id") or ""): feature for feature in bound.get("features") or []}
|
||||
body_id_for_feature: dict[str, str | None] = {"__current__": None}
|
||||
evidence: list[dict[str, Any]] = []
|
||||
for index, node in enumerate(execution.analysis.plan):
|
||||
feature = node.source_feature
|
||||
try:
|
||||
resolved = _bind_feature_selectors(
|
||||
feature,
|
||||
registry=execution.session.topology,
|
||||
body_id_for_feature=body_id_for_feature,
|
||||
)
|
||||
public_feature = bound_features[node.feature_id]
|
||||
public_feature["selectors"] = deepcopy(feature.get("selectors") or [])
|
||||
public_feature["params"] = deepcopy(feature.get("params") or {})
|
||||
evidence.append({
|
||||
"feature_id": node.feature_id,
|
||||
"prefix_feature_count": index,
|
||||
"selectors": public_feature["selectors"],
|
||||
"resolved": resolved,
|
||||
})
|
||||
execution.execute_next(strict=True)
|
||||
body_id_for_feature[node.feature_id] = execution.session.body_id
|
||||
body_id_for_feature["__current__"] = execution.session.body_id
|
||||
except Exception as error:
|
||||
setattr(error, "bound_cdsl", bound)
|
||||
setattr(error, "selector_binding", evidence)
|
||||
setattr(error, "incremental_execution", execution)
|
||||
setattr(error, "failed_feature_id", node.feature_id)
|
||||
raise
|
||||
return IncrementalBindingReplay(bound, evidence, execution)
|
||||
|
||||
|
||||
def bind_candidate_selectors(cdsl: dict[str, Any]) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
||||
"""Rebuild every selector-bearing prefix and bind against its active body."""
|
||||
"""Legacy diagnostic binder for incomplete or externally supplied CDSL.
|
||||
|
||||
The production CADFS rebuild path uses ``bind_and_execute_candidate_selectors``.
|
||||
This compatibility entry point deliberately keeps prefix replay explicit
|
||||
for callers that need to inspect a partial document before it is runtime
|
||||
eligible. It rehydrates exact exported topology facts and calls the same
|
||||
resolver; provenance selectors never become geometry guesses here.
|
||||
"""
|
||||
from engine.cdsl_engine.runtime import rebuild_cdsl
|
||||
bound = deepcopy(cdsl); evidence = []
|
||||
from engine.cdsl_engine.topology import TopologyRegistry
|
||||
|
||||
bound = deepcopy(cdsl)
|
||||
evidence: list[dict[str, Any]] = []
|
||||
with tempfile.TemporaryDirectory(prefix="cadfs-bind-") as temporary:
|
||||
for index, feature in enumerate(bound.get("features") or []):
|
||||
roots = list(feature.get("selectors") or [])
|
||||
for name in ("end_condition", "reverse_end_condition"):
|
||||
condition = (feature.get("params") or {}).get(name)
|
||||
reference = condition.get("reference") if isinstance(condition, dict) else None
|
||||
if isinstance(reference, dict): roots.append(reference)
|
||||
targets = [target for root in roots for target in _binding_targets(root)]
|
||||
if not targets: continue
|
||||
prefix_cache: dict[int, tuple[list[dict[str, Any]], str | None]] = {}
|
||||
targets = [target for root in _selector_roots(feature) for target in _binding_targets(root)]
|
||||
if not targets:
|
||||
continue
|
||||
prefix_cache: dict[int, tuple[list[dict[str, Any]], str | None, list[dict[str, Any]]]] = {}
|
||||
|
||||
def prefix_records(binding_feature_id: str | None, owner_feature_id: str | None) -> list[dict[str, Any]]:
|
||||
def prefix_snapshot(binding_feature_id: str | None) -> tuple[list[dict[str, Any]], str | None, list[dict[str, Any]]]:
|
||||
prefix_count = index
|
||||
if binding_feature_id is not None:
|
||||
binding_index = next((item_index for item_index, item in enumerate(bound["features"][:index]) if item["id"] == binding_feature_id), None)
|
||||
if binding_index is None: raise ValueError(f"{feature['id']}: selector binding feature is missing or forward")
|
||||
binding_index = next(
|
||||
(item_index for item_index, item in enumerate(bound["features"][:index]) if item["id"] == binding_feature_id),
|
||||
None,
|
||||
)
|
||||
if binding_index is None:
|
||||
raise ValueError(f"{feature['id']}: selector binding feature is missing or forward")
|
||||
prefix_count = binding_index + 1
|
||||
if prefix_count not in prefix_cache:
|
||||
prefix = deepcopy(bound); prefix["features"] = bound["features"][:prefix_count]
|
||||
if not prefix["features"]: raise ValueError(f"{feature['id']}: selector has no executable prefix")
|
||||
report = rebuild_cdsl(prefix, Path(temporary) / f"prefix-{index}-{prefix_count}.step", strict=True)
|
||||
body_id = next((item.get("body_id") for item in reversed(report.get("feature_results") or []) if item.get("body_id")), None)
|
||||
prefix_cache[prefix_count] = (list(report.get("topology_records") or []), body_id)
|
||||
all_records, body_id = prefix_cache[prefix_count]
|
||||
return [
|
||||
item for item in all_records
|
||||
# Reference planes and axes are session context, not body
|
||||
# topology. They must remain available while binding a mirror
|
||||
# or extent selector against a body-bearing prefix.
|
||||
if item.get("kind") in {"plane", "axis"}
|
||||
or not body_id
|
||||
or item.get("body_id") == body_id
|
||||
or str(item.get("body_id") or "").startswith(f"{body_id}:")
|
||||
]
|
||||
|
||||
resolved = []
|
||||
for placeholder in targets:
|
||||
records = prefix_records(placeholder.get("binding_feature_id"), placeholder.get("owner_feature_id"))
|
||||
output_role = str(placeholder.get("output_role") or "").strip()
|
||||
intent = placeholder.get("selector_intent")
|
||||
if (
|
||||
isinstance(intent, dict)
|
||||
and intent.get("query_family") != "GEOMETRIC"
|
||||
and not output_role
|
||||
):
|
||||
# Prefix rebuilding is retained as a diagnostic adapter.
|
||||
# It must not turn an unproven FeatureScript provenance
|
||||
# query into a geometry-scored stable selector.
|
||||
raise ValueError(
|
||||
f"{feature['id']}: selector_kernel_history_missing after prefix rebuild"
|
||||
prefix = deepcopy(bound)
|
||||
prefix["features"] = bound["features"][:prefix_count]
|
||||
if not prefix["features"]:
|
||||
raise ValueError(f"{feature['id']}: selector has no executable prefix")
|
||||
report = rebuild_cdsl(
|
||||
prefix,
|
||||
Path(temporary) / f"prefix-{index}-{prefix_count}.step",
|
||||
strict=True,
|
||||
)
|
||||
if output_role:
|
||||
# Builder output roles are not geometry placeholders. They
|
||||
# remain in the bound CDSL so runtime can resolve the
|
||||
# current active B-rep face through exact kernel history.
|
||||
# Replacing one with stable_id/geometry would mix evidence
|
||||
# and make a stale snapshot appear durable.
|
||||
owner = placeholder.get("owner_feature_id")
|
||||
role_source = placeholder.get("output_role_source")
|
||||
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
|
||||
candidates = [
|
||||
record for record in records
|
||||
if record.get("kind") == placeholder.get("kind")
|
||||
and owner in (record.get("owner_feature_ids") or [record.get("feature_id")])
|
||||
and output_role in (record.get("output_roles") or [])
|
||||
and (
|
||||
role_source is None
|
||||
or any(
|
||||
item.get("output_role") == output_role
|
||||
and item.get("owner_feature_id") == source_owner
|
||||
and item.get("source_output_role") == source_role
|
||||
for item in record.get("output_role_sources") or []
|
||||
)
|
||||
)
|
||||
]
|
||||
if len(candidates) != 1:
|
||||
status = "not_found" if not candidates else "ambiguous"
|
||||
raise ValueError(
|
||||
f"{feature['id']}: selector_output_role_{status} after prefix rebuild"
|
||||
)
|
||||
resolved.append(candidates[0])
|
||||
continue
|
||||
geometry = placeholder.get("geometry") or {}
|
||||
candidates = _circle_records(geometry, records) if geometry.get("source_circle_radius_mm") else []
|
||||
if not candidates:
|
||||
same_kind = [record for record in records if record.get("kind") == placeholder.get("kind")]
|
||||
owner = placeholder.get("owner_feature_id")
|
||||
owner_matches = [record for record in same_kind if owner in (record.get("owner_feature_ids") or [record.get("feature_id")])]
|
||||
# Pattern instance provenance is stronger than an ordinary
|
||||
# feature owner: an unresolved instance cannot fall back to
|
||||
# an aggregate face with matching geometry. Ordinary source
|
||||
# selectors retain their established geometry-binding path
|
||||
# across topology-changing dress-up operations.
|
||||
pool = owner_matches if placeholder.get("owner_match_required") else (owner_matches or same_kind)
|
||||
if not geometry:
|
||||
# Context selectors (notably a generated mirror plane)
|
||||
# may have no geometric snapshot. Their owner-qualified
|
||||
# singleton identity is sufficient and must not be scored
|
||||
# as a zero-information geometric match.
|
||||
if len(pool) != 1:
|
||||
status = "not_found" if not pool else "ambiguous"
|
||||
raise ValueError(f"{feature['id']}: selector_{status} after prefix rebuild")
|
||||
candidates = [pool[0]]
|
||||
else:
|
||||
scored = [(score, record) for record in pool if (score := _score(geometry, record.get("geometry") or {})) is not None and score >= 0.8]
|
||||
# A source owner is preferred for ordinary geometry
|
||||
# selectors, but can be retained by an unrelated exact
|
||||
# continuation after a dress-up. If none of that
|
||||
# owner's active records satisfies the full geometry
|
||||
# signature, bind against the current active body and
|
||||
# still require a unique threshold-qualified match.
|
||||
# Instance-qualified selectors never take this path.
|
||||
if not scored and not placeholder.get("owner_match_required"):
|
||||
scored = [
|
||||
(score, record)
|
||||
for record in same_kind
|
||||
if (score := _score(geometry, record.get("geometry") or {})) is not None
|
||||
and score >= 0.8
|
||||
]
|
||||
scored.sort(key=lambda value: (-value[0], str(value[1].get("record_id"))))
|
||||
if scored:
|
||||
if placeholder.get("match_mode") != "all" and len(scored) > 1 and abs(scored[0][0] - scored[1][0]) <= 1e-9:
|
||||
raise ValueError(f"{feature['id']}: selector_ambiguous after prefix rebuild")
|
||||
candidates = [item[1] for item in scored] if placeholder.get("match_mode") == "all" else [scored[0][1]]
|
||||
if not candidates: raise ValueError(f"{feature['id']}: selector_not_found after prefix rebuild")
|
||||
selector, bound_selectors = _bound_selector(placeholder, candidates)
|
||||
placeholder.clear(); placeholder.update(selector)
|
||||
resolved.extend(bound_selectors)
|
||||
feature_selectors = feature.get("selectors") or []
|
||||
def selector_key(selector: dict[str, Any]) -> tuple[Any, ...]:
|
||||
stable_id = selector.get("stable_id")
|
||||
if stable_id is not None:
|
||||
return ("stable_id", str(stable_id))
|
||||
source = selector.get("output_role_source")
|
||||
return (
|
||||
"output_role",
|
||||
selector.get("owner_feature_id"),
|
||||
selector.get("kind"),
|
||||
selector.get("output_role"),
|
||||
source.get("owner_feature_id") if isinstance(source, dict) else None,
|
||||
source.get("output_role") if isinstance(source, dict) else None,
|
||||
)
|
||||
body_id = next(
|
||||
(item.get("body_id") for item in reversed(report.get("feature_results") or []) if item.get("body_id")),
|
||||
None,
|
||||
)
|
||||
prefix_cache[prefix_count] = (
|
||||
list(report.get("topology_records") or []),
|
||||
body_id,
|
||||
list(report.get("topology_deltas") or []),
|
||||
)
|
||||
return prefix_cache[prefix_count]
|
||||
|
||||
unique = {selector_key(selector): selector for selector in feature_selectors}
|
||||
feature["selectors"] = list(unique.values())
|
||||
resolved: list[dict[str, Any]] = []
|
||||
for placeholder in targets:
|
||||
records, body_id, topology_deltas = prefix_snapshot(placeholder.get("binding_feature_id"))
|
||||
registry = TopologyRegistry.from_public_snapshot(records, topology_deltas)
|
||||
intent = placeholder.get("selector_intent")
|
||||
runtime_selector = _runtime_selector(placeholder)
|
||||
resolution = registry.resolve(runtime_selector, active_body_id=body_id)
|
||||
if (
|
||||
resolution.status != "resolved"
|
||||
and not isinstance(intent, dict)
|
||||
and not runtime_selector.get("owner_match_required")
|
||||
and not runtime_selector.get("output_role")
|
||||
):
|
||||
fallback = dict(runtime_selector)
|
||||
fallback.pop("owner_feature_id", None)
|
||||
resolution = registry.resolve(fallback, active_body_id=body_id)
|
||||
if resolution.status != "resolved":
|
||||
code = resolution.diagnostic.code if resolution.diagnostic is not None else f"selector_{resolution.status}"
|
||||
raise ValueError(f"{feature['id']}: {code} after prefix rebuild")
|
||||
selected = list(resolution.records or ((resolution.record,) if resolution.record is not None else ()))
|
||||
if not selected:
|
||||
raise ValueError(f"{feature['id']}: selector_not_found after prefix rebuild")
|
||||
public_records = [record.public_dict() for record in selected]
|
||||
if (isinstance(intent, dict) and intent.get("query_family") != "GEOMETRIC") or placeholder.get("output_role"):
|
||||
resolved.extend(public_records)
|
||||
continue
|
||||
selector, bound_selectors = _bound_selector(placeholder, public_records)
|
||||
placeholder.clear()
|
||||
placeholder.update(selector)
|
||||
resolved.extend(bound_selectors)
|
||||
feature["selectors"] = list({_selector_key(selector): selector for selector in feature.get("selectors") or []}.values())
|
||||
if feature.get("atomic_id") == "pattern_mirror":
|
||||
planes = [selector for selector in feature["selectors"] if selector.get("kind") == "plane"]
|
||||
if len(planes) != 1:
|
||||
raise ValueError(f"{feature['id']}: mirror plane binding is not unique")
|
||||
feature.setdefault("params", {})["mirror_plane"] = planes[0]
|
||||
evidence.append({"feature_id": feature["id"], "prefix_feature_count": index, "selectors": feature["selectors"], "resolved": resolved})
|
||||
evidence.append({
|
||||
"feature_id": feature["id"],
|
||||
"prefix_feature_count": index,
|
||||
"selectors": feature["selectors"],
|
||||
"resolved": resolved,
|
||||
})
|
||||
return bound, evidence
|
||||
|
||||
Reference in New Issue
Block a user