diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 90509a12..b7dd7f23 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -240,6 +240,17 @@ file-local and are not portable. `no-mesh` is `2` in `templates/ai-asset-pipeline-template/`; both are correct. Copy a template's own table from that template, not from this paragraph. +**Reserved in examples/.** Three codes mean one thing in every example. `2` +is argparse's usage exit only, never a check. `10` is the gallery framing +gate (`gallery_framing.EXIT_FRAMING`) and `11` is the asset-quality gate +(`gallery_asset_quality.EXIT_ASSET_QUALITY`). An example returns the +helper's constant or check call for those, and gives its own checks other +numbers. A shared code makes a falsifier impossible to target (catalog +falsifiers may not expect 2) or misattributes which check fired. +`tests/check_exit_code_readme.py` rejects a literal 2, 10 or 11 at an +example's exit site, and an Exit codes row for them that says anything +else or adds "also". + **FATAL wrapper.** `sys.exit(1)` on an uncaught exception in the `__main__` guard. Uniform across the examples. `1` means crashed, never a named check. diff --git a/docs/gallery/car-mirror-symmetry/index.html b/docs/gallery/car-mirror-symmetry/index.html index bd271243..0b42d7ac 100644 --- a/docs/gallery/car-mirror-symmetry/index.html +++ b/docs/gallery/car-mirror-symmetry/index.html @@ -80,7 +80,7 @@
Per-script sequential checks. 9 is a valid check code; there is no rule against it.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Body datablock is not the authored half |
| 4 | Authored centerline vert count ≠ 28 |
| 5 | Evaluated verts ≠ 2n − c (--no-mirror lands here) |
| 6 | Evaluated on-plane verts ≠ centerline; also --output produced no file |
| 7 | Evaluated Euler characteristic ≠ 2 |
| 8 | Non-manifold edges in the evaluated shell |
| 9 | Evaluated verts lack a mirrored partner |
| 10 | Mirror partner deviation above tolerance |
| 11 | Evaluated bbox not symmetric about X |
| 12 | Mirrored-part origin off the plane |
| 13 | Mirrored-part datablock is not the authored half (verts, faces, on-plane verts) |
| 14 | Mirrored-part evaluated counts ≠ 2n − weld / 2f, or on-plane verts ≠ weld |
| 15 | Mirrored-part partner check failed |
| 16 | Mirrored-part evaluated mesh stayed on one side |
| 17 | Gallery framing violation (--output path only; gallery_framing returns 10, remapped) |
| 18 | Asset-quality floor violation (--output path only; gallery_asset_quality returns 11, remapped) |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Body datablock is not the authored half |
| 4 | Authored centerline vert count ≠ 28 |
| 5 | Evaluated verts ≠ 2n − c (--no-mirror lands here) |
| 6 | Evaluated on-plane verts ≠ centerline; also --output produced no file |
| 7 | Evaluated Euler characteristic ≠ 2 |
| 8 | Non-manifold edges in the evaluated shell |
| 9 | Evaluated verts lack a mirrored partner |
| 10 | Gallery framing violation (--output path only; gallery_framing) |
| 11 | Asset-quality floor violation (--output path only; gallery_asset_quality) |
| 12 | Mirrored-part origin off the plane |
| 13 | Mirrored-part datablock is not the authored half (verts, faces, on-plane verts) |
| 14 | Mirrored-part evaluated counts ≠ 2n − weld / 2f, or on-plane verts ≠ weld |
| 15 | Mirrored-part partner check failed |
| 16 | Mirrored-part evaluated mesh stayed on one side |
| 17 | Mirror partner deviation above tolerance |
| 18 | Evaluated bbox not symmetric about X |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output. Its catalog falsifier is --no-mirror (expects exit 5).
Per-script sequential checks. 9 is a valid check code; there is no rule against it.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Topology ≠ closed form |
| 4 | Color attribute missing |
| 5 | Domain/type ≠ CORNER/FLOAT_COLOR (--point-domain lands here) |
| 6 | Attribute sized to verts, not loops |
| 7 | active_color not set |
| 8 | Probe loop color off HSV closed form |
| 9 | --output produced no file |
| 10 | Framing gate violation on the --output path (gallery_framing) |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output. Its catalog falsifier is --point-domain (expects exit 5).
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Topology ≠ closed form |
| 4 | Color attribute missing |
| 5 | Domain/type ≠ CORNER/FLOAT_COLOR (diagnostic, checked last) |
| 6 | Attribute sized to verts, not loops (--point-domain lands here) |
| 7 | active_color not set |
| 8 | Probe loop color off HSV closed form |
| 9 | --output produced no file |
| 10 | Framing gate violation on the --output path (gallery_framing) |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output. Its catalog falsifier is --point-domain (expects exit 6).
# Cheap correctness check (two tiny renders) — the CI check:
blender --background --python compositor_glare.py --
-# Falsifier: Threshold=10.0. Must exit non-zero (Threshold==1.0).
+# Falsifier: Threshold=1000.0, above the brightest ring (18). Must exit non-zero (no halo pixels, exit 4).
blender --background --python compositor_glare.py -- --threshold-high
# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts):
@@ -76,8 +76,8 @@ Run
blender --background --python compositor_glare.py -- --output rings.png --engine cycles
Per-script sequential checks. 9 is a valid check code; there is no rule against it.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Compositor tree / Glare / Threshold / link-chain contract (--threshold-high lands here) |
| 4 | Check render missing, tube dark, no halo, or halo does not fall off |
| 5 | Halo present with compositing off |
| 6 | --output produced no file |
| 10 | Gallery framing violation (render path; checked before the beauty render) |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output. Its catalog falsifier is --threshold-high (expects exit 3).
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Compositor tree / Glare / link-chain contract |
| 4 | Check render missing, tube dark, no halo, or halo does not fall off (--threshold-high lands here) |
| 5 | Halo present with compositing off |
| 6 | --output produced no file |
| 10 | Gallery framing violation (render path; checked before the beauty render) |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output. Its catalog falsifier is --threshold-high (expects exit 4).
The --output render path measures framing against the Layer 1 band via examples/gallery_framing.py (exit 10 on violation) before writing the still.
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage; also an active object existed after the data-API build |
| 3 | Custom ID property did not land |
| 4 | property_unset on a custom ID key did not TypeError, or it deleted the key |
| 5 | del did not report removal |
| 6 | Keep lamp lost the ID property |
| 7 | Clear lamp still has the ID property (--skip-delete / --unset-instead land here) |
| 8 | Registered-prop contract broke: attribute/property_unset failed, the prop sits on the wrong side of the 5.0 ID-property split, or scene["name"] raised KeyError (--subscript-registered on 5.0+; exits 0 on 4.5, where subscript access still works) |
| 10 | Gallery framing violation |
| 12 | --output produced no file |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Custom ID property did not land |
| 4 | property_unset on a custom ID key did not TypeError, or it deleted the key |
| 5 | del did not report removal |
| 6 | Keep lamp lost the ID property |
| 7 | Clear lamp still has the ID property (--skip-delete / --unset-instead land here) |
| 8 | Registered-prop contract broke: attribute/property_unset failed, the prop sits on the wrong side of the 5.0 ID-property split, or scene["name"] raised KeyError (--subscript-registered on 5.0+; exits 0 on 4.5, where subscript access still works) |
| 9 | An active object existed after the data-API build (checked first) |
| 10 | Gallery framing violation |
| 12 | --output produced no file |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output. Its catalog falsifiers are --skip-delete (expects exit 7); --unset-instead (expects exit 7); --subscript-registered (expects exit 8; 5.0+ only; SKIP on older Blender).
A runnable example that builds a beveled Bezier semicircle entirely through the curve data API — splines.new('BEZIER'), per-point bezier_points, bevel_depth, and use_fill_caps — so the curve renders as a solid tube without a prior mesh conversion.
What it witnesses: renderable curve tubes are curve datablocks, not meshes. The check asserts eight Bezier points, bevel_depth == 0.15, use_fill_caps, and that the depsgraph-evaluated mesh has the deterministic topology (1044 verts, 1028 faces for these resolution settings) with a Z span that rests on the floor ([0, 2 × bevel]) and an X span of 2 × radius + 2 × bevel.
What it witnesses: renderable curve tubes are curve datablocks, not meshes. The check asserts eight Bezier points and bevel_depth == 0.15, then that the depsgraph-evaluated mesh has the deterministic topology (1044 verts, 1028 faces for these resolution settings, the use_fill_caps cap faces included) with a Z span that rests on the floor ([0, 2 × bevel]) and an X span of 2 × radius + 2 × bevel.
Staging: the checked curve is presented as what a capped, beveled Bezier semicircle is in the world — a round-bar horseshoe magnet lying on the floor, open end to the camera. The two filled caps (use_fill_caps, the witness) are its ground-steel pole faces, turned face-on; an uncapped build would show two hollow pipe ends exactly there. The red enamel / bare-steel split is one material keyed on object-space Y, so the magnet stays a single curve datablock. The paper clips are beveled Bezier curves too (explicit FREE handles: straight legs plus quarter-circle bends), each lying along the field line that leaves its pole; the iron filings trace those lines — for two opposite line poles they are exact circular arcs through both pole faces. The cap's 12-sided outline is the pinned tessellation (bevel_resolution 4, locked by the 1044-vertex gate), not a presentation choice. The render path rotates the object only; the check reads object-space evaluated geometry and runs before any staging.
# Cheap correctness check (no render) — the CI check:
blender --background --python curve_bevel_arc.py --
-# Falsifier: uncapped tube. Must exit non-zero (use_fill_caps).
+# Falsifier: uncapped tube. Must exit non-zero (cap faces missing from the evaluated mesh).
blender --background --python curve_bevel_arc.py -- --no-caps
# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts):
@@ -76,8 +76,8 @@ Run
blender --background --python curve_bevel_arc.py -- --output arc.png --engine cycles
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is also the shared framing helper.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Spline type is not BEZIER |
| 4 | Bezier point count ≠ 8 |
| 5 | bevel_depth ≠ 0.15 |
| 6 | use_fill_caps is False (--no-caps lands here) |
| 7 | Evaluated vert/face count off measured tessellation |
| 8 | Tube does not rest on the floor |
| 9 | Tube height ≠ 2 × bevel |
| 10 | X span off closed form; also gallery framing violation |
| 11 | --output produced no file; also asset-quality floor violation (gallery_asset_quality) |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output. Its catalog falsifier is --no-caps (expects exit 6).
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Spline type is not BEZIER |
| 4 | Bezier point count ≠ 8 |
| 5 | bevel_depth ≠ 0.15 |
| 7 | Evaluated vert/face count off measured tessellation (--no-caps lands here: the caps are faces) |
| 8 | Tube does not rest on the floor |
| 9 | Tube height ≠ 2 × bevel |
| 10 | Gallery framing violation (render path) |
| 11 | Asset-quality floor violation (gallery_asset_quality, render path) |
| 12 | X span off closed form |
| 13 | --output produced no file |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output. Its catalog falsifier is --no-caps (expects exit 7).
Per-script sequential checks. 9 is a valid check code; there is no rule against it.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Lamp count ≠ 12 |
| 4 | Lamp does not carry exactly one DAMPED_TRACK |
| 5 | Constraint target is not Orb |
| 6 | track_axis is not TRACK_Z |
| 7 | Constraint muted or influence < 1 (--mute lands here) |
| 8 | TRACK_TO still present |
| 9 | Evaluated aim dot below 0.998 |
| 10 | Framing gate violation on the --output path (gallery_framing) |
| 12 | --output produced no file |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output. Its catalog falsifier is --mute (expects exit 7).
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Lamp count ≠ 12 |
| 4 | Lamp does not carry exactly one DAMPED_TRACK |
| 5 | Constraint target is not Orb |
| 6 | track_axis is not TRACK_Z |
| 7 | Constraint muted or influence < 1 while the evaluated aim still holds (diagnostic, after check 9) |
| 8 | TRACK_TO still present |
| 9 | Evaluated aim dot below 0.998 (--mute lands here) |
| 10 | Framing gate violation on the --output path (gallery_framing) |
| 12 | --output produced no file |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output. Its catalog falsifier is --mute (expects exit 9).
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper and 11 on the render path is the shared asset-quality helper.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage; also exporter RNA missing expected glTF kwargs |
| 3 | Source mast is not Z-dominant, or tip drifted |
| 4 | glTF reimport produced no mesh |
| 5 | Preset disk POSITION is not (x, z, -y), or preset node has rotation |
| 6 | Z-up disk POSITION is not raw Z-up; also --output produced no file |
| 7 | Preset reimport is not standing |
| 8 | Z-up reimport is not lying along Y, or reimported tip mismatch |
| 9 | Reimported orientations did not differ (--same-axis lands here) |
| 10 | Gallery framing violation |
| 11 | --same-axis did not collapse the axis difference; also asset-quality floor violation (render path) |
| 12 | Render path: no axis-aligned rotation maps the source onto a re-import, so the gizmo frame could not be measured |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Source mast is not Z-dominant, or tip drifted |
| 4 | glTF reimport produced no mesh |
| 5 | Preset disk POSITION is not (x, z, -y), or preset node has rotation |
| 6 | Z-up disk POSITION is not raw Z-up (checked after the re-import checks) |
| 7 | Preset reimport is not standing |
| 8 | Z-up reimport is not lying along Y, or reimported tip mismatch |
| 9 | Reimported orientations did not differ (--same-axis lands here; checked right after the preset stands) |
| 10 | Gallery framing violation (render path) |
| 11 | Asset-quality floor violation (render path) |
| 12 | Render path: no axis-aligned rotation maps the source onto a re-import, so the gizmo frame could not be measured |
| 13 | Exporter RNA missing expected glTF kwargs (checked first) |
| 14 | --output produced no file |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output. Its catalog falsifier is --same-axis (expects exit 9).
Two more authoring hazards are pinned in comments: exact face-plane coincidences between kit-bashed shells weld loops on export (the count check catches it), and read_factory_settings mid-check frees the original mesh — touching a freed RNA raises ReferenceError, so counts are captured before the wipe.
The render shows one crate. The solid crate is the authored one, rebuilt in place after the check. Laid over it is the actual re-imported mesh — the file's own triangles — drawn as a thin amber wire cage (a render-only Weld + Wireframe display stack on the imported object) and masked to the right of a vertical cut, so the left half reads as the authored asset and the right half as the file's geometry sitting on it. Neither object is moved: both keep the identity transform check 13 proved, and the camera does the turning. The cage lands on every bevel, rivet and vent slat only because the importer undid the +Y-up bake exactly and the file carried the evaluated (beveled) mesh. If the axis conversion broke, the cage would be rotated a quarter turn off the solid crate; a probe that re-applies that quarter turn to the imported object before rendering drops the cage through the floor and also fails the framing gate (bottom margin 0.000). If the modifier contract broke, the cage would trace square corners across the rounded ones.
Presentation only, after every check has run: the authored crate's bevel is baked into its display mesh (the asset-quality edge measure reads Object.data, which is still the square cage while the bevel lives in the modifier stack), and its paint and trim get a noise-mottled base colour and roughness. The exported file only ever carried the flat Principled values.
The render path gates framing through gallery_framing.check_framing and the asset floors through gallery_asset_quality.check_asset_quality on the authored crate. Both helpers' codes (10, 11) are already check codes here, so the call site remaps them to 22 and 23.
The render path gates framing through gallery_framing.check_framing and the asset floors through gallery_asset_quality.check_asset_quality on the authored crate. Both return their reserved codes (10, 11) directly; the POSITION-count and UV V-flip checks use 22 and 23.
# Cheap correctness check (no render) — the CI check:
blender --background --python gltf_export_roundtrip.py --
@@ -82,7 +82,7 @@ Run
blender --background --python gltf_export_roundtrip.py -- --output crate.png --engine cycles
Per-script sequential checks. 9 is a valid check code; there is no rule against it.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Exporter/importer RNA kwargs drifted |
| 4 | Base cage drifted from its closed form |
| 5 | Authored UVs drifted from the box-map closed form |
| 6 | Bevel produced no evaluated geometry |
| 7 | On-disk node/mesh/generator contract drifted |
| 8 | On-disk primitive/material binding count drifted |
| 9 | On-disk POSITION bounds ≠ axis-converted bbox (--no-yup lands here) |
| 10 | On-disk POSITION count ≠ evaluated loop count |
| 11 | On-disk UV V-flip failed |
| 12 | Re-import did not produce exactly one mesh |
| 13 | Re-imported object carries a transform |
| 14 | Material names drifted on re-import |
| 15 | Re-import vert count ≠ evaluated loop count |
| 16 | Round-trip position drift |
| 17 | Round-trip normal drift |
| 18 | Round-trip UV drift |
| 19 | Re-import triangle count drifted |
| 20 | Per-triangle material bindings drifted |
| 21 | --output produced no file |
| 22 | Gallery framing violation (render path only; the helper's 10, remapped) |
| 23 | Gallery asset-quality violation (render path only; the helper's 11, remapped) |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Exporter/importer RNA kwargs drifted |
| 4 | Base cage drifted from its closed form |
| 5 | Authored UVs drifted from the box-map closed form |
| 6 | Bevel produced no evaluated geometry |
| 7 | On-disk node/mesh/generator contract drifted |
| 8 | On-disk primitive/material binding count drifted |
| 9 | On-disk POSITION bounds ≠ axis-converted bbox (--no-yup lands here) |
| 10 | Gallery framing violation (render path only; gallery_framing) |
| 11 | Gallery asset-quality violation (render path only; gallery_asset_quality) |
| 12 | Re-import did not produce exactly one mesh |
| 13 | Re-imported object carries a transform |
| 14 | Material names drifted on re-import |
| 15 | Re-import vert count ≠ evaluated loop count |
| 16 | Round-trip position drift |
| 17 | Round-trip normal drift |
| 18 | Round-trip UV drift |
| 19 | Re-import triangle count drifted |
| 20 | Per-triangle material bindings drifted |
| 21 | --output produced no file |
| 22 | On-disk POSITION count ≠ evaluated loop count |
| 23 | On-disk UV V-flip failed |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output. Its catalog falsifier is --no-yup (expects exit 9).
Per-script sequential checks. 9 is a valid check code; there is no rule against it.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Exporter RNA missing expected kwargs |
| 4 | Vertex group count ≠ bone count |
| 5 | Disk skins ≠ 1 (--no-skins lands here) |
| 6 | Skin joints ≠ bone names |
| 7 | Missing JOINTS_0/WEIGHTS_0, or accessor length mismatch |
| 8 | Disk weight sums off 1.0 |
| 9 | Disk verts exceed evaluated loops |
| 10 | Armature count after import ≠ 1 |
| 11 | Bone count drifted |
| 12 | Named bone lost |
| 13 | Bone parent drifted |
| 14 | Rest matrices drifted |
| 15 | Skinned mesh count after import ≠ 1 |
| 16 | Re-import vert count ≠ disk |
| 17 | Vertex group names drifted |
| 18 | Weight round-trip drifted |
| 19 | Deformation round-trip drifted |
| 20 | --output produced no file |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Exporter RNA missing expected kwargs |
| 4 | Vertex group count ≠ bone count |
| 5 | Disk skins ≠ 1 (--no-skins lands here) |
| 6 | Skin joints ≠ bone names |
| 7 | Missing JOINTS_0/WEIGHTS_0, or accessor length mismatch |
| 8 | Disk weight sums off 1.0 |
| 9 | Disk verts exceed evaluated loops |
| 10 | Gallery framing violation (render path only; gallery_framing) |
| 11 | Gallery asset-quality violation (render path only; gallery_asset_quality) |
| 12 | Named bone lost |
| 13 | Bone parent drifted |
| 14 | Rest matrices drifted |
| 15 | Skinned mesh count after import ≠ 1 |
| 16 | Re-import vert count ≠ disk |
| 17 | Vertex group names drifted |
| 18 | Weight round-trip drifted |
| 19 | Deformation round-trip drifted |
| 20 | --output produced no file |
| 21 | Armature count after import ≠ 1 |
| 22 | Bone count drifted |
On the --output path only, which runs after every check has passed, 10 also means a gallery_framing violation and 11 a gallery_asset_quality floor violation. The shared helpers own those codes, and the log line says which one fired.
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output. Its catalog falsifier is --no-skins (expects exit 5).
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Height input identifier missing on the tree interface |
| 4 | Modifiers do not share one node_group |
| 5 | Version-path write raised (--api dict on 5.2, --api rna on 4.5) |
| 6 | Version-path read raised |
| 7 | Readback ≠ intended height (--same-height lands here) |
| 8 | Evaluated Z-extent ≠ intended height |
| 9 | Evaluated mesh not sitting on z=0 |
| 10 | Gallery framing violation |
| 11 | Evaluated extents not distinct |
| 12 | --output produced no file |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output. Forcing the RNA path is not smoke-tested (it raises on 4.5 and is the default on 5.2). Its catalog falsifiers are --same-height (expects exit 7) and --api dict (expects exit 5; 5.2+ only, where the dict write raises; SKIP on older Blender, where it exits 0).
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Height input identifier missing on the tree interface |
| 4 | Modifiers do not share one node_group |
| 5 | Version-path write raised (--api dict on 5.2, --api rna on 4.5) |
| 6 | Version-path read raised |
| 7 | Readback ≠ intended height (diagnostic, after the evaluated checks) |
| 8 | Evaluated Z-extent ≠ intended height (--same-height lands here) |
| 9 | Evaluated mesh not sitting on z=0 |
| 10 | Gallery framing violation |
| 12 | --output produced no file |
| 13 | Evaluated extents not distinct |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output. Forcing the RNA path is not smoke-tested (it raises on 4.5 and is the default on 5.2). Its catalog falsifiers are --same-height (expects exit 8) and --api dict (expects exit 5; 5.2+ only, where the dict write raises; SKIP on older Blender, where it exits 0).
| Probe | Binary | Result |
|---|---|---|
--api dict | 5.2.1 LTS | exit 5, TypeError: id properties not supported for this type |
--api rna | 4.5.11 LTS | exit 5, AttributeError: 'NodesModifier' object has no attribute 'properties' |
--api dict | 5.1.2 | exit 0 (5.1 still takes the dict form) |
--api rna | 5.1.2 | exit 5, AttributeError: 'NodesModifier' object has no attribute 'properties' |
--same-height | 5.2.1, 5.1.2, 4.5.11 | exit 7, readback 1.0 != written 2.0 on SpiralStair.H2 |
RAIL_H = -0.5 (treads climb past the post) | 5.2.1, 4.5.11 | exit 8, evaluated Z-extent 1.934334 != height 1.0 |
--api auto | 5.2.1, 5.1.2, 4.5.11 | exit 0, extents 1.000 / 2.000 / 3.000 |
| Probe | Binary | Result |
|---|---|---|
--api dict | 5.2.1 LTS | exit 5, TypeError: id properties not supported for this type |
--api rna | 4.5.11 LTS | exit 5, AttributeError: 'NodesModifier' object has no attribute 'properties' |
--api dict | 5.1.2 | exit 0 (5.1 still takes the dict form) |
--api rna | 5.1.2 | exit 5, AttributeError: 'NodesModifier' object has no attribute 'properties' |
--same-height | 5.2.1 | exit 8, evaluated Z-extent 1.000000 != height 2.0 on SpiralStair.H2 (the readback, exit 7, now runs after it) |
RAIL_H = -0.5 (treads climb past the post) | 5.2.1, 4.5.11 | exit 8, evaluated Z-extent 1.934334 != height 1.0 |
--api auto | 5.2.1, 5.1.2, 4.5.11 | exit 0, extents 1.000 / 2.000 / 3.000 |
Per-script sequential checks. 9 is a valid check code; there is no rule against it.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | SDF remesh produced no/unchanged geometry (--no-sdf lands here) |
| 4 | --output produced no file |
| 5 | Wrong-era EEVEE engine id was accepted |
| 6 | Input material dropped by remesh |
| 10 | Framing gate violation on the --output path (gallery_framing) |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output. Its catalog falsifier is --no-sdf (expects exit 3).
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | SDF remesh produced no/unchanged geometry (--no-sdf lands here) |
| 4 | --output produced no file |
| 5 | Wrong-era EEVEE engine id was accepted |
| 6 | Input material dropped by remesh (--no-set-material lands here) |
| 10 | Framing gate violation on the --output path (gallery_framing) |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output. Its catalog falsifiers are --no-sdf (expects exit 3) and --no-set-material (expects exit 6).
Staging. Everything presentational runs after check() has passed on the raw evaluated zones. Each zone's blocks are graded along the axis the zone iterates on, using Object-space coordinates (amber to red along X for the four Repeat blocks, deep teal to mint along Z for the six For Each blocks), so the colour steps count the iterations. A render-only Bevel modifier after the GN modifier chamfers the cubes into machined blocks. The Repeat row sits on a walnut plinth; the For Each tower is threaded on a steel spindle over a round walnut base, so its 0.18 m gaps read as spacing on a rod rather than floating cubes.
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage; also carrier mesh was rewritten |
| 3 | Repeat evaluated vert/face count off closed form |
| 4 | Repeat X-centers off closed form |
| 5 | For Each evaluated vert/face count off closed form |
| 6 | For Each Z-centers off closed form |
| 10 | Gallery framing violation |
| 12 | --output produced no file |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Repeat evaluated vert/face count off closed form |
| 4 | Repeat X-centers off closed form |
| 5 | For Each evaluated vert/face count off closed form |
| 6 | For Each Z-centers off closed form |
| 7 | Carrier mesh was rewritten (checked first) |
| 10 | Gallery framing violation |
| 12 | --output produced no file |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output. Its catalog falsifiers are --unpair-repeat (expects exit 3), --no-offset (expects exit 4), --unpair-foreach and --foreach-main (each expects exit 5).
A runnable example that builds a lighthouse on a rocky islet — tower, gallery, lantern room, keeper's cottage, a hauled-up rowboat and a diorama sea — as one cel-shaded mesh, attaches a Grease Pencil LINEART modifier with source_type='OBJECT', and proves the contour contract through the depsgraph, including the 4.5 → 5.1 stroke-width trap.
What it witnesses: the Line Art modifier contract AI-generated NPR code most often gets wrong.
modifiers.new(..., 'LINEART') on a GPv3 object, with target_layer / target_material set, evaluates the source mesh's edges into drawing strokes.source_object is load-bearing. Clearing it yields 0 evaluated strokes (proven in the same check pass). Restoring it recovers the drawing.thickness (legacy px, set to 22 here) and radius; 5.1 removes thickness (AttributeError) and keeps radius only (portable path: mod.radius = 0.021).grease-pencil-rosette: grease_pencils_v3 on 4.5, grease_pencils on 5.x.What each check catches on failure: wrong GPv3 collection (exit 2), thickness present/absent on the wrong side of 5.0 (exit 3), lost source_object assignment or use_contour (exit 4), drawing below the gates (exit 5), source clear not zeroing strokes (exit 6), flags-off not zeroing strokes or a rebuild not recovering the same counts (exit 7).
What each check catches on failure: wrong GPv3 collection (exit 9), thickness present/absent on the wrong side of 5.0 (exit 3), lost source_object assignment (exit 4), drawing below the gates (exit 5; use_contour off lands here), source clear not zeroing strokes (exit 6), flags-off not zeroing strokes or a rebuild not recovering the same counts (exit 7).
Version witness: stroke counts match on 4.5.11 LTS, 5.1.2 and 5.2.1 LTS (255 strokes / 1393 points). The divergence is thickness vs radius.
The still: the check and the still share one camera, because Line Art is view-dependent — the strokes the check counts are the strokes the still draws. The mesh is shaded flat (three cel tones), so the black ink is what makes it read as an inked illustration: clear source_object and the same render is a flat, lineless low-poly model — no band borders, no waterline, no outlines.
# Depsgraph contour check — the CI check:
blender --background --python gp_lineart_contour.py --
-# Falsifier: use_contour off. Must exit non-zero.
+# Falsifier: use_contour off. Must exit non-zero (evaluated strokes below the gates, exit 5).
blender --background --python gp_lineart_contour.py -- --no-contour
# Also render the gallery still:
blender --background --python gp_lineart_contour.py -- --output lineart.png
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage; also GPv3 collection address contract |
| 3 | LINEART thickness / radius trap for this Blender |
| 4 | LINEART type, source, or use_contour (--no-contour lands here) |
| 5 | Evaluated drawing below the stroke/point gates |
| 6 | Cleared source_object still produced strokes |
| 7 | Every edge type off still produced strokes, or the rebuild did not recover the same counts |
| 8 | --output produced no file |
| 10 | Gallery framing violation |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output. Its catalog falsifier is --no-contour (expects exit 4).
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | LINEART thickness / radius trap for this Blender |
| 4 | LINEART type or source |
| 5 | Evaluated drawing below the stroke/point gates (--no-contour lands here) |
| 6 | Cleared source_object still produced strokes |
| 7 | Every edge type off still produced strokes, or the rebuild did not recover the same counts |
| 8 | --output produced no file |
| 9 | GPv3 collection address contract (checked first) |
| 10 | Gallery framing violation |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output. Its catalog falsifier is --no-contour (expects exit 5).
# Two-render correctness check (tiny Cycles CPU renders) — the CI check:
blender --background --python light_link_studio.py --
-# Falsifier: receiver_collection unset. Must exit non-zero (assignment).
+# Falsifier: receiver_collection unset. Must exit non-zero (linked render: decoy lit).
blender --background --python light_link_studio.py -- --skip-link
# Also render the gallery still (Cycles, deterministic samples):
blender --background --python light_link_studio.py -- --output linked.png
Per-script sequential checks. 9 is a valid check code; there is no rule against it.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Light datablock gained light_linking |
| 4 | Light objects lost light_linking |
| 5 | receiver_collection assignment did not read back (--skip-link lands here) |
| 6 | Linked state: hero dark or hero/decoy ratio below gate |
| 7 | Decoy rise below gate when unlinked |
| 8 | Hero drifted across the unlink |
| 9 | --output produced no file |
| 10 | Framing gate violation on the --output path (gallery_framing) |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output. Its catalog falsifier is --skip-link (expects exit 5).
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Light datablock gained light_linking |
| 4 | Light objects lost light_linking |
| 6 | Linked state: hero dark or hero/decoy ratio below gate (--skip-link lands here) |
| 7 | Decoy rise below gate when unlinked |
| 8 | Hero drifted across the unlink |
| 9 | --output produced no file |
| 10 | Framing gate violation on the --output path (gallery_framing) |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output. Its catalog falsifier is --skip-link (expects exit 6).
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper. 11 is the shared asset-quality helper.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | UV layer names ≠ UV0 / UV1 |
| 4 | UVMap / UV1 render/clone flags wrong after re-assert |
| 5 | Channel 0 (UV0) touched by the UV1 unwrap |
| 6 | UV1 loops outside [0,1] |
| 7 | SAT overlap self-test failed, or UV1 triangle pairs overlap (--overlap-islands lands here) |
| 8 | Min island distance below margin |
| 9 | Part is not watertight |
| 10 | Gallery framing violation; also --output produced no file |
| 11 | Gallery asset-quality violation |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | UV layer names ≠ UV0 / UV1 |
| 4 | UVMap / UV1 render/clone flags wrong after re-assert |
| 5 | Channel 0 (UV0) touched by the UV1 unwrap |
| 6 | UV1 loops outside [0,1] |
| 7 | SAT overlap self-test failed, or UV1 triangle pairs overlap (--overlap-islands lands here) |
| 8 | Min island distance below margin |
| 9 | Part is not watertight |
| 10 | Gallery framing violation (--output only; gallery_framing) |
| 11 | Gallery asset-quality violation |
| 12 | --output produced no file |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output or --falsify. Its catalog falsifier is --overlap-islands (expect exit 7): the same island drag as --falsify, applied before the SAT scan.
Pipeline arc neighbors: watertight parametric solids in bmesh-gear, topology gates in mesh-hygiene-audit, origin/pivot discipline in prop-origin-transform, collision packaging in collision-hull-proxy.
What it witnesses (all closed form or independently re-derived):
x ∈ {0, 4} within 1e-6 m; every boundary edge lies on an end plane.(4, 0, 0) places its start ring at world positions matching the original's end ring within 1e-6 — no gap, no overlap at the joint.0..4 × ±1.5 × 0..3, all float32-exact values, deviation 0.0), and the whole-asset bbox matches it because detail is contained.Kit.CorridorSeg.* names, all part origins at the pivot.What each check catches on failure (probed with an unsnapped variant built by the same code minus the snap pass — 3 mm end-ring skew, 2 mm y-nudge on two verts): boundary verts off the end planes, worst 3.000e-03 m (exit 3); opposing rings displaced 2.000e-03 (exit 4); tiled joint gap/overlap (exit 5); bbox off the declared tile by 3.000e-03 (exit 6); boundary edges torn off the rim (exit 7); non-watertight detail (exit 8); detail escaping the tile (exit 9); unapplied transforms, default names, or wandering origins (exit 11).
+What each check catches on failure (probed with an unsnapped variant built by the same code minus the snap pass — 3 mm end-ring skew, 2 mm y-nudge on two verts): boundary verts off the end planes, worst 3.000e-03 m (exit 3); opposing rings displaced 2.000e-03 (exit 4); tiled joint gap/overlap (exit 5); bbox off the declared tile by 3.000e-03 (exit 6); boundary edges torn off the rim (exit 7); non-watertight detail (exit 8); detail escaping the tile (exit 9); unapplied transforms, default names, or wandering origins (exit 12).
Version witness: check output is byte-identical on Blender 4.5.11 LTS and 5.1.2 — same counts, same zero measured deviations.
Render as proof: a four-segment run — the joints vanish. The falsification variant (--falsify) accumulates a 120 mm gap, 50 mm lateral jogs, and 40 mm floor steps at each joint: floor plates split with dark seams and the trim rails visibly break. The check measures the same failure class at 3 mm; the render exaggerates it to read at frame scale. The render path also gates the asset through examples/gallery_asset_quality.py (naming, material variation, edge treatment — exit 11). An earlier revision shipped flat grey wall panels and was remodeled under that gate: each panel is now an assembly (backing plate + inset panel + four bolt heads), the palette shifted teal-slate so the card does not twin with lightmap-uv-channel's warm cart, and mean luminance was brought into the calibration range (82.7 → 66.1, ceiling 77.7) — no luminance deviation needed. The surface language is procedural and reads Object coordinates: every part's origin is the segment pivot, so painted-panel wear, the walkway's diamond-plate bump and the diagonal hazard stripes on the trim rails are functions of segment-local position — identical in every instance and continuous across a snapped joint, exactly like the geometry.
Framing deviation: the still is an interior corridor run — the envelope surrounds the camera on five sides and the tiling joints are the proof, so the subject reads as extending past the frame. The helper measures; this example enforces a projection-space fill-over cap (FRAME_DEV_MAX = 8.0). Gallery camera scores ~5.06 (pass). --close-camera scores ~30.9 and exits 10. Silhouette saturates at fill 1.0, so the cap is measured with strategy="projection".
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is this example's framing-deviation cap (call site, not the shared Layer 1 helper). 11 is the shared asset-quality helper.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Boundary vert count or end-plane membership |
| 4 | End rings do not partition evenly, or opposing loops differ |
| 5 | Tiled instances do not share boundary positions (--seam-gap lands here) |
| 6 | Shell or whole-asset bbox off the declared tile |
| 7 | Boundary edge count, non-manifold edges, or rim off the end planes |
| 8 | Detail part not watertight |
| 9 | Detail part reaches a tile boundary; also --output produced no file |
| 10 | Framing deviation score exceeds cap (--close-camera) |
| 11 | Kit part count, unapplied scale, namespace, or origin; also gallery asset-quality violation |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Boundary vert count or end-plane membership |
| 4 | End rings do not partition evenly, or opposing loops differ |
| 5 | Tiled instances do not share boundary positions (--seam-gap lands here) |
| 6 | Shell or whole-asset bbox off the declared tile |
| 7 | Boundary edge count, non-manifold edges, or rim off the end planes |
| 8 | Detail part not watertight |
| 9 | Detail part reaches a tile boundary; also --output produced no file |
| 10 | Framing deviation score exceeds cap (--close-camera; gallery_framing.measure_framing_deviation) |
| 11 | Gallery asset-quality violation (--output only) |
| 12 | Kit part count, unapplied scale, namespace, or origin |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output, --falsify, or --close-camera. Its catalog falsifier is --seam-gap (expect exit 5): the --falsify render's 120 mm gap and 50 mm jog, applied to the duplicate the joint check measures.
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is also the shared framing helper.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage; also closed-form palette no longer stresses unpremul |
| 3 | float_buffer=True image reports is_float=False |
| 4 | Float→PNG IHDR is not RGBA16 |
| 5 | Float→PNG RGB error below floor (--opaque-alpha lands here) |
| 6 | Float→PNG disagrees with closed-form false-unpremul model |
| 7 | Float→EXR round-trip above tolerance |
| 8 | float_buffer=False image reports is_float=True |
| 9 | Byte→PNG IHDR is not RGBA8 |
| 10 | Byte→PNG disagrees with straight-alpha 8-bit model; also gallery framing violation |
| 11 | Byte→PNG stress cell looks false-unpremul-mangled |
| 12 | EXR color_mode=RGB alpha-drop contract |
| 13 | --output produced no file |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | float_buffer=True image reports is_float=False |
| 4 | Float→PNG IHDR is not RGBA16 |
| 5 | Float→PNG RGB error below floor (--opaque-alpha lands here) |
| 6 | Float→PNG disagrees with closed-form false-unpremul model |
| 7 | Float→EXR round-trip above tolerance |
| 8 | float_buffer=False image reports is_float=True |
| 9 | Byte→PNG IHDR is not RGBA8 |
| 10 | Gallery framing violation (--output only; gallery_framing) |
| 12 | EXR color_mode=RGB alpha-drop contract |
| 13 | --output produced no file |
| 14 | Closed-form palette no longer stresses unpremul (checked first) |
| 15 | Byte→PNG disagrees with straight-alpha 8-bit model |
| 16 | Byte→PNG stress cell looks false-unpremul-mangled |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output. Its catalog falsifier is --opaque-alpha (expects exit 5).
A runnable example that declares a reusable DippedGlaze shader group through tree.interface.new_socket — the 4.x/5.x API that replaced tree.inputs/tree.outputs — and instances it in five mug materials with different parameters, following procedural-materials-and-shaders and the shader-node-group snippet.
What it witnesses: the grouping contract. Sockets declared on the interface appear on every group-node instance; all five materials share ONE group datablock (users == 5); and the per-material Tint lives on the group node, not inside the group — set it inside the tree and every mug changes at once.
What it witnesses: the grouping contract. Sockets declared on the interface appear on every group-node instance; all five materials share ONE group datablock (users == 5); and the per-material Tint lives on the group node, not inside the group — set it inside the tree and every mug changes at once. The check proves the last part with pixels: a 240×80 Cycles probe of the five mugs, rendered in a throwaway scene so the still is untouched, must show five pairwise-distinct glazes.
The render is the proof. The group is a dipped stoneware glaze: bare buff clay at the foot, a wavy dip line, iron speckle through clay and glaze alike, and a glaze that breaks back toward clay over the rim. All of that lives inside the group, so the five mugs — a warm-to-cool lineup of oxblood, amber, celadon, teal and cobalt — carry the same foot band, the same dip line, the same speckle field and the same rim break. Only the instance-level Tint differs. If Tint were baked into the group, the lineup would be one colour; if each material built its own tree, the shared character would be a copy rather than one datablock (the check's users gate).
A staggered shop-display lineup on the dark house stage: the odd mugs stand on a low walnut riser behind the even ones, so five mugs fill the frame without shrinking to a thin strip. Handles all turn the same way so the only thing that changes along the row is colour. The still renders under the Standard view transform (AgX would wash the five tints toward pastel); the key's spread is narrowed so it lights the mugs, not the back wall, and a warm wedge sits between the riser and the wall. Render path only.
@@ -78,8 +78,8 @@Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Interface sockets missing Tint / Roughness / Shader |
| 4 | Group datablock users ≠ number of mug materials (5) |
| 5 | Instance points at a different node tree |
| 6 | Instance Tint values not pairwise distinct (--same-tint lands here) |
| 7 | --output produced no file |
| 10 | Gallery framing violation |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output. Its catalog falsifier is --same-tint (expects exit 6).
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Interface sockets missing Tint / Roughness / Shader |
| 4 | Group datablock users ≠ number of mug materials (5) |
| 5 | Instance points at a different node tree |
| 6 | Instance Tint socket values not pairwise distinct (diagnostic, after check 8) |
| 7 | --output produced no file |
| 8 | Rendered probe: the five mug glazes are not pairwise distinct, or the probe render produced no file (--same-tint lands here) |
| 10 | Gallery framing violation |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output. Its catalog falsifier is --same-tint (expects exit 8).
A runnable example that builds a World whose Background Color is driven by ShaderNodeTexSky, then proves sun_elevation is load-bearing with two tiny Cycles EXR zenith probes. The gallery still is a sky study: a red-granite gnomon obelisk on a paved plaza, lit by nothing but the sky, shown at both elevations side by side so the contract reads at thumbnail scale.
What it witnesses: the Sky Texture contract AI-generated lighting code most often gets wrong across the 4.5 LTS → 5.1 window.
sky_type renamed. 4.5 LTS uses NISHITA; 5.1 replaced it with MULTIPLE_SCATTERING (Nishita's successor). Assigning NISHITA on 5.1 fails; the enum no longer lists it.dust_density → aerosol_density. 4.5 exposes dust_density; 5.1 raises AttributeError and ships aerosol_density instead. AI code still emits dust_density.sun_elevation brightens zenith. Two CPU Cycles OPEN_EXR probes with a straight-up camera, Background Strength 0.05 (unclipped), compare zenith luminance at 8° vs 55°. Measured rise 2.25x on 5.1.2 and 1.50x on 4.5.11 LTS (gate ≥ 1.25).What each check catches on failure: broken Sky→Background link (exit 6), wrong sky_type for the running version (exit 3), lost sun_elevation round-trip (exit 4), dust_density / NISHITA present on the wrong side of 5.0 (exit 5), a non-working sky (zenith floor, exit 7), and an elevation that does not brighten zenith (exit 8 — rise below 1.25).
What each check catches on failure: broken Sky→Background link (exit 6, checked after the renders), wrong sky_type for the running version (exit 3), lost sun_elevation round-trip (exit 4), dust_density / NISHITA present on the wrong side of 5.0 (exit 5), a non-working sky (zenith floor, exit 7 — where --unlink-sky lands), and an elevation that does not brighten zenith (exit 8 — rise below 1.25).
Version witness: sky_type and the dust/aerosol rename are the divergence; sun_elevation itself is stable. Zenith rise differs by model (Nishita vs multiple scattering) but clears the same gate on both binaries.
The World carries a Nishita / multiple-scattering Sky Texture instead of the default near-black Background, and there are no studio lights: the contract is the sky, so the sky's sun disc is the key and its dome is the fill. The still is a diptych, 8° | 55°, split by a dark 12 px rule, with one Background Strength (0.035) for both panels so sun_elevation is the only variable. If it did not take, the two panels would render identical.
# Zenith-luminance correctness check (tiny Cycles CPU EXR probes) — the CI check:
blender --background --python sky_texture_sun_elevation.py --
-# Falsifier: Sky→Background unlinked. Must exit non-zero (world links).
+# Falsifier: Sky→Background unlinked. Must exit non-zero (zenith probe: no sky).
blender --background --python sky_texture_sun_elevation.py -- --unlink-sky
# Also render the gallery diptych (Cycles; framing gate exit 10, clip gate exit 12):
blender --background --python sky_texture_sun_elevation.py -- --output sky.png
Per-script sequential checks. 9 is a valid check code; there is no rule against it.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | sky_type is not the identifier for this Blender |
| 4 | sun_elevation round-trip failed |
| 5 | dust_density / aerosol_density / NISHITA / MULTIPLE_SCATTERING trap |
| 6 | Sky → Background → World Output links broken (--unlink-sky lands here) |
| 7 | High-elevation zenith luma below floor |
| 8 | Zenith luma did not rise with elevation |
| 9 | --output produced no file |
| 10 | --output framing violation (gallery_framing, per panel) |
| 12 | --output still clips to white under Standard (> 0.05 % of pixels) |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output, so exits 10 and 12 are render-path only. Its catalog falsifier is --unlink-sky (expects exit 6).
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | sky_type is not the identifier for this Blender |
| 4 | sun_elevation round-trip failed |
| 5 | dust_density / aerosol_density / NISHITA / MULTIPLE_SCATTERING trap |
| 6 | Sky → Background → World Output links broken (diagnostic, after the zenith probes) |
| 7 | High-elevation zenith luma below floor (--unlink-sky lands here) |
| 8 | Zenith luma did not rise with elevation |
| 9 | --output produced no file |
| 10 | --output framing violation (gallery_framing, per panel) |
| 12 | --output still clips to white under Standard (> 0.05 % of pixels) |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output, so exits 10 and 12 are render-path only. Its catalog falsifier is --unlink-sky (expects exit 7).
The render is the proof: the faceted Goldberg cage is smoothed by an unapplied Subsurf modifier (panel materials carry through Catmull-Clark per face class), and the ball is grounded by its depsgraph-evaluated lowest vertex. Invert the panel binding and the still inverts with it: white pentagons on a black ball, wrong on sight.
None of this touches the checked mesh, its binding, or any check.
-gallery_framing.check_framing before writing the still. The helper returns 10, which this example already spends on the centroid check, so the call site maps a violation to 14. The larger, rounder ball touched the top edge at the old 55 mm lens; the lens is now 50 mm, and a TRACK_TO aim sits 0.25 below the ball's centre.gallery_framing.check_framing before writing the still and returns its exit 10 directly (10 is reserved for this gate; the centroid check uses 14). The larger, rounder ball touched the top edge at the old 55 mm lens; the lens is now 50 mm, and a TRACK_TO aim sits 0.25 below the ball's centre.The floor is dark mown turf rather than the near-black studio floor: a ball alone on a black stage read as an object in a void, and the pitch is the context that makes it a match ball. The back wall and warm wedge are the default stage; measured stage luminance 0.217 sits inside the calibration set.
Per-script sequential checks. 9 is a valid check code; there is no rule against it.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Topology ≠ 60/90/32 |
| 4 | Euler characteristic ≠ 2 |
| 5 | Face census ≠ 12 pentagons + 20 hexagons |
| 6 | Vertex degree not uniform 3; also --output produced no file |
| 7 | Non-manifold edges |
| 8 | Edge lengths not uniform |
| 9 | Face planarity off |
| 10 | Centroid off origin |
| 11 | Circumradius not uniform |
| 12 | Panel material count ≠ 2 |
| 13 | Panel binding not by vertex count (--invert-bind lands here) |
| 14 | Framing gate (--output path only; gallery_framing returns 10, remapped) |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Topology ≠ 60/90/32 |
| 4 | Euler characteristic ≠ 2 |
| 5 | Face census ≠ 12 pentagons + 20 hexagons |
| 6 | Vertex degree not uniform 3; also --output produced no file |
| 7 | Non-manifold edges |
| 8 | Edge lengths not uniform |
| 9 | Face planarity off |
| 10 | Framing gate (--output path only; gallery_framing) |
| 12 | Panel material count ≠ 2 |
| 13 | Panel binding not by vertex count (--invert-bind lands here) |
| 14 | Centroid off origin |
| 15 | Circumradius not uniform |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output. Its catalog falsifier is --invert-bind (expects exit 13).
# Cheap correctness check (materials + engine-id witness, no render):
blender --background --python swatch_grid.py --
-# Falsifier: same RGB on every swatch. Must exit non-zero.
-blender --background --python swatch_grid.py -- --same-base
+# Falsifiers (smoke runs them with --output; see the exit table):
+blender --background --python swatch_grid.py -- --output s.png --engine cycles --same-base
+blender --background --python swatch_grid.py -- --invert-engine-id
+blender --background --python swatch_grid.py -- --no-specular-shim
# Render and pixel-verify with the build's EEVEE engine (needs a GPU/display):
blender --background --python swatch_grid.py -- --output swatch.png
@@ -84,9 +86,9 @@ StagingExit codes#
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.
-Code Meaning 0 Success 1 Uncaught exception (FATAL wrapper) 2 argparse / usage 3 Distinct swatch colors ≠ 6 (--same-base lands here); also render not six distinct regions 4 --output produced no file5 Wrong-era EEVEE engine id was accepted 10 Gallery framing violation
+Code Meaning 0 Success 1 Uncaught exception (FATAL wrapper) 2 argparse / usage 3 Distinct swatch material colors ≠ 6 (check-only path; with --output a diagnostic after check 6) 4 --output produced no file5 Wrong-era EEVEE engine id was accepted (--invert-engine-id lands here) 6 Rendered still is black or not six distinct swatch regions (--same-base lands here) 7 set_specular did not resolve Specular IOR Level or the value did not land (--no-specular-shim lands here)10 Gallery framing violation
--no-verify was a skip-flag and has been removed. Pixel verification always runs when --output is passed.
-The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output. Its catalog falsifier is --same-base (expects exit 3).
+The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke passes --output (Cycles, 8 samples, 640 px) so the pixel check runs. Its catalog falsifiers are --same-base (expects exit 6, the rendered regions), --invert-engine-id (expects exit 5) and --no-specular-shim (expects exit 7).
Verified#
Runs headless on Blender 4.5.10 LTS and 5.1.1; exercised on both by the blender-smoke workflow on every PR and weekly schedule. The --output render path additionally measures framing against the Layer 1 band via examples/gallery_framing.py (exit 10 on violation) before writing the still.
The render scales the stamp to a constant width, so the frame holds for any version-string length.
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Not a FONT TextCurve with the built-in Bfont |
| 4 | body is not the live version_string (--wrong-body lands here) |
| 5 | Flat text is not a filled planar mesh |
| 6 | Extrude / bevel closed form failed |
| 7 | Appending characters did not widen the text |
| 8 | Mesh survived to_mesh_clear() |
| 9 | --output produced no file |
| 10 | Gallery framing violation |
| 13 | --output: bundled datafiles/fonts/Inter.woff2 not found |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Not a FONT TextCurve with the built-in Bfont |
| 4 | Evaluated glyph mesh does not match a reference TextCurve of the live version_string (--wrong-body lands here) |
| 5 | Flat text is not a filled planar mesh |
| 6 | Extrude / bevel closed form failed |
| 7 | Appending characters did not widen the text |
| 8 | Mesh survived to_mesh_clear() |
| 9 | --output produced no file |
| 10 | Gallery framing violation |
| 13 | --output: bundled datafiles/fonts/Inter.woff2 not found |
| 14 | version_string does not start with the dotted bpy.app.version tuple |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output. Its catalog falsifier is --wrong-body (expects exit 4).
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper, 11 the shared asset-quality helper.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Loop-triangle count ≠ closed form |
| 4 | Re-fetched UV layer drifted from the polar field (--zero-uv lands here) |
| 5 | Tangent basis not orthonormal |
| 6 | Bitangent ≠ sign × (n × t) |
| 7 | Tangents deviate from the edge/UV closed form |
| 8 | Flipped tangent inside a clean triangle |
| 10 | Gallery framing violation; also --output produced no file |
| 11 | Asset-quality floor violation (--output only; shared helper) |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Loop-triangle count ≠ closed form |
| 4 | Re-fetched UV layer drifted from the polar field (--zero-uv lands here) |
| 5 | Tangent basis not orthonormal |
| 6 | Bitangent ≠ sign × (n × t) |
| 7 | Tangents deviate from the edge/UV closed form |
| 8 | Flipped tangent inside a clean triangle |
| 10 | Gallery framing violation (--output only; gallery_framing) |
| 11 | Asset-quality floor violation (--output only; shared helper) |
| 12 | --output produced no file |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output. Its catalog falsifier is --zero-uv (expects exit 4).
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage; also base cube verts ≠ 8 |
| 3 | VIEWPORT TESSELLATE point/face count off closed form |
| 4 | RENDER TESSELLATE point/face count or scheme off closed form |
| 5 | BEST_MATCH cage point count or scheme off |
| 6 | RENDER and VIEWPORT USDA point counts are identical (check), or the re-imported RENDER goblet is not denser than the VIEWPORT one (--output) |
| 10 | Gallery framing violation |
| 12 | --output produced no file |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | VIEWPORT TESSELLATE point/face count off closed form |
| 4 | RENDER TESSELLATE point/face count or scheme off closed form |
| 5 | BEST_MATCH cage point count or scheme off |
| 6 | RENDER and VIEWPORT USDA point counts are identical (check), or the re-imported RENDER goblet is not denser than the VIEWPORT one (--output) |
| 7 | Base cube verts ≠ 8 (checked first) |
| 10 | Gallery framing violation |
| 12 | --output produced no file |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output. Its catalog falsifiers are --subdivision BEST_MATCH (expects exit 4) and --evaluation-mode VIEWPORT (expects exit 4).
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared gallery framing gate; 15 is the missing-render-file check.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Silent-no-op hazard gone (--precreate-on-hazard lands here) |
| 4 | Grid topology drifted |
| 5 | Pre-create + calc_uvs did not persist one UV layer |
| 6 | calc_uvs closed-form error |
| 7 | Mesh UV round-trip error |
| 8 | calc_uvs=False unexpectedly created a UV layer |
| 9 | Explicit UV assignment error |
| 10 | Framing gate violation on the --output path (gallery_framing) |
| 11 | Broken panel is not flat |
| 12 | Broken panel is not texel-(0,0) teal |
| 13 | Repaired panel is not a checker |
| 14 | Broken and repaired panels render identically |
| 15 | --output produced no file |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Silent-no-op hazard gone (--precreate-on-hazard lands here) |
| 4 | Grid topology drifted |
| 5 | Pre-create + calc_uvs did not persist one UV layer |
| 6 | calc_uvs closed-form error |
| 7 | Mesh UV round-trip error |
| 8 | calc_uvs=False unexpectedly created a UV layer |
| 9 | Explicit UV assignment error |
| 10 | Framing gate violation on the --output path (gallery_framing) |
| 12 | Broken panel is not texel-(0,0) teal |
| 13 | Repaired panel is not a checker |
| 14 | Broken and repaired panels render identically |
| 15 | --output produced no file |
| 16 | Broken panel is not flat |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output. Its catalog falsifier is --precreate-on-hazard (expects exit 3).
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper; it is also the missing-render code. 11 is the shared asset-quality helper.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Pre-limit max influences ≠ 5 |
| 4 | Vertex over the 4-influence cap (--skip-limit lands here) |
| 5 | Limit changed nothing |
| 6 | Weight sums off 1.0 after renormalize |
| 7 | Pose damaged by pruning, or evaluated vert count changed |
| 8 | Evaluated mesh off LBS over limited weights |
| 9 | Root-weighted mount moved |
| 10 | Gallery framing violation; also --output produced no file |
| 11 | Asset-quality floor violation (--output only) |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Pre-limit max influences ≠ 5 |
| 4 | Vertex over the 4-influence cap (--skip-limit lands here) |
| 5 | Limit changed nothing |
| 6 | Weight sums off 1.0 after renormalize |
| 7 | Pose damaged by pruning, or evaluated vert count changed |
| 8 | Evaluated mesh off LBS over limited weights |
| 9 | Root-weighted mount moved |
| 10 | Gallery framing violation (--output only; gallery_framing) |
| 11 | Asset-quality floor violation (--output only) |
| 12 | --output produced no file |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output. Its catalog falsifier is --skip-limit (expects exit 4).
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is also the shared framing helper.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | .sequences accessor contract for this Blender |
| 4 | Wrong-era new_effect end kwarg or TRANSFORM type did not TypeError |
| 5 | Strip span or channel off closed form |
| 6 | frame_final_* deprecation bridge for this Blender |
| 7 | GC wiring or strip .type enum (--swap-inputs lands here) |
| 8 | Scene strip span or Stage source |
| 9 | Mosaic transform, compositing defaults, text body, or strip color |
| 10 | Save/reload round-trip re-assert failed; also gallery framing violation |
| 11 | --output produced no file |
| 12 | --check-pixels compositing contract |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | .sequences accessor contract for this Blender |
| 4 | Wrong-era new_effect end kwarg or TRANSFORM type did not TypeError |
| 5 | Strip span or channel off closed form |
| 6 | frame_final_* deprecation bridge for this Blender |
| 7 | GC wiring or strip .type enum (--swap-inputs lands here) |
| 8 | Scene strip span or Stage source |
| 9 | Mosaic transform, compositing defaults, text body, or strip color |
| 10 | Gallery framing violation (--output only; gallery_framing) |
| 12 | --check-pixels compositing contract |
| 13 | Save/reload round-trip re-assert failed |
| 14 | --output produced no file |
The blender-smoke workflow runs the check and the pixel witness on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output. Its catalog falsifier is --swap-inputs (expects exit 7).