feat(cadfs): expand selector provenance coverage

This commit is contained in:
2026-09-14 11:00:14 +08:00
parent 994d06aaea
commit de21ee8a5b
45 changed files with 19837 additions and 842 deletions
+699 -54
View File
@@ -13,6 +13,7 @@ from __future__ import annotations
import argparse
from copy import deepcopy
import json
import math
import multiprocessing
from pathlib import Path
from typing import Any
@@ -29,10 +30,19 @@ 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):
@@ -63,7 +73,7 @@ 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:
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)
@@ -134,6 +144,41 @@ 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.
@@ -270,6 +315,8 @@ def _emit_strict_selector_record(
*,
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):
@@ -286,6 +333,7 @@ def _emit_strict_selector_record(
}
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(
@@ -322,20 +370,29 @@ def _emit_strict_selector_record(
result["reason"] = "no_featurescript_selector_binding"
return result
record_path = directory / "selector-record.json"
write_json(record_path, {
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": {
"geometry_probe_strict": True,
"compiled_cdsl_strict": True,
"compiled_cdsl": str(compiled_path),
},
"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),
@@ -359,41 +416,58 @@ def _searchable_selector_sites(feature: dict[str, Any]) -> list[tuple[str, dict[
]
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."""
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}")
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)
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
prefix = "params."
suffix = ".reference"
if location.startswith(prefix) and location.endswith(suffix):
name = location[len(prefix):-len(suffix)]
for name in ("end_condition", "reverse_end_condition"):
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}")
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_selector(placeholder: dict[str, Any], candidate: dict[str, Any]) -> dict[str, Any]:
"""Turn one public runtime record into a branch-local explicit selector."""
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
selector, _bound = _bound_selector(placeholder, [candidate])
if not isinstance(selector, dict):
raise ValueError("search branch produced an invalid explicit selector")
return 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:
@@ -432,12 +506,552 @@ def _search_candidate_records(
return [deepcopy(item) for item in candidates[:maximum]], len(candidates)
def _search_replay_worker(candidate: str, step: str, result: str, maximum_candidates: int) -> None:
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)
@@ -485,19 +1099,25 @@ def _search_replay_worker(candidate: str, step: str, result: str, maximum_candid
active_body_id=active_body_id,
)
if resolution.status != "resolved":
candidates, eligible_count = _search_candidate_records(
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 candidates:
if candidate_groups:
write_json(Path(result), {
"status": "branchable_selector",
"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,
"candidates": candidates,
"candidate_count": eligible_count,
"query_group": descriptor,
"candidate_groups": candidate_groups,
"candidate_group_count": group_count,
"bound_cdsl": bound,
"selector_binding": evidence,
})
@@ -572,11 +1192,18 @@ def _search_replay_worker(candidate: str, step: str, result: str, maximum_candid
})
def _run_search_replay(candidate_path: Path, step: Path, *, timeout_seconds: float, maximum_candidates: int) -> dict[str, Any]:
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(step), str(worker), maximum_candidates),
(str(candidate_path), str(provenance_path), str(step), str(worker), maximum_candidates),
worker,
timeout_seconds,
)
@@ -596,6 +1223,8 @@ def _public_branch_summary(result: dict[str, Any], choices: list[dict[str, Any]]
"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,
@@ -630,7 +1259,10 @@ def _run_selector_search(
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), [])]
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]] = []
@@ -653,32 +1285,43 @@ def _run_selector_search(
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_selector":
candidates = list(outcome.get("candidates") or [])
total = int(outcome.get("candidate_count") or len(candidates))
if total > len(candidates):
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 candidate_record in candidates:
for group in groups:
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)
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": location,
"record_id": candidate_record.get("record_id"),
"score": candidate_record.get("score"),
"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
@@ -731,6 +1374,8 @@ def _run_selector_search(
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