Files
cdsl-cad/cadfs_to_cdsl/selector_candidate_demo.py

1543 lines
66 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 math
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):
removed_intent = False
for key in ("selector_intent", "selector_intent_version"):
if key in value:
value.pop(key)
removed += 1
removed_intent = True
# Feature output roles are semantic builder evidence, just like a
# selector intent. The geometry-only probe must not leave an output
# role in place after discarding the evidence that validates it.
if removed_intent:
for key in ("output_role", "output_role_source"):
if key in value:
value.pop(key)
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[Any, ...], 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"))
_SEARCH_TOKEN = "_selector_search_token"
def _selector_search_token(feature_id: str, location: str) -> str:
"""Use a structural token while a group expands a public selector list."""
return f"{feature_id}:{location}"
def _annotate_selector_search_tokens(cdsl: dict[str, Any]) -> int:
"""Mark copied diagnostic selectors without creating a durable CDSL field."""
count = 0
for feature in cdsl.get("features") or []:
if not isinstance(feature, dict) or not isinstance(feature.get("id"), str):
continue
for location, selector in _selector_sites(feature):
selector[_SEARCH_TOKEN] = _selector_search_token(feature["id"], location)
count += 1
return count
def _strip_selector_search_tokens(value: Any) -> int:
"""Remove experiment-only branch tokens before a CDSL is persisted."""
removed = 0
if isinstance(value, dict):
if _SEARCH_TOKEN in value:
value.pop(_SEARCH_TOKEN)
removed += 1
for child in value.values():
removed += _strip_selector_search_tokens(child)
elif isinstance(value, list):
for child in value:
removed += _strip_selector_search_tokens(child)
return removed
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,
query_groups: list[dict[str, Any]] | None = None,
candidate_replay_kind: str = "geometry_probe",
) -> 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)
_strip_selector_search_tokens(compiled_candidate)
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"
verification = {
"compiled_cdsl_strict": True,
"compiled_cdsl": str(compiled_path),
}
if candidate_replay_kind == "geometry_probe":
verification["geometry_probe_strict"] = True
else:
verification["selector_search_branch_strict"] = True
record_payload: dict[str, Any] = {
"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": verification,
"records": records,
}
if query_groups:
# These are the query-to-record *sets* selected by the offline search.
# A FeatureScript query is not necessarily a single OCC record.
record_payload["query_groups"] = deepcopy(query_groups)
write_json(record_path, record_payload)
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_group(
cdsl: dict[str, Any],
feature_id: str,
token: str,
selectors: list[dict[str, Any]],
) -> None:
"""Expand one selector root to an explicit branch-local record set."""
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}")
roots = feature.get("selectors") or []
index = next(
(
item_index
for item_index, selector in enumerate(roots)
if isinstance(selector, dict) and selector.get(_SEARCH_TOKEN) == token
),
None,
)
if index is not None:
roots[index:index + 1] = [deepcopy(selector) for selector in selectors]
feature["selectors"] = roots
return
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) and reference.get(_SEARCH_TOKEN) == token:
if len(selectors) != 1:
raise ValueError("search group cannot expand an extent reference")
condition["reference"] = deepcopy(selectors[0])
return
raise ValueError(f"search branch selector token is unavailable: {feature_id}:{token}")
def _forced_candidate_selectors(placeholder: dict[str, Any], candidates: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Turn a query's runtime record set into explicit branch-local roots."""
from .selector_binding import _bound_selector
selectors: list[dict[str, Any]] = []
for candidate in candidates:
selector, _bound = _bound_selector(placeholder, [candidate])
if not isinstance(selector, dict):
raise ValueError("search branch produced an invalid explicit selector")
if placeholder.get(_SEARCH_TOKEN) is not None:
selector[_SEARCH_TOKEN] = placeholder[_SEARCH_TOKEN]
selectors.append(selector)
if not selectors:
raise ValueError("search group has no runtime records")
return selectors
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 _vector(value: Any) -> list[float] | None:
if not isinstance(value, (list, tuple)) or len(value) != 3:
return None
try:
result = [float(component) for component in value]
except (TypeError, ValueError):
return None
return result if all(math.isfinite(component) for component in result) else None
def _sub(left: list[float], right: list[float]) -> list[float]:
return [left[index] - right[index] for index in range(3)]
def _dot(left: list[float], right: list[float]) -> float:
return sum(left[index] * right[index] for index in range(3))
def _cross(left: list[float], right: list[float]) -> list[float]:
return [
left[1] * right[2] - left[2] * right[1],
left[2] * right[0] - left[0] * right[2],
left[0] * right[1] - left[1] * right[0],
]
def _norm(value: list[float]) -> float:
return math.sqrt(_dot(value, value))
def _unit(value: list[float] | None) -> list[float] | None:
if value is None or len(value) != 3:
return None
length = _norm(value)
if length <= 1e-9:
return None
return [component / length for component in value]
def _distance(left: list[float], right: list[float]) -> float:
return _norm(_sub(left, right))
def _score_distance(distance_mm: float, *, tolerance_mm: float = 0.05) -> float:
return max(0.0, 1.0 - distance_mm / tolerance_mm)
def _source_selector_map(provenance_candidate: dict[str, Any]) -> dict[str, dict[str, Any]]:
"""Index source selectors by an experiment-only structural token."""
selectors: dict[str, dict[str, Any]] = {}
for feature in provenance_candidate.get("features") or []:
if not isinstance(feature, dict) or not isinstance(feature.get("id"), str):
continue
for location, selector in _selector_sites(feature):
if isinstance(selector.get("selector_intent"), dict):
selectors[_selector_search_token(feature["id"], location)] = deepcopy(selector)
return selectors
def _source_feature(provenance_candidate: dict[str, Any], feature_id: Any) -> dict[str, Any] | None:
if not isinstance(feature_id, str):
return None
return next(
(
feature
for feature in provenance_candidate.get("features") or []
if isinstance(feature, dict) and feature.get("id") == feature_id
),
None,
)
def _source_sketch(provenance_candidate: dict[str, Any], producer: dict[str, Any], source_sketch_id: str | None) -> dict[str, Any] | None:
sketches = (provenance_candidate.get("geometry") or {}).get("sketches") or []
sketch_id = producer.get("sketch_id")
if isinstance(sketch_id, str):
sketch = next((item for item in sketches if isinstance(item, dict) and item.get("id") == sketch_id), None)
if isinstance(sketch, dict):
return sketch
if isinstance(source_sketch_id, str):
sketch = next(
(
item
for item in sketches
if isinstance(item, dict) and item.get("source_sketch_id") == source_sketch_id
),
None,
)
if isinstance(sketch, dict):
return sketch
return None
def _source_curve(sketch: dict[str, Any], entity_id: str) -> dict[str, Any] | None:
profile = sketch.get("profile") or {}
for entry in profile.get("source_entities") or []:
if isinstance(entry, dict) and entry.get("id") == entity_id and isinstance(entry.get("curve"), dict):
return entry["curve"]
for contour in profile.get("contours") or []:
if not isinstance(contour, dict):
continue
for segment in contour.get("segments") or []:
if isinstance(segment, dict) and segment.get("source_entity_id") == entity_id:
return segment
for segment in profile.get("construction") or []:
if isinstance(segment, dict) and segment.get("source_entity_id") == entity_id:
return segment
return None
def _producer_frame(
provenance_candidate: dict[str, Any],
source_selector: dict[str, Any],
) -> tuple[dict[str, Any], dict[str, Any], list[float], float] | None:
"""Recover only the direct sketch/extrude frame needed for candidate rank."""
intent = source_selector.get("selector_intent") or {}
source_entity = intent.get("source_entity") if isinstance(intent.get("source_entity"), dict) else None
source_entities = intent.get("source_entities") or []
source_sketch_id = (
source_entity.get("sketch_id")
if isinstance(source_entity, dict)
else next(
(
item.get("sketch_id")
for item in source_entities
if isinstance(item, dict) and isinstance(item.get("sketch_id"), str)
),
None,
)
)
producer = _source_feature(provenance_candidate, source_selector.get("owner_feature_id"))
if producer is None or not str(producer.get("atomic_id") or "").startswith("extrude_"):
return None
sketch = _source_sketch(provenance_candidate, producer, source_sketch_id)
if sketch is None:
return None
plane = sketch.get("workplane") or {}
origin = _vector(plane.get("origin_mm"))
normal = _unit(_vector(plane.get("normal")) or [])
if origin is None or normal is None:
return None
params = producer.get("params") or {}
try:
span = abs(float(params["distance_mm"]))
except (KeyError, TypeError, ValueError):
return None
if span <= 1e-9:
return None
if bool(params.get("reverse") or params.get("opposite_direction")):
normal = [-component for component in normal]
return producer, sketch, normal, span
def _world_point(sketch: dict[str, Any], point: Any) -> list[float] | None:
local = point if isinstance(point, (list, tuple)) and len(point) in {2, 3} else None
if local is None:
return None
try:
coordinates = [float(value) for value in local]
except (TypeError, ValueError):
return None
plane = sketch.get("workplane") or {}
origin = _vector(plane.get("origin_mm"))
x_axis = _unit(_vector(plane.get("x_dir")) or [])
normal = _unit(_vector(plane.get("normal")) or [])
if origin is None or x_axis is None or normal is None:
return None
y_axis = _unit(_cross(normal, x_axis))
if y_axis is None:
return None
z = coordinates[2] if len(coordinates) == 3 else 0.0
return [
origin[index] + x_axis[index] * coordinates[0] + y_axis[index] * coordinates[1] + normal[index] * z
for index in range(3)
]
def _source_entity_ids(source_selector: dict[str, Any]) -> list[str]:
intent = source_selector.get("selector_intent") or {}
entity = intent.get("source_entity")
if isinstance(entity, dict) and isinstance(entity.get("entity_id"), str):
return [entity["entity_id"]]
return sorted({
item["entity_id"]
for item in intent.get("source_entities") or []
if isinstance(item, dict) and isinstance(item.get("entity_id"), str)
})
def _cap_role_hint(source_selector: dict[str, Any]) -> str | None:
intent = source_selector.get("selector_intent") or {}
role = source_selector.get("output_role") or intent.get("output_role")
if isinstance(role, str) and role in {"extrude.start", "extrude.end"}:
return role.rsplit(".", 1)[1]
ast = ((intent.get("source_query") or {}).get("ast") or {})
args = ast.get("args") if isinstance(ast, dict) else None
options = args[3] if isinstance(args, list) and len(args) >= 4 and isinstance(args[3], dict) else {}
is_start = options.get("isStart")
if is_start in {True, "true"}:
return "start"
if is_start in {False, "false"}:
return "end"
return None
def _query_group_descriptor(token: str, source_selector: dict[str, Any]) -> dict[str, Any]:
intent = source_selector.get("selector_intent") or {}
policy = intent.get("derivation_policy") or {}
return {
"token": token,
"feature_id": token.split(":", 1)[0],
"source_location": token.split(":", 1)[1] if ":" in token else token,
"kind": source_selector.get("kind"),
"owner_feature_id": source_selector.get("owner_feature_id"),
"query_family": intent.get("query_family"),
"multiplicity": policy.get("multiplicity", "one"),
"source_entity_ids": _source_entity_ids(source_selector),
"source_make_query": deepcopy((intent.get("source_query") or {}).get("ast")),
}
def _rank_cap_face(
provenance_candidate: dict[str, Any],
source_selector: dict[str, Any],
candidate: dict[str, Any],
) -> tuple[float, dict[str, Any]] | None:
frame = _producer_frame(provenance_candidate, source_selector)
role = _cap_role_hint(source_selector)
if frame is None or role is None:
return None
_producer, sketch, direction, span = frame
origin = _vector((sketch.get("workplane") or {}).get("origin_mm"))
geometry = candidate.get("geometry") or {}
actual_normal = _unit(_vector(geometry.get("plane_normal") or geometry.get("normal")) or [])
if origin is None or actual_normal is None or geometry.get("surface_type") != "plane":
return None
point = origin if role == "start" else [origin[index] + direction[index] * span for index in range(3)]
expected_offset = _dot(direction, point)
actual_offset = geometry.get("plane_offset_mm")
try:
actual_offset = float(actual_offset)
except (TypeError, ValueError):
return None
alignment = abs(_dot(direction, actual_normal))
signed_offset = actual_offset if _dot(direction, actual_normal) >= 0 else -actual_offset
offset_error = abs(expected_offset - signed_offset)
return (alignment + _score_distance(offset_error)) / 2.0, {
"method": "extrude_cap_plane",
"cap_role": role,
"normal_alignment": round(alignment, 6),
"plane_offset_error_mm": round(offset_error, 9),
}
def _rank_swept_face(
provenance_candidate: dict[str, Any],
source_selector: dict[str, Any],
candidate: dict[str, Any],
) -> tuple[float, dict[str, Any]] | None:
frame = _producer_frame(provenance_candidate, source_selector)
entity_ids = _source_entity_ids(source_selector)
if frame is None or len(entity_ids) != 1:
return None
_producer, sketch, direction, _span = frame
curve = _source_curve(sketch, entity_ids[0])
geometry = candidate.get("geometry") or {}
if not isinstance(curve, dict):
return None
if curve.get("type") == "line":
start, end = _world_point(sketch, curve.get("start")), _world_point(sketch, curve.get("end"))
actual_normal = _unit(_vector(geometry.get("plane_normal") or geometry.get("normal")) or [])
if start is None or end is None or actual_normal is None or geometry.get("surface_type") != "plane":
return None
expected_normal = _unit(_cross(_sub(end, start), direction))
if expected_normal is None:
return None
try:
actual_offset = float(geometry["plane_offset_mm"])
except (KeyError, TypeError, ValueError):
return None
alignment = abs(_dot(expected_normal, actual_normal))
expected_offset = _dot(expected_normal, start)
signed_offset = actual_offset if _dot(expected_normal, actual_normal) >= 0 else -actual_offset
offset_error = abs(expected_offset - signed_offset)
return (alignment + _score_distance(offset_error)) / 2.0, {
"method": "source_line_supporting_plane",
"normal_alignment": round(alignment, 6),
"plane_offset_error_mm": round(offset_error, 9),
}
if curve.get("type") not in {"arc", "circle"}:
return None
center = _world_point(sketch, curve.get("center"))
actual_origin = _vector(geometry.get("axis_origin_mm"))
actual_direction = _unit(_vector(geometry.get("axis_direction")) or [])
try:
radius_error = abs(float(curve["radius_mm"]) - float(geometry["radius_mm"]))
except (KeyError, TypeError, ValueError):
return None
if center is None or actual_origin is None or actual_direction is None or geometry.get("surface_type") != "cylinder":
return None
alignment = abs(_dot(direction, actual_direction))
radial_offset = _norm(_cross(_sub(center, actual_origin), actual_direction))
return (
alignment + _score_distance(radial_offset) + _score_distance(radius_error)
) / 3.0, {
"method": "source_arc_cylinder",
"axis_alignment": round(alignment, 6),
"axis_offset_mm": round(radial_offset, 9),
"radius_error_mm": round(radius_error, 9),
}
def _point2(value: Any) -> tuple[float, float] | None:
if not isinstance(value, (list, tuple)) or len(value) < 2:
return None
try:
point = float(value[0]), float(value[1])
except (TypeError, ValueError):
return None
return point if all(math.isfinite(component) for component in point) else None
def _curve_contains_point(curve: dict[str, Any], point: tuple[float, float], *, tolerance_mm: float = 0.05) -> bool:
curve_type = curve.get("type")
if curve_type == "line":
start, end = _point2(curve.get("start")), _point2(curve.get("end"))
if start is None or end is None:
return False
direction = end[0] - start[0], end[1] - start[1]
length = math.hypot(*direction)
if length <= 1e-9:
return False
offset = point[0] - start[0], point[1] - start[1]
distance = abs(direction[0] * offset[1] - direction[1] * offset[0]) / length
parameter = (direction[0] * offset[0] + direction[1] * offset[1]) / (length * length)
return distance <= tolerance_mm and -tolerance_mm / length <= parameter <= 1.0 + tolerance_mm / length
center = _point2(curve.get("center"))
try:
radius = float(curve["radius_mm"])
except (KeyError, TypeError, ValueError):
return False
if center is None or radius <= 1e-9 or abs(math.dist(center, point) - radius) > tolerance_mm:
return False
if curve_type == "circle":
return True
if curve_type != "arc":
return False
start, end = _point2(curve.get("start")), _point2(curve.get("end"))
if start is None or end is None:
return False
angle = math.atan2(point[1] - center[1], point[0] - center[0])
start_angle = math.atan2(start[1] - center[1], start[0] - center[0])
end_angle = math.atan2(end[1] - center[1], end[0] - center[0])
full_turn = 2.0 * math.pi
if bool(curve.get("clockwise")):
travelled = (start_angle - angle) % full_turn
total = (start_angle - end_angle) % full_turn
else:
travelled = (angle - start_angle) % full_turn
total = (end_angle - start_angle) % full_turn
angular_tolerance = tolerance_mm / radius
return travelled <= total + angular_tolerance
def _curve_intersections_2d(left: dict[str, Any], right: dict[str, Any]) -> list[tuple[float, float]]:
"""Intersect line/arc/circle supports, retaining only source curve spans."""
left_type, right_type = left.get("type"), right.get("type")
points: list[tuple[float, float]] = []
if left_type == "line" and right_type == "line":
left_start, left_end = _point2(left.get("start")), _point2(left.get("end"))
right_start, right_end = _point2(right.get("start")), _point2(right.get("end"))
if None not in (left_start, left_end, right_start, right_end):
left_dx, left_dy = left_end[0] - left_start[0], left_end[1] - left_start[1]
right_dx, right_dy = right_end[0] - right_start[0], right_end[1] - right_start[1]
determinant = left_dx * right_dy - left_dy * right_dx
if abs(determinant) > 1e-9:
offset_x, offset_y = right_start[0] - left_start[0], right_start[1] - left_start[1]
parameter = (offset_x * right_dy - offset_y * right_dx) / determinant
points.append((left_start[0] + parameter * left_dx, left_start[1] + parameter * left_dy))
else:
line, circular = (left, right) if left_type == "line" else (right, left)
if line.get("type") == "line" and circular.get("type") in {"arc", "circle"}:
start, end, center = _point2(line.get("start")), _point2(line.get("end")), _point2(circular.get("center"))
try:
radius = float(circular["radius_mm"])
except (KeyError, TypeError, ValueError):
radius = 0.0
if start is not None and end is not None and center is not None and radius > 1e-9:
dx, dy = end[0] - start[0], end[1] - start[1]
ox, oy = start[0] - center[0], start[1] - center[1]
quadratic_a = dx * dx + dy * dy
quadratic_b = 2.0 * (dx * ox + dy * oy)
quadratic_c = ox * ox + oy * oy - radius * radius
discriminant = quadratic_b * quadratic_b - 4.0 * quadratic_a * quadratic_c
if quadratic_a > 1e-12 and discriminant >= -1e-9:
root = math.sqrt(max(0.0, discriminant))
for parameter in ((-quadratic_b - root) / (2.0 * quadratic_a), (-quadratic_b + root) / (2.0 * quadratic_a)):
points.append((start[0] + parameter * dx, start[1] + parameter * dy))
elif left_type in {"arc", "circle"} and right_type in {"arc", "circle"}:
left_center, right_center = _point2(left.get("center")), _point2(right.get("center"))
try:
left_radius, right_radius = float(left["radius_mm"]), float(right["radius_mm"])
except (KeyError, TypeError, ValueError):
left_radius = right_radius = 0.0
if left_center is not None and right_center is not None and left_radius > 1e-9 and right_radius > 1e-9:
dx, dy = right_center[0] - left_center[0], right_center[1] - left_center[1]
center_distance = math.hypot(dx, dy)
if 1e-9 < center_distance <= left_radius + right_radius + 0.05:
along = (left_radius * left_radius - right_radius * right_radius + center_distance * center_distance) / (2.0 * center_distance)
height_squared = left_radius * left_radius - along * along
if height_squared >= -1e-9:
height = math.sqrt(max(0.0, height_squared))
base_x, base_y = left_center[0] + along * dx / center_distance, left_center[1] + along * dy / center_distance
offset_x, offset_y = -dy * height / center_distance, dx * height / center_distance
points.extend(((base_x + offset_x, base_y + offset_y), (base_x - offset_x, base_y - offset_y)))
valid: list[tuple[float, float]] = []
for point in points:
if not _curve_contains_point(left, point) or not _curve_contains_point(right, point):
continue
if not any(math.dist(point, existing) <= 1e-6 for existing in valid):
valid.append(point)
return valid
def _source_curve_intersection(sketch: dict[str, Any], entity_ids: list[str]) -> list[float] | None:
if len(entity_ids) != 2:
return None
curves = [_source_curve(sketch, entity_id) for entity_id in entity_ids]
if any(not isinstance(curve, dict) for curve in curves):
return None
intersections = _curve_intersections_2d(curves[0], curves[1])
if len(intersections) != 1:
return None
return _world_point(sketch, intersections[0])
def _rank_swept_edge(
provenance_candidate: dict[str, Any],
source_selector: dict[str, Any],
candidate: dict[str, Any],
) -> tuple[float, dict[str, Any]] | None:
frame = _producer_frame(provenance_candidate, source_selector)
entity_ids = _source_entity_ids(source_selector)
if frame is None:
return None
_producer, sketch, direction, _span = frame
source_point = _source_curve_intersection(sketch, entity_ids)
geometry = candidate.get("geometry") or {}
start, end = _vector(geometry.get("start_mm")), _vector(geometry.get("end_mm"))
if source_point is None or start is None or end is None or geometry.get("curve_type") != "line":
return None
edge_direction = _unit(_sub(end, start))
if edge_direction is None:
return None
alignment = abs(_dot(direction, edge_direction))
supporting_line_error = _norm(_cross(_sub(source_point, start), direction))
return (alignment + _score_distance(supporting_line_error)) / 2.0, {
"method": "source_pair_extrusion_vertex",
"axis_alignment": round(alignment, 6),
"source_vertex_offset_mm": round(supporting_line_error, 9),
}
def _query_candidate_groups(
provenance_candidate: dict[str, Any],
token: str,
source_selector: dict[str, Any] | None,
resolution: Any,
*,
maximum: int,
) -> tuple[dict[str, Any], list[dict[str, Any]], int]:
"""Rank branch alternatives as FeatureScript query result sets.
This is deliberately an offline geometric heuristic. It gets one chance
to reduce a query to record *sets*, then final STEP strict replay decides
whether that set is useful. It never adds a geometry relation to the
production selector graph.
"""
normal_candidates, normal_count = _search_candidate_records(resolution, maximum=maximum)
if not isinstance(source_selector, dict):
descriptor = {"token": token, "ranking_status": "source_query_missing"}
return descriptor, [
{"records": [candidate], "score": candidate["score"], "ranking": {"method": "resolver_candidate"}}
for candidate in normal_candidates
], normal_count
descriptor = _query_group_descriptor(token, source_selector)
family = descriptor.get("query_family")
ranker = {
"CAP_FACE": _rank_cap_face,
"SWEPT_FACE": _rank_swept_face,
"SWEPT_EDGE": _rank_swept_edge,
}.get(family)
if ranker is None:
descriptor["ranking_status"] = "query_family_not_ranked"
return descriptor, [
{"records": [candidate], "score": candidate["score"], "ranking": {"method": "resolver_candidate"}}
for candidate in normal_candidates
], normal_count
ranked: list[tuple[float, dict[str, Any], dict[str, Any]]] = []
for candidate in resolution.candidates:
if not isinstance(candidate, dict) or not candidate.get("record_id") or float(candidate.get("score") or 0.0) < 0.8:
continue
result = ranker(provenance_candidate, source_selector, candidate)
if result is None:
continue
score, reason = result
if score >= 0.98:
ranked.append((score, deepcopy(candidate), reason))
ranked.sort(key=lambda item: (-item[0], str(item[1].get("record_id"))))
if not ranked:
descriptor["ranking_status"] = "source_geometry_unavailable"
return descriptor, [
{"records": [candidate], "score": candidate["score"], "ranking": {"method": "resolver_candidate"}}
for candidate in normal_candidates
], normal_count
descriptor["ranking_status"] = "source_geometry_ranked"
descriptor["ranked_record_count"] = len(ranked)
if descriptor.get("multiplicity") == "all_fragments":
records = [candidate for _score, candidate, _reason in ranked]
descriptor["selected_set_cardinality"] = len(records)
return descriptor, [{
"records": records,
"score": round(sum(score for score, _candidate, _reason in ranked) / len(ranked), 6),
"ranking": {"method": "all_fragments_source_geometry", "records": [reason for _score, _candidate, reason in ranked]},
}], 1
groups = [
{"records": [candidate], "score": round(score, 6), "ranking": reason}
for score, candidate, reason in ranked[:maximum]
]
return descriptor, groups, len(ranked)
def _search_replay_worker(
candidate: str,
provenance: 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))
provenance_candidate = read_json(Path(provenance))
source_selectors = _source_selector_map(provenance_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":
token = placeholder.get(_SEARCH_TOKEN)
descriptor, candidate_groups, group_count = _query_candidate_groups(
provenance_candidate,
str(token or f"{node.feature_id}:{location}"),
source_selectors.get(str(token)),
resolution,
maximum=maximum_candidates,
)
if candidate_groups:
write_json(Path(result), {
"status": "branchable_query_group",
"feature_id": node.feature_id,
"source_location": location,
"selector_token": token,
"resolution_status": resolution.status,
"diagnostic": resolution.diagnostic.as_dict() if resolution.diagnostic is not None else None,
"query_group": descriptor,
"candidate_groups": candidate_groups,
"candidate_group_count": group_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,
provenance_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(provenance_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"),
"query_group": result.get("query_group"),
"candidate_group_count": result.get("candidate_group_count"),
"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)
search_candidate = deepcopy(geometry_candidate)
_annotate_selector_search_tokens(search_candidate)
provenance_path = directory / "provenance.candidate.cdsl.json"
pending: list[tuple[dict[str, Any], list[dict[str, Any]]]] = [(search_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,
provenance_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_query_group":
groups = [item for item in outcome.get("candidate_groups") or [] if isinstance(item, dict)]
total = int(outcome.get("candidate_group_count") or len(groups))
if total > len(groups):
candidates_truncated = True
branch_summaries.append(_public_branch_summary(outcome, choices))
for group in groups:
child = deepcopy(outcome["bound_cdsl"])
feature_id = str(outcome["feature_id"])
source_feature = next(item for item in child["features"] if item.get("id") == feature_id)
token = outcome.get("selector_token")
source_selector = next(
(
selector
for _location, selector in _searchable_selector_sites(source_feature)
if selector.get(_SEARCH_TOKEN) == token
),
None,
)
if not isinstance(source_selector, dict):
raise ValueError(f"search branch selector token is unavailable: {feature_id}:{token}")
records = [item for item in group.get("records") or [] if isinstance(item, dict)]
forced = _forced_candidate_selectors(source_selector, records)
_replace_selector_group(child, feature_id, str(token), forced)
pending.append((child, [*choices, {
"feature_id": feature_id,
"source_location": outcome.get("source_location"),
"query_group": deepcopy(outcome.get("query_group") or {}),
"resolved_runtime_records": records,
"score": group.get("score"),
"ranking": deepcopy(group.get("ranking") or {}),
}]))
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,
query_groups=choices,
candidate_replay_kind="selector_search_branch",
)
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())