Files
cdsl-cad/backend/app/cad_agent/domain/verifier_registry.py
T
2026-09-02 13:51:35 +08:00

898 lines
52 KiB
Python

"""Deterministic acceptance-claim registry and closed-schema validation."""
from __future__ import annotations
from dataclasses import dataclass
from copy import deepcopy
from math import atan2, isclose, pi, sqrt
from typing import Any, Callable
from jsonschema import Draft202012Validator
from jsonschema.exceptions import SchemaError
ClaimResult = dict[str, Any]
ClaimEvaluator = Callable[[dict[str, Any], dict[str, Any]], ClaimResult]
_DEFAULT_TOLERANCE_MM = 0.1
def _closed_object(properties: dict[str, Any], required: list[str]) -> dict[str, Any]:
return {"type": "object", "properties": properties, "required": required, "additionalProperties": False}
def _vector(value: Any) -> tuple[float, float, float] | None:
if not isinstance(value, list) or len(value) != 3:
return None
try:
return tuple(float(item) for item in value) # type: ignore[return-value]
except (TypeError, ValueError):
return None
def _cylinder_axis_point(record: dict[str, Any]) -> tuple[float, float, float] | None:
"""Return an actual point on a cylinder's axis, never a surface centroid."""
geometry = record.get("geometry") if isinstance(record.get("geometry"), dict) else {}
return _vector(geometry.get("axis_origin_mm")) or _vector(geometry.get("center_mm"))
def _cylinder_axis_direction(record: dict[str, Any]) -> tuple[float, float, float] | None:
geometry = record.get("geometry") if isinstance(record.get("geometry"), dict) else {}
direction = _vector(geometry.get("axis_direction"))
if direction is None:
return None
length = sqrt(sum(component * component for component in direction))
return tuple(component / length for component in direction) if length > 1e-9 else None
def _cylinder_axis_span(record: dict[str, Any]) -> float | None:
"""Return the axial span of a cylinder face from its B-rep bounding box."""
interval = _cylinder_axis_interval(record)
return None if interval is None else interval[1] - interval[0]
def _cylinder_axis_interval(record: dict[str, Any]) -> tuple[float, float] | None:
"""Return the inclusive axial interval of a cylindrical face's bounds.
B-rep face orientation is not a physical property of a cylindrical shell.
In particular, a two-sided extrusion can return its two half-walls with
opposite axis directions. Use one canonical direction for an undirected
cylinder axis so those halves share the same coordinate interval.
"""
geometry = record.get("geometry") if isinstance(record.get("geometry"), dict) else {}
bbox = geometry.get("bbox_mm")
direction = _cylinder_axis_direction(record)
if not isinstance(bbox, list) or len(bbox) != 6 or direction is None:
return None
try:
minimum = tuple(float(value) for value in bbox[:3])
maximum = tuple(float(value) for value in bbox[3:])
except (TypeError, ValueError):
return None
dominant_index = max(range(3), key=lambda index: abs(direction[index]))
if direction[dominant_index] < 0:
direction = tuple(-component for component in direction)
projections = [
sum(direction[index] * point[index] for index in range(3))
for point in (
(x, y, z)
for x in (minimum[0], maximum[0])
for y in (minimum[1], maximum[1])
for z in (minimum[2], maximum[2])
)
]
return min(projections), max(projections)
def _records(facts: dict[str, Any]) -> list[dict[str, Any]]:
topology = facts.get("topology") if isinstance(facts.get("topology"), dict) else {}
return [item for item in topology.get("records") or () if isinstance(item, dict)]
def _pass(evidence: dict[str, Any]) -> ClaimResult:
return {"status": "pass", "evidence": evidence}
def _fail(evidence: dict[str, Any]) -> ClaimResult:
return {"status": "fail", "evidence": evidence}
def _pending(reason: str) -> ClaimResult:
return {"status": "pending", "evidence": {"reason": reason}}
def _solid_count(expected: dict[str, Any], facts: dict[str, Any]) -> ClaimResult:
health = facts.get("health") if isinstance(facts.get("health"), dict) else {}
actual = health.get("solid_count")
if not isinstance(actual, int):
return _pending("rebuild report has no solid_count")
return _pass({"solid_count": actual}) if actual == expected["value"] else _fail({"expected": expected["value"], "actual": actual})
def _volume_decreased(_expected: dict[str, Any], facts: dict[str, Any]) -> ClaimResult:
"""Prove that a subtractive operation removed a measurable amount of material."""
health = facts.get("health") if isinstance(facts.get("health"), dict) else {}
parent_health = facts.get("parent_health") if isinstance(facts.get("parent_health"), dict) else {}
parent_volume = parent_health.get("volume_mm3")
candidate_volume = health.get("volume_mm3")
if not isinstance(parent_volume, (int, float)) or not isinstance(candidate_volume, (int, float)):
return _pending("parent or candidate rebuild report has no measurable volume")
removed = float(parent_volume) - float(candidate_volume)
evidence = {
"parent_volume_mm3": float(parent_volume),
"candidate_volume_mm3": float(candidate_volume),
"removed_volume_mm3": removed,
}
return _pass(evidence) if removed > 1e-6 else _fail(evidence)
def _bbox(expected: dict[str, Any], facts: dict[str, Any]) -> ClaimResult:
health = facts.get("health") if isinstance(facts.get("health"), dict) else {}
dimensions = ((health.get("bbox_mm") or {}).get("dimensions") if isinstance(health.get("bbox_mm"), dict) else None)
axis = {"x": 0, "y": 1, "z": 2}[expected["axis"]]
if not isinstance(dimensions, list) or len(dimensions) != 3:
return _pending("rebuild report has no bounding-box dimensions")
actual = float(dimensions[axis])
tolerance = float(expected["tolerance_mm"])
return _pass({"axis": expected["axis"], "actual_mm": actual, "tolerance_mm": tolerance}) if abs(actual - expected["value"]) <= tolerance else _fail({"axis": expected["axis"], "expected_mm": expected["value"], "actual_mm": actual, "tolerance_mm": tolerance})
def _bbox_rank(expected: dict[str, Any], facts: dict[str, Any]) -> ClaimResult:
"""Measure a principal extent without assuming the author's world axes."""
health = facts.get("health") if isinstance(facts.get("health"), dict) else {}
dimensions = ((health.get("bbox_mm") or {}).get("dimensions") if isinstance(health.get("bbox_mm"), dict) else None)
if not isinstance(dimensions, list) or len(dimensions) != 3:
return _pending("rebuild report has no bounding-box dimensions")
try:
ranked = sorted(float(value) for value in dimensions)
except (TypeError, ValueError):
return _pending("rebuild report has invalid bounding-box dimensions")
index = {"minimum": 0, "median": 1, "maximum": 2}[expected["rank"]]
actual = ranked[index]
tolerance = float(expected["tolerance_mm"])
return _pass({"rank": expected["rank"], "actual_mm": actual, "tolerance_mm": tolerance}) if abs(actual - expected["value"]) <= tolerance else _fail({"rank": expected["rank"], "expected_mm": expected["value"], "actual_mm": actual, "tolerance_mm": tolerance})
def _matching_cylinders(expected: dict[str, Any], facts: dict[str, Any]) -> list[dict[str, Any]]:
results: list[dict[str, Any]] = []
diameter = float(expected["diameter_mm"])
tolerance = float(expected.get("tolerance_mm", 0.01))
for record in _records(facts):
geometry = record.get("geometry") if isinstance(record.get("geometry"), dict) else {}
if geometry.get("surface_type") != "cylinder":
continue
# A cylinder bounded by two opposing planes can be either an external
# wall or an internal bore. Modern topology snapshots make this
# distinction explicit from B-rep face orientation. Do not accept an
# unknown/export-incomplete cylinder as a bore: that would reintroduce
# the false positive this verifier exists to prevent.
if geometry.get("cylinder_role") != "inner":
continue
radius = geometry.get("radius_mm")
if isinstance(radius, (int, float)) and abs(float(radius) * 2 - diameter) <= tolerance:
results.append(record)
by_id = {
str(record.get("record_id") or ""): record
for record in _records(facts)
if isinstance(record.get("record_id"), str)
}
return [
_merged_inner_cylindrical_shell(group, by_id)
for group in _cylindrical_shells(results, tolerance)
]
def _matching_outer_cylinders(expected: dict[str, Any], facts: dict[str, Any]) -> list[dict[str, Any]]:
results: list[dict[str, Any]] = []
diameter = float(expected["diameter_mm"])
tolerance = float(expected.get("tolerance_mm", 0.01))
for record in _records(facts):
geometry = record.get("geometry") if isinstance(record.get("geometry"), dict) else {}
if geometry.get("surface_type") != "cylinder" or geometry.get("cylinder_role") != "outer":
continue
radius = geometry.get("radius_mm")
if isinstance(radius, (int, float)) and abs(float(radius) * 2 - diameter) <= tolerance:
results.append(record)
return results
def _same_cylindrical_shell(left: dict[str, Any], right: dict[str, Any], tolerance: float) -> bool:
"""Identify B-rep patches that represent one continuous cylindrical shell.
OpenCascade can split an extruded circular wall into four periodic-face
patches. Those patches have the same axis/radius and axial interval, but
are not four physical outer diameters. Conversely, equal-diameter shaft
sections separated along their axis must remain distinct claim instances.
"""
left_axis = _cylinder_axis_direction(left)
right_axis = _cylinder_axis_direction(right)
left_origin = _cylinder_axis_point(left)
right_origin = _cylinder_axis_point(right)
left_interval = _cylinder_axis_interval(left)
right_interval = _cylinder_axis_interval(right)
if None in {left_axis, right_axis, left_origin, right_origin, left_interval, right_interval}:
return False
assert left_axis is not None and right_axis is not None
assert left_origin is not None and right_origin is not None
assert left_interval is not None and right_interval is not None
alignment = abs(sum(left_axis[index] * right_axis[index] for index in range(3)))
if alignment < 1.0 - 1e-6:
return False
delta = tuple(right_origin[index] - left_origin[index] for index in range(3))
axial = sum(delta[index] * left_axis[index] for index in range(3))
radial_offset = sqrt(sum((delta[index] - axial * left_axis[index]) ** 2 for index in range(3)))
if radial_offset > tolerance:
return False
return left_interval[0] <= right_interval[1] + tolerance and right_interval[0] <= left_interval[1] + tolerance
def _cylindrical_shells(records: list[dict[str, Any]], tolerance: float) -> list[list[dict[str, Any]]]:
"""Group periodic B-rep cylinder patches into physical cylindrical shells."""
groups: list[list[dict[str, Any]]] = []
for record in records:
overlapping = [index for index, group in enumerate(groups) if any(_same_cylindrical_shell(record, member, tolerance) for member in group)]
if not overlapping:
groups.append([record])
continue
first = overlapping[0]
groups[first].append(record)
for index in reversed(overlapping[1:]):
groups[first].extend(groups.pop(index))
return groups
def _merged_inner_cylindrical_shell(
members: list[dict[str, Any]],
records_by_id: dict[str, dict[str, Any]],
) -> dict[str, Any]:
"""Build one verifier-facing bore record from periodic inner-face patches.
Extruding an analytic circle can produce several cylindrical B-rep faces.
Individual patches touch only one end plane, while the union represents a
through bore. Keep topology records raw for selectors, but aggregate the
measured surface for acceptance claims.
"""
representative = members[0]
geometry = representative.get("geometry") if isinstance(representative.get("geometry"), dict) else {}
merged_geometry = dict(geometry)
member_ids = [str(member.get("record_id") or "") for member in members if member.get("record_id")]
connected_plane_ids = list(dict.fromkeys(
str(plane_id)
for member in members
for plane_id in ((member.get("geometry") or {}).get("connected_plane_ids") or ())
if isinstance(plane_id, str)
))
merged_geometry["member_record_ids"] = member_ids
merged_geometry["connected_plane_ids"] = connected_plane_ids
axis = _cylinder_axis_direction(representative)
merged_geometry["through"] = bool(geometry.get("through")) or _planes_prove_through(
connected_plane_ids,
axis,
records_by_id,
)
return {**representative, "geometry": merged_geometry}
def _planes_prove_through(
plane_ids: list[str],
axis: tuple[float, float, float] | None,
records_by_id: dict[str, dict[str, Any]],
) -> bool:
if axis is None:
return False
planes = [
records_by_id[record_id].get("geometry")
for record_id in plane_ids
if isinstance(records_by_id.get(record_id), dict)
and isinstance(records_by_id[record_id].get("geometry"), dict)
and records_by_id[record_id]["geometry"].get("surface_type") == "plane"
]
for first_index, first in enumerate(planes):
if not isinstance(first, dict):
continue
first_normal = _vector(first.get("normal"))
if first_normal is None:
continue
for second in planes[first_index + 1:]:
if not isinstance(second, dict):
continue
second_normal = _vector(second.get("normal"))
if second_normal is None:
continue
opposite = sum(first_normal[index] * second_normal[index] for index in range(3)) <= -0.99
axial = all(abs(sum(normal[index] * axis[index] for index in range(3))) >= 0.99 for normal in (first_normal, second_normal))
if opposite and axial:
return True
return False
def _outer_cylinder_shells(expected: dict[str, Any], facts: dict[str, Any]) -> list[list[dict[str, Any]]]:
"""Coalesce periodic B-rep patches into physical outer-cylinder shells."""
matched = _matching_outer_cylinders(expected, facts)
tolerance = float(expected.get("tolerance_mm", 0.01))
return _cylindrical_shells(matched, tolerance)
def _matching_cones(expected: dict[str, Any], facts: dict[str, Any]) -> list[dict[str, Any]]:
results: list[dict[str, Any]] = []
small = float(expected["small_diameter_mm"])
large = float(expected["large_diameter_mm"])
included_angle = float(expected["included_angle_deg"])
tolerance = float(expected.get("tolerance_mm", 0.01))
for record in _records(facts):
geometry = record.get("geometry") if isinstance(record.get("geometry"), dict) else {}
if geometry.get("surface_type") != "cone" or geometry.get("cylinder_role") != "inner":
continue
radii = geometry.get("boundary_radii_mm")
angle = geometry.get("semi_angle_deg")
if not isinstance(radii, list) or not isinstance(angle, (int, float)):
continue
values = [float(value) for value in radii if isinstance(value, (int, float))]
if not values:
continue
if (
abs(min(values) * 2 - small) <= tolerance
and abs(max(values) * 2 - large) <= tolerance
and abs(abs(float(angle)) * 2 - included_angle) <= 1e-4
):
results.append(record)
return results
def _cylindrical_bore(expected: dict[str, Any], facts: dict[str, Any]) -> ClaimResult:
matched = _matching_cylinders(expected, facts)
count = int(expected.get("count", 1))
if not _records(facts):
return _pending("topology is unavailable")
if not matched:
return _pending("the target bore has not been introduced at this checkpoint")
if len(matched) < count:
return _pending("the target bore pattern is incomplete at this checkpoint")
return _pass({"matched_cylindrical_faces": [item.get("record_id") for item in matched]}) if len(matched) == count else _fail({"expected_count": count, "actual_count": len(matched), "diameter_mm": expected["diameter_mm"]})
def _through_cylindrical_bore(expected: dict[str, Any], facts: dict[str, Any]) -> ClaimResult:
matched = _matching_cylinders(expected, facts)
if not _records(facts):
return _pending("topology is unavailable")
if not matched:
return _pending("the target bore has not been introduced at this checkpoint")
# A bore is through only when the B-rep exporter established that its
# cylindrical face links two oppositely oriented plane faces. The flag is
# derived from shared topology edges, not from its requested depth.
through = [item for item in matched if bool((item.get("geometry") or {}).get("through"))]
count = int(expected.get("count", 1))
if len(matched) < count and len(through) == len(matched):
return _pending("the target through-bore pattern is incomplete at this checkpoint")
return _pass({"through_bores": [item.get("record_id") for item in through]}) if len(matched) == count and len(through) == count else _fail({"expected_count": count, "actual_count": len(matched), "actual_through_count": len(through), "diameter_mm": expected["diameter_mm"]})
def _cylindrical_bore_depth(expected: dict[str, Any], facts: dict[str, Any]) -> ClaimResult:
matched = _matching_cylinders(expected, facts)
count = int(expected.get("count", 1))
if not _records(facts):
return _pending("topology is unavailable")
if not matched:
return _pending("the target bore has not been introduced at this checkpoint")
if len(matched) < count:
return _pending("the target bore pattern is incomplete at this checkpoint")
if len(matched) != count:
return _fail({"expected_count": count, "actual_count": len(matched), "diameter_mm": expected["diameter_mm"]})
spans = [_cylinder_axis_span(record) for record in matched]
if any(span is None for span in spans):
return _pending("target bore faces have no measurable axial span")
depth = float(expected["depth_mm"])
tolerance = float(expected["tolerance_mm"])
actual = [float(span) for span in spans if span is not None]
return _pass({"diameter_mm": expected["diameter_mm"], "depths_mm": actual, "tolerance_mm": tolerance}) if all(abs(value - depth) <= tolerance for value in actual) else _fail({"diameter_mm": expected["diameter_mm"], "expected_depth_mm": depth, "actual_depths_mm": actual, "tolerance_mm": tolerance})
def _conical_bore(expected: dict[str, Any], facts: dict[str, Any]) -> ClaimResult:
matched = _matching_cones(expected, facts)
count = int(expected.get("count", 1))
if not _records(facts):
return _pending("topology is unavailable")
if not matched:
return _pending("the target conical bore has not been introduced at this checkpoint")
if len(matched) < count:
return _pending("the target conical-bore pattern is incomplete at this checkpoint")
return _pass({"conical_bores": [item.get("record_id") for item in matched], "small_diameter_mm": expected["small_diameter_mm"], "large_diameter_mm": expected["large_diameter_mm"], "included_angle_deg": expected["included_angle_deg"]}) if len(matched) == count else _fail({"expected_count": count, "actual_count": len(matched), "small_diameter_mm": expected["small_diameter_mm"], "large_diameter_mm": expected["large_diameter_mm"], "included_angle_deg": expected["included_angle_deg"]})
def _outer_cylindrical_surface(expected: dict[str, Any], facts: dict[str, Any]) -> ClaimResult:
shells = _outer_cylinder_shells(expected, facts)
count = int(expected.get("count", 1))
if not _records(facts):
return _pending("topology is unavailable")
if not shells:
return _pending("the target external cylindrical surface has not been introduced at this checkpoint")
if len(shells) < count:
return _pending("the target external cylindrical-surface pattern is incomplete at this checkpoint")
if len(shells) != count:
return _fail({"expected_count": count, "actual_count": len(shells), "diameter_mm": expected["diameter_mm"]})
axial_span = expected.get("axial_span_mm")
evidence: dict[str, Any] = {
"external_cylindrical_faces": [item.get("record_id") for shell in shells for item in shell],
"external_cylindrical_surface_groups": [[item.get("record_id") for item in shell] for shell in shells],
"diameter_mm": expected["diameter_mm"],
}
if axial_span is None:
return _pass(evidence)
intervals = [_cylinder_axis_interval(shell[0]) for shell in shells]
if any(interval is None for interval in intervals):
return _pending("target external cylindrical faces have no measurable axial span")
actual_spans = []
for shell in shells:
shell_intervals = [_cylinder_axis_interval(record) for record in shell]
if any(interval is None for interval in shell_intervals):
return _pending("target external cylindrical faces have no measurable axial span")
actual_spans.append(
max(interval[1] for interval in shell_intervals if interval is not None)
- min(interval[0] for interval in shell_intervals if interval is not None)
)
tolerance = float(expected.get("tolerance_mm", _DEFAULT_TOLERANCE_MM))
evidence.update({"axial_spans_mm": actual_spans, "tolerance_mm": tolerance})
return _pass(evidence) if all(abs(span - float(axial_span)) <= tolerance for span in actual_spans) else _fail({"diameter_mm": expected["diameter_mm"], "expected_axial_span_mm": axial_span, "actual_axial_spans_mm": actual_spans, "tolerance_mm": tolerance})
def _hole_pattern(expected: dict[str, Any], facts: dict[str, Any]) -> ClaimResult:
# The same topology evidence supports the count/diameter component; the
# runtime can add per-cylinder centres to prove PCD and angular spacing.
matched = _matching_cylinders(expected, facts)
count = int(expected["count"])
if not _records(facts):
return _pending("topology is unavailable")
if not matched:
return _pending("the target hole pattern has not been introduced at this checkpoint")
centers = [_cylinder_axis_point(item) for item in matched]
centers = [center for center in centers if center is not None]
if len(matched) < count:
return _pending("the target hole pattern is incomplete at this checkpoint")
if len(matched) != count or len(centers) != count:
return _fail({"expected_count": count, "actual_count": len(matched), "centred_count": len(centers), "diameter_mm": expected["diameter_mm"]})
radius = float(expected["pitch_radius_mm"])
tolerance = float(expected["tolerance_mm"])
selected = centers
centroid = tuple(sum(point[index] for point in selected) / count for index in range(3))
radial = [sqrt(sum((point[index] - centroid[index]) ** 2 for index in range(3))) for point in selected]
if not all(abs(value - radius) <= tolerance for value in radial):
return _fail({"expected_pitch_radius_mm": radius, "measured_radii_mm": radial, "tolerance_mm": tolerance})
central_bore_diameter = expected.get("concentric_bore_diameter_mm")
if central_bore_diameter is not None:
central = _matching_cylinders({"diameter_mm": central_bore_diameter, "tolerance_mm": tolerance}, facts)
central_axes = [_cylinder_axis_point(record) for record in central if bool((record.get("geometry") or {}).get("through"))]
central_axes = [axis for axis in central_axes if axis is not None]
if not central_axes:
return _pending("the required concentric reference bore is not available")
if len(central_axes) != 1:
return _fail({"reason": "concentric reference bore is ambiguous", "diameter_mm": central_bore_diameter, "count": len(central_axes)})
axis = central_axes[0]
normal = _cylinder_axis_direction(central[0])
if normal is None:
return _pending("the required concentric reference bore has no measurable axis")
delta = tuple(centroid[index] - axis[index] for index in range(3))
axial = sum(delta[index] * normal[index] for index in range(3))
concentric_error = sqrt(sum((delta[index] - axial * normal[index]) ** 2 for index in range(3)))
if concentric_error > tolerance:
return _fail({"expected_concentric_bore_diameter_mm": central_bore_diameter, "pattern_centroid_mm": centroid, "reference_axis_point_mm": axis, "concentric_error_mm": concentric_error, "tolerance_mm": tolerance})
angular = _equal_angular_spacing(selected, centroid, radius, tolerance)
if angular is None:
return _fail({"reason": "hole centres are degenerate or not evenly spaced", "centres_mm": selected, "tolerance_mm": tolerance})
evidence = {"count": count, "pitch_radius_mm": radius, "centres_mm": selected, "angular_spacing_rad": angular}
if central_bore_diameter is not None:
evidence["concentric_bore_diameter_mm"] = central_bore_diameter
return _pass(evidence)
def _equal_angular_spacing(
centres: list[tuple[float, float, float]],
centroid: tuple[float, float, float],
radius: float,
tolerance: float,
) -> list[float] | None:
"""Return cyclic angular increments only for a genuinely even circle."""
vectors = [tuple(point[index] - centroid[index] for index in range(3)) for point in centres]
base_length = sqrt(sum(value * value for value in vectors[0]))
if base_length == 0:
return None
normal: tuple[float, float, float] | None = None
for vector in vectors[1:]:
cross = (
vectors[0][1] * vector[2] - vectors[0][2] * vector[1],
vectors[0][2] * vector[0] - vectors[0][0] * vector[2],
vectors[0][0] * vector[1] - vectors[0][1] * vector[0],
)
length = sqrt(sum(value * value for value in cross))
if length > 1e-9:
normal = tuple(value / length for value in cross)
break
if normal is None:
# Two diametrically opposed holes are evenly spaced exactly when both
# are at the requested radius around their midpoint.
return [pi, pi] if len(centres) == 2 else None
basis_x = tuple(value / base_length for value in vectors[0])
basis_y = (
normal[1] * basis_x[2] - normal[2] * basis_x[1],
normal[2] * basis_x[0] - normal[0] * basis_x[2],
normal[0] * basis_x[1] - normal[1] * basis_x[0],
)
angles = sorted(
atan2(
sum(vector[index] * basis_y[index] for index in range(3)),
sum(vector[index] * basis_x[index] for index in range(3)),
)
for vector in vectors
)
increments = [
(angles[(index + 1) % len(angles)] - angles[index]) % (2 * pi)
for index in range(len(angles))
]
target = 2 * pi / len(centres)
return increments if all(abs(increment - target) * radius <= tolerance for increment in increments) else None
def _collinear_bore_chain(expected: dict[str, Any], facts: dict[str, Any]) -> ClaimResult:
"""Prove a coplanar bore-centre chain in either direction.
``axis_origin_mm`` may be either end of a through bore, depending on the
host-face orientation used to create that feature. Project each axis line
onto the common plane normal to the bore direction before comparing its
centre location; otherwise equivalent top- and bottom-hosted holes appear
to be offset by the part thickness.
"""
matched = [
record for record in _matching_cylinders(expected, facts)
if bool((record.get("geometry") or {}).get("through"))
]
expected_distances = [float(value) for value in expected["adjacent_distances_mm"]]
count = len(expected_distances) + 1
tolerance = float(expected["tolerance_mm"])
centres = [_cylinder_axis_point(record) for record in matched]
centres = [centre for centre in centres if centre is not None]
if not _records(facts):
return _pending("topology is unavailable")
if not matched:
return _pending("the target bore chain has not been introduced at this checkpoint")
# A checkpoint may deliberately add this chain across more than one
# action. Fewer already-through matching bores are therefore unfinished
# future work, while a matching bore that is not through is a real
# violation of this claim.
if len(centres) < count and len(centres) == len(matched):
return _pending("the target bore chain is incomplete at this checkpoint")
if len(centres) != count:
return _fail({"expected_count": count, "actual_count": len(centres), "diameter_mm": expected["diameter_mm"]})
directions = [_cylinder_axis_direction(record) for record in matched]
if all(direction is not None for direction in directions):
reference_direction = directions[0]
assert reference_direction is not None
alignments = [
abs(sum(reference_direction[index] * direction[index] for index in range(3)))
for direction in directions
if direction is not None
]
if any(alignment < 1.0 - 1e-6 for alignment in alignments):
return _fail({"reason": "bore axes are not parallel", "axis_alignments": alignments})
# This is the unique point where each parallel bore axis intersects
# the plane through the world origin normal to ``reference_direction``.
# It is invariant to choosing either axial endpoint as axis_origin_mm.
centres = [
tuple(
centre[index]
- sum(centre[component] * reference_direction[component] for component in range(3))
* reference_direction[index]
for index in range(3)
)
for centre in centres
]
origin = centres[0]
endpoint = max(centres[1:], key=lambda centre: sum((centre[index] - origin[index]) ** 2 for index in range(3)))
direction = tuple(endpoint[index] - origin[index] for index in range(3))
direction_length = sqrt(sum(value * value for value in direction))
if direction_length <= tolerance:
return _fail({"reason": "bore centres do not define a non-zero chain axis"})
unit = tuple(value / direction_length for value in direction)
projections: list[tuple[float, tuple[float, float, float]]] = []
for centre in centres:
relative = tuple(centre[index] - origin[index] for index in range(3))
projection = sum(relative[index] * unit[index] for index in range(3))
residual = sqrt(sum((relative[index] - projection * unit[index]) ** 2 for index in range(3)))
if residual > tolerance:
return _fail({"reason": "bore centres are not collinear", "residual_mm": residual, "tolerance_mm": tolerance})
projections.append((projection, centre))
projections.sort(key=lambda item: item[0])
actual_distances = [projections[index + 1][0] - projections[index][0] for index in range(count - 1)]
matches_forward = all(
abs(actual - target) <= tolerance
for actual, target in zip(actual_distances, expected_distances, strict=True)
)
matches_reverse = all(
abs(actual - target) <= tolerance
for actual, target in zip(actual_distances, reversed(expected_distances), strict=True)
)
if not matches_forward and not matches_reverse:
return _fail({"expected_adjacent_distances_mm": expected_distances, "actual_adjacent_distances_mm": actual_distances, "tolerance_mm": tolerance})
return _pass({
"centres_mm": [centre for _projection, centre in projections],
"adjacent_distances_mm": actual_distances,
"expected_orientation": "forward" if matches_forward else "reversed",
"tolerance_mm": tolerance,
})
def _coaxial_through_bore_group(expected: dict[str, Any], facts: dict[str, Any]) -> ClaimResult:
matched = _matching_cylinders(expected, facts)
count = int(expected["count"])
if not _records(facts):
return _pending("topology is unavailable")
if not matched:
return _pending("the target bore has not been introduced at this checkpoint")
through = [record for record in matched if bool((record.get("geometry") or {}).get("through"))]
if len(matched) < count and len(through) == len(matched):
return _pending("the target coaxial bore group is incomplete at this checkpoint")
if len(matched) != count or len(through) != count:
return _fail({"expected_count": count, "actual_count": len(matched), "actual_through_count": len(through), "diameter_mm": expected["diameter_mm"]})
points = [_cylinder_axis_point(record) for record in through]
directions = [_cylinder_axis_direction(record) for record in through]
if any(point is None for point in points) or any(direction is None for direction in directions):
return _pending("target bore axes are unavailable")
point_values = [point for point in points if point is not None]
direction_values = [direction for direction in directions if direction is not None]
tolerance = float(expected["tolerance_mm"])
reference_point, reference_direction = point_values[0], direction_values[0]
deviations: list[dict[str, Any]] = []
for record, point, direction in zip(through[1:], point_values[1:], direction_values[1:], strict=True):
dot = abs(sum(reference_direction[index] * direction[index] for index in range(3)))
offset = tuple(point[index] - reference_point[index] for index in range(3))
distance = sqrt(sum((offset[index] - sum(offset[component] * reference_direction[component] for component in range(3)) * reference_direction[index]) ** 2 for index in range(3)))
deviations.append({"record_id": record.get("record_id"), "axis_dot": dot, "axis_distance_mm": distance})
return _pass({"diameter_mm": expected["diameter_mm"], "axes": deviations, "tolerance_mm": tolerance}) if all(item["axis_dot"] >= 1 - 1e-6 and item["axis_distance_mm"] <= tolerance for item in deviations) else _fail({"diameter_mm": expected["diameter_mm"], "axes": deviations, "tolerance_mm": tolerance})
def _orthogonal_intersecting_through_bores(expected: dict[str, Any], facts: dict[str, Any]) -> ClaimResult:
first = _matching_cylinders({"diameter_mm": expected["first_diameter_mm"], "tolerance_mm": expected["tolerance_mm"]}, facts)
second = _matching_cylinders({"diameter_mm": expected["second_diameter_mm"], "tolerance_mm": expected["tolerance_mm"]}, facts)
if not _records(facts):
return _pending("topology is unavailable")
if not first or not second:
return _pending("one or both target bores have not been introduced at this checkpoint")
first_through = [record for record in first if bool((record.get("geometry") or {}).get("through"))]
second_through = [record for record in second if bool((record.get("geometry") or {}).get("through"))]
if len(first) != 1 or len(second) != 1 or len(first_through) != 1 or len(second_through) != 1:
return _fail({"first_diameter_mm": expected["first_diameter_mm"], "first_count": len(first), "first_through_count": len(first_through), "second_diameter_mm": expected["second_diameter_mm"], "second_count": len(second), "second_through_count": len(second_through)})
first_point, second_point = _cylinder_axis_point(first_through[0]), _cylinder_axis_point(second_through[0])
first_axis, second_axis = _cylinder_axis_direction(first_through[0]), _cylinder_axis_direction(second_through[0])
if first_point is None or second_point is None or first_axis is None or second_axis is None:
return _pending("target bore axes are unavailable")
expected_first_axis = {"x": (1.0, 0.0, 0.0), "y": (0.0, 1.0, 0.0), "z": (0.0, 0.0, 1.0)}[expected["first_axis"]]
expected_second_axis = {"x": (1.0, 0.0, 0.0), "y": (0.0, 1.0, 0.0), "z": (0.0, 0.0, 1.0)}[expected["second_axis"]]
first_axis_alignment = abs(sum(first_axis[index] * expected_first_axis[index] for index in range(3)))
second_axis_alignment = abs(sum(second_axis[index] * expected_second_axis[index] for index in range(3)))
dot = sum(first_axis[index] * second_axis[index] for index in range(3))
denominator = 1 - dot * dot
if denominator <= 1e-9:
return _fail({"reason": "bore axes are parallel", "axis_dot": dot})
offset = tuple(first_point[index] - second_point[index] for index in range(3))
first_parameter = (dot * sum(second_axis[index] * offset[index] for index in range(3)) - sum(first_axis[index] * offset[index] for index in range(3))) / denominator
second_parameter = (sum(second_axis[index] * offset[index] for index in range(3)) - dot * sum(first_axis[index] * offset[index] for index in range(3))) / denominator
closest_first = tuple(first_point[index] + first_parameter * first_axis[index] for index in range(3))
closest_second = tuple(second_point[index] + second_parameter * second_axis[index] for index in range(3))
separation = sqrt(sum((closest_first[index] - closest_second[index]) ** 2 for index in range(3)))
tolerance = float(expected["tolerance_mm"])
evidence = {
"axis_dot": dot,
"axis_intersection_separation_mm": separation,
"first_axis": expected["first_axis"],
"first_axis_alignment": first_axis_alignment,
"second_axis": expected["second_axis"],
"second_axis_alignment": second_axis_alignment,
"tolerance_mm": tolerance,
}
return _pass(evidence) if abs(dot) <= 1e-6 and separation <= tolerance and first_axis_alignment >= 1 - 1e-6 and second_axis_alignment >= 1 - 1e-6 else _fail(evidence)
def _rectangular_corner_bore_pattern(expected: dict[str, Any], facts: dict[str, Any]) -> ClaimResult:
"""Prove four through bores lie at one equal offset from a rectangular plate's edges."""
matched = _matching_cylinders(expected, facts)
count = int(expected["count"])
if not _records(facts):
return _pending("topology is unavailable")
if not matched:
return _pending("the target bore has not been introduced at this checkpoint")
through = [record for record in matched if bool((record.get("geometry") or {}).get("through"))]
if len(matched) < count and len(through) == len(matched):
return _pending("the target corner-bore pattern is incomplete at this checkpoint")
if len(matched) != count or len(through) != count:
return _fail({"expected_count": count, "actual_count": len(matched), "actual_through_count": len(through), "diameter_mm": expected["diameter_mm"]})
health = facts.get("health") if isinstance(facts.get("health"), dict) else {}
bbox = health.get("bbox_mm") if isinstance(health.get("bbox_mm"), dict) else {}
minimum, maximum = bbox.get("min"), bbox.get("max")
if not isinstance(minimum, list) or not isinstance(maximum, list) or len(minimum) != 3 or len(maximum) != 3:
return _pending("rebuild report has no bounding-box extrema")
normal = _cylinder_axis_direction(through[0])
centres = [_cylinder_axis_point(record) for record in through]
if normal is None or any(centre is None for centre in centres):
return _pending("target bore axes are unavailable")
normal_axis = max(range(3), key=lambda index: abs(normal[index]))
if abs(normal[normal_axis]) < 0.99:
return _pending("corner-bore pattern is not aligned to a measurable cardinal plate normal")
plane_axes = [index for index in range(3) if index != normal_axis]
try:
lower = [float(value) for value in minimum]
upper = [float(value) for value in maximum]
except (TypeError, ValueError):
return _pending("rebuild report has invalid bounding-box extrema")
offset = float(expected["edge_offset_mm"])
tolerance = float(expected["tolerance_mm"])
centre_values = [centre for centre in centres if centre is not None]
positions: list[tuple[int, int]] = []
for centre in centre_values:
position: list[int] = []
for axis in plane_axes:
low_error = abs((centre[axis] - lower[axis]) - offset)
high_error = abs((upper[axis] - centre[axis]) - offset)
if low_error <= tolerance:
position.append(0)
elif high_error <= tolerance:
position.append(1)
else:
return _fail({"reason": "bore centre has the wrong edge offset", "centre_mm": centre, "axis": ["x", "y", "z"][axis], "low_offset_mm": centre[axis] - lower[axis], "high_offset_mm": upper[axis] - centre[axis], "expected_edge_offset_mm": offset, "tolerance_mm": tolerance})
positions.append((position[0], position[1]))
expected_positions = {(0, 0), (0, 1), (1, 0), (1, 1)}
return _pass({"diameter_mm": expected["diameter_mm"], "edge_offset_mm": offset, "normal_axis": ["x", "y", "z"][normal_axis], "corner_positions": positions, "tolerance_mm": tolerance}) if set(positions) == expected_positions and len(set(positions)) == count else _fail({"reason": "bores do not occupy all rectangular corners", "corner_positions": positions, "expected_positions": sorted(expected_positions)})
def _coaxial(expected: dict[str, Any], facts: dict[str, Any]) -> ClaimResult:
ids = set(expected["record_ids"])
matching = [record for record in _records(facts) if str(record.get("record_id") or "") in ids]
if len(matching) != 2:
return _pending("referenced topology records are not both available")
axes = [_vector((record.get("geometry") or {}).get("axis_direction")) for record in matching]
if not all(axes):
return _pending("referenced records have no measurable axes")
a, b = axes # type: ignore[misc]
dot = abs(sum(a[index] * b[index] for index in range(3)))
return _pass({"axis_dot": dot}) if isclose(dot, 1.0, abs_tol=float(expected["tolerance"])) else _fail({"axis_dot": dot, "tolerance": expected["tolerance"]})
def _coplanar(expected: dict[str, Any], facts: dict[str, Any]) -> ClaimResult:
ids = set(expected["record_ids"])
matching = [record for record in _records(facts) if str(record.get("record_id") or "") in ids]
if len(matching) != 2:
return _pending("referenced topology records are not both available")
geometry = [(record.get("geometry") or {}) for record in matching]
normals = [_vector(value.get("plane_normal") or value.get("normal")) for value in geometry]
offsets = [value.get("plane_offset_mm") for value in geometry]
if not all(normals) or not all(isinstance(value, (int, float)) for value in offsets):
return _pending("referenced records have no measurable planes")
dot = abs(sum(normals[0][index] * normals[1][index] for index in range(3))) # type: ignore[index]
offset = abs(float(offsets[0]) - float(offsets[1]))
tolerance = float(expected["tolerance_mm"])
return _pass({"normal_dot": dot, "offset_delta_mm": offset}) if isclose(dot, 1.0, abs_tol=1e-5) and offset <= tolerance else _fail({"normal_dot": dot, "offset_delta_mm": offset, "tolerance_mm": tolerance})
@dataclass(frozen=True, slots=True)
class ClaimDefinition:
claim_kind: str
expected_schema: dict[str, Any]
required_artifacts: tuple[str, ...]
evaluator: ClaimEvaluator
deterministic: bool = True
class VerifierRegistry:
def __init__(self, definitions: tuple[ClaimDefinition, ...]) -> None:
self._definitions = {item.claim_kind: item for item in definitions}
if len(self._definitions) != len(definitions):
raise ValueError("Verifier claim_kind values must be unique")
for definition in definitions:
if not _is_closed_schema(definition.expected_schema):
raise ValueError(f"Verifier schema is not closed: {definition.claim_kind}")
try:
Draft202012Validator.check_schema(definition.expected_schema)
except SchemaError as error:
raise ValueError(f"Invalid verifier schema: {definition.claim_kind}") from error
@property
def claim_kinds(self) -> tuple[str, ...]:
return tuple(sorted(self._definitions))
def definition(self, claim_kind: str) -> ClaimDefinition:
try:
return self._definitions[claim_kind]
except KeyError as error:
raise ValueError(f"VERIFIER_UNAVAILABLE: {claim_kind}") from error
def expected_one_of_schema(self, *, exclude_claim_kinds: set[str] | frozenset[str] | tuple[str, ...] = ()) -> dict[str, Any]:
excluded = set(exclude_claim_kinds)
return {
"oneOf": [
_closed_object({"claim_kind": {"const": item.claim_kind}, "expected": item.expected_schema}, ["claim_kind", "expected"])
for item in self._definitions.values()
if item.claim_kind not in excluded
]
}
def validate_expected(self, claim_kind: str, expected: dict[str, Any]) -> list[dict[str, str]]:
definition = self.definition(claim_kind)
validator = Draft202012Validator(definition.expected_schema)
return [
{"path": "/" + "/".join(str(part) for part in error.absolute_path), "message": error.message}
for error in sorted(validator.iter_errors(expected), key=lambda item: (list(item.absolute_path), item.message))
]
def normalize_expected(self, claim_kind: str, expected: dict[str, Any]) -> dict[str, Any]:
"""Apply protocol defaults before a compiled contract is frozen.
These defaults describe verifier mechanics, never user geometry. The
outer-cylinder verifier can match a diameter without a tolerance, but
measuring its optional axial span needs one. Persist the default so
the resulting contract is complete and independently reproducible.
"""
normalized = deepcopy(expected)
if claim_kind == "outer_cylindrical_surface" and "axial_span_mm" in normalized:
normalized.setdefault("tolerance_mm", _DEFAULT_TOLERANCE_MM)
return normalized
def evaluate(self, claim_kind: str, expected: dict[str, Any], facts: dict[str, Any]) -> ClaimResult:
errors = self.validate_expected(claim_kind, expected)
if errors:
return {"status": "unavailable", "evidence": {"schema_errors": errors}}
return self.definition(claim_kind).evaluator(expected, facts)
def _is_closed_schema(schema: Any) -> bool:
if not isinstance(schema, dict):
return False
if schema.get("type") == "object" and schema.get("additionalProperties") is not False:
return False
for key in ("properties", "$defs", "definitions"):
values = schema.get(key)
if isinstance(values, dict) and not all(_is_closed_schema(value) for value in values.values()):
return False
for key in ("items", "additionalItems"):
if key in schema and isinstance(schema[key], dict) and not _is_closed_schema(schema[key]):
return False
for key in ("oneOf", "anyOf", "allOf"):
if key in schema and (not isinstance(schema[key], list) or not all(_is_closed_schema(value) for value in schema[key])):
return False
return True
def default_registry() -> VerifierRegistry:
# Acceptance tolerances are author-selected but never author-unbounded.
# A broad value could otherwise turn an exact dimensions claim into a
# vacuous pass. A tenth of a millimetre is a deliberately generous upper bound for
# this CAD protocol; requests needing looser acceptance must be made
# explicit through a separate verifier rather than weakening all claims.
positive = {"type": "number", "exclusiveMinimum": 0, "maximum": 100_000}
tolerance = {"type": "number", "minimum": 0, "maximum": 0.1}
alignment_tolerance = {"type": "number", "minimum": 0, "maximum": 0.01}
cylindrical = _closed_object({"diameter_mm": positive, "count": {"type": "integer", "minimum": 1}, "tolerance_mm": tolerance}, ["diameter_mm"])
outer_cylindrical = _closed_object({"diameter_mm": positive, "count": {"type": "integer", "minimum": 1}, "axial_span_mm": positive, "tolerance_mm": tolerance}, ["diameter_mm"])
bore_depth = _closed_object({"diameter_mm": positive, "depth_mm": positive, "count": {"type": "integer", "minimum": 1}, "tolerance_mm": tolerance}, ["diameter_mm", "depth_mm", "tolerance_mm"])
conical_bore = _closed_object({"small_diameter_mm": positive, "large_diameter_mm": positive, "included_angle_deg": {"type": "number", "exclusiveMinimum": 0, "maximum": 179.999}, "count": {"type": "integer", "minimum": 1}, "tolerance_mm": tolerance}, ["small_diameter_mm", "large_diameter_mm", "included_angle_deg", "tolerance_mm"])
bore_chain = _closed_object({
"diameter_mm": positive,
"adjacent_distances_mm": {"type": "array", "items": positive, "minItems": 1, "maxItems": 63},
"tolerance_mm": tolerance,
}, ["diameter_mm", "adjacent_distances_mm", "tolerance_mm"])
return VerifierRegistry((
ClaimDefinition("solid_count_equals", _closed_object({"value": {"type": "integer", "minimum": 1}}, ["value"]), ("rebuild_report",), _solid_count),
ClaimDefinition("volume_decreased", _closed_object({}, []), ("rebuild_report", "parent_rebuild_report"), _volume_decreased),
ClaimDefinition("single_connected_body", _closed_object({}, []), ("rebuild_report", "topology"), lambda _expected, facts: _solid_count({"value": 1}, facts)),
ClaimDefinition("bbox_dimension_mm", _closed_object({"axis": {"enum": ["x", "y", "z"]}, "value": positive, "tolerance_mm": tolerance}, ["axis", "value", "tolerance_mm"]), ("rebuild_report",), _bbox),
ClaimDefinition("bbox_rank_dimension_mm", _closed_object({"rank": {"enum": ["minimum", "median", "maximum"]}, "value": positive, "tolerance_mm": tolerance}, ["rank", "value", "tolerance_mm"]), ("rebuild_report",), _bbox_rank),
ClaimDefinition("cylindrical_bore", cylindrical, ("topology",), _cylindrical_bore),
ClaimDefinition("through_cylindrical_bore", cylindrical, ("topology",), _through_cylindrical_bore),
ClaimDefinition("cylindrical_bore_depth", bore_depth, ("topology",), _cylindrical_bore_depth),
ClaimDefinition("conical_bore", conical_bore, ("topology",), _conical_bore),
ClaimDefinition("outer_cylindrical_surface", outer_cylindrical, ("topology",), _outer_cylindrical_surface),
ClaimDefinition("circular_hole_pattern", _closed_object({"count": {"type": "integer", "minimum": 2}, "diameter_mm": positive, "pitch_radius_mm": positive, "concentric_bore_diameter_mm": positive, "tolerance_mm": tolerance}, ["count", "diameter_mm", "pitch_radius_mm", "tolerance_mm"]), ("topology",), _hole_pattern),
ClaimDefinition("collinear_through_bore_chain", bore_chain, ("topology",), _collinear_bore_chain),
ClaimDefinition("coaxial_through_bore_group", _closed_object({"diameter_mm": positive, "count": {"type": "integer", "minimum": 2, "maximum": 16}, "tolerance_mm": alignment_tolerance}, ["diameter_mm", "count", "tolerance_mm"]), ("topology",), _coaxial_through_bore_group),
ClaimDefinition("orthogonal_intersecting_through_bores", _closed_object({"first_diameter_mm": positive, "second_diameter_mm": positive, "first_axis": {"enum": ["x", "y", "z"]}, "second_axis": {"enum": ["x", "y", "z"]}, "tolerance_mm": alignment_tolerance}, ["first_diameter_mm", "second_diameter_mm", "first_axis", "second_axis", "tolerance_mm"]), ("topology",), _orthogonal_intersecting_through_bores),
ClaimDefinition("rectangular_corner_through_bore_pattern", _closed_object({"diameter_mm": positive, "count": {"const": 4}, "edge_offset_mm": positive, "tolerance_mm": tolerance}, ["diameter_mm", "count", "edge_offset_mm", "tolerance_mm"]), ("rebuild_report", "topology"), _rectangular_corner_bore_pattern),
ClaimDefinition("coaxial", _closed_object({"record_ids": {"type": "array", "items": {"type": "string", "minLength": 1}, "minItems": 2, "maxItems": 2, "uniqueItems": True}, "tolerance": alignment_tolerance}, ["record_ids", "tolerance"]), ("topology",), _coaxial),
ClaimDefinition("coplanar", _closed_object({"record_ids": {"type": "array", "items": {"type": "string", "minLength": 1}, "minItems": 2, "maxItems": 2, "uniqueItems": True}, "tolerance_mm": tolerance}, ["record_ids", "tolerance_mm"]), ("topology",), _coplanar),
ClaimDefinition("visual", _closed_object({"description": {"type": "string", "minLength": 1, "maxLength": 360}}, ["description"]), ("render",), lambda _expected, _facts: _pending("requires independent visual review"), deterministic=False),
))