Files
cdsl-cad/cadfs_to_cdsl/selector_candidate_demo.py
T
likang 994d06aaea feat(selector): 增加离线候选遍历与严格回放验证 Demo
- 新增 selector_candidate_demo,移除 provenance intent 后枚举候选 selector
- 对候选分支执行有界重建与严格 STEP 比较
- 仅在候选遍历完整且唯一 strict 通过时生成 selector 映射记录
- 增加 selector 候选搜索、预算限制和记录生成的测试
- 保持生产 selector resolver 不受 Demo 逻辑影响
- 更新 CADFS 能力台账,记录 IMPRINT 派生 profile 的 lineage selector 缺口
2026-09-10 15:12:57 +08:00

898 lines
38 KiB
Python

"""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())