feat: integrate SimpleCADAPI 2.0.2 CAD workflows

This commit is contained in:
Jerry
2026-08-03 11:17:05 +08:00
parent b5738e9109
commit c3a0f269b7
481 changed files with 110229 additions and 12826 deletions
@@ -0,0 +1 @@
"""Builders for the immutable cross-language Scene 1.0 fixture corpus."""
@@ -0,0 +1,167 @@
"""Presentation, connector binding, and normalized product corpus cases."""
from __future__ import annotations
from copy import deepcopy
from simplecadapi.scene import (
validate_connector_binding,
validate_normalized_product,
validate_presentation,
)
from .common import JsonObject, validation_case
def build_presentation_matrix_cases(
presentation: JsonObject,
) -> list[dict[str, object]]:
orthographic = deepcopy(presentation)
orthographic["cameras"][0].pop("vertical_fov_degrees")
orthographic["cameras"][0].update(
{"projection": "orthographic", "vertical_span": 1000}
)
visible_only = deepcopy(presentation)
visible_only["node_overrides"][0].pop("appearance_name")
appearance_only = deepcopy(presentation)
appearance_only["node_overrides"][0].pop("visible")
neither = deepcopy(presentation)
neither["node_overrides"][0] = {"node_id": "instance/root"}
unknown_projection = deepcopy(presentation)
unknown_projection["cameras"][0]["projection"] = "unknown"
return [
validation_case("valid_orthographic_camera", orthographic, validate_presentation),
validation_case("valid_visible_only_override", visible_only, validate_presentation),
validation_case(
"valid_appearance_only_override", appearance_only, validate_presentation
),
validation_case("empty_node_override", neither, validate_presentation),
validation_case(
"unknown_camera_projection", unknown_projection, validate_presentation
),
]
def build_connector_binding_matrix_cases(
binding: JsonObject,
) -> list[dict[str, object]]:
cases: list[dict[str, object]] = []
for topology_kind, entity_id, flip in (
("FACE", "entity/face/0", True),
("EDGE", "entity/edge/0", True),
("VERTEX", "entity/vertex/0", False),
):
value = deepcopy(binding)
value["target"]["entity_id"] = entity_id
value["target"]["flip"] = flip
value["target"]["expected_source"] = {
"graph_id": "fixture_graph",
"kind": "model_topology",
"node_id": "fixture_body",
"output_slot": 0,
"topo_id": f"topo-{topology_kind.lower()}",
"topology_kind": topology_kind,
}
cases.append(
validation_case(
f"valid_binding_model_topology_{topology_kind.lower()}",
value,
validate_connector_binding,
)
)
named = deepcopy(binding)
named["name"] = "Fixture connector"
cases.append(validation_case("valid_binding_named", named, validate_connector_binding))
missing_name = deepcopy(binding)
del missing_name["name"]
cases.append(
validation_case("missing_binding_name", missing_name, validate_connector_binding)
)
unsupported_version = deepcopy(binding)
unsupported_version["source_model"]["model_schema_version"] = "3.0"
cases.append(
validation_case(
"unsupported_binding_model_schema_version",
unsupported_version,
validate_connector_binding,
)
)
return cases
def build_normalized_product_matrix_cases(
part: JsonObject, assembly: JsonObject
) -> list[dict[str, object]]:
material_part = deepcopy(part)
material_part["material"] = {
"color": [0.2, 0.3, 0.4],
"density": 7.85,
"density_unit": "g/cm3",
"material_id": "steel",
"metadata": {},
"name": "Steel",
}
model_part = deepcopy(part)
model_part["body_source"] = {
"graph_id": "graph",
"kind": "model_output",
"node_id": "body",
"output_slot": 0,
}
placement = deepcopy(part)
placement["connectors"] = [
{
"anchor": {
"anchor_kind": "placement",
"placement": {
"origin": [0, 0, 0],
"x_axis": [1, 0, 0],
"y_axis": [0, 1, 0],
"z_axis": [0, 0, 1],
},
},
"connector_id": "placement",
"name": None,
}
]
forwarded = deepcopy(assembly)
forwarded["connectors"] = [
{
"anchor": {
"anchor_kind": "forwarded",
"offset": None,
"source_component_id": "component_a",
"source_connector_id": "placement",
},
"connector_id": "forwarded",
"name": None,
}
]
forwarded_offset = deepcopy(forwarded)
forwarded_offset["connectors"][0]["anchor"]["offset"] = {
"origin": [0, 0, 5],
"x_axis": [1, 0, 0],
"y_axis": [0, 1, 0],
"z_axis": [0, 0, 1],
}
return [
validation_case(
"valid_complete_material", material_part, validate_normalized_product
),
validation_case(
"valid_model_body_source", model_part, validate_normalized_product
),
validation_case(
"valid_placement_connector", placement, validate_normalized_product
),
validation_case(
"valid_forwarded_connector_null_offset",
forwarded,
validate_normalized_product,
),
validation_case(
"valid_forwarded_connector_offset",
forwarded_offset,
validate_normalized_product,
),
]
@@ -0,0 +1,471 @@
"""Base Scene package and canonical tetrahedron asset construction."""
from __future__ import annotations
import hashlib
import math
import struct
from simplecadapi.scene import canonical_json_bytes, with_scene_revision
from .binary_cases import glb
from .common import content_hash, sha256_hex
IDENTITY = {
"origin": [0, 0, 0],
"x_axis": [1, 0, 0],
"y_axis": [0, 1, 0],
"z_axis": [0, 0, 1],
}
ASSET_TO_SCENE = [1000, 0, 0, 0, 0, 0, -1000, 0, 0, 1000, 0, 0, 0, 0, 0, 1]
def _normalize(vector):
length = math.sqrt(sum(component * component for component in vector))
return tuple(component / length for component in vector)
def _cross(left, right):
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 _frame(origin, x_axis):
x_axis = _normalize(x_axis)
seed = (0.0, 0.0, 1.0) if abs(x_axis[2]) < 0.9 else (0.0, 1.0, 0.0)
projection = sum(left * right for left, right in zip(seed, x_axis))
y_axis = _normalize(
tuple(seed[index] - projection * x_axis[index] for index in range(3))
)
z_axis = _normalize(_cross(x_axis, y_axis))
return {
"origin": list(origin),
"x_axis": list(x_axis),
"y_axis": list(y_axis),
"z_axis": list(z_axis),
}
def _canonical_triangle_asset():
points = {
"a": (0.0, 0.0, 0.0),
"b": (1.0, 0.0, 0.0),
"c": (0.0, 1.0, 0.0),
"d": (0.0, 0.0, 1.0),
}
oriented_faces = {
"abc": ("a", "c", "b"),
"abd": ("a", "b", "d"),
"acd": ("a", "d", "c"),
"bcd": ("b", "c", "d"),
}
blocks = []
for name, corners in oriented_faces.items():
a, b, c = (points[corner] for corner in corners)
normal = _normalize(
_cross(
tuple(b[i] - a[i] for i in range(3)),
tuple(c[i] - a[i] for i in range(3)),
)
)
local_positions = sorted(
(points[corner] for corner in corners),
key=lambda point: struct.pack("<fff", *point),
)
remap = {point: index for index, point in enumerate(local_positions)}
triple = tuple(remap[points[corner]] for corner in corners)
rotations = (triple, triple[1:] + triple[:1], triple[2:] + triple[:2])
triple = min(rotations)
block = b"".join(
struct.pack("<ffffff", *point, *normal) for point in local_positions
) + struct.pack("<HHH", *triple)
blocks.append(
(
hashlib.sha256(b"\x01" + block).digest(),
block,
name,
local_positions,
normal,
triple,
)
)
blocks.sort(key=lambda item: (item[0], item[1]))
positions = []
normals = []
indices = []
group_names = []
for _render_key, _block, name, local_positions, normal, triple in blocks:
offset = len(positions)
positions.extend(local_positions)
normals.extend([normal] * len(local_positions))
indices.extend(offset + index for index in triple)
group_names.append(name)
return glb("triangle", positions, indices, normals), group_names
def _canonical_line_asset():
points = {
"a": (0.0, 0.0, 0.0),
"b": (1.0, 0.0, 0.0),
"c": (0.0, 1.0, 0.0),
"d": (0.0, 0.0, 1.0),
}
blocks = []
for name in ("ab", "ac", "ad", "bc", "bd", "cd"):
local_positions = sorted(
(points[name[0]], points[name[1]]),
key=lambda point: struct.pack("<fff", *point),
)
block = b"".join(struct.pack("<fff", *point) for point in local_positions)
block += struct.pack("<HH", 0, 1)
blocks.append(
(
hashlib.sha256(b"\x02" + block).digest(),
block,
name,
local_positions,
)
)
blocks.sort(key=lambda item: (item[0], item[1]))
positions = []
indices = []
group_names = []
for _render_key, _block, name, local_positions in blocks:
offset = len(positions)
positions.extend(local_positions)
indices.extend((offset, offset + 1))
group_names.append(name)
return glb("line", positions, indices), group_names
def _entity(kind, index, parents, children, geometry, properties, frame, status):
return {
"child_entity_ids": sorted(children),
"connector_binding_status": status,
"entity_id": f"entity/{kind}/{index}",
"evaluated_tags": [],
"geometry": geometry,
"kind": kind,
"parent_entity_ids": sorted(parents),
"properties": properties,
"render_status": "rendered",
"sdk_connector_frame": frame,
"sdk_metadata": {},
"semantic_binding_ids": [],
"source": {"kind": "unbound"},
}
def build_scene_package():
triangle_glb, face_order = _canonical_triangle_asset()
line_glb, edge_order = _canonical_line_asset()
geometry_hash = content_hash(triangle_glb)
edge_hash = content_hash(line_glb)
face_ids = {name: f"entity/face/{index}" for index, name in enumerate(face_order)}
edge_ids = {name: f"entity/edge/{index}" for index, name in enumerate(edge_order)}
vertex_ids = {name: f"entity/vertex/{index}" for index, name in enumerate("abcd")}
cad_points = {
"a": (0.0, 0.0, 0.0),
"b": (1000.0, 0.0, 0.0),
"c": (0.0, 0.0, 1000.0),
"d": (0.0, -1000.0, 0.0),
}
face_vertices = {
"abc": "abc",
"abd": "abd",
"acd": "acd",
"bcd": "bcd",
}
face_edges = {
"abc": ("ab", "ac", "bc"),
"abd": ("ab", "ad", "bd"),
"acd": ("ac", "ad", "cd"),
"bcd": ("bc", "bd", "cd"),
}
edge_faces = {
edge: tuple(name for name, edges in face_edges.items() if edge in edges)
for edge in edge_order
}
vertex_edges = {
vertex: tuple(edge for edge in edge_order if vertex in edge)
for vertex in "abcd"
}
entities = []
entities.append(
_entity(
"solid",
0,
[],
list(face_ids.values()),
{"type": "brep_solid"},
{
"bounds": {"max": [1000, 0, 1000], "min": [0, -1000, 0]},
"centroid": [250, -250, 250],
"quality": "kernel_evaluated",
"surface_area": 1_500_000 + 500_000 * math.sqrt(3),
"volume": 1_000_000_000 / 6,
},
None,
"not_applicable",
)
)
face_normals = {
"abc": (0.0, 1.0, 0.0),
"abd": (0.0, 0.0, -1.0),
"acd": (-1.0, 0.0, 0.0),
"bcd": _normalize((1.0, -1.0, 1.0)),
}
for name in face_order:
vertices = [cad_points[vertex] for vertex in face_vertices[name]]
minimum = [min(point[axis] for point in vertices) for axis in range(3)]
maximum = [max(point[axis] for point in vertices) for axis in range(3)]
centroid = [sum(point[axis] for point in vertices) / 3 for axis in range(3)]
edge_vector = tuple(vertices[1][axis] - vertices[0][axis] for axis in range(3))
frame = _frame(centroid, edge_vector)
frame["z_axis"] = list(face_normals[name])
frame["y_axis"] = list(_normalize(_cross(face_normals[name], frame["x_axis"])))
entities.append(
_entity(
"face",
int(face_ids[name].rsplit("/", 1)[1]),
["entity/solid/0"],
[edge_ids[edge] for edge in face_edges[name]],
{
"normal": list(face_normals[name]),
"origin": centroid,
"type": "plane",
"x_direction": frame["x_axis"],
},
{
"area": 500_000 * (math.sqrt(3) if name == "bcd" else 1),
"bounds": {"max": maximum, "min": minimum},
"centroid": centroid,
"orientation": "forward",
"quality": "kernel_evaluated",
},
frame,
"owner_not_part",
)
)
for name in edge_order:
start, end = (cad_points[vertex] for vertex in name)
direction = tuple(end[axis] - start[axis] for axis in range(3))
midpoint = [(start[axis] + end[axis]) / 2 for axis in range(3)]
minimum = [min(start[axis], end[axis]) for axis in range(3)]
maximum = [max(start[axis], end[axis]) for axis in range(3)]
entities.append(
_entity(
"edge",
int(edge_ids[name].rsplit("/", 1)[1]),
[face_ids[face] for face in edge_faces[name]],
[vertex_ids[vertex] for vertex in name],
{
"direction": list(_normalize(direction)),
"origin": list(start),
"type": "line",
},
{
"bounds": {"max": maximum, "min": minimum},
"centroid": midpoint,
"length": math.sqrt(
sum(component * component for component in direction)
),
"quality": "kernel_evaluated",
},
_frame(midpoint, direction),
"owner_not_part",
)
)
for name in "abcd":
point = cad_points[name]
entities.append(
_entity(
"vertex",
int(vertex_ids[name].rsplit("/", 1)[1]),
[edge_ids[edge] for edge in vertex_edges[name]],
[],
{"position": list(point), "type": "point"},
{
"bounds": {"max": list(point), "min": list(point)},
"position": list(point),
"quality": "kernel_evaluated",
},
{**IDENTITY, "origin": list(point)},
"owner_not_part",
)
)
definition_id = "definition/root/shape/manual/fixture"
entity_asset = {
"definition_id": definition_id,
"edge_asset_id": edge_hash,
"edge_groups": [
{
"entity_id": edge_ids[name],
"first_index": 2 * index,
"group_id": index,
"index_count": 2,
"mesh_index": 0,
"primitive_index": 0,
}
for index, name in enumerate(edge_order)
],
"entities": entities,
"face_groups": [
{
"entity_id": face_ids[name],
"first_index": 3 * index,
"group_id": index,
"index_count": 3,
"mesh_index": 0,
"primitive_index": 0,
}
for index, name in enumerate(face_order)
],
"geometry_asset_id": geometry_hash,
"geometry_engine": {
"name": "OpenCascade",
"profile": "ocp-evaluated-properties-1",
"version": "fixture",
},
"schema_version": "1.0",
}
entity_asset["entities"].sort(
key=lambda entity: entity["entity_id"].encode("utf-8")
)
entity_bytes = canonical_json_bytes(entity_asset)
entity_hash = content_hash(entity_bytes)
appearance_draft = {
"alpha_mode": "opaque",
"base_color": [0.72, 0.75, 0.78, 1],
"double_sided": False,
"edge_color": [0.08, 0.09, 0.1, 1],
"metallic": 0,
"name": None,
"roughness": 0.55,
"sdk_metadata": {},
"source": None,
}
appearance_id = "appearance/evaluated/" + sha256_hex(
canonical_json_bytes(appearance_draft)
)
appearance = {"appearance_id": appearance_id, **appearance_draft}
geometry_uri = f"geometry/sha256-{geometry_hash.removeprefix('sha256:')}.glb"
edge_uri = f"edges/sha256-{edge_hash.removeprefix('sha256:')}.glb"
entity_uri = f"entities/sha256-{entity_hash.removeprefix('sha256:')}.json"
bounds = {"max": [1000, 0, 1000], "min": [0, -1000, 0]}
scene = with_scene_revision(
{
"annotations": [],
"appearances": [appearance],
"cameras": [],
"compile_options": {
"angular_tolerance": 0.22,
"embed_presentation": False,
"embed_source": False,
"linear_tolerance": 0.35,
},
"connectors": [],
"coordinate_system": {
"handedness": "right",
"length_unit": "mm",
"up_axis": "+Z",
},
"definitions": [
{
"appearance_id": appearance_id,
"definition_id": definition_id,
"edge_asset_id": edge_hash,
"entity_asset_id": entity_hash,
"geometry_asset_id": geometry_hash,
"kind": "shape",
"name": None,
"sdk_metadata": {},
"source": {
"kind": "manual",
"root_id": "root",
"source_id": "fixture",
},
}
],
"diagnostics": [],
"edge_assets": [
{
"asset_id": edge_hash,
"asset_to_scene": ASSET_TO_SCENE,
"byte_length": len(line_glb),
"content_hash": edge_hash,
"media_type": "model/gltf-binary",
"scene_local_bounds": bounds,
"tessellation": {"linear_tolerance": 0.35},
"uri": edge_uri,
}
],
"entity_assets": [
{
"byte_length": len(entity_bytes),
"content_hash": entity_hash,
"entity_asset_id": entity_hash,
"media_type": "application/vnd.simplecad.entities+json",
"uri": entity_uri,
}
],
"extensions": {},
"extensions_required": [],
"extensions_used": [],
"generator": {
"name": "simplecadapi",
"ocp_bindings_version": "fixture",
"ocp_version": "fixture",
"platform_tag": "fixture",
"profile": "scene-1.0-ocp-glb-2",
"python_abi": "fixture",
"simplecadapi_version": "fixture",
"toolchain_hash": "sha256:" + "0" * 64,
},
"geometry_assets": [
{
"asset_id": geometry_hash,
"asset_to_scene": ASSET_TO_SCENE,
"byte_length": len(triangle_glb),
"content_hash": geometry_hash,
"media_type": "model/gltf-binary",
"scene_local_bounds": bounds,
"tessellation": {
"angular_tolerance": 0.22,
"linear_tolerance": 0.35,
},
"uri": geometry_uri,
}
],
"lights": [],
"nodes": [
{
"appearance_override_id": None,
"definition_id": definition_id,
"name": None,
"node_id": "instance/root",
"order": 0,
"parent_node_id": None,
"sdk_metadata": {},
"selectable": True,
"source": {"kind": "shape_root", "root_id": "root"},
"transform": IDENTITY,
"visible": True,
}
],
"scene_id": "fixture",
"schema_version": "1.0",
"source": {"kind": "manual", "source_id": "fixture"},
}
)
scene_bytes = canonical_json_bytes(scene)
blobs = {
geometry_uri: triangle_glb,
edge_uri: line_glb,
entity_uri: entity_bytes,
}
return scene, scene_bytes, entity_asset, entity_bytes, blobs
@@ -0,0 +1,703 @@
"""GLB and ZIP fixture construction, mutation, and case records."""
from __future__ import annotations
import binascii
import struct
import zlib
from copy import deepcopy
from typing import Callable
from simplecadapi.scene import (
canonical_json_bytes,
canonical_zip_bytes,
parse_strict_json,
preflight_glb,
preflight_zip_bytes,
)
from .common import JsonObject, b64
LOCAL = struct.Struct("<IHHHHHIIIHH")
CENTRAL = struct.Struct("<IHHHHHHIIIHHHHHII")
EOCD = struct.Struct("<IHHHHIIH")
def _f32(value: float) -> float:
result = struct.unpack("<f", struct.pack("<f", value))[0]
return 0.0 if result == 0 else result
def glb(
kind: str,
positions: list[tuple[float, float, float]],
indices: list[int],
normals: list[tuple[float, float, float]] | None = None,
) -> bytes:
positions = [tuple(_f32(component) for component in point) for point in positions]
position_bytes = b"".join(struct.pack("<fff", *point) for point in positions)
chunks = [position_bytes]
views = [
{"buffer": 0, "byteLength": len(position_bytes), "byteOffset": 0, "target": 34962}
]
accessors = [
{
"bufferView": 0,
"componentType": 5126,
"count": len(positions),
"max": [max(point[axis] for point in positions) for axis in range(3)],
"min": [min(point[axis] for point in positions) for axis in range(3)],
"type": "VEC3",
}
]
if kind == "triangle":
assert normals is not None
normals = [tuple(_f32(component) for component in vector) for vector in normals]
normal_bytes = b"".join(struct.pack("<fff", *vector) for vector in normals)
views.append(
{
"buffer": 0,
"byteLength": len(normal_bytes),
"byteOffset": len(position_bytes),
"target": 34962,
}
)
accessors.append(
{
"bufferView": 1,
"componentType": 5126,
"count": len(normals),
"type": "VEC3",
}
)
chunks.append(normal_bytes)
index_view = 2
attributes = {"NORMAL": 1, "POSITION": 0}
mode = 4
else:
index_view = 1
attributes = {"POSITION": 0}
mode = 1
prefix = b"".join(chunks)
prefix += b"\0" * ((-len(prefix)) % 4)
component_type = 5123 if len(positions) <= 65536 else 5125
index_format = "H" if component_type == 5123 else "I"
index_bytes = struct.pack("<" + index_format * len(indices), *indices)
views.append(
{
"buffer": 0,
"byteLength": len(index_bytes),
"byteOffset": len(prefix),
"target": 34963,
}
)
accessors.append(
{
"bufferView": index_view,
"componentType": component_type,
"count": len(indices),
"type": "SCALAR",
}
)
unpadded_bin = prefix + index_bytes
document = {
"accessors": accessors,
"asset": {"generator": "SimpleCAD Scene GLB Profile 1", "version": "2.0"},
"bufferViews": views,
"buffers": [{"byteLength": len(unpadded_bin)}],
"meshes": [
{"primitives": [{"attributes": attributes, "indices": index_view, "mode": mode}]}
],
"nodes": [{"mesh": 0}],
"scene": 0,
"scenes": [{"nodes": [0]}],
}
json_bytes = canonical_json_bytes(document)
json_chunk = json_bytes + b" " * ((-len(json_bytes)) % 4)
bin_chunk = unpadded_bin + b"\0" * ((-len(unpadded_bin)) % 4)
total_length = 12 + 8 + len(json_chunk) + 8 + len(bin_chunk)
return b"".join(
(
struct.pack("<III", 0x46546C67, 2, total_length),
struct.pack("<II", len(json_chunk), 0x4E4F534A),
json_chunk,
struct.pack("<II", len(bin_chunk), 0x004E4942),
bin_chunk,
)
)
def replace_glb_json(data: bytes, mutate: Callable[[JsonObject], None]) -> bytes:
json_length = struct.unpack_from("<I", data, 12)[0]
document = parse_strict_json(data[20 : 20 + json_length].rstrip(b" "))
mutate(document)
replacement = canonical_json_bytes(document)
replacement += b" " * ((-len(replacement)) % 4)
bin_header = 20 + json_length
bin_chunk = data[bin_header:]
return b"".join(
(
struct.pack(
"<III", 0x46546C67, 2, 12 + 8 + len(replacement) + len(bin_chunk)
),
struct.pack("<II", len(replacement), 0x4E4F534A),
replacement,
bin_chunk,
)
)
def _deflate_zip(members: dict[str, bytes]) -> bytes:
local_parts: list[bytes] = []
central_parts: list[bytes] = []
local_offset = 0
for name in sorted(members, key=lambda value: value.encode("ascii")):
name_bytes = name.encode("ascii")
payload = members[name]
compressor = zlib.compressobj(level=9, wbits=-zlib.MAX_WBITS)
compressed = compressor.compress(payload) + compressor.flush()
crc = binascii.crc32(payload) & 0xFFFFFFFF
local_header = LOCAL.pack(
0x04034B50,
20,
0x0800,
8,
0,
0x0021,
crc,
len(compressed),
len(payload),
len(name_bytes),
0,
)
local_parts.extend((local_header, name_bytes, compressed))
central_parts.extend(
(
CENTRAL.pack(
0x02014B50,
0x0314,
20,
0x0800,
8,
0,
0x0021,
crc,
len(compressed),
len(payload),
len(name_bytes),
0,
0,
0,
0,
0x81A40000,
local_offset,
),
name_bytes,
)
)
local_offset += len(local_header) + len(name_bytes) + len(compressed)
central = b"".join(central_parts)
return b"".join(local_parts) + central + EOCD.pack(
0x06054B50,
0,
0,
len(members),
len(members),
len(central),
local_offset,
0,
)
def _replace_u16(data: bytes, offset: int, value: int) -> bytes:
result = bytearray(data)
struct.pack_into("<H", result, offset, value)
return bytes(result)
def _replace_u32(data: bytes, offset: int, value: int) -> bytes:
result = bytearray(data)
struct.pack_into("<I", result, offset, value)
return bytes(result)
def _glb_bin_start(data: bytes) -> int:
return 28 + struct.unpack_from("<I", data, 12)[0]
def _replace_glb_bin_u32(data: bytes, relative_offset: int, value: int) -> bytes:
return _replace_u32(data, _glb_bin_start(data) + relative_offset, value)
def _set_path(value: object, path: tuple[object, ...], replacement: object) -> None:
current = value
for part in path[:-1]:
current = current[part] # type: ignore[index]
current[path[-1]] = replacement # type: ignore[index]
def _add_path(value: object, path: tuple[object, ...], key: str, item: object) -> None:
current = value
for part in path:
current = current[part] # type: ignore[index]
current[key] = item # type: ignore[index]
def _append_path(value: object, path: tuple[object, ...], item: object) -> None:
current = value
for part in path:
current = current[part] # type: ignore[index]
current.append(item) # type: ignore[attr-defined]
def _nonminimal_glb_json_padding(data: bytes) -> bytes:
json_length = struct.unpack_from("<I", data, 12)[0]
json_end = 20 + json_length
result = data[:json_end] + b" " + data[json_end:]
result = _replace_u32(result, 8, len(result))
return _replace_u32(result, 12, json_length + 4)
def _append_glb_bin_word(data: bytes, value: int = 0) -> bytes:
json_length = struct.unpack_from("<I", data, 12)[0]
bin_header = 20 + json_length
bin_length = struct.unpack_from("<I", data, bin_header)[0]
result = data + struct.pack("<I", value)
result = _replace_u32(result, 8, len(result))
return _replace_u32(result, bin_header, bin_length + 4)
def _f32_from_bits(value: int) -> float:
return struct.unpack("<f", struct.pack("<I", value))[0]
def _glb_case(
name: str,
payload: bytes,
expected_kind: str,
valid: bool,
) -> dict[str, object]:
try:
info = preflight_glb(payload, expected_kind=expected_kind)
error = None
actual_valid = True
actual_kind = info.kind
except ValueError as exc:
error = str(exc)
actual_valid = False
actual_kind = None
assert actual_valid == valid, name
return {
"error": error,
"expected_kind": expected_kind,
"kind": actual_kind,
"name": name,
"payload_base64": b64(payload),
"valid": valid,
}
def _zip_case(
name: str,
payload: bytes,
valid: bool,
used_deflate: bool | None,
) -> dict[str, object]:
try:
info = preflight_zip_bytes(payload)
error = None
actual_valid = True
actual_deflate = info.used_deflate
except ValueError as exc:
error = str(exc)
actual_valid = False
actual_deflate = None
assert actual_valid == valid, name
assert actual_deflate == used_deflate, name
return {
"error": error,
"name": name,
"payload_base64": b64(payload),
"used_deflate": used_deflate,
"valid": valid,
}
def _zip_layout(data: bytes) -> tuple[list[tuple[int, int, int]], int, int]:
eocd = len(data) - EOCD.size
count = struct.unpack_from("<H", data, eocd + 10)[0]
central = struct.unpack_from("<I", data, eocd + 16)[0]
entries: list[tuple[int, int, int]] = []
cursor = central
for _index in range(count):
name_length = struct.unpack_from("<H", data, cursor + 28)[0]
extra_length = struct.unpack_from("<H", data, cursor + 30)[0]
comment_length = struct.unpack_from("<H", data, cursor + 32)[0]
local_offset = struct.unpack_from("<I", data, cursor + 42)[0]
entries.append((cursor, local_offset, name_length))
cursor += CENTRAL.size + name_length + extra_length + comment_length
return entries, central, eocd
def _replace_zip_name(data: bytes, index: int, name: bytes) -> bytes:
entries, _central, _eocd = _zip_layout(data)
central_offset, local_offset, name_length = entries[index]
assert len(name) == name_length
assert struct.unpack_from("<H", data, local_offset + 26)[0] == name_length
result = bytearray(data)
result[local_offset + LOCAL.size : local_offset + LOCAL.size + name_length] = name
result[central_offset + CENTRAL.size : central_offset + CENTRAL.size + name_length] = name
return bytes(result)
def _zip_with_prefix(data: bytes) -> bytes:
entries, central, eocd = _zip_layout(data)
result = bytearray(b"x" + data)
for central_offset, local_offset, _name_length in entries:
struct.pack_into("<I", result, central_offset + 1 + 42, local_offset + 1)
struct.pack_into("<I", result, eocd + 1 + 16, central + 1)
return bytes(result)
def _zip_with_local_gap(data: bytes) -> bytes:
_entries, central, eocd = _zip_layout(data)
result = bytearray(data[:central] + b"x" + data[central:])
struct.pack_into("<I", result, eocd + 1 + 16, central + 1)
return bytes(result)
def build_binary_cases(
triangle_glb: bytes,
line_glb: bytes,
members: dict[str, bytes],
) -> tuple[bytes, bytes, list[dict[str, object]], list[dict[str, object]]]:
canonical_zip = canonical_zip_bytes(members)
deflate_zip = _deflate_zip(members)
string_bounds_glb = replace_glb_json(
triangle_glb,
lambda document: document["accessors"][0]["min"].__setitem__(0, "0"),
)
boolean_scene_glb = replace_glb_json(
triangle_glb, lambda document: document.__setitem__("scene", False)
)
simple_triangle = glb(
"triangle",
[(0, 0, 0), (1, 0, 0), (0, 1, 0)],
[0, 1, 2],
[(0, 0, 1)] * 3,
)
float_extreme_line = glb(
"line",
[
(
_f32_from_bits(0xFF7FFFFF),
_f32_from_bits(0x80000001),
_f32_from_bits(0x007FFFFF),
),
(
_f32_from_bits(0x80800000),
0.0,
_f32_from_bits(0x00000001),
),
(
_f32_from_bits(0x00800000),
_f32_from_bits(0x007FFFFF),
_f32_from_bits(0x7F7FFFFF),
),
],
[0, 1, 1, 2],
)
glb_payloads: list[tuple[str, bytes, str, bool]] = [
("valid_triangle", triangle_glb, "triangle", True),
("valid_line", line_glb, "line", True),
("valid_float32_finite_extremes", float_extreme_line, "line", True),
("string_accessor_bounds", string_bounds_glb, "triangle", False),
("boolean_scene_index", boolean_scene_glb, "triangle", False),
]
for name, base, path, key, value in (
("root_extensions_used", triangle_glb, (), "extensionsUsed", []),
("asset_extras", triangle_glb, ("asset",), "extras", {}),
("node_name", triangle_glb, ("nodes", 0), "name", "node"),
("scene_name", triangle_glb, ("scenes", 0), "name", "scene"),
("mesh_name", triangle_glb, ("meshes", 0), "name", "mesh"),
(
"primitive_material",
triangle_glb,
("meshes", 0, "primitives", 0),
"material",
0,
),
("buffer_uri", triangle_glb, ("buffers", 0), "uri", "buffer.bin"),
("buffer_view_byte_stride", triangle_glb, ("bufferViews", 0), "byteStride", 12),
("position_accessor_byte_offset", triangle_glb, ("accessors", 0), "byteOffset", 0),
("normal_accessor_normalized", triangle_glb, ("accessors", 1), "normalized", False),
("index_accessor_sparse", triangle_glb, ("accessors", 2), "sparse", {}),
):
glb_payloads.append(
(
name,
replace_glb_json(
base,
lambda document, path=path, key=key, value=value: _add_path(
document, path, key, value
),
),
"triangle",
False,
)
)
for name, base, path, value, kind in (
("empty_position_accessor", line_glb, ("accessors", 0, "count"), 0, "line"),
("empty_index_accessor", line_glb, ("accessors", 1, "count"), 0, "line"),
(
"wrong_triangle_mode",
triangle_glb,
("meshes", 0, "primitives", 0, "mode"),
1,
"triangle",
),
(
"wrong_position_component_type",
triangle_glb,
("accessors", 0, "componentType"),
5125,
"triangle",
),
("wrong_normal_count", triangle_glb, ("accessors", 1, "count"), 1, "triangle"),
(
"wrong_index_component_type",
triangle_glb,
("accessors", 2, "componentType"),
5125,
"triangle",
),
("buffer_view_gap", triangle_glb, ("bufferViews", 1, "byteOffset"), 4, "triangle"),
(
"buffer_view_wrong_target",
triangle_glb,
("bufferViews", 0, "target"),
34963,
"triangle",
),
(
"buffer_view_wrong_length",
triangle_glb,
("bufferViews", 0, "byteLength"),
4,
"triangle",
),
):
glb_payloads.append(
(
name,
replace_glb_json(
base,
lambda document, path=path, value=value: _set_path(
document, path, value
),
),
kind,
False,
)
)
glb_payloads.extend(
(
("bad_glb_magic", _replace_u32(triangle_glb, 0, 0), "triangle", False),
("bad_glb_version", _replace_u32(triangle_glb, 4, 1), "triangle", False),
(
"bad_glb_total_length",
_replace_u32(triangle_glb, 8, len(triangle_glb) + 4),
"triangle",
False,
),
("bad_json_chunk_type", _replace_u32(triangle_glb, 16, 0), "triangle", False),
(
"nonminimal_json_padding",
_nonminimal_glb_json_padding(triangle_glb),
"triangle",
False,
),
(
"bad_bin_chunk_type",
_replace_u32(
triangle_glb,
24 + struct.unpack_from("<I", triangle_glb, 12)[0],
0,
),
"triangle",
False,
),
(
"extra_accessor",
replace_glb_json(
triangle_glb,
lambda document: _append_path(
document,
("accessors",),
deepcopy(document["accessors"][0]),
),
),
"triangle",
False,
),
(
"buffer_unpadded_length",
replace_glb_json(
triangle_glb,
lambda document: _set_path(
document,
("buffers", 0, "byteLength"),
document["buffers"][0]["byteLength"] + 4,
),
),
"triangle",
False,
),
("bin_extra_zero_word", _append_glb_bin_word(triangle_glb), "triangle", False),
(
"bin_nonzero_padding",
simple_triangle[:-1] + b"\x01",
"triangle",
False,
),
(
"position_negative_zero",
_replace_glb_bin_u32(simple_triangle, 0, 0x80000000),
"triangle",
False,
),
(
"position_positive_infinity",
_replace_glb_bin_u32(simple_triangle, 0, 0x7F800000),
"triangle",
False,
),
(
"position_negative_infinity",
_replace_glb_bin_u32(simple_triangle, 0, 0xFF800000),
"triangle",
False,
),
(
"position_quiet_nan",
_replace_glb_bin_u32(simple_triangle, 0, 0x7FC00000),
"triangle",
False,
),
(
"normal_negative_zero",
_replace_glb_bin_u32(simple_triangle, 36, 0x80000000),
"triangle",
False,
),
(
"normal_nonfinite",
_replace_glb_bin_u32(simple_triangle, 44, 0x7FC00000),
"triangle",
False,
),
(
"normal_norm_below_lower_limit",
_replace_glb_bin_u32(simple_triangle, 44, 0x3F7FFFEF),
"triangle",
False,
),
(
"normal_norm_above_upper_limit",
_replace_glb_bin_u32(simple_triangle, 44, 0x3F800009),
"triangle",
False,
),
)
)
valid_normal_boundaries = _replace_glb_bin_u32(simple_triangle, 44, 0x3F7FFFF0)
valid_normal_boundaries = _replace_glb_bin_u32(valid_normal_boundaries, 56, 0x3F800008)
glb_payloads.append(
("valid_normal_norm_inner_neighbors", valid_normal_boundaries, "triangle", True)
)
tiny_zip = canonical_zip_bytes({"other.json": b"x", "scene.json": b"{}"})
tiny_entries, tiny_central, tiny_eocd = _zip_layout(tiny_zip)
first_central, first_local, _first_name_length = tiny_entries[0]
first_payload = first_local + LOCAL.size + struct.unpack_from("<H", tiny_zip, first_local + 26)[0]
crc_zip = bytearray(tiny_zip)
crc_zip[first_payload] ^= 1
exact_ratio_zip = _deflate_zip({"scene.json": b"a" * 1200})
over_ratio_zip = _deflate_zip({"scene.json": b"a" * 1201})
zip_payloads: list[tuple[str, bytes, bool, bool | None]] = [
("canonical_stored", canonical_zip, True, False),
("allowlisted_deflate", deflate_zip, True, True),
("exact_member_compression_ratio", exact_ratio_zip, True, True),
("over_member_compression_ratio", over_ratio_zip, False, None),
("crc_mismatch", bytes(crc_zip), False, None),
("trailing_data", tiny_zip + b"x", False, None),
("leading_prefix", _zip_with_prefix(tiny_zip), False, None),
("local_record_gap", _zip_with_local_gap(tiny_zip), False, None),
("case_collision", _replace_zip_name(tiny_zip, 0, b"SCENE.JSON"), False, None),
("path_traversal", _replace_zip_name(tiny_zip, 0, b"a/../b.txt"), False, None),
("backslash_name", _replace_zip_name(tiny_zip, 0, b"other\\json"), False, None),
("nul_name", _replace_zip_name(tiny_zip, 0, b"other.\0son"), False, None),
("bad_eocd_signature", _replace_u32(tiny_zip, tiny_eocd, 0), False, None),
("eocd_comment", _replace_u16(tiny_zip, tiny_eocd + 20, 1), False, None),
("multi_disk", _replace_u16(tiny_zip, tiny_eocd + 4, 1), False, None),
(
"member_count_over_limit",
_replace_u16(
_replace_u16(tiny_zip, tiny_eocd + 8, 50_001),
tiny_eocd + 10,
50_001,
),
False,
None,
),
(
"central_offset_mismatch",
_replace_u32(tiny_zip, tiny_eocd + 16, tiny_central + 1),
False,
None,
),
("bad_central_signature", _replace_u32(tiny_zip, first_central, 0), False, None),
("central_made_by", _replace_u16(tiny_zip, first_central + 4, 20), False, None),
("central_needed", _replace_u16(tiny_zip, first_central + 6, 45), False, None),
("central_flags", _replace_u16(tiny_zip, first_central + 8, 0x0808), False, None),
("central_method", _replace_u16(tiny_zip, first_central + 10, 99), False, None),
("central_time", _replace_u16(tiny_zip, first_central + 12, 1), False, None),
("central_date", _replace_u16(tiny_zip, first_central + 14, 0), False, None),
("central_extra", _replace_u16(tiny_zip, first_central + 30, 1), False, None),
("central_comment", _replace_u16(tiny_zip, first_central + 32, 1), False, None),
("central_disk_start", _replace_u16(tiny_zip, first_central + 34, 1), False, None),
("central_internal_attr", _replace_u16(tiny_zip, first_central + 36, 1), False, None),
("central_external_attr", _replace_u32(tiny_zip, first_central + 38, 0), False, None),
("central_crc", _replace_u32(tiny_zip, first_central + 16, 0), False, None),
(
"central_compressed_size",
_replace_u32(tiny_zip, first_central + 20, 2),
False,
None,
),
(
"central_uncompressed_size",
_replace_u32(tiny_zip, first_central + 24, 2),
False,
None,
),
("central_local_offset", _replace_u32(tiny_zip, first_central + 42, 1), False, None),
("local_signature", _replace_u32(tiny_zip, first_local, 0), False, None),
("local_needed", _replace_u16(tiny_zip, first_local + 4, 45), False, None),
("local_flags", _replace_u16(tiny_zip, first_local + 6, 0), False, None),
("local_method", _replace_u16(tiny_zip, first_local + 8, 8), False, None),
("local_time", _replace_u16(tiny_zip, first_local + 10, 1), False, None),
("local_date", _replace_u16(tiny_zip, first_local + 12, 0), False, None),
("local_crc", _replace_u32(tiny_zip, first_local + 14, 0), False, None),
("local_compressed_size", _replace_u32(tiny_zip, first_local + 18, 2), False, None),
("local_uncompressed_size", _replace_u32(tiny_zip, first_local + 22, 2), False, None),
("local_extra", _replace_u16(tiny_zip, first_local + 28, 1), False, None),
]
return (
canonical_zip,
deflate_zip,
[_glb_case(*case) for case in glb_payloads],
[_zip_case(*case) for case in zip_payloads],
)
@@ -0,0 +1,145 @@
"""Shared fixture construction and package-record helpers."""
from __future__ import annotations
import base64
import hashlib
from copy import deepcopy
from typing import Any, Callable
from simplecadapi.scene import (
canonical_json_bytes,
validate_scene_package,
)
JsonObject = dict[str, Any]
ScenePackage = tuple[JsonObject, JsonObject, dict[str, bytes]]
Validator = Callable[[Any], Any]
IDENTITY = {
"origin": [0, 0, 0],
"x_axis": [1, 0, 0],
"y_axis": [0, 1, 0],
"z_axis": [0, 0, 1],
}
def b64(value: bytes) -> str:
return base64.b64encode(value).decode("ascii")
def sha256_hex(value: bytes) -> str:
return hashlib.sha256(value).hexdigest()
def content_hash(value: bytes) -> str:
return "sha256:" + sha256_hex(value)
def first_issue(report: Any) -> dict[str, str] | None:
issue = report.first_error
return None if issue is None else {"code": issue.code, "path": issue.path}
def report_case(name: str, payload: bytes, report: Any) -> dict[str, object]:
return {
"expected": first_issue(report),
"name": name,
"payload_base64": b64(payload),
"valid": report.valid,
}
def transform(origin: list[float] | None = None) -> JsonObject:
value = deepcopy(IDENTITY)
if origin is not None:
value["origin"] = origin
return value
def validation_case(
name: str, value: Any, validator: Validator
) -> dict[str, object]:
payload = value if isinstance(value, bytes) else canonical_json_bytes(value)
return report_case(name, payload, validator(payload))
def replace_entity_sidecar(
scene: JsonObject,
entity: JsonObject,
blobs: dict[str, bytes],
) -> ScenePackage:
entity_bytes = canonical_json_bytes(entity)
entity_hash = content_hash(entity_bytes)
entity_uri = f"entities/sha256-{entity_hash.removeprefix('sha256:')}.json"
entity_record = scene["entity_assets"][0]
old_uri = entity_record["uri"]
entity_record.update(
{
"byte_length": len(entity_bytes),
"content_hash": entity_hash,
"entity_asset_id": entity_hash,
"uri": entity_uri,
}
)
for definition in scene["definitions"]:
if definition["kind"] in {"part", "shape"}:
definition["entity_asset_id"] = entity_hash
updated_blobs = {uri: payload for uri, payload in blobs.items() if uri != old_uri}
updated_blobs[entity_uri] = entity_bytes
return scene, entity, updated_blobs
def set_product_material_appearance(
scene: JsonObject, material_id: str = "fixture_material"
) -> None:
appearance = scene["appearances"][0]
appearance["name"] = "Fixture material"
appearance["source"] = {
"kind": "product_material",
"material_id": material_id,
"root_id": "root",
}
draft = dict(appearance)
draft.pop("appearance_id")
appearance_id = "appearance/evaluated/" + sha256_hex(canonical_json_bytes(draft))
appearance["appearance_id"] = appearance_id
for definition in scene["definitions"]:
if definition["kind"] in {"part", "shape"}:
definition["appearance_id"] = appearance_id
def inline_package_case(
name: str,
scene: JsonObject,
blobs: dict[str, bytes],
blob_pool: dict[str, str],
) -> dict[str, object]:
scene_bytes = canonical_json_bytes(scene)
report = validate_scene_package(scene_bytes, blobs)
for uri, payload in blobs.items():
encoded = b64(payload)
previous = blob_pool.setdefault(uri, encoded)
if previous != encoded:
raise AssertionError(f"one fixture URI resolved to different bytes: {uri}")
return {
"blob_uris": sorted(blobs),
"expected": first_issue(report),
"manifest_base64": b64(scene_bytes),
"name": name,
"valid": report.valid,
}
def scene_shape_case(
name: str,
scene: JsonObject,
blobs: dict[str, bytes],
expected: dict[str, object],
blob_pool: dict[str, str],
) -> dict[str, object]:
package = inline_package_case(name, scene, blobs, blob_pool)
if not package["valid"]:
raise AssertionError(f"invalid scene shape fixture {name}: {package['expected']}")
return {**package, "expected_shape": expected}
@@ -0,0 +1,103 @@
"""Entity provenance, topology adjacency, and range corpus cases."""
from __future__ import annotations
from copy import deepcopy
from simplecadapi.scene import validate_entity_asset
from .common import JsonObject, validation_case
def _entity_with_source(entity: JsonObject, source_kind: str) -> JsonObject:
result = deepcopy(entity)
topology_kind = {
"solid": "SOLID",
"face": "FACE",
"edge": "EDGE",
"vertex": "VERTEX",
}
for record in result["entities"]:
if source_kind == "model_output":
record["source"] = {
"graph_id": "graph",
"kind": "model_output",
"node_id": "body",
"output_slot": 0,
}
elif source_kind == "model_topology":
record["source"] = {
"graph_id": "graph",
"kind": "model_topology",
"node_id": "body",
"output_slot": 0,
"topo_id": record["entity_id"],
"topology_kind": topology_kind[record["kind"]],
}
else:
record["source"] = {
"kind": "imported_primitive",
"source_element_id": record["entity_id"],
}
return result
def build_entity_matrix_cases(entity: JsonObject) -> list[dict[str, object]]:
cases = [
validation_case(
f"valid_entity_source_{source_kind}",
_entity_with_source(entity, source_kind),
validate_entity_asset,
)
for source_kind in ("model_output", "model_topology", "imported_primitive")
]
topology_mismatch = _entity_with_source(entity, "model_topology")
face = next(
record for record in topology_mismatch["entities"] if record["kind"] == "face"
)
face["source"]["topology_kind"] = "EDGE"
cases.append(
validation_case(
"model_topology_kind_mismatch", topology_mismatch, validate_entity_asset
)
)
missing_reciprocal = deepcopy(entity)
face = next(
record for record in missing_reciprocal["entities"] if record["kind"] == "face"
)
edge_id = face["child_entity_ids"][0]
edge = next(
record
for record in missing_reciprocal["entities"]
if record["entity_id"] == edge_id
)
edge["parent_entity_ids"].remove(face["entity_id"])
cases.append(
validation_case(
"missing_reciprocal_adjacency", missing_reciprocal, validate_entity_asset
)
)
range_gap = deepcopy(entity)
range_gap["face_groups"][1]["first_index"] += 3
cases.append(
validation_case("face_group_range_gap", range_gap, validate_entity_asset)
)
wrong_cardinality = deepcopy(entity)
wrong_cardinality["edge_groups"][0]["index_count"] = 3
cases.append(
validation_case(
"edge_group_wrong_cardinality", wrong_cardinality, validate_entity_asset
)
)
unsupported_version = deepcopy(entity)
unsupported_version["schema_version"] = "2.0"
cases.append(
validation_case(
"unsupported_entity_schema_version",
unsupported_version,
validate_entity_asset,
)
)
return cases
@@ -0,0 +1,215 @@
"""Declarative required, nullable, and discriminated-union mutations."""
from __future__ import annotations
from copy import deepcopy
from typing import Any, Iterable
from simplecadapi.scene import canonical_json_bytes, with_scene_revision
from .common import sha256_hex
Path = tuple[str | int, ...]
def _case(
name: str,
operation: str,
path: Path,
value: Any = None,
*,
recompute_revision: bool = False,
) -> dict[str, Any]:
mutation = {"operation": operation, "path": list(path)}
if operation != "delete":
mutation["value"] = value
return {
"mutations": [mutation],
"name": name,
"recompute_revision": recompute_revision,
}
def _missing_fields(path: Path, fields: Iterable[str]) -> Iterable[dict[str, Any]]:
for field in fields:
yield _case(
"missing_" + "_".join((*map(str, path), field)),
"delete",
(*path, field),
)
def scene_field_matrix(scene: dict[str, Any]) -> list[dict[str, Any]]:
"""Return compact mutations covering the frozen structural field matrix."""
records: list[tuple[Path, tuple[str, ...]]] = [
(
(),
(
"schema_version", "extensions_used", "extensions_required",
"extensions", "scene_id", "revision", "generator", "source",
"coordinate_system", "compile_options", "definitions", "nodes",
"geometry_assets", "edge_assets", "appearances", "entity_assets",
"connectors", "cameras", "lights", "annotations", "diagnostics",
),
),
(
("compile_options",),
("linear_tolerance", "angular_tolerance", "embed_source", "embed_presentation"),
),
(
("definitions", 0),
("definition_id", "kind", "name", "source", "sdk_metadata"),
),
(
("nodes", 0),
(
"node_id", "parent_node_id", "order", "definition_id", "name",
"transform", "visible", "selectable", "appearance_override_id",
"source", "sdk_metadata",
),
),
(
("appearances", 0),
(
"appearance_id", "name", "source", "base_color", "metallic",
"roughness", "alpha_mode", "double_sided", "edge_color", "sdk_metadata",
),
),
(
("geometry_assets", 0),
(
"asset_id", "uri", "media_type", "byte_length", "content_hash",
"scene_local_bounds", "asset_to_scene", "tessellation",
),
),
(
("entity_assets", 0),
("entity_asset_id", "uri", "media_type", "byte_length", "content_hash"),
),
]
result = [
mutation
for path, fields in records
for mutation in _missing_fields(path, fields)
]
result.extend(
_case(name, "add", path, True)
for name, path in (
("unknown_scene_field", ("unknown",)),
("unknown_generator_field", ("generator", "unknown")),
("unknown_compile_options_field", ("compile_options", "unknown")),
("unknown_definition_field", ("definitions", 0, "unknown")),
("unknown_node_field", ("nodes", 0, "unknown")),
("unknown_appearance_field", ("appearances", 0, "unknown")),
)
)
result.extend(
(
_case("optional_presentation_source_null", "add", ("presentation_source",), None),
_case("unknown_scene_source_kind", "set", ("source", "kind"), "unknown"),
_case(
"unknown_definition_source_kind",
"set",
("definitions", 0, "source", "kind"),
"unknown",
),
_case("missing_node_source_kind", "delete", ("nodes", 0, "source", "kind")),
_case("unsupported_scene_schema_version", "set", ("schema_version",), "2.0"),
_case(
"unsupported_generator_profile",
"set",
("generator", "profile"),
"scene-2.0",
),
_case(
"no_presentation_visibility_override",
"set",
("nodes", 0, "visible"),
False,
recompute_revision=True,
),
_case(
"no_presentation_selectability_override",
"set",
("nodes", 0, "selectable"),
False,
recompute_revision=True,
),
_case(
"no_presentation_appearance_override",
"set",
("nodes", 0, "appearance_override_id"),
scene["appearances"][0]["appearance_id"],
recompute_revision=True,
),
_case(
"no_presentation_camera",
"set",
("cameras",),
[
{
"camera_id": "camera/fixture/overview",
"far": 1000,
"name": "overview",
"near": 1,
"parent_node_id": None,
"projection": "perspective",
"transform": {
"origin": [10, -10, 10],
"x_axis": [1, 0, 0],
"y_axis": [0, 1, 0],
"z_axis": [0, 0, 1],
},
"vertical_fov_degrees": 45,
}
],
recompute_revision=True,
),
)
)
return result
def apply_scene_field_case(scene: dict[str, Any], case: dict[str, Any]) -> dict[str, Any]:
"""Replay one mutation descriptor to produce its validator input."""
result = deepcopy(scene)
for mutation in case["mutations"]:
path = mutation["path"]
current: Any = result
for part in path[:-1]:
current = current[part]
if mutation["operation"] == "delete":
del current[path[-1]]
else:
current[path[-1]] = deepcopy(mutation.get("value"))
if case.get("recompute_revision"):
result = with_scene_revision(result)
return result
def nullable_fields_nonnull_case(scene: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]:
"""Return the multi-field valid nullable vector and its compact descriptor."""
value = deepcopy(scene)
value["nodes"][0]["name"] = "Named occurrence"
value["appearances"][0]["name"] = "Named appearance"
appearance = value["appearances"][0]
draft = dict(appearance)
draft.pop("appearance_id")
appearance_id = "appearance/evaluated/" + sha256_hex(canonical_json_bytes(draft))
appearance["appearance_id"] = appearance_id
value["definitions"][0]["appearance_id"] = appearance_id
descriptor = {
"mutations": [
{"operation": "set", "path": ["nodes", 0, "name"], "value": "Named occurrence"},
{"operation": "set", "path": ["appearances", 0, "name"], "value": "Named appearance"},
{"operation": "set", "path": ["appearances", 0, "appearance_id"], "value": appearance_id},
{"operation": "set", "path": ["definitions", 0, "appearance_id"], "value": appearance_id},
],
"name": "nullable_fields_nonnull",
"recompute_revision": True,
}
return with_scene_revision(value), descriptor
@@ -0,0 +1,128 @@
"""Numeric profile fixture vector construction."""
from __future__ import annotations
import struct
from simplecadapi.scene import (
profile_cross,
profile_f32_bits,
profile_normalize,
)
def _f64_from_bits(value: str) -> float:
return struct.unpack(">d", bytes.fromhex(value))[0]
def _f64_bits(value: float) -> str:
return struct.pack(">d", value).hex()
def build_numeric_vectors() -> list[dict[str, object]]:
vectors: list[dict[str, object]] = []
for name, input_bits in (
("positive_zero", "0000000000000000"),
("negative_zero", "8000000000000000"),
("smallest_binary64_subnormal", "0000000000000001"),
("negative_smallest_binary64_subnormal", "8000000000000001"),
("binary32_smallest_subnormal", "36a0000000000000"),
("halfway_tie_to_even", "3ff0000010000000"),
("just_above_halfway", "3ff0000010000001"),
("binary32_largest_finite", "47efffffe0000000"),
):
expected = profile_f32_bits(_f64_from_bits(input_bits))
vectors.append(
{
"expected_bits": f"{expected:08x}",
"input_bits": [input_bits],
"name": name,
"operation": "f32",
"valid": True,
}
)
for name, input_bits in (
("positive_overflow", "7fefffffffffffff"),
("negative_overflow", "ffefffffffffffff"),
("positive_infinity", "7ff0000000000000"),
("quiet_nan", "7ff8000000000000"),
):
try:
profile_f32_bits(_f64_from_bits(input_bits))
except ValueError as exc:
error = str(exc)
else:
raise AssertionError(name)
vectors.append(
{
"error": error,
"input_bits": [input_bits],
"name": name,
"operation": "f32",
"valid": False,
}
)
cross_inputs = (
("basis_cross", (1.0, 0.0, 0.0), (0.0, 1.0, 0.0)),
(
"cancellation_cross",
(134217729.0, 134217728.0, 0.0),
(134217728.0, 134217727.0, 0.0),
),
)
for name, left, right in cross_inputs:
result = profile_cross(left, right)
vectors.append(
{
"expected_bits": [_f64_bits(component) for component in result],
"input_bits": [
[_f64_bits(component) for component in left],
[_f64_bits(component) for component in right],
],
"name": name,
"operation": "cross",
"valid": True,
}
)
normalize_inputs = (
("normalize_3_4_0", (3.0, 4.0, -0.0)),
("normalize_equal_components", (1.0, 1.0, 1.0)),
)
for name, value in normalize_inputs:
result = profile_normalize(value)
vectors.append(
{
"expected_bits": [
f"{profile_f32_bits(component):08x}" for component in result
],
"input_bits": [[_f64_bits(component) for component in value]],
"name": name,
"operation": "normalize",
"valid": True,
}
)
for name, value in (
("normalize_zero", (0.0, 0.0, 0.0)),
(
"normalize_overflow",
(_f64_from_bits("7fefffffffffffff"), 1.0, 0.0),
),
):
try:
profile_normalize(value)
except ValueError as exc:
error = str(exc)
else:
raise AssertionError(name)
vectors.append(
{
"error": error,
"input_bits": [[_f64_bits(component) for component in value]],
"name": name,
"operation": "normalize",
"valid": False,
}
)
return vectors
@@ -0,0 +1,263 @@
"""Scene package provenance and connector-binding fixture builders."""
from __future__ import annotations
from copy import deepcopy
from typing import Callable
from simplecadapi.scene import with_scene_revision
from .common import (
JsonObject,
ScenePackage,
inline_package_case,
replace_entity_sidecar,
set_product_material_appearance,
)
def build_source_scene_package(
base_scene: JsonObject,
base_entity: JsonObject,
base_blobs: dict[str, bytes],
variant: str,
) -> ScenePackage:
scene = deepcopy(base_scene)
entity = deepcopy(base_entity)
definition = scene["definitions"][0]
source_for: Callable[[JsonObject], JsonObject]
status_for: Callable[[JsonObject], str]
if variant == "imported_shape":
definition_id = "definition/root/shape/imported/element"
scene["scene_id"] = "imported_fixture"
scene["source"] = {
"artifact_hash": "sha256:" + "1" * 64,
"format": "step",
"kind": "imported",
}
definition.update(
{
"definition_id": definition_id,
"kind": "shape",
"source": {
"kind": "imported",
"root_id": "root",
"source_element_id": "element",
},
}
)
source_for = lambda record: {
"kind": "imported_primitive",
"source_element_id": record["entity_id"],
}
status_for = lambda record: (
"not_applicable" if record["kind"] == "solid" else "owner_not_part"
)
else:
definition_kind = "part" if variant == "model_part" else "shape"
definition_id = (
"definition/root/part/model_part"
if definition_kind == "part"
else "definition/root/shape/model/graph/body/0"
)
scene["scene_id"] = f"{variant}_fixture"
scene["source"] = {
"artifact_hash": "sha256:" + "2" * 64,
"graph_id": "graph",
"kind": "model",
"model_schema_version": "2.0",
}
definition_source: JsonObject = {
"graph_id": "graph",
"kind": "product_model" if definition_kind == "part" else "model_output",
"node_id": "body",
"output_slot": 0,
"root_id": "root",
}
if definition_kind == "part":
definition_source.update(
{"semantic_id": "model_part", "semantic_type": "Part"}
)
definition.update(
{
"definition_id": definition_id,
"kind": definition_kind,
"source": definition_source,
}
)
topology_kind = {
"solid": "SOLID",
"face": "FACE",
"edge": "EDGE",
"vertex": "VERTEX",
}
if variant == "model_part":
source_for = lambda record: {
"graph_id": "graph",
"kind": "model_topology",
"node_id": "body",
"output_slot": 0,
"topo_id": record["entity_id"],
"topology_kind": topology_kind[record["kind"]],
}
statuses = iter(
["supported", "selector_ambiguous", "selector_unstable", "supported"]
)
def status_for(record: JsonObject) -> str:
return (
"not_applicable"
if record["kind"] == "solid"
else next(statuses, "supported")
)
else:
source_for = lambda _record: {
"graph_id": "graph",
"kind": "model_output",
"node_id": "body",
"output_slot": 0,
}
status_for = lambda record: (
"not_applicable"
if record["kind"] == "solid"
else "owner_not_part"
)
if definition["kind"] == "part":
scene["nodes"][0]["source"] = {
"component_path": [],
"kind": "product_occurrence",
"root_id": "root",
}
scene["nodes"][0]["definition_id"] = definition_id
entity["definition_id"] = definition_id
for record in entity["entities"]:
record["source"] = source_for(record)
record["connector_binding_status"] = status_for(record)
if variant == "model_part":
edge = next(record for record in entity["entities"] if record["kind"] == "edge")
edge["sdk_connector_frame"] = None
edge["connector_binding_status"] = "frame_undefined"
scene, entity, blobs = replace_entity_sidecar(
scene, entity, dict(base_blobs)
)
if definition["kind"] == "part":
set_product_material_appearance(scene)
return with_scene_revision(scene), entity, blobs
def build_package_matrix_cases(
scene: JsonObject,
entity: JsonObject,
blobs: dict[str, bytes],
scene_packages: dict[str, ScenePackage],
blob_pool: dict[str, str],
) -> list[dict[str, object]]:
cases: list[dict[str, object]] = []
for variant in ("imported_shape", "model_shape", "model_part"):
package = build_source_scene_package(scene, entity, blobs, variant)
cases.append(
inline_package_case(
f"valid_{variant}", package[0], package[2], blob_pool
)
)
nested = scene_packages["nested_assembly"]
model_part = build_source_scene_package(scene, entity, blobs, "model_part")
status_mutations = [
(
"solid_status_precedence",
(deepcopy(scene), deepcopy(entity), dict(blobs)),
"solid",
"supported",
),
(
"owner_not_part_status_precedence",
(deepcopy(scene), deepcopy(entity), dict(blobs)),
"face",
"source_not_model",
),
(
"source_not_model_status_precedence",
deepcopy(nested),
"face",
"supported",
),
(
"frame_undefined_status_precedence",
deepcopy(model_part),
"edge",
"owner_not_part",
),
]
for name, package, kind, status in status_mutations:
status_scene, status_entity, status_blobs = package
target = next(
record for record in status_entity["entities"] if record["kind"] == kind
)
target["connector_binding_status"] = status
status_scene, _status_entity, status_blobs = replace_entity_sidecar(
status_scene, status_entity, status_blobs
)
cases.append(
inline_package_case(
name, with_scene_revision(status_scene), status_blobs, blob_pool
)
)
forwarded_scene, _forwarded_entity, forwarded_blobs = deepcopy(nested)
root_connector = next(
record
for record in forwarded_scene["connectors"]
if record["connector_id"] == "root_mount"
)
root_connector["local_transform"]["origin"] = [0, 0, 6]
cases.append(
inline_package_case(
"forwarded_transform_mismatch",
with_scene_revision(forwarded_scene),
forwarded_blobs,
blob_pool,
)
)
missing_child_scene, _missing_child_entity, missing_child_blobs = deepcopy(nested)
root_connector = next(
record
for record in missing_child_scene["connectors"]
if record["connector_id"] == "root_mount"
)
root_connector["forwarded_from"]["source_component_id"] = "missing"
cases.append(
inline_package_case(
"forwarded_direct_child_missing",
with_scene_revision(missing_child_scene),
missing_child_blobs,
blob_pool,
)
)
part_owner_scene, _part_owner_entity, part_owner_blobs = deepcopy(nested)
root_connector = next(
record
for record in part_owner_scene["connectors"]
if record["connector_id"] == "root_mount"
)
root_connector["owner_definition_id"] = "definition/root/part/shared_part"
root_connector["connector_snapshot_id"] = (
"connector/root/part/shared_part/root_mount"
)
part_owner_scene["connectors"].sort(
key=lambda record: record["connector_snapshot_id"]
)
cases.append(
inline_package_case(
"forwarded_owner_not_assembly",
with_scene_revision(part_owner_scene),
part_owner_blobs,
blob_pool,
)
)
return cases
@@ -0,0 +1,710 @@
"""Resource-limit fixture case construction."""
from __future__ import annotations
from copy import deepcopy
from dataclasses import replace
from typing import Any, Callable
from simplecadapi.scene import (
BASE_LIMITS,
SceneResourceLimits,
canonical_archive_size,
json_resource_issues,
preflight_aggregate_compression_ratio,
preflight_archive_member_sizes,
preflight_glb_counts,
preflight_input_archive_size,
preflight_member_compression_ratio,
resource_count_issues,
validate_scene_manifest,
with_scene_revision,
)
from simplecadapi.scene.validation import (
_compute_package_budget_totals,
_package_budget_issues,
_report,
)
from .common import JsonObject, first_issue
def _limits(overrides: dict[str, int]) -> SceneResourceLimits:
return replace(BASE_LIMITS, **overrides)
def _preflight_case(
name: str,
operation: str,
parameters: dict[str, object],
callback: Callable[[], object],
) -> dict[str, object]:
try:
callback()
except ValueError as exc:
valid, error = False, str(exc)
else:
valid, error = True, None
return {
"error": error,
"name": name,
"operation": operation,
"parameters": parameters,
"valid": valid,
}
def _issues_case(
name: str,
operation: str,
parameters: dict[str, object],
issues: tuple[Any, ...] | list[Any],
) -> dict[str, object]:
first = issues[0] if issues else None
return {
"expected": (
None if first is None else {"code": first.code, "path": first.path}
),
"name": name,
"operation": operation,
"parameters": parameters,
"valid": not issues,
}
def _validation_case(
name: str,
operation: str,
parameters: dict[str, object],
report: Any,
) -> dict[str, object]:
return {
"expected": first_issue(report),
"name": name,
"operation": operation,
"parameters": parameters,
"valid": report.valid,
}
def _input_size_case(name: str, size: int, overrides: dict[str, int]) -> dict[str, object]:
parameters: dict[str, object] = {"limits": overrides, "size": size}
return _preflight_case(
name,
"input_archive_size",
parameters,
lambda: preflight_input_archive_size(size, limits=_limits(overrides)),
)
def _archive_sizes_case(
name: str, sizes: dict[str, int], overrides: dict[str, int]
) -> dict[str, object]:
parameters: dict[str, object] = {"limits": overrides, "sizes": sizes}
return _preflight_case(
name,
"archive_member_sizes",
parameters,
lambda: preflight_archive_member_sizes(sizes, limits=_limits(overrides)),
)
def _archive_count_case(
name: str, count: int, overrides: dict[str, int]
) -> dict[str, object]:
sizes = {"scene.json": 0}
sizes.update({f"x/{index}": 0 for index in range(count - 1)})
parameters: dict[str, object] = {"count": count, "limits": overrides}
return _preflight_case(
name,
"archive_member_count",
parameters,
lambda: preflight_archive_member_sizes(sizes, limits=_limits(overrides)),
)
def _compression_case(
name: str,
operation: str,
uncompressed_size: int,
compressed_size: int,
) -> dict[str, object]:
overrides = {"compression_ratio": 2}
parameters = {
"compressed_size": compressed_size,
"limits": overrides,
"uncompressed_size": uncompressed_size,
}
if operation == "aggregate_compression_ratio":
callback = lambda: preflight_aggregate_compression_ratio(
uncompressed_size, compressed_size, limits=_limits(overrides)
)
else:
callback = lambda: preflight_member_compression_ratio(
uncompressed_size, compressed_size, limits=_limits(overrides)
)
return _preflight_case(name, operation, parameters, callback)
def _json_value(kind: str, text: str, field: str | None = None) -> object:
if kind == "value":
return {"value": text}
if kind == "object_key":
return {text: 0}
if kind == "metadata_key":
return {"metadata": {text: 0}}
if kind == "sdk_metadata_key":
return {"sdk_metadata": {text: 0}}
if kind == "identifier":
return {field or "node_id": text}
if kind == "identifier_array":
return {field or "component_path": [text]}
if kind == "uri":
return {"uri": text}
raise AssertionError(kind)
def _json_domain_case(
name: str,
kind: str,
text: str,
overrides: dict[str, int],
*,
field: str | None = None,
) -> dict[str, object]:
parameters: dict[str, object] = {
"field": field,
"kind": kind,
"limits": overrides,
"text": text,
}
issues = json_resource_issues(
_json_value(kind, text, field), limits=_limits(overrides)
)
return _issues_case(name, "json_domain", parameters, issues)
def _nested_value(depth: int) -> object:
value: dict[str, object] = {}
current = value
for _index in range(depth):
current["x"] = {}
current = current["x"] # type: ignore[assignment]
return value
def _json_depth_case(name: str, depth: int) -> dict[str, object]:
overrides = {"json_depth": 2}
parameters: dict[str, object] = {"depth": depth, "limits": overrides}
issues = json_resource_issues(_nested_value(depth), limits=_limits(overrides))
return _issues_case(name, "json_depth", parameters, issues)
def _resource_count_value(
artifact: str, kind: str, count: int, field: str | None
) -> object:
if kind == "collection":
assert field is not None
return {field: [None] * count}
if kind == "hierarchy":
return {"nodes": [{"source": {"component_path": ["x"] * count}}]}
if kind == "forwarded":
return {
"connectors": [
{
"anchor_kind": "forwarded",
"connector_snapshot_id": f"c{index}",
"forwarded_from": {
"source_connector_snapshot_id": f"c{index + 1}"
},
}
for index in range(count)
]
}
raise AssertionError((artifact, kind))
def _resource_count_case(
name: str,
artifact: str,
kind: str,
count: int,
overrides: dict[str, int],
*,
field: str | None = None,
) -> dict[str, object]:
parameters: dict[str, object] = {
"artifact": artifact,
"count": count,
"field": field,
"kind": kind,
"limits": overrides,
}
issues = resource_count_issues(
_resource_count_value(artifact, kind, count, field),
artifact,
limits=_limits(overrides),
)
return _issues_case(name, "resource_count", parameters, issues)
def _glb_count_case(
name: str,
kind: str,
vertex_count: int,
index_count: int,
overrides: dict[str, int],
) -> dict[str, object]:
parameters: dict[str, object] = {
"index_count": index_count,
"kind": kind,
"limits": overrides,
"vertex_count": vertex_count,
}
return _preflight_case(
name,
"glb_counts",
parameters,
lambda: preflight_glb_counts(
kind, # type: ignore[arg-type]
vertex_count,
index_count,
limits=_limits(overrides),
),
)
_BUDGET_FIELDS = {
"entities_total": "entity_count",
"line_segments_total": "line_segment_count",
"line_vertices_total": "line_vertex_count",
"static_decoded_buffer_bytes": "scene_json_bytes",
"triangle_vertices_total": "triangle_vertex_count",
"triangles_total": "triangle_count",
}
def _package_budget_case(
name: str, limit_name: str, value: int
) -> dict[str, object]:
contributions = {
"scene_json_bytes": 0,
"glb_decoded_buffer_bytes": 0,
"entity_json_bytes": 0,
"other_immutable_json_bytes": 0,
"entity_count": 0,
"entity_vertex_count": 0,
"triangle_vertex_count": 0,
"triangle_count": 0,
"line_vertex_count": 0,
"line_segment_count": 0,
}
contributions[_BUDGET_FIELDS[limit_name]] = value
overrides = {limit_name: 2}
totals = _compute_package_budget_totals(**contributions)
budget_report = _report(
_package_budget_issues(totals, limits=_limits(overrides)), artifact="package"
)
return _validation_case(
name,
"package_budget",
{"contributions": contributions, "limits": overrides},
budget_report,
)
def build_resource_cases(
scene: JsonObject,
normalized_part: JsonObject,
) -> list[dict[str, object]]:
input_limits = {"input_archive_bytes": 8}
role_limits = {
"canonical_archive_bytes": 1_000,
"one_member_bytes": 100,
"total_uncompressed_bytes": 100,
}
canonical_limits = {
"canonical_archive_bytes": 256,
"one_member_bytes": 100,
"total_uncompressed_bytes": 100,
}
canonical_envelope = canonical_archive_size(
{"geometry/a.glb": 0, "scene.json": 0}
)
canonical_payload = canonical_limits["canonical_archive_bytes"] - canonical_envelope
one_member_limits = {
"canonical_archive_bytes": 1_000,
"one_member_bytes": 8,
"total_uncompressed_bytes": 100,
}
total_limits = {
"canonical_archive_bytes": 1_000,
"one_member_bytes": 8,
"total_uncompressed_bytes": 16,
}
count_limits = {"canonical_archive_bytes": 10_000, "zip_members": 3}
cases = [
_input_size_case("input_archive_exact_limit", 8, input_limits),
_input_size_case("input_archive_over_limit", 9, input_limits),
_archive_sizes_case(
"scene_json_exact_limit",
{"scene.json": 8},
{**role_limits, "scene_json_bytes": 8},
),
_archive_sizes_case(
"scene_json_over_limit",
{"scene.json": 9},
{**role_limits, "scene_json_bytes": 8},
),
_archive_sizes_case(
"entity_json_exact_limit",
{"entities/a.json": 8, "scene.json": 0},
{**role_limits, "entity_json_bytes": 8},
),
_archive_sizes_case(
"entity_json_over_limit",
{"entities/a.json": 9, "scene.json": 0},
{**role_limits, "entity_json_bytes": 8},
),
_archive_sizes_case(
"model_json_exact_limit",
{"model/model.json": 8, "scene.json": 0},
{**role_limits, "model_json_bytes": 8},
),
_archive_sizes_case(
"model_json_over_limit",
{"model/model.json": 9, "scene.json": 0},
{**role_limits, "model_json_bytes": 8},
),
_archive_sizes_case(
"presentation_json_exact_limit",
{"presentation/presentation.json": 8, "scene.json": 0},
{**role_limits, "presentation_json_bytes": 8},
),
_archive_sizes_case(
"presentation_json_over_limit",
{"presentation/presentation.json": 9, "scene.json": 0},
{**role_limits, "presentation_json_bytes": 8},
),
_archive_sizes_case(
"canonical_archive_exact_limit",
{"geometry/a.glb": canonical_payload, "scene.json": 0},
canonical_limits,
),
_archive_sizes_case(
"canonical_archive_over_limit",
{"geometry/a.glb": canonical_payload + 1, "scene.json": 0},
canonical_limits,
),
_archive_sizes_case(
"one_member_exact_limit",
{"geometry/a.glb": 8, "scene.json": 0},
one_member_limits,
),
_archive_sizes_case(
"one_member_over_limit",
{"geometry/a.glb": 9, "scene.json": 0},
one_member_limits,
),
_archive_sizes_case(
"total_uncompressed_exact_limit",
{"geometry/a.glb": 8, "geometry/b.glb": 8, "scene.json": 0},
total_limits,
),
_archive_sizes_case(
"total_uncompressed_over_limit",
{
"geometry/a.glb": 8,
"geometry/b.glb": 8,
"geometry/c.glb": 1,
"scene.json": 0,
},
total_limits,
),
_archive_count_case("archive_member_count_exact_limit", 3, count_limits),
_archive_count_case("archive_member_count_over_limit", 4, count_limits),
_compression_case(
"aggregate_compression_ratio_exact_limit",
"aggregate_compression_ratio",
4,
2,
),
_compression_case(
"aggregate_compression_ratio_over_limit",
"aggregate_compression_ratio",
5,
2,
),
_compression_case(
"member_compression_ratio_exact_limit",
"member_compression_ratio",
4,
2,
),
_compression_case(
"member_compression_ratio_over_limit",
"member_compression_ratio",
5,
2,
),
_json_depth_case("json_depth_exact_limit", 2),
_json_depth_case("json_depth_over_limit", 3),
]
for kind, overrides in (
("value", {"json_string_bytes": 4}),
("object_key", {"json_string_bytes": 4}),
(
"metadata_key",
{"json_string_bytes": 100, "structural_id_bytes": 4},
),
(
"sdk_metadata_key",
{"json_string_bytes": 100, "structural_id_bytes": 4},
),
("uri", {"json_string_bytes": 100, "uri_bytes": 4}),
):
cases.append(
_json_domain_case(
f"utf8_{kind}_exact_limit", kind, "éé", overrides
)
)
cases.append(
_json_domain_case(
f"utf8_{kind}_over_limit", kind, "ééa", overrides
)
)
identifier_fields = (
"definition_ref",
"node_id",
"source_element_id",
"topo_id",
)
for field in identifier_fields:
overrides = {"json_string_bytes": 100, "structural_id_bytes": 4}
cases.append(
_json_domain_case(
f"utf8_{field}_exact_limit",
"identifier",
"éé",
overrides,
field=field,
)
)
cases.append(
_json_domain_case(
f"utf8_{field}_over_limit",
"identifier",
"ééa",
overrides,
field=field,
)
)
for field in (
"child_entity_ids",
"component_path",
"evaluated_tags",
"grounded_component_ids",
"parent_entity_ids",
"semantic_binding_ids",
):
overrides = {"json_string_bytes": 100, "structural_id_bytes": 4}
cases.append(
_json_domain_case(
f"utf8_{field}_item_exact_limit",
"identifier_array",
"éé",
overrides,
field=field,
)
)
cases.append(
_json_domain_case(
f"utf8_{field}_item_over_limit",
"identifier_array",
"ééa",
overrides,
field=field,
)
)
collection_limits = (
("scene", "definitions", "definitions"),
("scene", "nodes", "nodes"),
("scene", "geometry_assets", "assets_per_kind"),
("scene", "edge_assets", "assets_per_kind"),
("scene", "entity_assets", "assets_per_kind"),
("scene", "appearances", "appearances"),
("scene", "connectors", "connectors"),
("scene", "cameras", "cameras"),
("entities", "entities", "entities_per_sidecar"),
("entities", "face_groups", "entities_per_sidecar"),
("entities", "edge_groups", "entities_per_sidecar"),
("presentation", "node_overrides", "nodes"),
("presentation", "appearances", "appearances"),
("presentation", "cameras", "cameras"),
)
for artifact, field, limit_name in collection_limits:
overrides = {limit_name: 2}
cases.append(
_resource_count_case(
f"{artifact}_{field}_exact_limit",
artifact,
"collection",
2,
overrides,
field=field,
)
)
cases.append(
_resource_count_case(
f"{artifact}_{field}_over_limit",
artifact,
"collection",
3,
overrides,
field=field,
)
)
cases.extend(
[
_resource_count_case(
"hierarchy_depth_exact_limit",
"scene",
"hierarchy",
2,
{"hierarchy_depth": 2, "nodes": 10},
),
_resource_count_case(
"hierarchy_depth_over_limit",
"scene",
"hierarchy",
3,
{"hierarchy_depth": 2, "nodes": 10},
),
_resource_count_case(
"forwarded_connector_depth_exact_limit",
"scene",
"forwarded",
2,
{"connectors": 10, "forwarded_connector_depth": 2},
),
_resource_count_case(
"forwarded_connector_depth_over_limit",
"scene",
"forwarded",
3,
{"connectors": 10, "forwarded_connector_depth": 2},
),
_glb_count_case(
"triangle_vertices_per_asset_exact_limit",
"triangle",
2,
3,
{"triangle_vertices_per_asset": 2},
),
_glb_count_case(
"triangle_vertices_per_asset_over_limit",
"triangle",
3,
3,
{"triangle_vertices_per_asset": 2},
),
_glb_count_case(
"triangles_per_asset_exact_limit",
"triangle",
3,
6,
{"triangles_per_asset": 2},
),
_glb_count_case(
"triangles_per_asset_over_limit",
"triangle",
3,
9,
{"triangles_per_asset": 2},
),
_glb_count_case(
"line_vertices_per_asset_exact_limit",
"line",
2,
2,
{"line_vertices_per_asset": 2},
),
_glb_count_case(
"line_vertices_per_asset_over_limit",
"line",
3,
2,
{"line_vertices_per_asset": 2},
),
_glb_count_case(
"line_segments_per_asset_exact_limit",
"line",
2,
4,
{"line_segments_per_asset": 2},
),
_glb_count_case(
"line_segments_per_asset_over_limit",
"line",
2,
6,
{"line_segments_per_asset": 2},
),
]
)
for limit_name in _BUDGET_FIELDS:
cases.append(
_package_budget_case(f"{limit_name}_exact_limit", limit_name, 2)
)
cases.append(
_package_budget_case(f"{limit_name}_over_limit", limit_name, 3)
)
for name, value_number in (
("safe_integer_exact_limit", 9_007_199_254_740_991),
("safe_integer_over_limit", 9_007_199_254_740_992),
):
value = deepcopy(scene)
value["geometry_assets"][0]["byte_length"] = value_number
if value_number <= 9_007_199_254_740_991:
value = with_scene_revision(value)
cases.append(
_validation_case(
name,
"scene_geometry_byte_length",
{"value": str(value_number)},
validate_scene_manifest(value),
)
)
for name, field, value_number in (
("linear_tolerance_exact_max", "linear_tolerance", 1_000_000),
("linear_tolerance_over_max", "linear_tolerance", 1_000_001),
("angular_tolerance_exact_max", "angular_tolerance", 3.141592653589793),
("angular_tolerance_over_max", "angular_tolerance", 3.1415926535897936),
):
value = deepcopy(scene)
value["compile_options"][field] = value_number
value["geometry_assets"][0]["tessellation"][field] = value_number
if field == "linear_tolerance":
value["edge_assets"][0]["tessellation"][field] = value_number
value = with_scene_revision(value)
cases.append(
_validation_case(
name,
"scene_compile_option",
{"field": field, "value": value_number},
validate_scene_manifest(value),
)
)
# Keep the argument in active use so fixture construction catches drift in
# the valid normalized product even though resource probes are synthetic.
assert normalized_part
return cases
@@ -0,0 +1,218 @@
"""Minimal, nested, and repeated-instance Scene fixture builders."""
from __future__ import annotations
from copy import deepcopy
from simplecadapi.scene import with_scene_revision
from .common import (
IDENTITY,
JsonObject,
ScenePackage,
replace_entity_sidecar,
set_product_material_appearance,
transform,
)
def _product_definition(
definition_id: str,
kind: str,
semantic_id: str,
renderable: JsonObject | None = None,
) -> JsonObject:
definition: JsonObject = {
"definition_id": definition_id,
"kind": kind,
"name": semantic_id.replace("_", " ").title(),
"sdk_metadata": {},
"source": {
"kind": "product_manual",
"root_id": "root",
"semantic_id": semantic_id,
"semantic_type": "Part" if kind == "part" else "Assembly",
},
}
if renderable is not None:
definition.update(
{
field: renderable[field]
for field in (
"appearance_id",
"edge_asset_id",
"entity_asset_id",
"geometry_asset_id",
)
}
)
return definition
def _product_node(
component_path: list[str],
definition_id: str,
order: int,
local_transform: JsonObject | None = None,
) -> JsonObject:
node_id = "instance/root" + "".join(f"/{part}" for part in component_path)
parent_id = (
None
if not component_path
else "instance/root" + "".join(f"/{part}" for part in component_path[:-1])
)
return {
"appearance_override_id": None,
"definition_id": definition_id,
"name": component_path[-1] if component_path else "root",
"node_id": node_id,
"order": order,
"parent_node_id": parent_id,
"sdk_metadata": {},
"selectable": True,
"source": {
"component_path": component_path,
"kind": "product_occurrence",
"root_id": "root",
},
"transform": deepcopy(local_transform or IDENTITY),
"visible": True,
}
def _manual_connector(
owner_definition_id: str,
owner_kind: str,
owner_semantic_id: str,
connector_id: str,
anchor_kind: str,
local_transform: JsonObject,
*,
forwarded_from: JsonObject | None = None,
) -> JsonObject:
connector: JsonObject = {
"anchor_kind": anchor_kind,
"connector_id": connector_id,
"connector_snapshot_id": (
f"connector/root/{owner_kind}/{owner_semantic_id}/{connector_id}"
),
"local_transform": local_transform,
"name": connector_id.replace("_", " ").title(),
"owner_definition_id": owner_definition_id,
"sdk_metadata": {},
"source": {"kind": "manual", "source_id": "fixture"},
}
if forwarded_from is not None:
connector["forwarded_from"] = forwarded_from
return connector
def build_product_scene_package(
base_scene: JsonObject,
base_entity: JsonObject,
base_blobs: dict[str, bytes],
*,
nested: bool,
) -> ScenePackage:
scene = deepcopy(base_scene)
entity = deepcopy(base_entity)
root_definition_id = "definition/root/assembly/root_assembly"
part_definition_id = "definition/root/part/shared_part"
renderable = deepcopy(scene["definitions"][0])
definitions = [
_product_definition(root_definition_id, "assembly", "root_assembly"),
_product_definition(part_definition_id, "part", "shared_part", renderable),
]
nodes = [_product_node([], root_definition_id, 0)]
connectors: list[JsonObject] = []
if nested:
nested_definition_id = "definition/root/assembly/nested_assembly"
definitions.append(
_product_definition(
nested_definition_id, "assembly", "nested_assembly"
)
)
nodes.extend(
[
_product_node(["subassembly"], nested_definition_id, 0),
_product_node(["subassembly", "part"], part_definition_id, 0),
]
)
part_connector = _manual_connector(
part_definition_id,
"part",
"shared_part",
"mount",
"placement",
transform(),
)
nested_connector = _manual_connector(
nested_definition_id,
"assembly",
"nested_assembly",
"nested_mount",
"forwarded",
transform(),
forwarded_from={
"offset": None,
"source_component_id": "part",
"source_connector_id": "mount",
"source_connector_snapshot_id": part_connector[
"connector_snapshot_id"
],
"source_definition_id": part_definition_id,
},
)
root_offset = transform([0, 0, 5])
root_connector = _manual_connector(
root_definition_id,
"assembly",
"root_assembly",
"root_mount",
"forwarded",
deepcopy(root_offset),
forwarded_from={
"offset": root_offset,
"source_component_id": "subassembly",
"source_connector_id": "nested_mount",
"source_connector_snapshot_id": nested_connector[
"connector_snapshot_id"
],
"source_definition_id": nested_definition_id,
},
)
connectors = [part_connector, nested_connector, root_connector]
scene["scene_id"] = "nested_fixture"
else:
nodes.extend(
[
_product_node(
["left"], part_definition_id, 0, transform([-1000, 0, 0])
),
_product_node(
["right"], part_definition_id, 1, transform([1000, 0, 0])
),
]
)
scene["scene_id"] = "repeated_fixture"
scene["definitions"] = sorted(
definitions, key=lambda value: value["definition_id"].encode("utf-8")
)
scene["nodes"] = sorted(
nodes, key=lambda value: value["node_id"].encode("utf-8")
)
scene["connectors"] = sorted(
connectors,
key=lambda value: value["connector_snapshot_id"].encode("utf-8"),
)
entity["definition_id"] = part_definition_id
for record in entity["entities"]:
if record["kind"] != "solid":
record["connector_binding_status"] = "source_not_model"
scene, entity, blobs = replace_entity_sidecar(
scene, entity, dict(base_blobs)
)
set_product_material_appearance(scene)
return with_scene_revision(scene), entity, blobs
@@ -0,0 +1,140 @@
"""Scene shape, field-matrix, and two-pass revision corpus sections."""
from __future__ import annotations
from copy import deepcopy
from simplecadapi.scene import canonical_json_bytes, validate_scene_manifest
from .common import (
JsonObject,
ScenePackage,
b64,
content_hash,
first_issue,
scene_shape_case,
)
from .field_matrix import (
apply_scene_field_case,
nullable_fields_nonnull_case,
scene_field_matrix,
)
from .scene_cases import build_product_scene_package
def build_scene_shape_cases(
scene: JsonObject,
entity: JsonObject,
blobs: dict[str, bytes],
blob_pool: dict[str, str],
) -> tuple[list[dict[str, object]], dict[str, ScenePackage]]:
nested = build_product_scene_package(scene, entity, blobs, nested=True)
repeated = build_product_scene_package(scene, entity, blobs, nested=False)
packages = {
"minimal_standalone_shape": (deepcopy(scene), deepcopy(entity), dict(blobs)),
"nested_assembly": nested,
"repeated_part_instance": repeated,
}
cases = [
scene_shape_case(
"minimal_standalone_shape",
scene,
blobs,
{
"definition_count": 1,
"definition_occurrence_counts": {
"definition/root/shape/manual/fixture": 1
},
"edge_asset_count": 1,
"entity_asset_count": 1,
"geometry_asset_count": 1,
"maximum_depth": 0,
"node_count": 1,
"root_node_ids": ["instance/root"],
},
blob_pool,
),
scene_shape_case(
"nested_assembly",
nested[0],
nested[2],
{
"definition_count": 3,
"definition_occurrence_counts": {
"definition/root/assembly/nested_assembly": 1,
"definition/root/assembly/root_assembly": 1,
"definition/root/part/shared_part": 1,
},
"edge_asset_count": 1,
"entity_asset_count": 1,
"geometry_asset_count": 1,
"maximum_depth": 2,
"node_count": 3,
"root_node_ids": ["instance/root"],
},
blob_pool,
),
scene_shape_case(
"repeated_part_instance",
repeated[0],
repeated[2],
{
"definition_count": 2,
"definition_occurrence_counts": {
"definition/root/assembly/root_assembly": 1,
"definition/root/part/shared_part": 2,
},
"edge_asset_count": 1,
"entity_asset_count": 1,
"geometry_asset_count": 1,
"maximum_depth": 1,
"node_count": 3,
"root_node_ids": ["instance/root"],
},
blob_pool,
),
]
return cases, packages
def build_scene_field_cases(scene: JsonObject) -> list[dict[str, object]]:
descriptors = scene_field_matrix(scene)
nullable_value, nullable_descriptor = nullable_fields_nonnull_case(scene)
descriptors.append(nullable_descriptor)
result = []
for descriptor in descriptors:
value = (
nullable_value
if descriptor["name"] == "nullable_fields_nonnull"
else apply_scene_field_case(scene, descriptor)
)
report = validate_scene_manifest(canonical_json_bytes(value))
result.append(
{
**descriptor,
"expected": first_issue(report),
"valid": report.valid,
}
)
return result
def build_revision_vectors(
scene_packages: dict[str, ScenePackage],
) -> list[dict[str, object]]:
vectors = []
for name, (scene, _entity, _blobs) in scene_packages.items():
draft = deepcopy(scene)
revision = draft.pop("revision")
draft_bytes = canonical_json_bytes(draft)
scene_bytes = canonical_json_bytes(scene)
vectors.append(
{
"canonical_base64": b64(scene_bytes),
"draft_base64": b64(draft_bytes),
"name": name,
"revision": revision,
"sha256": content_hash(scene_bytes),
}
)
return vectors
@@ -0,0 +1,330 @@
"""Schema-grounded structural field matrices for all contract artifacts."""
from __future__ import annotations
import json
import re
from copy import deepcopy
from pathlib import Path
from typing import Any, Mapping
from jsonschema import Draft202012Validator
Json = Any
SCHEMAS = {
"scene": "scene-1.0.schema.json",
"entities": "entities-1.0.schema.json",
"presentation": "presentation-1.0.schema.json",
"connector_binding": "connector-binding-1.0.schema.json",
"normalized_product": "normalized-product-1.schema.json",
}
def _resolve(root: Mapping[str, Any], schema: Mapping[str, Any]) -> Mapping[str, Any]:
reference = schema.get("$ref")
if not isinstance(reference, str) or not reference.startswith("#/"):
return schema
current: Any = root
for part in reference[2:].split("/"):
current = current[part.replace("~1", "/").replace("~0", "~")]
return current
def _wrapper(root: Mapping[str, Any], pointer: str) -> dict[str, Any]:
if pointer == "#":
return deepcopy(dict(root))
return {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$ref": pointer,
"$defs": deepcopy(root.get("$defs", {})),
}
def _string_example(schema: Mapping[str, Any]) -> str:
pattern = str(schema.get("pattern", ""))
if pattern == "^sha256:[0-9a-f]{64}$":
return "sha256:" + "0" * 64
if pattern.startswith("^appearance/evaluated/"):
return "appearance/evaluated/" + "0" * 64
if pattern.startswith("^entity/"):
return "entity/face/0"
for candidate in ("a", "x", "root", "entity/face/0"):
if not pattern or re.fullmatch(pattern, candidate):
return candidate
raise AssertionError(f"no structural string example for pattern {pattern!r}")
def _accepts_null(root: Mapping[str, Any], schema: Mapping[str, Any]) -> bool:
wrapper = deepcopy(dict(schema))
wrapper["$defs"] = deepcopy(root.get("$defs", {}))
return Draft202012Validator(wrapper).is_valid(None)
def _nonnull_example(root: Mapping[str, Any], schema: Mapping[str, Any]) -> Json:
resolved = _resolve(root, schema)
if "oneOf" in resolved:
for branch in resolved["oneOf"]:
if not _accepts_null(root, branch):
return _example(root, branch, prefer_nonnull=True)
types = resolved.get("type")
if isinstance(types, list):
draft = dict(resolved)
draft["type"] = next(item for item in types if item != "null")
return _example(root, draft, prefer_nonnull=True)
return _example(root, resolved, prefer_nonnull=True)
def _condition_matches(
root: Mapping[str, Any], condition: Mapping[str, Any], value: Json
) -> bool:
schema = deepcopy(dict(condition))
schema["$defs"] = deepcopy(root.get("$defs", {}))
return Draft202012Validator(schema).is_valid(value)
def _example(
root: Mapping[str, Any],
schema: Mapping[str, Any],
*,
overrides: Mapping[str, Json] | None = None,
prefer_nonnull: bool = False,
) -> Json:
resolved = _resolve(root, schema)
if "const" in resolved:
return deepcopy(resolved["const"])
if "enum" in resolved:
return deepcopy(resolved["enum"][0])
for union_key in ("oneOf", "anyOf"):
if union_key in resolved and not (
resolved.get("type") == "object" or "properties" in resolved
):
branches = resolved[union_key]
if prefer_nonnull:
branches = sorted(
branches,
key=lambda branch: _accepts_null(root, branch),
)
return _example(root, branches[0], prefer_nonnull=prefer_nonnull)
types = resolved.get("type")
if isinstance(types, list):
if "null" in types and not prefer_nonnull:
return None
types = next(item for item in types if item != "null")
if types == "object" or "properties" in resolved:
properties = resolved.get("properties", {})
result = {
field: _example(root, properties.get(field, {}))
for field in resolved.get("required", [])
}
if overrides:
result.update(deepcopy(dict(overrides)))
for branch in resolved.get("anyOf", []):
branch_required = branch.get("required", [])
if branch_required:
for field in branch_required:
result.setdefault(field, _example(root, properties.get(field, {})))
break
for condition in resolved.get("allOf", []):
then = condition.get("then")
if isinstance(then, dict) and _condition_matches(
root, condition.get("if", {}), result
):
for field in then.get("required", []):
result.setdefault(field, _example(root, properties.get(field, {})))
return result
if types == "array" or "prefixItems" in resolved:
result = [_example(root, item) for item in resolved.get("prefixItems", [])]
minimum = resolved.get("minItems", 0)
while len(result) < minimum:
result.append(_example(root, resolved.get("items", {})))
return result
if types == "string":
return _string_example(resolved)
if types == "integer":
return int(resolved.get("minimum", 0))
if types == "number":
if "exclusiveMinimum" in resolved:
return resolved["exclusiveMinimum"] + 1
return resolved.get("minimum", 0)
if types == "boolean":
return False
if types == "null":
return None
return {}
def _closed_records(schema: Mapping[str, Any]) -> list[tuple[str, Mapping[str, Any]]]:
result: dict[str, Mapping[str, Any]] = {}
if schema.get("type") == "object" and schema.get("additionalProperties") is False:
result["#"] = schema
def visit(value: Any, pointer: str) -> None:
if not isinstance(value, dict):
return
if value.get("type") == "object" and value.get("additionalProperties") is False:
result[pointer] = value
for key in ("properties", "items", "oneOf", "anyOf", "allOf"):
child = value.get(key)
if isinstance(child, dict):
for name, nested in child.items():
visit(nested, f"{pointer}/{key}/{name}")
elif isinstance(child, list):
for index, nested in enumerate(child):
visit(nested, f"{pointer}/{key}/{index}")
for name, definition in schema.get("$defs", {}).items():
visit(definition, f"#/$defs/{name}")
return list(result.items())
def _mutate(base: Json, operation: str, field: str, value: Json = None) -> Json:
result = deepcopy(base)
if operation == "delete":
del result[field]
else:
result[field] = deepcopy(value)
return result
def _record_matrix(
artifact: str,
root: Mapping[str, Any],
pointer: str,
schema: Mapping[str, Any],
*,
overrides: Mapping[str, Json] | None = None,
variant: str = "base",
) -> dict[str, Any]:
validator = Draft202012Validator(_wrapper(root, pointer))
base = _example(root, schema, overrides=overrides)
if not validator.is_valid(base):
errors = [error.message for error in validator.iter_errors(base)]
raise AssertionError(
f"invalid generated {artifact} {pointer} {variant}: {errors}"
)
properties = schema.get("properties", {})
cases: list[dict[str, Any]] = [
{"mutations": [], "name": "base_valid", "valid": True}
]
for field in base:
cases.append(
{
"mutations": [{"operation": "delete", "path": [field]}],
"name": f"missing_{field}",
"valid": validator.is_valid(_mutate(base, "delete", field)),
}
)
cases.append(
{
"mutations": [
{"operation": "set", "path": ["unknown"], "value": True}
],
"name": "unknown_field",
"valid": validator.is_valid(_mutate(base, "set", "unknown", True)),
}
)
optional = [field for field in properties if field not in base]
for field in optional:
present = _example(root, properties[field])
cases.append(
{
"mutations": [
{"operation": "set", "path": [field], "value": present}
],
"name": f"optional_{field}_present",
"valid": validator.is_valid(_mutate(base, "set", field, present)),
}
)
if optional:
all_present = deepcopy(base)
mutations = []
for field in optional:
present = _example(root, properties[field])
all_present[field] = present
mutations.append(
{"operation": "set", "path": [field], "value": present}
)
cases.append(
{
"mutations": mutations,
"name": "optional_fields_all_present",
"valid": validator.is_valid(all_present),
}
)
for field, field_schema in properties.items():
if field in base and base[field] is None and _accepts_null(root, field_schema):
nonnull = _nonnull_example(root, field_schema)
cases.append(
{
"mutations": [
{"operation": "set", "path": [field], "value": nonnull}
],
"name": f"nullable_{field}_nonnull",
"valid": validator.is_valid(
_mutate(base, "set", field, nonnull)
),
}
)
return {
"artifact": artifact,
"base": base,
"cases": cases,
"schema_pointer": pointer,
"variant": variant,
}
def build_schema_field_matrices(schema_dir: Path) -> list[dict[str, Any]]:
"""Build declarative matrices for every closed object and discriminator."""
matrices: list[dict[str, Any]] = []
for artifact, filename in SCHEMAS.items():
root = json.loads(schema_dir.joinpath(filename).read_text(encoding="utf-8"))
for pointer, schema in _closed_records(root):
discriminator = next(
(
(field, field_schema["enum"])
for field, field_schema in schema.get("properties", {}).items()
if isinstance(field_schema, dict)
and len(field_schema.get("enum", [])) > 1
),
None,
)
variants: list[tuple[str, Mapping[str, Json] | None]] = [("base", None)]
if discriminator is not None:
field, values = discriminator
variants = [
(f"{field}={value}", {field: value}) for value in values
]
if artifact == "normalized_product" and pointer == "#/$defs/connector":
variants = []
for anchor_name in (
"geometryAnchor",
"placementAnchor",
"forwardedAnchor",
):
anchor = _example(root, root["$defs"][anchor_name])
variants.append(
(f"anchor={anchor['anchor_kind']}", {"anchor": anchor})
)
for variant, overrides in variants:
matrices.append(
_record_matrix(
artifact,
root,
pointer,
schema,
overrides=overrides,
variant=variant,
)
)
identities = [
(matrix["artifact"], matrix["schema_pointer"], matrix["variant"])
for matrix in matrices
]
if len(identities) != len(set(identities)):
raise AssertionError("duplicate schema field matrix identity")
return matrices