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:
@@ -0,0 +1,897 @@
|
||||
"""Offline experiment for geometry-only CADFS selector candidates.
|
||||
|
||||
The production resolver must not consume this module. It removes provenance
|
||||
intent only in a copied CDSL document, then compares that diagnostic replay to
|
||||
the source's final STEP. A strict passing probe is replayed a second time from
|
||||
its bound CDSL and can emit a *behavioural* selector record. The record is
|
||||
training/diagnostic evidence, not a claim that a heuristic recovered Onshape's
|
||||
hidden topology identity.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from copy import deepcopy
|
||||
import json
|
||||
import multiprocessing
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .compare import compare_steps
|
||||
from .dataset import Sample, scan_dataset
|
||||
from .featurescript_parser import parse_featurescript
|
||||
from .lowering import lower_model
|
||||
from .rebuild import rebuild_candidate
|
||||
from .reports import read_json, write_json, write_manifest
|
||||
|
||||
|
||||
def strip_provenance_intents(value: Any) -> int:
|
||||
"""Remove provenance-only selector fields from one copied CDSL value."""
|
||||
removed = 0
|
||||
if isinstance(value, dict):
|
||||
for key in ("selector_intent", "selector_intent_version"):
|
||||
if key in value:
|
||||
value.pop(key)
|
||||
removed += 1
|
||||
for child in value.values():
|
||||
removed += strip_provenance_intents(child)
|
||||
elif isinstance(value, list):
|
||||
for child in value:
|
||||
removed += strip_provenance_intents(child)
|
||||
return removed
|
||||
|
||||
|
||||
def _write_rebuild_artifacts(directory: Path, name: str, outcome: dict[str, Any]) -> None:
|
||||
"""Persist the same useful replay artifacts as the regular pipeline."""
|
||||
artifact = deepcopy(outcome)
|
||||
bound = artifact.pop("bound_cdsl", None)
|
||||
prefix = artifact.get("last_executable_prefix")
|
||||
if isinstance(prefix, dict):
|
||||
prefix_bound = prefix.pop("bound_cdsl", None)
|
||||
if bound is None:
|
||||
bound = prefix_bound
|
||||
write_json(directory / f"{name}.rebuild.json", artifact)
|
||||
if isinstance(bound, dict):
|
||||
write_json(directory / f"{name}.bound.cdsl.json", bound)
|
||||
|
||||
|
||||
def _rebuild_worker(candidate: str, step: str, result: str) -> None:
|
||||
write_json(Path(result), rebuild_candidate(read_json(Path(candidate)), Path(step)))
|
||||
|
||||
|
||||
def _compare_worker(gold: str, rebuilt: str, result: str) -> None:
|
||||
write_json(Path(result), compare_steps(Path(gold), Path(rebuilt)))
|
||||
|
||||
|
||||
def _isolated(target: Any, args: tuple[str, ...], result_path: Path, timeout_seconds: float) -> str:
|
||||
"""Bound an OCC experiment so one candidate cannot stall the demo."""
|
||||
result_path.unlink(missing_ok=True)
|
||||
process = multiprocessing.get_context("spawn").Process(target=target, args=args)
|
||||
process.start()
|
||||
process.join(timeout_seconds)
|
||||
if process.is_alive():
|
||||
process.terminate()
|
||||
process.join(5)
|
||||
if process.is_alive():
|
||||
process.kill()
|
||||
process.join()
|
||||
return "timeout"
|
||||
return "completed" if process.exitcode == 0 and result_path.exists() else "failed"
|
||||
|
||||
|
||||
def _run_rebuild(candidate_path: Path, step: Path, *, timeout_seconds: float) -> dict[str, Any]:
|
||||
worker = step.with_suffix(".worker.json")
|
||||
outcome = _isolated(_rebuild_worker, (str(candidate_path), str(step), str(worker)), worker, timeout_seconds)
|
||||
if outcome == "completed":
|
||||
result = read_json(worker)
|
||||
worker.unlink(missing_ok=True)
|
||||
return result
|
||||
step.unlink(missing_ok=True)
|
||||
return {
|
||||
"status": "rebuild_timeout" if outcome == "timeout" else "rebuild_failed",
|
||||
"error": {
|
||||
"type": "TimeoutError" if outcome == "timeout" else "WorkerProcessError",
|
||||
"message": f"selector candidate rebuild exceeded {timeout_seconds:g} seconds" if outcome == "timeout" else "selector candidate rebuild worker exited without a result",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _run_comparison(gold_step: Path, rebuilt_step: Path, output: Path, *, timeout_seconds: float) -> tuple[dict[str, Any] | None, dict[str, Any] | None]:
|
||||
worker = output.with_suffix(".worker.json")
|
||||
outcome = _isolated(_compare_worker, (str(gold_step), str(rebuilt_step), str(worker)), worker, timeout_seconds)
|
||||
if outcome == "completed":
|
||||
comparison = read_json(worker)
|
||||
worker.unlink(missing_ok=True)
|
||||
return comparison, None
|
||||
return None, {
|
||||
"type": "TimeoutError" if outcome == "timeout" else "WorkerProcessError",
|
||||
"message": f"selector candidate comparison exceeded {timeout_seconds:g} seconds" if outcome == "timeout" else "selector candidate comparison worker exited without a result",
|
||||
}
|
||||
|
||||
|
||||
def _outcome_summary(outcome: dict[str, Any], comparison: dict[str, Any] | None) -> dict[str, Any]:
|
||||
summary: dict[str, Any] = {
|
||||
"rebuild_status": outcome.get("status"),
|
||||
"last_executable_prefix": {
|
||||
key: value
|
||||
for key, value in (outcome.get("last_executable_prefix") or {}).items()
|
||||
if key in {"failed_feature_id", "feature_count", "last_feature_id"}
|
||||
},
|
||||
}
|
||||
error = outcome.get("error")
|
||||
if isinstance(error, dict):
|
||||
summary["error"] = {key: error.get(key) for key in ("type", "message")}
|
||||
if comparison is not None:
|
||||
summary["comparison"] = {
|
||||
"decision": comparison.get("decision"),
|
||||
"strict_passed": bool((comparison.get("strict") or {}).get("passed")),
|
||||
"rp_passed": bool((comparison.get("rp") or {}).get("passed")),
|
||||
}
|
||||
return summary
|
||||
|
||||
|
||||
def _strict_passed(comparison: dict[str, Any] | None) -> bool:
|
||||
return bool(isinstance(comparison, dict) and (comparison.get("strict") or {}).get("passed"))
|
||||
|
||||
|
||||
def _selector_sites(feature: dict[str, Any]) -> list[tuple[str, dict[str, Any]]]:
|
||||
"""Return the public selector positions of one CDSL feature.
|
||||
|
||||
The locations are deliberately structural rather than runtime IDs. A
|
||||
selector record must remain useful when a fresh replay creates different
|
||||
topology-record instances.
|
||||
"""
|
||||
sites: list[tuple[str, dict[str, Any]]] = []
|
||||
for index, selector in enumerate(feature.get("selectors") or []):
|
||||
if isinstance(selector, dict):
|
||||
sites.append((f"selectors[{index}]", selector))
|
||||
params = feature.get("params") or {}
|
||||
for name in ("end_condition", "reverse_end_condition"):
|
||||
condition = params.get(name)
|
||||
reference = condition.get("reference") if isinstance(condition, dict) else None
|
||||
if isinstance(reference, dict):
|
||||
sites.append((f"params.{name}.reference", reference))
|
||||
return sites
|
||||
|
||||
|
||||
def _semantic_source_selector(selector: dict[str, Any]) -> dict[str, Any] | None:
|
||||
"""Keep the FeatureScript meaning, never a source runtime topology ID."""
|
||||
intent = selector.get("selector_intent")
|
||||
if not isinstance(intent, dict):
|
||||
return None
|
||||
result = {
|
||||
key: deepcopy(selector[key])
|
||||
for key in ("kind", "owner_feature_id", "binding_feature_id", "output_role")
|
||||
if selector.get(key) is not None
|
||||
}
|
||||
result["selector_intent"] = deepcopy(intent)
|
||||
return result
|
||||
|
||||
|
||||
def _selector_record_entries(
|
||||
sample_id: str,
|
||||
provenance_candidate: dict[str, Any],
|
||||
compiled_cdsl: dict[str, Any],
|
||||
selector_binding: list[dict[str, Any]],
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Extract FeatureScript meaning plus a freshly replayable binding plan.
|
||||
|
||||
A geometry probe has only the selected record, not rejected alternatives.
|
||||
We preserve that limitation explicitly. When source and compiled selector
|
||||
counts differ (for example, one source query expands to several topology
|
||||
records), emit a feature group instead of inventing a one-to-one mapping.
|
||||
"""
|
||||
source_by_id = {
|
||||
str(feature.get("id")): feature
|
||||
for feature in provenance_candidate.get("features") or []
|
||||
if isinstance(feature, dict) and isinstance(feature.get("id"), str)
|
||||
}
|
||||
compiled_by_id = {
|
||||
str(feature.get("id")): feature
|
||||
for feature in compiled_cdsl.get("features") or []
|
||||
if isinstance(feature, dict) and isinstance(feature.get("id"), str)
|
||||
}
|
||||
entries: list[dict[str, Any]] = []
|
||||
for binding in selector_binding:
|
||||
feature_id = binding.get("feature_id")
|
||||
if not isinstance(feature_id, str):
|
||||
continue
|
||||
source_feature = source_by_id.get(feature_id)
|
||||
compiled_feature = compiled_by_id.get(feature_id)
|
||||
if source_feature is None or compiled_feature is None:
|
||||
continue
|
||||
source_sites = [
|
||||
(location, semantic)
|
||||
for location, selector in _selector_sites(source_feature)
|
||||
if (semantic := _semantic_source_selector(selector)) is not None
|
||||
]
|
||||
if not source_sites:
|
||||
continue
|
||||
compiled_sites = _selector_sites(compiled_feature)
|
||||
resolved = [deepcopy(item) for item in binding.get("resolved") or [] if isinstance(item, dict)]
|
||||
feature_context = {
|
||||
"sample_id": sample_id,
|
||||
"feature_id": feature_id,
|
||||
"atomic_id": compiled_feature.get("atomic_id"),
|
||||
"prefix_feature_count": binding.get("prefix_feature_count"),
|
||||
"verification_scope": "behavioural_strict_replay",
|
||||
}
|
||||
if len(source_sites) == len(compiled_sites) == len(resolved):
|
||||
for index, ((location, source_selector), (_compiled_location, compiled_selector), resolved_record) in enumerate(
|
||||
zip(source_sites, compiled_sites, resolved)
|
||||
):
|
||||
entries.append({
|
||||
"schema": "cadfs_to_cdsl.selector_record.v1",
|
||||
"entry_id": f"{sample_id}:{feature_id}:{location}",
|
||||
**feature_context,
|
||||
"source_location": location,
|
||||
"source_selector": source_selector,
|
||||
"compiled_selector": deepcopy(compiled_selector),
|
||||
"resolved_record": resolved_record,
|
||||
"mapping": "one_to_one",
|
||||
"limitations": [
|
||||
"The source dataset has no per-feature Onshape query witness.",
|
||||
"The compiled selector is verified only by a fresh local strict replay.",
|
||||
"Runtime snapshot IDs are replay evidence, not portable topology identities.",
|
||||
],
|
||||
})
|
||||
continue
|
||||
entries.append({
|
||||
"schema": "cadfs_to_cdsl.selector_record.v1",
|
||||
"entry_id": f"{sample_id}:{feature_id}:feature_group",
|
||||
**feature_context,
|
||||
"source_selectors": [
|
||||
{"location": location, "selector": selector}
|
||||
for location, selector in source_sites
|
||||
],
|
||||
"compiled_selectors": [
|
||||
{"location": location, "selector": deepcopy(selector)}
|
||||
for location, selector in compiled_sites
|
||||
],
|
||||
"resolved_records": resolved,
|
||||
"mapping": "feature_group",
|
||||
"limitations": [
|
||||
"The source query expanded or contracted during local binding, so this record does not claim a one-to-one mapping.",
|
||||
"The source dataset has no per-feature Onshape query witness.",
|
||||
"The compiled selector is verified only by a fresh local strict replay.",
|
||||
"Runtime snapshot IDs are replay evidence, not portable topology identities.",
|
||||
],
|
||||
})
|
||||
return entries
|
||||
|
||||
|
||||
def _emit_strict_selector_record(
|
||||
sample: Sample,
|
||||
directory: Path,
|
||||
provenance_candidate: dict[str, Any],
|
||||
geometry_probe: dict[str, Any],
|
||||
geometry_comparison: dict[str, Any] | None,
|
||||
gold_step: Path,
|
||||
*,
|
||||
rebuild_timeout_seconds: float,
|
||||
comparison_timeout_seconds: float,
|
||||
) -> dict[str, Any]:
|
||||
"""Create and independently verify a compiled CDSL only after strict proof."""
|
||||
if not _strict_passed(geometry_comparison):
|
||||
return {
|
||||
"status": "not_emitted",
|
||||
"reason": "geometry_probe_not_strict",
|
||||
}
|
||||
bound_cdsl = geometry_probe.get("bound_cdsl")
|
||||
binding = geometry_probe.get("selector_binding")
|
||||
if not isinstance(bound_cdsl, dict) or not isinstance(binding, list):
|
||||
return {
|
||||
"status": "not_emitted",
|
||||
"reason": "geometry_probe_binding_missing",
|
||||
}
|
||||
|
||||
compiled_candidate = deepcopy(bound_cdsl)
|
||||
compiled_path = directory / "selector_record.compiled.cdsl.json"
|
||||
write_json(compiled_path, compiled_candidate)
|
||||
compiled = _run_rebuild(
|
||||
compiled_path,
|
||||
directory / "selector_record.compiled.rebuild.step",
|
||||
timeout_seconds=rebuild_timeout_seconds,
|
||||
)
|
||||
_write_rebuild_artifacts(directory, "selector_record.compiled", compiled)
|
||||
comparison = None
|
||||
comparison_error = None
|
||||
if compiled.get("status") == "rebuilt":
|
||||
comparison, comparison_error = _run_comparison(
|
||||
gold_step,
|
||||
directory / "selector_record.compiled.rebuild.step",
|
||||
directory / "selector_record.compiled.comparison.json",
|
||||
timeout_seconds=comparison_timeout_seconds,
|
||||
)
|
||||
if comparison is not None:
|
||||
write_json(directory / "selector_record.compiled.comparison.json", comparison)
|
||||
|
||||
result = {
|
||||
"status": "not_emitted",
|
||||
"compiled_cdsl": str(compiled_path),
|
||||
"compiled_replay": _outcome_summary(compiled, comparison),
|
||||
}
|
||||
if comparison_error is not None:
|
||||
result["compiled_replay"]["comparison_error"] = comparison_error
|
||||
if not _strict_passed(comparison):
|
||||
result["reason"] = "compiled_cdsl_not_strict"
|
||||
return result
|
||||
|
||||
records = _selector_record_entries(sample.sample_id, provenance_candidate, compiled_candidate, binding)
|
||||
if not records:
|
||||
result["reason"] = "no_featurescript_selector_binding"
|
||||
return result
|
||||
record_path = directory / "selector-record.json"
|
||||
write_json(record_path, {
|
||||
"schema": "cadfs_to_cdsl.selector_record_set.v1",
|
||||
"sample_id": sample.sample_id,
|
||||
"source": {
|
||||
"featurescript": sample.files.get("featurescript"),
|
||||
"step": sample.files.get("step"),
|
||||
},
|
||||
"verification": {
|
||||
"geometry_probe_strict": True,
|
||||
"compiled_cdsl_strict": True,
|
||||
"compiled_cdsl": str(compiled_path),
|
||||
},
|
||||
"records": records,
|
||||
})
|
||||
result.update({
|
||||
"status": "strict_replayed",
|
||||
"record_count": len(records),
|
||||
"record_path": str(record_path),
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
def _searchable_selector_sites(feature: dict[str, Any]) -> list[tuple[str, dict[str, Any]]]:
|
||||
"""Return direct selector roots that can be replaced as a search branch.
|
||||
|
||||
The experimental search does not flatten an intersection query into an
|
||||
unrelated member. It can search direct roots and extent references; other
|
||||
query trees remain a bounded unsupported search outcome.
|
||||
"""
|
||||
sites = _selector_sites(feature)
|
||||
return [
|
||||
(location, selector)
|
||||
for location, selector in sites
|
||||
if not isinstance(selector.get("intersection_of"), list)
|
||||
]
|
||||
|
||||
|
||||
def _replace_selector_site(cdsl: dict[str, Any], feature_id: str, location: str, selector: dict[str, Any]) -> None:
|
||||
"""Replace one public direct selector site in a copied candidate CDSL."""
|
||||
feature = next(
|
||||
(item for item in cdsl.get("features") or [] if isinstance(item, dict) and item.get("id") == feature_id),
|
||||
None,
|
||||
)
|
||||
if feature is None:
|
||||
raise ValueError(f"search branch cannot find feature {feature_id}")
|
||||
if location.startswith("selectors[") and location.endswith("]"):
|
||||
index = int(location[len("selectors["):-1])
|
||||
selectors = feature.get("selectors") or []
|
||||
if index < 0 or index >= len(selectors) or not isinstance(selectors[index], dict):
|
||||
raise ValueError(f"search branch selector site is unavailable: {feature_id}:{location}")
|
||||
selectors[index] = deepcopy(selector)
|
||||
return
|
||||
prefix = "params."
|
||||
suffix = ".reference"
|
||||
if location.startswith(prefix) and location.endswith(suffix):
|
||||
name = location[len(prefix):-len(suffix)]
|
||||
condition = (feature.get("params") or {}).get(name)
|
||||
if not isinstance(condition, dict) or not isinstance(condition.get("reference"), dict):
|
||||
raise ValueError(f"search branch selector site is unavailable: {feature_id}:{location}")
|
||||
condition["reference"] = deepcopy(selector)
|
||||
return
|
||||
raise ValueError(f"search branch does not support nested selector site: {feature_id}:{location}")
|
||||
|
||||
|
||||
def _forced_candidate_selector(placeholder: dict[str, Any], candidate: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Turn one public runtime record into a branch-local explicit selector."""
|
||||
from .selector_binding import _bound_selector
|
||||
|
||||
selector, _bound = _bound_selector(placeholder, [candidate])
|
||||
if not isinstance(selector, dict):
|
||||
raise ValueError("search branch produced an invalid explicit selector")
|
||||
return selector
|
||||
|
||||
|
||||
def _resolve_search_selector(placeholder: dict[str, Any], *, registry: Any, active_body_id: str | None) -> Any:
|
||||
"""Mirror the legacy diagnostic binding policy without changing it."""
|
||||
from .selector_binding import _runtime_selector
|
||||
|
||||
runtime_selector = _runtime_selector(placeholder)
|
||||
resolution = registry.resolve(runtime_selector, active_body_id=active_body_id)
|
||||
intent = placeholder.get("selector_intent")
|
||||
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)
|
||||
return resolution
|
||||
|
||||
|
||||
def _search_candidate_records(
|
||||
resolution: Any,
|
||||
*,
|
||||
maximum: int,
|
||||
minimum_score: float = 0.8,
|
||||
) -> tuple[list[dict[str, Any]], int]:
|
||||
"""Keep only candidates the normal resolver considers geometrically valid."""
|
||||
candidates = [
|
||||
item for item in resolution.candidates
|
||||
if isinstance(item, dict)
|
||||
and item.get("record_id")
|
||||
and float(item.get("score") or 0.0) >= minimum_score
|
||||
]
|
||||
candidates.sort(key=lambda item: (-float(item.get("score") or 0.0), str(item.get("record_id"))))
|
||||
return [deepcopy(item) for item in candidates[:maximum]], len(candidates)
|
||||
|
||||
|
||||
def _search_replay_worker(candidate: str, step: str, result: str, maximum_candidates: int) -> None:
|
||||
"""Replay one branch until it builds or exposes its next selector decision."""
|
||||
from engine.cdsl_engine.runtime import finalize_cdsl_execution, prepare_cdsl_execution
|
||||
from .selector_binding import _bound_selector, _selector_key
|
||||
|
||||
bound = read_json(Path(candidate))
|
||||
execution = prepare_cdsl_execution(bound)
|
||||
if not execution.analysis.runtime_eligible:
|
||||
first = next((item for item in execution.analysis.feature_results if not item.executable), None)
|
||||
code = first.blockers[0].code if first and first.blockers else "runtime_ineligible"
|
||||
write_json(Path(result), {"status": "runtime_ineligible", "reason": code})
|
||||
return
|
||||
bound_features = {
|
||||
str(feature.get("id") or ""): feature
|
||||
for feature in bound.get("features") or []
|
||||
if isinstance(feature, dict)
|
||||
}
|
||||
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
|
||||
sites = _searchable_selector_sites(feature)
|
||||
if len(sites) != len(_selector_sites(feature)):
|
||||
write_json(Path(result), {
|
||||
"status": "search_unsupported",
|
||||
"feature_id": node.feature_id,
|
||||
"reason": "nested_selector_tree",
|
||||
"bound_cdsl": bound,
|
||||
"selector_binding": evidence,
|
||||
})
|
||||
return
|
||||
resolved: list[dict[str, Any]] = []
|
||||
for location, placeholder in sites:
|
||||
binding_feature_id = placeholder.get("binding_feature_id")
|
||||
if binding_feature_id is None:
|
||||
active_body_id = body_id_for_feature["__current__"]
|
||||
elif isinstance(binding_feature_id, str) and binding_feature_id in body_id_for_feature:
|
||||
active_body_id = body_id_for_feature[binding_feature_id]
|
||||
else:
|
||||
write_json(Path(result), {
|
||||
"status": "search_failed",
|
||||
"feature_id": node.feature_id,
|
||||
"reason": "selector_binding_feature_missing_or_forward",
|
||||
"bound_cdsl": bound,
|
||||
"selector_binding": evidence,
|
||||
})
|
||||
return
|
||||
resolution = _resolve_search_selector(
|
||||
placeholder,
|
||||
registry=execution.session.topology,
|
||||
active_body_id=active_body_id,
|
||||
)
|
||||
if resolution.status != "resolved":
|
||||
candidates, eligible_count = _search_candidate_records(
|
||||
resolution,
|
||||
maximum=maximum_candidates,
|
||||
)
|
||||
if candidates:
|
||||
write_json(Path(result), {
|
||||
"status": "branchable_selector",
|
||||
"feature_id": node.feature_id,
|
||||
"source_location": location,
|
||||
"resolution_status": resolution.status,
|
||||
"diagnostic": resolution.diagnostic.as_dict() if resolution.diagnostic is not None else None,
|
||||
"candidates": candidates,
|
||||
"candidate_count": eligible_count,
|
||||
"bound_cdsl": bound,
|
||||
"selector_binding": evidence,
|
||||
})
|
||||
return
|
||||
code = resolution.diagnostic.code if resolution.diagnostic is not None else f"selector_{resolution.status}"
|
||||
write_json(Path(result), {
|
||||
"status": "search_failed",
|
||||
"feature_id": node.feature_id,
|
||||
"reason": code,
|
||||
"bound_cdsl": bound,
|
||||
"selector_binding": evidence,
|
||||
})
|
||||
return
|
||||
selected = list(resolution.records or ((resolution.record,) if resolution.record is not None else ()))
|
||||
if not selected:
|
||||
write_json(Path(result), {
|
||||
"status": "search_failed",
|
||||
"feature_id": node.feature_id,
|
||||
"reason": "selector_not_found",
|
||||
"bound_cdsl": bound,
|
||||
"selector_binding": evidence,
|
||||
})
|
||||
return
|
||||
public_records = [record.public_dict() for record in selected]
|
||||
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:
|
||||
write_json(Path(result), {
|
||||
"status": "search_failed",
|
||||
"feature_id": node.feature_id,
|
||||
"reason": "mirror_plane_binding_not_unique",
|
||||
"bound_cdsl": bound,
|
||||
"selector_binding": evidence,
|
||||
})
|
||||
return
|
||||
feature.setdefault("params", {})["mirror_plane"] = planes[0]
|
||||
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,
|
||||
})
|
||||
try:
|
||||
execution.execute_next(strict=True)
|
||||
except Exception as error:
|
||||
write_json(Path(result), {
|
||||
"status": "search_failed",
|
||||
"feature_id": node.feature_id,
|
||||
"reason": str(error),
|
||||
"bound_cdsl": bound,
|
||||
"selector_binding": evidence,
|
||||
})
|
||||
return
|
||||
body_id_for_feature[node.feature_id] = execution.session.body_id
|
||||
body_id_for_feature["__current__"] = execution.session.body_id
|
||||
write_json(Path(result), {
|
||||
"status": "rebuilt",
|
||||
"bound_cdsl": bound,
|
||||
"selector_binding": evidence,
|
||||
"result": finalize_cdsl_execution(execution, Path(step)),
|
||||
})
|
||||
|
||||
|
||||
def _run_search_replay(candidate_path: Path, step: Path, *, timeout_seconds: float, maximum_candidates: int) -> dict[str, Any]:
|
||||
worker = step.with_suffix(".worker.json")
|
||||
outcome = _isolated(
|
||||
_search_replay_worker,
|
||||
(str(candidate_path), str(step), str(worker), maximum_candidates),
|
||||
worker,
|
||||
timeout_seconds,
|
||||
)
|
||||
if outcome == "completed":
|
||||
result = read_json(worker)
|
||||
worker.unlink(missing_ok=True)
|
||||
return result
|
||||
step.unlink(missing_ok=True)
|
||||
return {
|
||||
"status": "search_timeout" if outcome == "timeout" else "search_failed",
|
||||
"reason": "search_worker_timeout" if outcome == "timeout" else "search_worker_failed",
|
||||
}
|
||||
|
||||
|
||||
def _public_branch_summary(result: dict[str, Any], choices: list[dict[str, Any]], comparison: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
summary = {
|
||||
"status": result.get("status"),
|
||||
"feature_id": result.get("feature_id"),
|
||||
"source_location": result.get("source_location"),
|
||||
"reason": result.get("reason"),
|
||||
"resolution_status": result.get("resolution_status"),
|
||||
"choice_path": choices,
|
||||
}
|
||||
if comparison is not None:
|
||||
summary["comparison"] = {
|
||||
"decision": comparison.get("decision"),
|
||||
"strict_passed": _strict_passed(comparison),
|
||||
}
|
||||
return {key: value for key, value in summary.items() if value is not None}
|
||||
|
||||
|
||||
def _run_selector_search(
|
||||
sample: Sample,
|
||||
directory: Path,
|
||||
provenance_candidate: dict[str, Any],
|
||||
geometry_candidate: dict[str, Any],
|
||||
gold_step: Path,
|
||||
*,
|
||||
rebuild_timeout_seconds: float,
|
||||
comparison_timeout_seconds: float,
|
||||
maximum_branches: int,
|
||||
maximum_candidates: int,
|
||||
) -> tuple[dict[str, Any], dict[str, Any] | None]:
|
||||
"""Explore selector alternatives, accepting only a unique strict winner.
|
||||
|
||||
Each branch is re-executed from an untouched CDSL document. We therefore
|
||||
never share mutable OCC topology between alternatives and cannot quietly
|
||||
inherit the result of a previous candidate.
|
||||
"""
|
||||
if maximum_branches <= 0 or maximum_candidates <= 0:
|
||||
return {"status": "disabled"}, None
|
||||
search_dir = directory / "selector-search"
|
||||
search_dir.mkdir(parents=True, exist_ok=True)
|
||||
pending: list[tuple[dict[str, Any], list[dict[str, Any]]]] = [(deepcopy(geometry_candidate), [])]
|
||||
seen: set[str] = set()
|
||||
terminals: list[tuple[dict[str, Any], dict[str, Any], list[dict[str, Any]], dict[str, Any] | None]] = []
|
||||
branch_summaries: list[dict[str, Any]] = []
|
||||
budget_exhausted = False
|
||||
candidates_truncated = False
|
||||
branch_index = 0
|
||||
while pending:
|
||||
if branch_index >= maximum_branches:
|
||||
budget_exhausted = True
|
||||
break
|
||||
candidate, choices = pending.pop(0)
|
||||
fingerprint = json.dumps(candidate, sort_keys=True, separators=(",", ":"))
|
||||
if fingerprint in seen:
|
||||
continue
|
||||
seen.add(fingerprint)
|
||||
branch_index += 1
|
||||
branch_name = f"branch-{branch_index:03d}"
|
||||
candidate_path = search_dir / f"{branch_name}.candidate.cdsl.json"
|
||||
step_path = search_dir / f"{branch_name}.rebuild.step"
|
||||
write_json(candidate_path, candidate)
|
||||
outcome = _run_search_replay(
|
||||
candidate_path,
|
||||
step_path,
|
||||
timeout_seconds=rebuild_timeout_seconds,
|
||||
maximum_candidates=maximum_candidates,
|
||||
)
|
||||
_write_rebuild_artifacts(search_dir, branch_name, outcome)
|
||||
if outcome.get("status") == "branchable_selector":
|
||||
candidates = list(outcome.get("candidates") or [])
|
||||
total = int(outcome.get("candidate_count") or len(candidates))
|
||||
if total > len(candidates):
|
||||
candidates_truncated = True
|
||||
branch_summaries.append(_public_branch_summary(outcome, choices))
|
||||
for candidate_record in candidates:
|
||||
child = deepcopy(outcome["bound_cdsl"])
|
||||
feature_id = str(outcome["feature_id"])
|
||||
location = str(outcome["source_location"])
|
||||
source_feature = next(item for item in child["features"] if item.get("id") == feature_id)
|
||||
source_selector = dict(_searchable_selector_sites(source_feature)[
|
||||
next(index for index, (name, _selector) in enumerate(_searchable_selector_sites(source_feature)) if name == location)
|
||||
][1])
|
||||
forced = _forced_candidate_selector(source_selector, candidate_record)
|
||||
_replace_selector_site(child, feature_id, location, forced)
|
||||
pending.append((child, [*choices, {
|
||||
"feature_id": feature_id,
|
||||
"source_location": location,
|
||||
"record_id": candidate_record.get("record_id"),
|
||||
"score": candidate_record.get("score"),
|
||||
}]))
|
||||
continue
|
||||
comparison = None
|
||||
if outcome.get("status") == "rebuilt":
|
||||
comparison, comparison_error = _run_comparison(
|
||||
gold_step,
|
||||
step_path,
|
||||
search_dir / f"{branch_name}.comparison.json",
|
||||
timeout_seconds=comparison_timeout_seconds,
|
||||
)
|
||||
if comparison is not None:
|
||||
write_json(search_dir / f"{branch_name}.comparison.json", comparison)
|
||||
elif comparison_error is not None:
|
||||
outcome["comparison_error"] = comparison_error
|
||||
terminals.append((outcome, candidate, choices, comparison))
|
||||
branch_summaries.append(_public_branch_summary(outcome, choices, comparison))
|
||||
if pending:
|
||||
budget_exhausted = True
|
||||
strict_winners = [item for item in terminals if _strict_passed(item[3])]
|
||||
search_complete = not budget_exhausted and not candidates_truncated
|
||||
report: dict[str, Any] = {
|
||||
"status": "unique_strict" if search_complete and len(strict_winners) == 1 else "no_unique_strict",
|
||||
"branches_executed": branch_index,
|
||||
"maximum_branches": maximum_branches,
|
||||
"maximum_candidates_per_selector": maximum_candidates,
|
||||
"search_complete": search_complete,
|
||||
"budget_exhausted": budget_exhausted,
|
||||
"candidates_truncated": candidates_truncated,
|
||||
"strict_winner_count": len(strict_winners),
|
||||
"branches": branch_summaries,
|
||||
}
|
||||
if not search_complete:
|
||||
report["reason"] = "search_budget_incomplete"
|
||||
return report, None
|
||||
if not strict_winners:
|
||||
report["reason"] = "no_strict_branch"
|
||||
return report, None
|
||||
if len(strict_winners) > 1:
|
||||
report["reason"] = "multiple_strict_branches"
|
||||
report["strict_choice_paths"] = [item[2] for item in strict_winners]
|
||||
return report, None
|
||||
winner, _candidate, choices, comparison = strict_winners[0]
|
||||
report["strict_choice_path"] = choices
|
||||
selector_record = _emit_strict_selector_record(
|
||||
sample,
|
||||
search_dir,
|
||||
provenance_candidate,
|
||||
winner,
|
||||
comparison,
|
||||
gold_step,
|
||||
rebuild_timeout_seconds=rebuild_timeout_seconds,
|
||||
comparison_timeout_seconds=comparison_timeout_seconds,
|
||||
)
|
||||
report["selector_record"] = selector_record
|
||||
return report, selector_record if selector_record.get("status") == "strict_replayed" else None
|
||||
|
||||
|
||||
def run_geometry_probe(
|
||||
sample: Sample,
|
||||
output: Path,
|
||||
*,
|
||||
rebuild_timeout_seconds: float = 30.0,
|
||||
comparison_timeout_seconds: float = 60.0,
|
||||
search_maximum_branches: int = 32,
|
||||
search_maximum_candidates: int = 8,
|
||||
) -> dict[str, Any]:
|
||||
"""Compare normal provenance replay with an explicitly heuristic replay.
|
||||
|
||||
The geometry replay reuses only geometry/stable-ID fields already emitted as
|
||||
diagnostic context. It never invents a selector, writes a new source
|
||||
parameter, or changes the production candidate.
|
||||
"""
|
||||
featurescript = Path(sample.files["featurescript"])
|
||||
gold_step = Path(sample.files["step"])
|
||||
if not featurescript.is_file() or not gold_step.is_file():
|
||||
raise FileNotFoundError(f"{sample.sample_id}: the FeatureScript and source STEP are required")
|
||||
directory = output / "samples" / sample.sample_id
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
result = lower_model(parse_featurescript(featurescript.read_text(encoding="utf-8"), sample.sample_id), {})
|
||||
if not isinstance(result.cdsl, dict):
|
||||
raise ValueError(f"{sample.sample_id}: lowering produced no CDSL candidate")
|
||||
|
||||
provenance_candidate = result.cdsl
|
||||
geometry_candidate = deepcopy(provenance_candidate)
|
||||
removed_intents = strip_provenance_intents(geometry_candidate)
|
||||
write_json(directory / "provenance.candidate.cdsl.json", provenance_candidate)
|
||||
write_json(directory / "geometry_probe.candidate.cdsl.json", geometry_candidate)
|
||||
|
||||
provenance = _run_rebuild(
|
||||
directory / "provenance.candidate.cdsl.json",
|
||||
directory / "provenance.rebuild.step",
|
||||
timeout_seconds=rebuild_timeout_seconds,
|
||||
)
|
||||
geometry_probe = _run_rebuild(
|
||||
directory / "geometry_probe.candidate.cdsl.json",
|
||||
directory / "geometry_probe.rebuild.step",
|
||||
timeout_seconds=rebuild_timeout_seconds,
|
||||
)
|
||||
_write_rebuild_artifacts(directory, "provenance", provenance)
|
||||
_write_rebuild_artifacts(directory, "geometry_probe", geometry_probe)
|
||||
|
||||
comparison = None
|
||||
comparison_error = None
|
||||
if geometry_probe.get("status") == "rebuilt":
|
||||
comparison, comparison_error = _run_comparison(
|
||||
gold_step,
|
||||
directory / "geometry_probe.rebuild.step",
|
||||
directory / "geometry_probe.comparison.json",
|
||||
timeout_seconds=comparison_timeout_seconds,
|
||||
)
|
||||
if comparison is not None:
|
||||
write_json(directory / "geometry_probe.comparison.json", comparison)
|
||||
|
||||
selector_record = _emit_strict_selector_record(
|
||||
sample,
|
||||
directory,
|
||||
provenance_candidate,
|
||||
geometry_probe,
|
||||
comparison,
|
||||
gold_step,
|
||||
rebuild_timeout_seconds=rebuild_timeout_seconds,
|
||||
comparison_timeout_seconds=comparison_timeout_seconds,
|
||||
)
|
||||
selector_search = {"status": "not_needed"}
|
||||
if not _strict_passed(comparison):
|
||||
selector_search, searched_record = _run_selector_search(
|
||||
sample,
|
||||
directory,
|
||||
provenance_candidate,
|
||||
geometry_candidate,
|
||||
gold_step,
|
||||
rebuild_timeout_seconds=rebuild_timeout_seconds,
|
||||
comparison_timeout_seconds=comparison_timeout_seconds,
|
||||
maximum_branches=search_maximum_branches,
|
||||
maximum_candidates=search_maximum_candidates,
|
||||
)
|
||||
if searched_record is not None:
|
||||
selector_record = searched_record
|
||||
|
||||
report = {
|
||||
"schema": "cadfs_to_cdsl.selector_candidate_geometry_probe.v1",
|
||||
"sample_id": sample.sample_id,
|
||||
"source": {"featurescript": str(featurescript), "step": str(gold_step)},
|
||||
"strategy": {
|
||||
"name": "geometry_diagnostic_context",
|
||||
"removed_provenance_intent_fields": removed_intents,
|
||||
"classification": "heuristic_only",
|
||||
"limitations": [
|
||||
"The source dataset contains only a final STEP, not per-feature source checkpoints.",
|
||||
"A final strict or RP pass does not prove that the FeatureScript selector is semantically correct.",
|
||||
"This probe must not be used by the production resolver or counted as provenance-selector coverage.",
|
||||
],
|
||||
},
|
||||
"provenance": _outcome_summary(provenance, None),
|
||||
"geometry_probe": _outcome_summary(geometry_probe, comparison),
|
||||
"selector_record": selector_record,
|
||||
"selector_search": selector_search,
|
||||
}
|
||||
if comparison_error is not None:
|
||||
report["geometry_probe"]["comparison_error"] = comparison_error
|
||||
write_json(directory / "report.json", report)
|
||||
return report
|
||||
|
||||
|
||||
def _arguments() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Run an offline geometry-only selector candidate probe and strict record replay")
|
||||
parser.add_argument("--input", type=Path, default=Path("data/cadfs-sample/CADFS_test"))
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument("--sample-id", action="append", required=True)
|
||||
parser.add_argument("--rebuild-timeout-seconds", type=float, default=30.0)
|
||||
parser.add_argument("--comparison-timeout-seconds", type=float, default=60.0)
|
||||
parser.add_argument("--search-maximum-branches", type=int, default=32)
|
||||
parser.add_argument("--search-maximum-candidates", type=int, default=8)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = _arguments()
|
||||
samples = {sample.sample_id: sample for sample in scan_dataset(args.input, include_hashes=False)}
|
||||
missing = sorted(set(args.sample_id) - samples.keys())
|
||||
if missing:
|
||||
raise ValueError("unknown sample ids: " + ", ".join(missing))
|
||||
if args.rebuild_timeout_seconds <= 0 or args.comparison_timeout_seconds <= 0:
|
||||
raise ValueError("candidate probe timeouts must be positive")
|
||||
if args.search_maximum_branches < 0 or args.search_maximum_candidates < 0:
|
||||
raise ValueError("search budgets cannot be negative")
|
||||
reports = [
|
||||
run_geometry_probe(
|
||||
samples[sample_id],
|
||||
args.output,
|
||||
rebuild_timeout_seconds=args.rebuild_timeout_seconds,
|
||||
comparison_timeout_seconds=args.comparison_timeout_seconds,
|
||||
search_maximum_branches=args.search_maximum_branches,
|
||||
search_maximum_candidates=args.search_maximum_candidates,
|
||||
)
|
||||
for sample_id in args.sample_id
|
||||
]
|
||||
records: list[dict[str, Any]] = []
|
||||
for report in reports:
|
||||
record = report.get("selector_record") or {}
|
||||
record_path = record.get("record_path") if isinstance(record, dict) else None
|
||||
if isinstance(record_path, str) and Path(record_path).is_file():
|
||||
payload = read_json(Path(record_path))
|
||||
records.extend(item for item in payload.get("records") or [] if isinstance(item, dict))
|
||||
write_manifest(args.output / "selector-records.jsonl", records)
|
||||
write_json(args.output / "summary.json", {
|
||||
"schema": "cadfs_to_cdsl.selector_candidate_geometry_probe_summary.v2",
|
||||
"sample_count": len(reports),
|
||||
"strict_selector_record_count": len(records),
|
||||
"reports": reports,
|
||||
})
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user