Files
Fr0zkaandClaude Opus 5.5 753cc9c259 tools(verify): fast/medium/full verification tiers with a compiled-in source stamp (L39); research R6
BuildLinux.sh hashes a sorted manifest of compiled inputs into VOXELFORGE_SOURCE_STAMP, printed first by
every executable; Standalone/Tools/verify/verify.py checks it before and after each tier and records
binary/input hashes and the resolved recipe, style map and layout. Fast (163 s) runs on the canonical owner
world only and asserts FI Style 2 / SW Style 1; medium (8 min) and full (26 min) add the style matrix,
GCC, determinism and cost. Controls: stale source, mislabelled style-0 owner, all-black capture, L30/L31,
L35/L36 and the L38 step are all rejected. No geometry change; owner hashes identical to L37.
Research R6: leak table, generator contract and a 10-step metre-native migration plan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-23 15:27:52 +02:00

544 lines
24 KiB
Python

#!/usr/bin/env python3
"""Measure player-scale planar aprons, straight boundaries and corners at passage mouths."""
import argparse
import importlib.util
import math
from collections import defaultdict, deque
from pathlib import Path
ROOT = Path(__file__).resolve().parents[4]
PROFILE_PATH = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
SPEC = importlib.util.spec_from_file_location("ring_profile_l37", PROFILE_PATH)
PROFILE = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(PROFILE)
# Frozen from player scale before measuring the L37 candidate. The 0.75 m line limit
# is just over the 0.68 m capsule width; 0.50 m2 is one capsule-width footprint.
STRAIGHT_LIMIT_M = 0.75
PLANE_LIMIT_M2 = 0.50
CORNER_TURN_LIMIT_DEG = 30.0
PLANE_TOLERANCE_M = 0.02
NORMAL_BUCKET = 0.07
PLANE_BUCKET_M = 0.03
MOUTH_CORRIDOR_HALF_WIDTH_M = 1.50
MOUTH_CORRIDOR_LENGTH_M = 12.0
MOUTH_Z_BELOW_M = 4.0
MOUTH_Z_ABOVE_M = 3.0
PLAYER_FLOOR_OFFSET_M = 1.13
DELTA_THRESHOLD_M = 0.05
DELTA_GRID_M = 0.125
SCENES = {
"owner-fi": ("owner-fi-ground-mouth.vfmesh", "layout-owner-fi.json", "FloatingIslands", "upper"),
"owner-sw": ("owner-sw-ground-mouth.vfmesh", "layout-owner-sw.json", "SurfaceWorld", "lower"),
"style-override-upper-sw-fixture-style0": (
"style-override-upper-sw-fixture-style0-ground-mouth.vfmesh",
"layout-style-override-upper-sw-fixture-style0.json", "SurfaceWorld", "upper"),
}
def sub(a, b):
return tuple(a[i] - b[i] for i in range(3))
def dot(a, b):
return sum(a[i] * b[i] for i in range(3))
def cross(a, b):
return (a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0])
def length(a):
return math.sqrt(dot(a, a))
def face(points):
n = cross(sub(points[1], points[0]), sub(points[2], points[0]))
twice_area = length(n)
if twice_area <= 1.0e-12:
return None
area = twice_area * 0.5
n = tuple(v / twice_area for v in n)
if n[2] < 0.0:
n = tuple(-v for v in n)
d = -dot(n, points[0])
return n, d, area
def vertex_key(point):
return tuple(round(value, 4) for value in point)
def edge_key(a, b):
ka, kb = vertex_key(a), vertex_key(b)
return (ka, kb) if ka < kb else (kb, ka)
def rdp(points, epsilon):
if len(points) <= 2:
return points
a, b = points[0], points[-1]
ab = sub(b, a)
ab2 = dot(ab, ab)
maximum, index = -1.0, -1
for i in range(1, len(points) - 1):
ap = sub(points[i], a)
t = max(0.0, min(1.0, dot(ap, ab) / ab2)) if ab2 else 0.0
closest = tuple(a[j] + t * ab[j] for j in range(3))
distance = length(sub(points[i], closest))
if distance > maximum:
maximum, index = distance, i
if maximum <= epsilon:
return [a, b]
left = rdp(points[:index + 1], epsilon)
right = rdp(points[index:], epsilon)
return left[:-1] + right
def simplified_boundary(component, records):
counts = defaultdict(int)
actual_edges = {}
for index in component:
points = records[index]["points"]
for a, b in ((points[0], points[1]), (points[1], points[2]),
(points[2], points[0])):
key = edge_key(a, b)
counts[key] += 1
actual_edges[key] = (key[0], key[1])
boundary = [edge for edge, count in counts.items() if count == 1]
graph = defaultdict(list)
for a, b in boundary:
graph[a].append(b)
graph[b].append(a)
if not graph:
return [], []
def direction(a, b):
dx, dy = b[0] - a[0], b[1] - a[1]
magnitude = math.hypot(dx, dy)
return ((dx / magnitude, dy / magnitude), magnitude) if magnitude > 1.0e-8 else (None, 0.0)
def straight_run(origin, first):
prior, current = origin, first
initial, edge_length = direction(prior, current)
if initial is None:
return 0.0
total = edge_length
seen = {origin, first}
while current in graph and len(graph[current]) == 2:
choices = [value for value in graph[current] if value != prior]
if len(choices) != 1 or choices[0] in seen:
break
nxt = choices[0]
outgoing, edge_length = direction(current, nxt)
if outgoing is None:
break
deflection = math.degrees(math.acos(max(-1.0, min(1.0,
initial[0] * outgoing[0] + initial[1] * outgoing[1]))))
if deflection > 8.0:
break
total += edge_length
prior, current = current, nxt
initial = outgoing
seen.add(current)
return total
longest = 0.0
corners = []
for vertex, neighbours in graph.items():
if len(neighbours) == 2:
incoming, _ = direction(neighbours[0], vertex)
outgoing, _ = direction(vertex, neighbours[1])
if incoming and outgoing:
turn = math.degrees(math.acos(max(-1.0, min(1.0,
incoming[0] * outgoing[0] + incoming[1] * outgoing[1]))))
if turn >= CORNER_TURN_LIMIT_DEG:
left_run = straight_run(vertex, neighbours[0])
right_run = straight_run(vertex, neighbours[1])
if left_run >= STRAIGHT_LIMIT_M and right_run >= STRAIGHT_LIMIT_M:
corners.append(turn)
for neighbour in neighbours:
longest = max(longest, straight_run(vertex, neighbour))
return [longest], corners
def analyze_triangles(triangles, center, center_z, crop_to_center=True,
include_sides=False):
records = []
for lod, average_nz, vertices in triangles:
points = tuple(vertex[0] for vertex in vertices)
centroid = tuple(sum(point[axis] for point in points) / 3.0 for axis in range(3))
if (crop_to_center and
(abs(centroid[0] - center[0]) > MOUTH_CORRIDOR_LENGTH_M + 2.0
or abs(centroid[1] - center[1]) > MOUTH_CORRIDOR_LENGTH_M + 2.0
or centroid[2] < center_z - MOUTH_Z_BELOW_M
or centroid[2] > center_z + MOUTH_Z_ABOVE_M)):
continue
measured = face(points)
if not measured:
continue
normal, plane_d, area = measured
if not include_sides and (normal[2] < 0.35 or average_nz < 0.25):
continue
bucket = (round(normal[0] / NORMAL_BUCKET), round(normal[1] / NORMAL_BUCKET),
round(normal[2] / NORMAL_BUCKET), round(plane_d / PLANE_BUCKET_M))
records.append({"points": points, "centroid": centroid, "normal": normal,
"d": plane_d, "area": area, "bucket": bucket})
by_bucket = defaultdict(list)
for index, record in enumerate(records):
by_bucket[record["bucket"]].append(index)
edge_indices = defaultdict(list)
for index, record in enumerate(records):
points = record["points"]
for a, b in ((points[0], points[1]), (points[1], points[2]),
(points[2], points[0])):
edge_indices[edge_key(a, b)].append(index)
adjacency = defaultdict(list)
for indices in edge_indices.values():
if len(indices) == 2:
a, b = indices
# Adjacent triangles can differ by one normal bucket on a smooth mesh.
# Plane membership is checked against the seed below, so let that test
# decide whether a connected patch is planar instead of splitting it at
# the bucket boundary.
adjacency[a].append(b)
adjacency[b].append(a)
max_area = 0.0
max_straight = 0.0
corner_count = 0
max_corner_turn = 0.0
max_bounds = None
max_normal = None
visited = set()
for indices in by_bucket.values():
for seed in indices:
if seed in visited:
continue
component = []
queue = deque([seed])
queued = {seed}
seed_record = records[seed]
while queue:
index = queue.popleft()
record = records[index]
if (abs(dot(seed_record["normal"], record["normal"])) < math.cos(math.radians(4.5))
or max(abs(dot(seed_record["normal"], point) + seed_record["d"])
for point in record["points"]) > PLANE_TOLERANCE_M):
continue
visited.add(index)
component.append(index)
for neighbour in adjacency[index]:
if neighbour not in visited and neighbour not in queued:
queued.add(neighbour)
queue.append(neighbour)
area = sum(records[index]["area"] for index in component)
if area <= max_area:
continue
segments, corners = simplified_boundary(component, records)
max_area = area
max_straight = max(segments, default=0.0)
corner_count = len(corners)
max_corner_turn = max(corners, default=0.0)
points = [point for index in component for point in records[index]["points"]]
max_bounds = tuple((min(point[axis] for point in points),
max(point[axis] for point in points)) for axis in range(3))
max_normal = seed_record["normal"]
return {"triangles": len(records), "planar_area": max_area,
"straight": max_straight, "corners": corner_count,
"corner_turn_max": max_corner_turn,
"bounds": max_bounds, "normal": max_normal}
def synthetic_box(center, z):
x, y = center
corners = ((x - 1.5, y - 1.0, z), (x + 1.5, y - 1.0, z),
(x + 1.5, y + 1.0, z), (x - 1.5, y + 1.0, z))
return [(0, 1.0, tuple((point, (0.0, 0.0, 1.0)) for point in tri))
for tri in ((corners[0], corners[1], corners[2]),
(corners[0], corners[2], corners[3]))]
def fail(metrics, include_planarity=True):
planar_fail = (include_planarity and
(metrics["planar_area"] >= PLANE_LIMIT_M2
or metrics.get("all_face_planar_area", 0.0) >= PLANE_LIMIT_M2))
return (planar_fail
or metrics["straight"] > STRAIGHT_LIMIT_M
or metrics.get("all_face_straight", 0.0) > STRAIGHT_LIMIT_M
or metrics["corners"] > 0
or metrics.get("all_face_corners", 0) > 0)
def mouth_route_points(passage, side, scale):
controls = [
(tuple(value * scale for value in point["position_q"]),
point["radius_q"] * scale)
for point in passage["control_points"]
]
if side == "upper":
return controls[:4]
lower_mouth = tuple(value * scale for value in passage["lower_mouth_q"])
lower_index = min(range(len(controls)), key=lambda index: sum(
(controls[index][0][axis] - lower_mouth[axis]) ** 2 for axis in range(3)))
return list(reversed(controls[max(0, lower_index - 3):lower_index + 1]))
def select_corridor_triangles(triangles, route, half_width, path_length):
selected = []
for triangle in triangles:
vertices = triangle[2]
centroid = tuple(sum(vertex[0][axis] for vertex in vertices) / 3.0
for axis in range(3))
closest_distance = math.inf
closest_along = 0.0
closest_floor = 0.0
prefix = 0.0
for (a, radius_a), (b, radius_b) in zip(route, route[1:]):
dx, dy, dz = (b[axis] - a[axis] for axis in range(3))
horizontal_length_squared = dx * dx + dy * dy
horizontal_length = math.sqrt(horizontal_length_squared)
if horizontal_length_squared <= 1.0e-12:
t = 0.0
else:
t = max(0.0, min(1.0,
((centroid[0] - a[0]) * dx + (centroid[1] - a[1]) * dy)
/ horizontal_length_squared))
center_x, center_y = a[0] + t * dx, a[1] + t * dy
lateral = math.hypot(centroid[0] - center_x, centroid[1] - center_y)
segment_length = math.sqrt(horizontal_length_squared + dz * dz)
if lateral < closest_distance:
closest_distance = lateral
closest_along = prefix + t * segment_length
center_z = a[2] + t * dz
radius = radius_a + t * (radius_b - radius_a)
closest_floor = center_z - radius + PLAYER_FLOOR_OFFSET_M
prefix += segment_length
if (closest_distance <= half_width
and 0.0 <= closest_along <= path_length
and abs(centroid[2] - closest_floor) <= 1.5):
selected.append(triangle)
return selected
def route_floor_at(route, x, y):
closest = None
prefix = 0.0
for (a, radius_a), (b, radius_b) in zip(route, route[1:]):
dx, dy, dz = (b[axis] - a[axis] for axis in range(3))
horizontal_sq = dx * dx + dy * dy
t = max(0.0, min(1.0, ((x - a[0]) * dx + (y - a[1]) * dy)
/ horizontal_sq)) if horizontal_sq > 1.0e-12 else 0.0
center_x, center_y = a[0] + t * dx, a[1] + t * dy
lateral = math.hypot(x - center_x, y - center_y)
length = math.sqrt(horizontal_sq + dz * dz)
along = prefix + t * length
center_z = a[2] + t * dz
radius = radius_a + t * (radius_b - radius_a)
floor = center_z - radius + PLAYER_FLOOR_OFFSET_M
candidate = (lateral, along, floor)
if closest is None or lateral < closest[0]:
closest = candidate
prefix += length
if closest and closest[0] <= MOUTH_CORRIDOR_HALF_WIDTH_M \
and closest[1] <= MOUTH_CORRIDOR_LENGTH_M:
return closest[2]
return None
def differential_surface(candidate_triangles, baseline_triangles, route, bounds):
cell_size = 0.5
candidate_buckets = PROFILE.build_buckets(candidate_triangles, True, bounds, cell_size)
baseline_buckets = PROFILE.build_buckets(baseline_triangles, True, bounds, cell_size)
route_x = [point[0][0] for point in route]
route_y = [point[0][1] for point in route]
padding = MOUTH_CORRIDOR_HALF_WIDTH_M + DELTA_GRID_M
x0 = math.floor((min(route_x) - padding) / DELTA_GRID_M) * DELTA_GRID_M
x1 = math.ceil((max(route_x) + padding) / DELTA_GRID_M) * DELTA_GRID_M
y0 = math.floor((min(route_y) - padding) / DELTA_GRID_M) * DELTA_GRID_M
y1 = math.ceil((max(route_y) + padding) / DELTA_GRID_M) * DELTA_GRID_M
nx = round((x1 - x0) / DELTA_GRID_M)
ny = round((y1 - y0) / DELTA_GRID_M)
samples = {}
for iy in range(ny + 1):
y = y0 + iy * DELTA_GRID_M
for ix in range(nx + 1):
x = x0 + ix * DELTA_GRID_M
floor = route_floor_at(route, x, y)
if floor is None:
continue
candidate = PROFILE.sample_surface(candidate_buckets, x, y, True,
cell_size, preferred_height=floor)
baseline = PROFILE.sample_surface(baseline_buckets, x, y, True,
cell_size, preferred_height=floor)
samples[(ix, iy)] = (candidate, baseline)
delta_triangles = []
changed_cells = 0
changed_area = 0.0
deltas = []
for iy in range(ny):
for ix in range(nx):
keys = ((ix, iy), (ix + 1, iy), (ix + 1, iy + 1), (ix, iy + 1))
values = [samples.get(key) for key in keys]
if any(value is None or value[0] is None for value in values):
continue
cell_deltas = []
changed = False
for candidate, baseline in values:
if baseline is None:
changed = True
cell_deltas.append(math.inf)
else:
delta = candidate[0] - baseline[0]
cell_deltas.append(abs(delta))
deltas.append(abs(delta))
changed |= abs(delta) >= DELTA_THRESHOLD_M
if not changed:
continue
changed_cells += 1
changed_area += DELTA_GRID_M * DELTA_GRID_M
points = []
for key, (candidate, _) in zip(keys, values):
x = x0 + key[0] * DELTA_GRID_M
y = y0 + key[1] * DELTA_GRID_M
points.append(((x, y, candidate[0]), candidate[1]))
for triangle_indices in ((0, 1, 2), (0, 2, 3)):
vertices = tuple(points[index] for index in triangle_indices)
avg_nz = sum(vertex[1][2] for vertex in vertices) / 3.0
delta_triangles.append((0, avg_nz, vertices))
metrics = analyze_triangles(delta_triangles, (0.0, 0.0), 0.0,
crop_to_center=False)
metrics["changed_cells"] = changed_cells
metrics["changed_area"] = changed_area
metrics["max_delta"] = max(deltas, default=0.0)
metrics["mean_abs_delta"] = sum(deltas) / len(deltas) if deltas else 0.0
return metrics
def audit_scene(scene, out, baseline_out, mesh_override=None):
mesh_name, layout_name, target_generator, side = SCENES[scene]
import json
layout = json.loads((out / layout_name).read_text(encoding="utf-8"))
scale = layout["core_voxel_size_um"] / (256.0 * 1_000_000.0)
strata = {int(value["strate_index"]): value for value in layout["strates"]}
choices = []
for passage in layout["passages"]:
side_index = int(passage[side + "_strate_index"])
if strata[side_index]["generator_kind"] == target_generator:
choices.append(passage)
if not choices:
raise RuntimeError(f"no {target_generator} {side} mouth in {scene}")
passage = min(choices, key=lambda value: value["stable_id"])
mouth = tuple(value * scale for value in passage[f"{side}_mouth_q"])
bounds = (mouth[0] - MOUTH_CORRIDOR_LENGTH_M - MOUTH_CORRIDOR_HALF_WIDTH_M - 1.0,
mouth[0] + MOUTH_CORRIDOR_LENGTH_M + MOUTH_CORRIDOR_HALF_WIDTH_M + 1.0,
mouth[1] - MOUTH_CORRIDOR_LENGTH_M - MOUTH_CORRIDOR_HALF_WIDTH_M - 1.0,
mouth[1] + MOUTH_CORRIDOR_LENGTH_M + MOUTH_CORRIDOR_HALF_WIDTH_M + 1.0)
voxel_um, triangles, tiles = PROFILE.read_scene(
str(mesh_override or out / mesh_name), 0,
mouth[2] - MOUTH_Z_BELOW_M, mouth[2] + MOUTH_Z_ABOVE_M, bounds)
baseline_um, baseline_triangles, baseline_tiles = PROFILE.read_scene(
str(baseline_out / mesh_name), 0, mouth[2] - MOUTH_Z_BELOW_M,
mouth[2] + MOUTH_Z_ABOVE_M, bounds)
if voxel_um != baseline_um:
raise RuntimeError(f"{scene} candidate/control voxel size differs")
route = mouth_route_points(passage, side, scale)
corridor = select_corridor_triangles(triangles, route,
MOUTH_CORRIDOR_HALF_WIDTH_M, MOUTH_CORRIDOR_LENGTH_M)
baseline_corridor = select_corridor_triangles(baseline_triangles, route,
MOUTH_CORRIDOR_HALF_WIDTH_M, MOUTH_CORRIDOR_LENGTH_M)
differential = differential_surface(corridor, baseline_corridor, route, bounds)
metrics = analyze_triangles(corridor, mouth[:2], mouth[2], crop_to_center=False)
baseline_metrics = analyze_triangles(baseline_corridor, mouth[:2], mouth[2],
crop_to_center=False)
full_surface = analyze_triangles(corridor, mouth[:2], mouth[2],
crop_to_center=False, include_sides=True)
proxy_alert = fail(differential, include_planarity=False)
corner_gate = (metrics["corners"] == 0 and full_surface["corners"] == 0
and differential["corners"] == 0)
metrics["all_face_planar_area"] = full_surface["planar_area"]
metrics["all_face_straight"] = full_surface["straight"]
metrics["all_face_corners"] = full_surface["corners"]
metrics["all_face_corner_turn_max"] = full_surface["corner_turn_max"]
print(f"[MouthBuiltShape] scene={scene} mesh={mesh_name} mouth_m=({mouth[0]:.3f},"
f"{mouth[1]:.3f},{mouth[2]:.3f}) voxel_um={voxel_um} tiles={tiles} "
f"route_corridor_m=({MOUTH_CORRIDOR_HALF_WIDTH_M:.2f}m_halfwidth,"
f"{MOUTH_CORRIDOR_LENGTH_M:.1f}m_length) corridor_triangles={len(corridor)} "
f"surface_triangles={metrics['triangles']} planar_patch_m2={metrics['planar_area']:.4f} "
f"straight_run_m={metrics['straight']:.4f} sharp_corners={metrics['corners']} "
f"corner_turn_max_deg={metrics['corner_turn_max']:.2f} "
f"all_face_planar_patch_m2={full_surface['planar_area']:.4f} "
f"all_face_straight_run_m={full_surface['straight']:.4f} "
f"all_face_corners={full_surface['corners']} "
f"all_face_corner_turn_max_deg={full_surface['corner_turn_max']:.2f} "
f"matched_no_support_planar_patch_m2={baseline_metrics['planar_area']:.4f} "
f"matched_no_support_straight_run_m={baseline_metrics['straight']:.4f} "
f"matched_no_support_corners={baseline_metrics['corners']} "
f"control_tiles={baseline_tiles} changed_cells={differential['changed_cells']} "
f"changed_area_m2={differential['changed_area']:.4f} "
f"max_abs_delta_m={differential['max_delta']:.4f} "
f"mean_abs_delta_m={differential['mean_abs_delta']:.4f} "
f"added_planar_patch_m2={differential['planar_area']:.4f} "
f"added_straight_run_m={differential['straight']:.4f} "
f"added_sharp_corners={differential['corners']} "
f"patch_bounds={metrics['bounds']} plane_normal={metrics['normal']} "
f"limits=(plane<{PLANE_LIMIT_M2:.2f}m2 informational,line<={STRAIGHT_LIMIT_M:.2f}m,"
f"corner_turn<{CORNER_TURN_LIMIT_DEG:.0f}deg) "
f"straight_or_planar_proxy={'ALERT' if proxy_alert else 'PASS'} "
f"delta_threshold_m={DELTA_THRESHOLD_M:.2f} status="
f"{'PASS' if corner_gate else 'FAIL'}")
metrics["differential"] = differential
metrics["matched_no_support"] = baseline_metrics
metrics["corner_gate"] = corner_gate
metrics["proxy_alert"] = proxy_alert
return metrics
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--out", type=Path, default=ROOT / "Saved/Linux/l37")
parser.add_argument("--baseline-out", type=Path,
default=ROOT / "Saved/Linux/l37/no-tread-control")
parser.add_argument("--control-out", type=Path, default=ROOT / "Saved/Linux/l36")
parser.add_argument("--scene", choices=tuple(SCENES) + ("all",), default="all")
args = parser.parse_args()
out = args.out if args.out.is_absolute() else ROOT / args.out
baseline_out = (args.baseline_out if args.baseline_out.is_absolute()
else ROOT / args.baseline_out)
control_out = (args.control_out if args.control_out.is_absolute()
else ROOT / args.control_out)
scenes = tuple(SCENES) if args.scene == "all" else (args.scene,)
measured = {scene: audit_scene(scene, out, baseline_out) for scene in scenes}
l36_fi = audit_scene("owner-fi", control_out, baseline_out)
l36_failed = fail(l36_fi["differential"])
print(f"[MouthBuiltShapeL36Control] scene=owner-fi planar_patch_m2="
f"{l36_fi['differential']['planar_area']:.4f} "
f"straight_run_m={l36_fi['differential']['straight']:.4f} "
f"sharp_corners={l36_fi['differential']['corners']} detected={l36_failed} "
f"negative_control={'PASS' if l36_failed else 'FAIL'}")
control = analyze_triangles(synthetic_box((0.0, 0.0), 0.0), (0.0, 0.0), 0.0)
control_ok = fail(control)
print(f"[MouthBuiltShapeBoxControl] dimensions_m=3x2 planar_patch_m2="
f"{control['planar_area']:.4f} straight_run_m={control['straight']:.4f} "
f"sharp_corners={control['corners']} detected={control_ok} "
f"negative_control={'PASS' if control_ok else 'FAIL'}")
candidates_pass = all(metrics["corner_gate"] for metrics in measured.values())
passed = control_ok and l36_failed and candidates_pass
print(f"[MouthBuiltShapeSummary] scenes={len(measured)} candidates_pass="
f"{sum(metrics['corner_gate'] for metrics in measured.values())}/{len(measured)} "
f"L36_control_fail={'PASS' if l36_failed else 'FAIL'} "
f"box_control_fail={'PASS' if control_ok else 'FAIL'} "
f"corner_gate={'PASS' if candidates_pass else 'FAIL'} "
f"straight_or_planar_proxy_alerts={sum(metrics['proxy_alert'] for metrics in measured.values())} "
f"planar_gate=informational_owner_accepted "
f"status={'PASS' if passed else 'FAIL'}")
raise SystemExit(0 if passed else 1)
if __name__ == "__main__":
main()