From 98fbd22ce809c13e4d109b6954cdcea0e49043f5 Mon Sep 17 00:00:00 2001 From: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Date: Wed, 7 Oct 2026 10:32:07 -0400 Subject: [PATCH] fix(examples): reserve exits 2/10/11 and make falsifiers reach behavioral checks Exit codes are how smoke and the falsifiers tell which check fired. Twenty examples spent argparse's 2 or the render gates' 10/11 on their own checks, so those contracts could never be falsifier targets or were misattributed; gltf-skin-roundtrip's gates even returned the same 10/11 as its armature and bone checks. exit-pre-sidecar's --force-run returned 2 on both its expected and its unexpected branch (now 3 and 4). check_exit_code_readme now rejects a literal 2/10/11 at an example's exit site and any 2/10/11 table row that is not the reserved meaning or adds "also" (#473). Twelve falsifiers stopped at a read-back of the value their own flag had just written, so the behavioral check was never reached and could be deleted with smoke still green. Each read-back is gone or runs after the behavioral check as a diagnostic; shader-node-group gains a tiny Cycles probe in a throwaway scene and text-version-stamp compares evaluated glyphs against a reference TextCurve. Every row was re-proven by deleting its behavioral check: the falsifier then misses its expected exit (#469). png-exr-alpha no longer sets scene image_settings that Image.save() ignores, inside a silent except. gn-sdf-remesh gains --no-set-material, swatch-grid gains --invert-engine-id and --no-specular-shim and asserts the specular value. Bulk per-element writes become foreach_set / foreach_get, and check_example_rules covers .loops/.edges/.data on mesh and attribute layers (#474). Signed-off-by: TMHSDigital Co-Authored-By: Claude Opus 5.5 --- CONTRIBUTING.md | 11 + docs/gallery/car-mirror-symmetry/index.html | 2 +- docs/gallery/color-attribute-wheel/index.html | 4 +- docs/gallery/compositor-glare/index.html | 6 +- .../cross-version-property-delete/index.html | 2 +- docs/gallery/curve-bevel-arc/index.html | 8 +- docs/gallery/damped-track-aim/index.html | 4 +- docs/gallery/export-preset-axis/index.html | 2 +- docs/gallery/gltf-export-roundtrip/index.html | 4 +- docs/gallery/gltf-skin-roundtrip/index.html | 2 +- docs/gallery/gn-modifier-inputs/index.html | 6 +- docs/gallery/gn-sdf-remesh/index.html | 7 +- docs/gallery/gn-zone-iterate/index.html | 2 +- docs/gallery/gp-lineart-contour/index.html | 8 +- docs/gallery/light-link-studio/index.html | 6 +- docs/gallery/lightmap-uv-channel/index.html | 2 +- docs/gallery/modular-kit-snap/index.html | 4 +- docs/gallery/png-exr-alpha/index.html | 2 +- docs/gallery/shader-node-group/index.html | 6 +- .../sky-texture-sun-elevation/index.html | 8 +- docs/gallery/soccer-ball-goldberg/index.html | 4 +- docs/gallery/swatch-grid/index.html | 10 +- docs/gallery/text-version-stamp/index.html | 2 +- docs/gallery/triangulate-tangents/index.html | 2 +- .../usd-export-evaluation-mode/index.html | 2 +- docs/gallery/uv-layer-grid/index.html | 2 +- docs/gallery/vertex-weight-limit/index.html | 2 +- docs/gallery/vse-cut-list/index.html | 2 +- examples/car-mirror-symmetry/README.md | 8 +- .../car_mirror_symmetry.py | 8 +- examples/coincident-vert-weld/README.md | 3 +- .../coincident_vert_weld.py | 2 +- examples/color-attribute-wheel/README.md | 6 +- .../color_attribute_wheel.py | 12 +- examples/compositor-glare/README.md | 8 +- examples/compositor-glare/compositor_glare.py | 16 +- .../cross-version-property-delete/README.md | 3 +- .../cross_version_property_delete.py | 2 +- examples/curve-bevel-arc/README.md | 19 +- examples/curve-bevel-arc/curve_bevel_arc.py | 17 +- examples/damped-track-aim/README.md | 6 +- examples/damped-track-aim/damped_track_aim.py | 27 ++- examples/exit-pre-sidecar/README.md | 15 +- examples/exit-pre-sidecar/exit_pre_sidecar.py | 6 +- examples/export-preset-axis/README.md | 12 +- .../export-preset-axis/export_preset_axis.py | 43 ++-- examples/gltf-export-roundtrip/README.md | 12 +- .../gltf_export_roundtrip.py | 22 +- examples/gltf-skin-roundtrip/README.md | 6 +- .../gltf_skin_roundtrip.py | 4 +- examples/gn-instance-grid/gn_instance_grid.py | 13 +- examples/gn-modifier-inputs/README.md | 10 +- .../gn-modifier-inputs/gn_modifier_inputs.py | 23 +- examples/gn-sdf-remesh/README.md | 7 +- examples/gn-sdf-remesh/gn_sdf_remesh.py | 6 +- examples/gn-zone-iterate/README.md | 3 +- examples/gn-zone-iterate/gn_zone_iterate.py | 2 +- examples/gp-lineart-contour/README.md | 17 +- .../gp-lineart-contour/gp_lineart_contour.py | 19 +- examples/index.json | 208 +++++++++++------- examples/light-link-studio/README.md | 7 +- .../light-link-studio/light_link_studio.py | 14 +- examples/lightmap-uv-channel/README.md | 3 +- .../lightmap_uv_channel.py | 2 +- .../mesh-hygiene-audit/mesh_hygiene_audit.py | 3 +- examples/modular-kit-snap/README.md | 7 +- examples/modular-kit-snap/modular_kit_snap.py | 10 +- examples/png-exr-alpha/README.md | 8 +- examples/png-exr-alpha/png_exr_alpha.py | 30 +-- examples/shader-node-group/README.md | 9 +- .../shader-node-group/shader_node_group.py | 103 ++++++++- examples/sky-texture-sun-elevation/README.md | 12 +- .../sky_texture_sun_elevation.py | 15 +- examples/soccer-ball-goldberg/README.md | 10 +- .../soccer_ball_goldberg.py | 7 +- examples/swatch-grid/README.md | 14 +- examples/swatch-grid/swatch_grid.py | 78 +++++-- examples/text-version-stamp/README.md | 3 +- .../text-version-stamp/text_version_stamp.py | 39 +++- examples/triangulate-tangents/README.md | 3 +- .../triangulate_tangents.py | 5 +- examples/unapplied-scale-gltf/README.md | 3 +- .../unapplied_scale_gltf.py | 9 +- examples/usd-export-evaluation-mode/README.md | 3 +- .../usd_export_evaluation_mode.py | 2 +- examples/uv-layer-grid/README.md | 2 +- examples/uv-layer-grid/uv_layer_grid.py | 2 +- examples/vertex-weight-limit/README.md | 3 +- .../vertex_weight_limit.py | 2 +- examples/vse-cut-list/README.md | 5 +- examples/vse-cut-list/vse_cut_list.py | 10 +- skills/geometry-nodes-python/SKILL.md | 2 +- skills/mesh-editing-and-bmesh/SKILL.md | 2 +- .../procedural-materials-and-shaders/SKILL.md | 6 +- tests/check_example_rules.py | 76 ++++++- tests/check_exit_code_readme.py | 96 ++++++++ tests/smoke/catalog.json | 22 +- tests/test_exit_code_readme.py | 38 ++++ 98 files changed, 913 insertions(+), 439 deletions(-) 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 @@

Run blender --background --python car_mirror_symmetry.py -- --output car.png --engine cycles

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it.

-
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Body datablock is not the authored half
4Authored centerline vert count ≠ 28
5Evaluated verts ≠ 2n − c (--no-mirror lands here)
6Evaluated on-plane verts ≠ centerline; also --output produced no file
7Evaluated Euler characteristic ≠ 2
8Non-manifold edges in the evaluated shell
9Evaluated verts lack a mirrored partner
10Mirror partner deviation above tolerance
11Evaluated bbox not symmetric about X
12Mirrored-part origin off the plane
13Mirrored-part datablock is not the authored half (verts, faces, on-plane verts)
14Mirrored-part evaluated counts ≠ 2n − weld / 2f, or on-plane verts ≠ weld
15Mirrored-part partner check failed
16Mirrored-part evaluated mesh stayed on one side
17Gallery framing violation (--output path only; gallery_framing returns 10, remapped)
18Asset-quality floor violation (--output path only; gallery_asset_quality returns 11, remapped)
+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Body datablock is not the authored half
4Authored centerline vert count ≠ 28
5Evaluated verts ≠ 2n − c (--no-mirror lands here)
6Evaluated on-plane verts ≠ centerline; also --output produced no file
7Evaluated Euler characteristic ≠ 2
8Non-manifold edges in the evaluated shell
9Evaluated verts lack a mirrored partner
10Gallery framing violation (--output path only; gallery_framing)
11Asset-quality floor violation (--output path only; gallery_asset_quality)
12Mirrored-part origin off the plane
13Mirrored-part datablock is not the authored half (verts, faces, on-plane verts)
14Mirrored-part evaluated counts ≠ 2n − weld / 2f, or on-plane verts ≠ weld
15Mirrored-part partner check failed
16Mirrored-part evaluated mesh stayed on one side
17Mirror partner deviation above tolerance
18Evaluated 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).

diff --git a/docs/gallery/color-attribute-wheel/index.html b/docs/gallery/color-attribute-wheel/index.html index c7949912..10f9b3b8 100644 --- a/docs/gallery/color-attribute-wheel/index.html +++ b/docs/gallery/color-attribute-wheel/index.html @@ -77,8 +77,8 @@

Run blender --background --python color_attribute_wheel.py -- --output wheel.png --engine cycles

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it.

-
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Topology ≠ closed form
4Color attribute missing
5Domain/type ≠ CORNER/FLOAT_COLOR (--point-domain lands here)
6Attribute sized to verts, not loops
7active_color not set
8Probe loop color off HSV closed form
9--output produced no file
10Framing 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).

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Topology ≠ closed form
4Color attribute missing
5Domain/type ≠ CORNER/FLOAT_COLOR (diagnostic, checked last)
6Attribute sized to verts, not loops (--point-domain lands here)
7active_color not set
8Probe loop color off HSV closed form
9--output produced no file
10Framing 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).

Source

diff --git a/docs/gallery/compositor-glare/index.html b/docs/gallery/compositor-glare/index.html index b52d0d67..c1c6b5b5 100644 --- a/docs/gallery/compositor-glare/index.html +++ b/docs/gallery/compositor-glare/index.html @@ -68,7 +68,7 @@

Run
# 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

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it.

-
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Compositor tree / Glare / Threshold / link-chain contract (--threshold-high lands here)
4Check render missing, tube dark, no halo, or halo does not fall off
5Halo present with compositing off
6--output produced no file
10Gallery 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).

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Compositor tree / Glare / link-chain contract
4Check render missing, tube dark, no halo, or halo does not fall off (--threshold-high lands here)
5Halo present with compositing off
6--output produced no file
10Gallery 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).

Source

diff --git a/docs/gallery/cross-version-property-delete/index.html b/docs/gallery/cross-version-property-delete/index.html index 627a9bb6..c84d6d07 100644 --- a/docs/gallery/cross-version-property-delete/index.html +++ b/docs/gallery/cross-version-property-delete/index.html @@ -78,7 +78,7 @@

Run

The --output render path measures framing against the Layer 1 band via examples/gallery_framing.py (exit 10 on violation) before writing the still.

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.

-
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage; also an active object existed after the data-API build
3Custom ID property did not land
4property_unset on a custom ID key did not TypeError, or it deleted the key
5del did not report removal
6Keep lamp lost the ID property
7Clear lamp still has the ID property (--skip-delete / --unset-instead land here)
8Registered-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)
10Gallery framing violation
12--output produced no file
+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Custom ID property did not land
4property_unset on a custom ID key did not TypeError, or it deleted the key
5del did not report removal
6Keep lamp lost the ID property
7Clear lamp still has the ID property (--skip-delete / --unset-instead land here)
8Registered-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)
9An active object existed after the data-API build (checked first)
10Gallery 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).

diff --git a/docs/gallery/curve-bevel-arc/index.html b/docs/gallery/curve-bevel-arc/index.html index 28881928..f5624b29 100644 --- a/docs/gallery/curve-bevel-arc/index.html +++ b/docs/gallery/curve-bevel-arc/index.html @@ -62,13 +62,13 @@

Curve Bevel Arc

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.

Run#

# 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

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is also the shared framing helper.

-
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Spline type is not BEZIER
4Bezier point count ≠ 8
5bevel_depth ≠ 0.15
6use_fill_caps is False (--no-caps lands here)
7Evaluated vert/face count off measured tessellation
8Tube does not rest on the floor
9Tube height ≠ 2 × bevel
10X 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).

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Spline type is not BEZIER
4Bezier point count ≠ 8
5bevel_depth ≠ 0.15
7Evaluated vert/face count off measured tessellation (--no-caps lands here: the caps are faces)
8Tube does not rest on the floor
9Tube height ≠ 2 × bevel
10Gallery framing violation (render path)
11Asset-quality floor violation (gallery_asset_quality, render path)
12X 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).

Source

diff --git a/docs/gallery/damped-track-aim/index.html b/docs/gallery/damped-track-aim/index.html index ae244f6f..b64b560c 100644 --- a/docs/gallery/damped-track-aim/index.html +++ b/docs/gallery/damped-track-aim/index.html @@ -77,8 +77,8 @@

Run blender --background --python damped_track_aim.py -- --output aim.png --engine cycles

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it.

-
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Lamp count ≠ 12
4Lamp does not carry exactly one DAMPED_TRACK
5Constraint target is not Orb
6track_axis is not TRACK_Z
7Constraint muted or influence < 1 (--mute lands here)
8TRACK_TO still present
9Evaluated aim dot below 0.998
10Framing 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).

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Lamp count ≠ 12
4Lamp does not carry exactly one DAMPED_TRACK
5Constraint target is not Orb
6track_axis is not TRACK_Z
7Constraint muted or influence < 1 while the evaluated aim still holds (diagnostic, after check 9)
8TRACK_TO still present
9Evaluated aim dot below 0.998 (--mute lands here)
10Framing 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).

Source

diff --git a/docs/gallery/export-preset-axis/index.html b/docs/gallery/export-preset-axis/index.html index 92c99621..ac791715 100644 --- a/docs/gallery/export-preset-axis/index.html +++ b/docs/gallery/export-preset-axis/index.html @@ -79,7 +79,7 @@

Run blender --background --python export_preset_axis.py -- --output mast.png --engine cycles

Exit codes#

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.

-
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage; also exporter RNA missing expected glTF kwargs
3Source mast is not Z-dominant, or tip drifted
4glTF reimport produced no mesh
5Preset disk POSITION is not (x, z, -y), or preset node has rotation
6Z-up disk POSITION is not raw Z-up; also --output produced no file
7Preset reimport is not standing
8Z-up reimport is not lying along Y, or reimported tip mismatch
9Reimported orientations did not differ (--same-axis lands here)
10Gallery framing violation
11--same-axis did not collapse the axis difference; also asset-quality floor violation (render path)
12Render path: no axis-aligned rotation maps the source onto a re-import, so the gizmo frame could not be measured
+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Source mast is not Z-dominant, or tip drifted
4glTF reimport produced no mesh
5Preset disk POSITION is not (x, z, -y), or preset node has rotation
6Z-up disk POSITION is not raw Z-up (checked after the re-import checks)
7Preset reimport is not standing
8Z-up reimport is not lying along Y, or reimported tip mismatch
9Reimported orientations did not differ (--same-axis lands here; checked right after the preset stands)
10Gallery framing violation (render path)
11Asset-quality floor violation (render path)
12Render path: no axis-aligned rotation maps the source onto a re-import, so the gizmo frame could not be measured
13Exporter 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).

diff --git a/docs/gallery/gltf-export-roundtrip/index.html b/docs/gallery/gltf-export-roundtrip/index.html index f3643853..8b836cff 100644 --- a/docs/gallery/gltf-export-roundtrip/index.html +++ b/docs/gallery/gltf-export-roundtrip/index.html @@ -69,7 +69,7 @@

glTF Export Roundtrip

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.

Run#

# 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

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it.

-
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Exporter/importer RNA kwargs drifted
4Base cage drifted from its closed form
5Authored UVs drifted from the box-map closed form
6Bevel produced no evaluated geometry
7On-disk node/mesh/generator contract drifted
8On-disk primitive/material binding count drifted
9On-disk POSITION bounds ≠ axis-converted bbox (--no-yup lands here)
10On-disk POSITION count ≠ evaluated loop count
11On-disk UV V-flip failed
12Re-import did not produce exactly one mesh
13Re-imported object carries a transform
14Material names drifted on re-import
15Re-import vert count ≠ evaluated loop count
16Round-trip position drift
17Round-trip normal drift
18Round-trip UV drift
19Re-import triangle count drifted
20Per-triangle material bindings drifted
21--output produced no file
22Gallery framing violation (render path only; the helper's 10, remapped)
23Gallery asset-quality violation (render path only; the helper's 11, remapped)
+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Exporter/importer RNA kwargs drifted
4Base cage drifted from its closed form
5Authored UVs drifted from the box-map closed form
6Bevel produced no evaluated geometry
7On-disk node/mesh/generator contract drifted
8On-disk primitive/material binding count drifted
9On-disk POSITION bounds ≠ axis-converted bbox (--no-yup lands here)
10Gallery framing violation (render path only; gallery_framing)
11Gallery asset-quality violation (render path only; gallery_asset_quality)
12Re-import did not produce exactly one mesh
13Re-imported object carries a transform
14Material names drifted on re-import
15Re-import vert count ≠ evaluated loop count
16Round-trip position drift
17Round-trip normal drift
18Round-trip UV drift
19Re-import triangle count drifted
20Per-triangle material bindings drifted
21--output produced no file
22On-disk POSITION count ≠ evaluated loop count
23On-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).

diff --git a/docs/gallery/gltf-skin-roundtrip/index.html b/docs/gallery/gltf-skin-roundtrip/index.html index 508e93d5..75d7d1cc 100644 --- a/docs/gallery/gltf-skin-roundtrip/index.html +++ b/docs/gallery/gltf-skin-roundtrip/index.html @@ -82,7 +82,7 @@

Run blender --background --python gltf_skin_roundtrip.py -- --output scorp.png --engine cycles

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it.

-
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Exporter RNA missing expected kwargs
4Vertex group count ≠ bone count
5Disk skins ≠ 1 (--no-skins lands here)
6Skin joints ≠ bone names
7Missing JOINTS_0/WEIGHTS_0, or accessor length mismatch
8Disk weight sums off 1.0
9Disk verts exceed evaluated loops
10Armature count after import ≠ 1
11Bone count drifted
12Named bone lost
13Bone parent drifted
14Rest matrices drifted
15Skinned mesh count after import ≠ 1
16Re-import vert count ≠ disk
17Vertex group names drifted
18Weight round-trip drifted
19Deformation round-trip drifted
20--output produced no file
+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Exporter RNA missing expected kwargs
4Vertex group count ≠ bone count
5Disk skins ≠ 1 (--no-skins lands here)
6Skin joints ≠ bone names
7Missing JOINTS_0/WEIGHTS_0, or accessor length mismatch
8Disk weight sums off 1.0
9Disk verts exceed evaluated loops
10Gallery framing violation (render path only; gallery_framing)
11Gallery asset-quality violation (render path only; gallery_asset_quality)
12Named bone lost
13Bone parent drifted
14Rest matrices drifted
15Skinned mesh count after import ≠ 1
16Re-import vert count ≠ disk
17Vertex group names drifted
18Weight round-trip drifted
19Deformation round-trip drifted
20--output produced no file
21Armature count after import ≠ 1
22Bone 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).

diff --git a/docs/gallery/gn-modifier-inputs/index.html b/docs/gallery/gn-modifier-inputs/index.html index 83743534..30de41f3 100644 --- a/docs/gallery/gn-modifier-inputs/index.html +++ b/docs/gallery/gn-modifier-inputs/index.html @@ -83,10 +83,10 @@

Run blender --background --python gn_modifier_inputs.py -- --output stairs.png --engine cycles

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.

-
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Height input identifier missing on the tree interface
4Modifiers do not share one node_group
5Version-path write raised (--api dict on 5.2, --api rna on 4.5)
6Version-path read raised
7Readback ≠ intended height (--same-height lands here)
8Evaluated Z-extent ≠ intended height
9Evaluated mesh not sitting on z=0
10Gallery framing violation
11Evaluated 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).

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Height input identifier missing on the tree interface
4Modifiers do not share one node_group
5Version-path write raised (--api dict on 5.2, --api rna on 4.5)
6Version-path read raised
7Readback ≠ intended height (diagnostic, after the evaluated checks)
8Evaluated Z-extent ≠ intended height (--same-height lands here)
9Evaluated mesh not sitting on z=0
10Gallery framing violation
12--output produced no file
13Evaluated 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).

Falsification#

-
ProbeBinaryResult
--api dict5.2.1 LTSexit 5, TypeError: id properties not supported for this type
--api rna4.5.11 LTSexit 5, AttributeError: 'NodesModifier' object has no attribute 'properties'
--api dict5.1.2exit 0 (5.1 still takes the dict form)
--api rna5.1.2exit 5, AttributeError: 'NodesModifier' object has no attribute 'properties'
--same-height5.2.1, 5.1.2, 4.5.11exit 7, readback 1.0 != written 2.0 on SpiralStair.H2
RAIL_H = -0.5 (treads climb past the post)5.2.1, 4.5.11exit 8, evaluated Z-extent 1.934334 != height 1.0
--api auto5.2.1, 5.1.2, 4.5.11exit 0, extents 1.000 / 2.000 / 3.000
+
ProbeBinaryResult
--api dict5.2.1 LTSexit 5, TypeError: id properties not supported for this type
--api rna4.5.11 LTSexit 5, AttributeError: 'NodesModifier' object has no attribute 'properties'
--api dict5.1.2exit 0 (5.1 still takes the dict form)
--api rna5.1.2exit 5, AttributeError: 'NodesModifier' object has no attribute 'properties'
--same-height5.2.1exit 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.11exit 8, evaluated Z-extent 1.934334 != height 1.0
--api auto5.2.1, 5.1.2, 4.5.11exit 0, extents 1.000 / 2.000 / 3.000

Source

diff --git a/docs/gallery/gn-sdf-remesh/index.html b/docs/gallery/gn-sdf-remesh/index.html index c989b455..ba6bfcce 100644 --- a/docs/gallery/gn-sdf-remesh/index.html +++ b/docs/gallery/gn-sdf-remesh/index.html @@ -73,13 +73,16 @@

Run # Falsifier: no SDF modifier. Must exit non-zero (evaluated == base). blender --background --python gn_sdf_remesh.py -- --no-sdf +# Falsifier: no Set Material node in the tree. Must exit non-zero (exit 6). +blender --background --python gn_sdf_remesh.py -- --no-set-material + # Also render the remeshed result (EEVEE on a GPU host; --engine cycles on GPU-less hosts): blender --background --python gn_sdf_remesh.py -- --output remesh.png blender --background --python gn_sdf_remesh.py -- --output remesh.png --engine cycles

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it.

-
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3SDF remesh produced no/unchanged geometry (--no-sdf lands here)
4--output produced no file
5Wrong-era EEVEE engine id was accepted
6Input material dropped by remesh
10Framing 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).

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3SDF remesh produced no/unchanged geometry (--no-sdf lands here)
4--output produced no file
5Wrong-era EEVEE engine id was accepted
6Input material dropped by remesh (--no-set-material lands here)
10Framing 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).

Source

diff --git a/docs/gallery/gn-zone-iterate/index.html b/docs/gallery/gn-zone-iterate/index.html index b46d5a7a..f04db28f 100644 --- a/docs/gallery/gn-zone-iterate/index.html +++ b/docs/gallery/gn-zone-iterate/index.html @@ -76,7 +76,7 @@

Run

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.

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.

-
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage; also carrier mesh was rewritten
3Repeat evaluated vert/face count off closed form
4Repeat X-centers off closed form
5For Each evaluated vert/face count off closed form
6For Each Z-centers off closed form
10Gallery framing violation
12--output produced no file
+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Repeat evaluated vert/face count off closed form
4Repeat X-centers off closed form
5For Each evaluated vert/face count off closed form
6For Each Z-centers off closed form
7Carrier mesh was rewritten (checked first)
10Gallery 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).

diff --git a/docs/gallery/gp-lineart-contour/index.html b/docs/gallery/gp-lineart-contour/index.html index 7389a4ef..55f4a2b3 100644 --- a/docs/gallery/gp-lineart-contour/index.html +++ b/docs/gallery/gp-lineart-contour/index.html @@ -64,7 +64,7 @@

GP Lineart Contour

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.

  • Contours are a modifier, not Freestyle and not hand-authored strokes. 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.
  • Edge-type flags matter. A freshly added LINEART with every edge type off (contour, crease, material borders, intersections, loose, edge marks) emits 0 strokes. The configured modifier draws 255 strokes / 1393 points on every binary, and the gates (≥ 240 strokes, ≥ 1320 points) sit above the count left when any one edge type is dropped — contour 234 strokes, crease 171 / 1099, material borders 247 / 1305, intersections 179 / 921 — so each is load-bearing: the band borders on the tower are material-border ink, the waterline around each rock is intersection ink. Rebuilding the modifier must recover exactly the first pass's counts.
  • Stroke width renamed. 4.5 exposes both thickness (legacy px, set to 22 here) and radius; 5.1 removes thickness (AttributeError) and keeps radius only (portable path: mod.radius = 0.021).
  • GPv3 address matches 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.

Stage deviation#

@@ -73,15 +73,15 @@

Run
# 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

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.

-
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage; also GPv3 collection address contract
3LINEART thickness / radius trap for this Blender
4LINEART type, source, or use_contour (--no-contour lands here)
5Evaluated drawing below the stroke/point gates
6Cleared source_object still produced strokes
7Every edge type off still produced strokes, or the rebuild did not recover the same counts
8--output produced no file
10Gallery 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).

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3LINEART thickness / radius trap for this Blender
4LINEART type or source
5Evaluated drawing below the stroke/point gates (--no-contour lands here)
6Cleared source_object still produced strokes
7Every edge type off still produced strokes, or the rebuild did not recover the same counts
8--output produced no file
9GPv3 collection address contract (checked first)
10Gallery 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).

Source

diff --git a/docs/gallery/light-link-studio/index.html b/docs/gallery/light-link-studio/index.html index 0cd2b305..3c82b582 100644 --- a/docs/gallery/light-link-studio/index.html +++ b/docs/gallery/light-link-studio/index.html @@ -71,15 +71,15 @@

Run
# 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

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it.

-
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Light datablock gained light_linking
4Light objects lost light_linking
5receiver_collection assignment did not read back (--skip-link lands here)
6Linked state: hero dark or hero/decoy ratio below gate
7Decoy rise below gate when unlinked
8Hero drifted across the unlink
9--output produced no file
10Framing 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).

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Light datablock gained light_linking
4Light objects lost light_linking
6Linked state: hero dark or hero/decoy ratio below gate (--skip-link lands here)
7Decoy rise below gate when unlinked
8Hero drifted across the unlink
9--output produced no file
10Framing 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).

Source

diff --git a/docs/gallery/lightmap-uv-channel/index.html b/docs/gallery/lightmap-uv-channel/index.html index d0b0f9a8..a09d4a92 100644 --- a/docs/gallery/lightmap-uv-channel/index.html +++ b/docs/gallery/lightmap-uv-channel/index.html @@ -77,7 +77,7 @@

Run blender --background --python lightmap_uv_channel.py -- --falsify overlap.png

Exit codes#

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.

-
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3UV layer names ≠ UV0 / UV1
4UVMap / UV1 render/clone flags wrong after re-assert
5Channel 0 (UV0) touched by the UV1 unwrap
6UV1 loops outside [0,1]
7SAT overlap self-test failed, or UV1 triangle pairs overlap (--overlap-islands lands here)
8Min island distance below margin
9Part is not watertight
10Gallery framing violation; also --output produced no file
11Gallery asset-quality violation
+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3UV layer names ≠ UV0 / UV1
4UVMap / UV1 render/clone flags wrong after re-assert
5Channel 0 (UV0) touched by the UV1 unwrap
6UV1 loops outside [0,1]
7SAT overlap self-test failed, or UV1 triangle pairs overlap (--overlap-islands lands here)
8Min island distance below margin
9Part is not watertight
10Gallery framing violation (--output only; gallery_framing)
11Gallery 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.

diff --git a/docs/gallery/modular-kit-snap/index.html b/docs/gallery/modular-kit-snap/index.html index 7bba99c2..a599776c 100644 --- a/docs/gallery/modular-kit-snap/index.html +++ b/docs/gallery/modular-kit-snap/index.html @@ -66,7 +66,7 @@

Modular Kit Snap

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):

  • Snap. Exactly 16 boundary verts (8 per open end, from the hollow-rectangle profile), every one on its end plane x ∈ {0, 4} within 1e-6 m; every boundary edge lies on an end plane.
  • Loop coincidence. The two end rings, matched by nearest (y, z) key, agree within 1e-6 — opposing loops are coincident under the tile offset.
  • Tiling. A linked duplicate offset by (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.
  • Bounding box. The shell's local bbox equals the declared tile exactly (0..4 × ±1.5 × 0..3, all float32-exact values, deviation 0.0), and the whole-asset bbox matches it because detail is contained.
  • Manifold. Every edge has exactly 2 link faces except the 16 boundary ring edges (1 face each) — the open ends are the only boundaries.
  • Reuse hygiene. Identity scales, 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".

@@ -78,7 +78,7 @@

Run blender --background --python modular_kit_snap.py -- --output close.png --close-camera

Exit codes#

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.

-
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Boundary vert count or end-plane membership
4End rings do not partition evenly, or opposing loops differ
5Tiled instances do not share boundary positions (--seam-gap lands here)
6Shell or whole-asset bbox off the declared tile
7Boundary edge count, non-manifold edges, or rim off the end planes
8Detail part not watertight
9Detail part reaches a tile boundary; also --output produced no file
10Framing deviation score exceeds cap (--close-camera)
11Kit part count, unapplied scale, namespace, or origin; also gallery asset-quality violation
+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Boundary vert count or end-plane membership
4End rings do not partition evenly, or opposing loops differ
5Tiled instances do not share boundary positions (--seam-gap lands here)
6Shell or whole-asset bbox off the declared tile
7Boundary edge count, non-manifold edges, or rim off the end planes
8Detail part not watertight
9Detail part reaches a tile boundary; also --output produced no file
10Framing deviation score exceeds cap (--close-camera; gallery_framing.measure_framing_deviation)
11Gallery asset-quality violation (--output only)
12Kit 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.

diff --git a/docs/gallery/png-exr-alpha/index.html b/docs/gallery/png-exr-alpha/index.html index 9dfbe07d..f80f9394 100644 --- a/docs/gallery/png-exr-alpha/index.html +++ b/docs/gallery/png-exr-alpha/index.html @@ -81,7 +81,7 @@

Run blender --background --python png_exr_alpha.py -- --output alpha.png --engine cycles

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is also the shared framing helper.

-
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage; also closed-form palette no longer stresses unpremul
3float_buffer=True image reports is_float=False
4Float→PNG IHDR is not RGBA16
5Float→PNG RGB error below floor (--opaque-alpha lands here)
6Float→PNG disagrees with closed-form false-unpremul model
7Float→EXR round-trip above tolerance
8float_buffer=False image reports is_float=True
9Byte→PNG IHDR is not RGBA8
10Byte→PNG disagrees with straight-alpha 8-bit model; also gallery framing violation
11Byte→PNG stress cell looks false-unpremul-mangled
12EXR color_mode=RGB alpha-drop contract
13--output produced no file
+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3float_buffer=True image reports is_float=False
4Float→PNG IHDR is not RGBA16
5Float→PNG RGB error below floor (--opaque-alpha lands here)
6Float→PNG disagrees with closed-form false-unpremul model
7Float→EXR round-trip above tolerance
8float_buffer=False image reports is_float=True
9Byte→PNG IHDR is not RGBA8
10Gallery framing violation (--output only; gallery_framing)
12EXR color_mode=RGB alpha-drop contract
13--output produced no file
14Closed-form palette no longer stresses unpremul (checked first)
15Byte→PNG disagrees with straight-alpha 8-bit model
16Byte→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).

diff --git a/docs/gallery/shader-node-group/index.html b/docs/gallery/shader-node-group/index.html index 705802e6..9d55c475 100644 --- a/docs/gallery/shader-node-group/index.html +++ b/docs/gallery/shader-node-group/index.html @@ -62,7 +62,7 @@

Shader Node Group

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).

Staging#

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 @@

Run blender --background --python shader_node_group.py -- --output mugs.png --engine cycles

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.

-
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Interface sockets missing Tint / Roughness / Shader
4Group datablock users ≠ number of mug materials (5)
5Instance points at a different node tree
6Instance Tint values not pairwise distinct (--same-tint lands here)
7--output produced no file
10Gallery 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).

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Interface sockets missing Tint / Roughness / Shader
4Group datablock users ≠ number of mug materials (5)
5Instance points at a different node tree
6Instance Tint socket values not pairwise distinct (diagnostic, after check 8)
7--output produced no file
8Rendered probe: the five mug glazes are not pairwise distinct, or the probe render produced no file (--same-tint lands here)
10Gallery 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).

Source

diff --git a/docs/gallery/sky-texture-sun-elevation/index.html b/docs/gallery/sky-texture-sun-elevation/index.html index d272abbd..b3c6d55b 100644 --- a/docs/gallery/sky-texture-sun-elevation/index.html +++ b/docs/gallery/sky-texture-sun-elevation/index.html @@ -64,7 +64,7 @@

Sky Texture Sun Elevation

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 must drive Background Color. A near-black Background Strength alone is not a sky — the check asserts the Sky → Background → World Output links.
  • 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.

Stage deviation#

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.

@@ -77,15 +77,15 @@

Run
# 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

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it.

-
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3sky_type is not the identifier for this Blender
4sun_elevation round-trip failed
5dust_density / aerosol_density / NISHITA / MULTIPLE_SCATTERING trap
6Sky → Background → World Output links broken (--unlink-sky lands here)
7High-elevation zenith luma below floor
8Zenith 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).

+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3sky_type is not the identifier for this Blender
4sun_elevation round-trip failed
5dust_density / aerosol_density / NISHITA / MULTIPLE_SCATTERING trap
6Sky → Background → World Output links broken (diagnostic, after the zenith probes)
7High-elevation zenith luma below floor (--unlink-sky lands here)
8Zenith 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).

Source

diff --git a/docs/gallery/soccer-ball-goldberg/index.html b/docs/gallery/soccer-ball-goldberg/index.html index bfc642d2..fb7dac75 100644 --- a/docs/gallery/soccer-ball-goldberg/index.html +++ b/docs/gallery/soccer-ball-goldberg/index.html @@ -70,7 +70,7 @@

Soccer Ball Goldberg

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.

Staging (render path only)#

None of this touches the checked mesh, its binding, or any check.

-
  • Round, not lumpy. Catmull-Clark on a 60-vertex cage leaves flats over every hexagon; the clay pass read as a lumpy stone. An unapplied Cast modifier after the Subsurf pulls the surface onto a sphere of the cage's own circumradius.
  • Every panel visible. The white hexagons used to merge into one white shell, so the still showed 12 of the 32 panels the check counts. A render-only copy of the mesh, turned into a wire, subdivided and cast onto a sphere 2 mm proud of the ball, draws every panel boundary as a stitched seam. It is parented to the ball and bound to the dark slot on the copy only.
  • Match leather. Both panel materials carry a fine Voronoi pebble grain in the bump and a thin clear coat, so each panel holds a soft sheen.
  • On the pitch. The ball sits pressed 1 cm into a patch of mown turf: alternating mowing stripes, blade noise in colour and bump, and chalk (a penalty spot under the ball, a touchline behind it) broken up by the same blade noise. The chalk and stripes read Object coordinates on the turf, so the spot centres under the ball's origin.
  • Framing gate. The render path calls 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.
+
  • Round, not lumpy. Catmull-Clark on a 60-vertex cage leaves flats over every hexagon; the clay pass read as a lumpy stone. An unapplied Cast modifier after the Subsurf pulls the surface onto a sphere of the cage's own circumradius.
  • Every panel visible. The white hexagons used to merge into one white shell, so the still showed 12 of the 32 panels the check counts. A render-only copy of the mesh, turned into a wire, subdivided and cast onto a sphere 2 mm proud of the ball, draws every panel boundary as a stitched seam. It is parented to the ball and bound to the dark slot on the copy only.
  • Match leather. Both panel materials carry a fine Voronoi pebble grain in the bump and a thin clear coat, so each panel holds a soft sheen.
  • On the pitch. The ball sits pressed 1 cm into a patch of mown turf: alternating mowing stripes, blade noise in colour and bump, and chalk (a penalty spot under the ball, a touchline behind it) broken up by the same blade noise. The chalk and stripes read Object coordinates on the turf, so the spot centres under the ball's origin.
  • Framing gate. The render path calls 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.

Stage deviation#

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.

Run#

@@ -85,7 +85,7 @@

Run blender --background --python soccer_ball_goldberg.py -- --output ball.png --engine cycles

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it.

-
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Topology ≠ 60/90/32
4Euler characteristic ≠ 2
5Face census ≠ 12 pentagons + 20 hexagons
6Vertex degree not uniform 3; also --output produced no file
7Non-manifold edges
8Edge lengths not uniform
9Face planarity off
10Centroid off origin
11Circumradius not uniform
12Panel material count ≠ 2
13Panel binding not by vertex count (--invert-bind lands here)
14Framing gate (--output path only; gallery_framing returns 10, remapped)
+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Topology ≠ 60/90/32
4Euler characteristic ≠ 2
5Face census ≠ 12 pentagons + 20 hexagons
6Vertex degree not uniform 3; also --output produced no file
7Non-manifold edges
8Edge lengths not uniform
9Face planarity off
10Framing gate (--output path only; gallery_framing)
12Panel material count ≠ 2
13Panel binding not by vertex count (--invert-bind lands here)
14Centroid off origin
15Circumradius 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).

diff --git a/docs/gallery/swatch-grid/index.html b/docs/gallery/swatch-grid/index.html index e08f4635..78e6c4d9 100644 --- a/docs/gallery/swatch-grid/index.html +++ b/docs/gallery/swatch-grid/index.html @@ -68,8 +68,10 @@

Run
# 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.

-
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Distinct swatch colors ≠ 6 (--same-base lands here); also render not six distinct regions
4--output produced no file
5Wrong-era EEVEE engine id was accepted
10Gallery framing violation
+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Distinct swatch material colors ≠ 6 (check-only path; with --output a diagnostic after check 6)
4--output produced no file
5Wrong-era EEVEE engine id was accepted (--invert-engine-id lands here)
6Rendered still is black or not six distinct swatch regions (--same-base lands here)
7set_specular did not resolve Specular IOR Level or the value did not land (--no-specular-shim lands here)
10Gallery 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.

diff --git a/docs/gallery/text-version-stamp/index.html b/docs/gallery/text-version-stamp/index.html index 94509884..9a851eb0 100644 --- a/docs/gallery/text-version-stamp/index.html +++ b/docs/gallery/text-version-stamp/index.html @@ -80,7 +80,7 @@

Run

The render scales the stamp to a constant width, so the frame holds for any version-string length.

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.

-
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Not a FONT TextCurve with the built-in Bfont
4body is not the live version_string (--wrong-body lands here)
5Flat text is not a filled planar mesh
6Extrude / bevel closed form failed
7Appending characters did not widen the text
8Mesh survived to_mesh_clear()
9--output produced no file
10Gallery framing violation
13--output: bundled datafiles/fonts/Inter.woff2 not found
+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Not a FONT TextCurve with the built-in Bfont
4Evaluated glyph mesh does not match a reference TextCurve of the live version_string (--wrong-body lands here)
5Flat text is not a filled planar mesh
6Extrude / bevel closed form failed
7Appending characters did not widen the text
8Mesh survived to_mesh_clear()
9--output produced no file
10Gallery framing violation
13--output: bundled datafiles/fonts/Inter.woff2 not found
14version_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).

diff --git a/docs/gallery/triangulate-tangents/index.html b/docs/gallery/triangulate-tangents/index.html index 60f91b68..290bbf00 100644 --- a/docs/gallery/triangulate-tangents/index.html +++ b/docs/gallery/triangulate-tangents/index.html @@ -80,7 +80,7 @@

Run blender --background --python triangulate_tangents.py -- --output buckler.png --engine cycles

Exit codes#

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.

-
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Loop-triangle count ≠ closed form
4Re-fetched UV layer drifted from the polar field (--zero-uv lands here)
5Tangent basis not orthonormal
6Bitangent ≠ sign × (n × t)
7Tangents deviate from the edge/UV closed form
8Flipped tangent inside a clean triangle
10Gallery framing violation; also --output produced no file
11Asset-quality floor violation (--output only; shared helper)
+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Loop-triangle count ≠ closed form
4Re-fetched UV layer drifted from the polar field (--zero-uv lands here)
5Tangent basis not orthonormal
6Bitangent ≠ sign × (n × t)
7Tangents deviate from the edge/UV closed form
8Flipped tangent inside a clean triangle
10Gallery framing violation (--output only; gallery_framing)
11Asset-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).

diff --git a/docs/gallery/usd-export-evaluation-mode/index.html b/docs/gallery/usd-export-evaluation-mode/index.html index 2be34e15..ac766442 100644 --- a/docs/gallery/usd-export-evaluation-mode/index.html +++ b/docs/gallery/usd-export-evaluation-mode/index.html @@ -73,7 +73,7 @@

Run blender --background --python usd_export_evaluation_mode.py -- --output u.png

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper.

-
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage; also base cube verts ≠ 8
3VIEWPORT TESSELLATE point/face count off closed form
4RENDER TESSELLATE point/face count or scheme off closed form
5BEST_MATCH cage point count or scheme off
6RENDER and VIEWPORT USDA point counts are identical (check), or the re-imported RENDER goblet is not denser than the VIEWPORT one (--output)
10Gallery framing violation
12--output produced no file
+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3VIEWPORT TESSELLATE point/face count off closed form
4RENDER TESSELLATE point/face count or scheme off closed form
5BEST_MATCH cage point count or scheme off
6RENDER and VIEWPORT USDA point counts are identical (check), or the re-imported RENDER goblet is not denser than the VIEWPORT one (--output)
7Base cube verts ≠ 8 (checked first)
10Gallery 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).

diff --git a/docs/gallery/uv-layer-grid/index.html b/docs/gallery/uv-layer-grid/index.html index 2d3abe77..bdaa0fcc 100644 --- a/docs/gallery/uv-layer-grid/index.html +++ b/docs/gallery/uv-layer-grid/index.html @@ -81,7 +81,7 @@

Run blender --background --python uv_layer_grid.py -- --output uv.png --engine cycles

Exit codes#

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.

-
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Silent-no-op hazard gone (--precreate-on-hazard lands here)
4Grid topology drifted
5Pre-create + calc_uvs did not persist one UV layer
6calc_uvs closed-form error
7Mesh UV round-trip error
8calc_uvs=False unexpectedly created a UV layer
9Explicit UV assignment error
10Framing gate violation on the --output path (gallery_framing)
11Broken panel is not flat
12Broken panel is not texel-(0,0) teal
13Repaired panel is not a checker
14Broken and repaired panels render identically
15--output produced no file
+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Silent-no-op hazard gone (--precreate-on-hazard lands here)
4Grid topology drifted
5Pre-create + calc_uvs did not persist one UV layer
6calc_uvs closed-form error
7Mesh UV round-trip error
8calc_uvs=False unexpectedly created a UV layer
9Explicit UV assignment error
10Framing gate violation on the --output path (gallery_framing)
12Broken panel is not texel-(0,0) teal
13Repaired panel is not a checker
14Broken and repaired panels render identically
15--output produced no file
16Broken 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).

diff --git a/docs/gallery/vertex-weight-limit/index.html b/docs/gallery/vertex-weight-limit/index.html index 0d261e83..b47fb88a 100644 --- a/docs/gallery/vertex-weight-limit/index.html +++ b/docs/gallery/vertex-weight-limit/index.html @@ -82,7 +82,7 @@

Run blender --background --python vertex_weight_limit.py -- --output arm.png --engine cycles

Exit codes#

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.

-
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Pre-limit max influences ≠ 5
4Vertex over the 4-influence cap (--skip-limit lands here)
5Limit changed nothing
6Weight sums off 1.0 after renormalize
7Pose damaged by pruning, or evaluated vert count changed
8Evaluated mesh off LBS over limited weights
9Root-weighted mount moved
10Gallery framing violation; also --output produced no file
11Asset-quality floor violation (--output only)
+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Pre-limit max influences ≠ 5
4Vertex over the 4-influence cap (--skip-limit lands here)
5Limit changed nothing
6Weight sums off 1.0 after renormalize
7Pose damaged by pruning, or evaluated vert count changed
8Evaluated mesh off LBS over limited weights
9Root-weighted mount moved
10Gallery framing violation (--output only; gallery_framing)
11Asset-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).

diff --git a/docs/gallery/vse-cut-list/index.html b/docs/gallery/vse-cut-list/index.html index e27e9448..d107f810 100644 --- a/docs/gallery/vse-cut-list/index.html +++ b/docs/gallery/vse-cut-list/index.html @@ -89,7 +89,7 @@

Run blender --background --python vse_cut_list.py -- --output vse.png

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is also the shared framing helper.

-
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3.sequences accessor contract for this Blender
4Wrong-era new_effect end kwarg or TRANSFORM type did not TypeError
5Strip span or channel off closed form
6frame_final_* deprecation bridge for this Blender
7GC wiring or strip .type enum (--swap-inputs lands here)
8Scene strip span or Stage source
9Mosaic transform, compositing defaults, text body, or strip color
10Save/reload round-trip re-assert failed; also gallery framing violation
11--output produced no file
12--check-pixels compositing contract
+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3.sequences accessor contract for this Blender
4Wrong-era new_effect end kwarg or TRANSFORM type did not TypeError
5Strip span or channel off closed form
6frame_final_* deprecation bridge for this Blender
7GC wiring or strip .type enum (--swap-inputs lands here)
8Scene strip span or Stage source
9Mosaic transform, compositing defaults, text body, or strip color
10Gallery framing violation (--output only; gallery_framing)
12--check-pixels compositing contract
13Save/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).

diff --git a/examples/car-mirror-symmetry/README.md b/examples/car-mirror-symmetry/README.md index eadcdb47..c30441c3 100644 --- a/examples/car-mirror-symmetry/README.md +++ b/examples/car-mirror-symmetry/README.md @@ -124,15 +124,15 @@ against it. | 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 | +| 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 | 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) | +| 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). diff --git a/examples/car-mirror-symmetry/car_mirror_symmetry.py b/examples/car-mirror-symmetry/car_mirror_symmetry.py index 44720458..f8bcdd26 100644 --- a/examples/car-mirror-symmetry/car_mirror_symmetry.py +++ b/examples/car-mirror-symmetry/car_mirror_symmetry.py @@ -529,14 +529,14 @@ def check(objs, no_mirror=False): if dev > TOL_SYMM: print(f"ERROR: mirror partner deviation {dev:.3e} > tol {TOL_SYMM:.1e}", file=sys.stderr) - return 10 + return 17 xmin = min(v.x for v in verts) xmax = max(v.x for v in verts) bbox_asym = abs(xmin + xmax) if bbox_asym > TOL_BBOX: print(f"ERROR: evaluated bbox asymmetric by {bbox_asym:.3e} " f"(tol {TOL_BBOX:.1e})", file=sys.stderr) - return 11 + return 18 # 4. mirrored parts (wheels, lamps, grille, mirrors, handles): each mirrored # about an object origin that sits ON the plane — the data is offset, @@ -850,10 +850,10 @@ def light(name, loc, energy, size, col, rot=None, at=None, size_y=None): if gallery_framing.check_framing(scene, cam, hero=parts, elements=parts, stage=[floor, wall]): - return 17 # the helper's 10, remapped: 10 is the partner-deviation check + return gallery_framing.EXIT_FRAMING if gallery_asset_quality.check_asset_quality(scene, cam, hero=parts, stage=[floor, wall]): - return 18 # the helper's 11, remapped: 11 is the bbox check + return gallery_asset_quality.EXIT_ASSET_QUALITY bpy.ops.render.render(write_still=True) if not (os.path.exists(path) and os.path.getsize(path) > 0): print("ERROR: render produced no file", file=sys.stderr) diff --git a/examples/coincident-vert-weld/README.md b/examples/coincident-vert-weld/README.md index 8eca3cb4..2689b831 100644 --- a/examples/coincident-vert-weld/README.md +++ b/examples/coincident-vert-weld/README.md @@ -82,9 +82,10 @@ against it. | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | -| 2 | argparse / usage; also exporter RNA missing expected glTF kwargs | +| 2 | argparse / usage | | 3 | Pathology missing: coincident shells (`--no-duplicate` lands here) | | 4 | glTF export handling failed (`--weld` lands here) | +| 5 | Exporter RNA missing expected glTF kwargs (checked first) | 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). diff --git a/examples/coincident-vert-weld/coincident_vert_weld.py b/examples/coincident-vert-weld/coincident_vert_weld.py index 58c5eec8..b73a9b6b 100644 --- a/examples/coincident-vert-weld/coincident_vert_weld.py +++ b/examples/coincident-vert-weld/coincident_vert_weld.py @@ -128,7 +128,7 @@ def check(ob, weld): missing = [k for k in EXPORT_KWARGS if k not in props] if missing: print(f"ERROR: exporter RNA drifted, missing {missing}", file=sys.stderr) - return 2 + return 5 me = ob.data uniq = unique_count(me) valence = edge_valence(me) diff --git a/examples/color-attribute-wheel/README.md b/examples/color-attribute-wheel/README.md index 531ce49c..214d1a05 100644 --- a/examples/color-attribute-wheel/README.md +++ b/examples/color-attribute-wheel/README.md @@ -57,8 +57,8 @@ against it. | 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 | +| 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 | @@ -66,5 +66,5 @@ against it. 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). +Smoke does not pass `--output`. Its catalog falsifier is `--point-domain` (expects exit 6). diff --git a/examples/color-attribute-wheel/color_attribute_wheel.py b/examples/color-attribute-wheel/color_attribute_wheel.py index b079bbce..87bd1c0d 100644 --- a/examples/color-attribute-wheel/color_attribute_wheel.py +++ b/examples/color-attribute-wheel/color_attribute_wheel.py @@ -118,10 +118,9 @@ def check(obj, hsv): if attr is None: print(f"ERROR: color attribute '{ATTR_NAME}' missing", file=sys.stderr) return 4 - if attr.domain != 'CORNER' or attr.data_type != 'FLOAT_COLOR': - print(f"ERROR: attribute domain/type = {attr.domain}/{attr.data_type}, " - f"expected CORNER/FLOAT_COLOR", file=sys.stderr) - return 5 + # The storage size is the domain trap itself: `--point-domain` lands here + # on the layer's measured length, not on a read-back of `attr.domain` + # (that read-back runs last, as a diagnostic, #469). if len(attr.data) != len(me.loops) or len(attr.data) == len(me.vertices): print(f"ERROR: attribute is sized {len(attr.data)}, expected loop count " f"{len(me.loops)} and distinct from vertex count {len(me.vertices)} " @@ -150,6 +149,11 @@ def check(obj, hsv): f"expected {(er, eg, eb, 1.0)}", file=sys.stderr) return 8 + if attr.domain != 'CORNER' or attr.data_type != 'FLOAT_COLOR': + print(f"ERROR: attribute domain/type = {attr.domain}/{attr.data_type}, " + f"expected CORNER/FLOAT_COLOR", file=sys.stderr) + return 5 + print(f"verts={got[0]} faces={got[1]} loops={got[2]} attribute='{attr.name}' " f"domain={attr.domain} active=True probes_ok={len(probes)}") return 0 diff --git a/examples/compositor-glare/README.md b/examples/compositor-glare/README.md index d74f700a..a34a6b57 100644 --- a/examples/compositor-glare/README.md +++ b/examples/compositor-glare/README.md @@ -29,7 +29,7 @@ absence of any EEVEE bloom toggle. # 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): @@ -47,12 +47,12 @@ against it. | 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 | +| 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 3). +Smoke does not pass `--output`. Its catalog falsifier is `--threshold-high` (expects exit 4). diff --git a/examples/compositor-glare/compositor_glare.py b/examples/compositor-glare/compositor_glare.py index 324c0839..77e51b59 100644 --- a/examples/compositor-glare/compositor_glare.py +++ b/examples/compositor-glare/compositor_glare.py @@ -17,9 +17,10 @@ the ring silhouette, falls off strictly with distance, and vanishes entirely when `scene.render.use_compositing` is off. -``--threshold-high`` sets the shared ``Threshold`` input to 10.0 and still -asserts it equals 1.0. That is the falsifier (``--same-axis`` in -export-preset-axis). The 4.x/5.x Type-menu shim is untouched. +``--threshold-high`` sets the shared ``Threshold`` input to 1000.0, above the brightest ring's +emission (18). That is the falsifier: nothing re-reads the socket, so the +rings stop blooming and the halo pixel check exits 4. The 4.x/5.x Type-menu +shim is untouched. By default it runs only the correctness check (two 96x54 single-sample Cycles renders, compositor on vs off) — the CI smoke check. Pass --output to also @@ -187,9 +188,8 @@ def check_structure(scene, tree, glare): if not (glare.glare_type == 'FOG_GLOW' and glare.quality == 'HIGH' and glare.size == 6): print("ERROR: 4.x Glare legacy properties not as configured", file=sys.stderr) return 3 - if glare.inputs['Threshold'].default_value != 1.0: - print("ERROR: Glare Threshold input != 1.0", file=sys.stderr) - return 3 + # No Threshold read-back: `--threshold-high` must be caught by the halo + # pixels in check_pixels, not by re-reading the value it just wrote (#469). if not link_chain_ok(tree, glare, out_node): print("ERROR: Render Layers -> Glare -> output link chain broken", file=sys.stderr) return 3 @@ -363,12 +363,12 @@ def main(): p.add_argument("--samples", type=int, default=32, help="--output sample count") p.add_argument("--width", type=int, default=1280, help="--output width; height is width*9/16") p.add_argument("--threshold-high", action="store_true", - help="falsifier: Threshold=10.0, still assert Threshold==1.0") + help="falsifier: Threshold=1000.0 (above every ring's emission); the halo pixel check must fail") args = p.parse_args(argv) scene, tree, glare = build_scene() if args.threshold_high: - glare.inputs['Threshold'].default_value = 10.0 + glare.inputs['Threshold'].default_value = 1000.0 code = check_structure(scene, tree, glare) if code: diff --git a/examples/cross-version-property-delete/README.md b/examples/cross-version-property-delete/README.md index e33ac6e0..362ee54c 100644 --- a/examples/cross-version-property-delete/README.md +++ b/examples/cross-version-property-delete/README.md @@ -78,13 +78,14 @@ against it. `10` is the shared framing helper. | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | -| 2 | argparse / usage; also an active object existed after the data-API build | +| 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 | diff --git a/examples/cross-version-property-delete/cross_version_property_delete.py b/examples/cross-version-property-delete/cross_version_property_delete.py index d0c0f486..18e46310 100644 --- a/examples/cross-version-property-delete/cross_version_property_delete.py +++ b/examples/cross-version-property-delete/cross_version_property_delete.py @@ -152,7 +152,7 @@ def check(keep_plate, clear_plate, skip_delete=False, unset_instead=False): "the snippet __main__ would have been dark for the same reason", file=sys.stderr, ) - return 2 + return 9 keep_plate[KEY] = VALUE clear_plate[KEY] = VALUE diff --git a/examples/curve-bevel-arc/README.md b/examples/curve-bevel-arc/README.md index cddccb13..c17c818e 100644 --- a/examples/curve-bevel-arc/README.md +++ b/examples/curve-bevel-arc/README.md @@ -6,9 +6,9 @@ curve data API — `splines.new('BEZIER')`, per-point `bezier_points`, 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 +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 @@ -32,7 +32,7 @@ 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): @@ -53,13 +53,14 @@ against it. `10` is also the shared framing helper. | 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 | +| 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 | X span off closed form; also gallery framing violation | -| 11 | `--output` produced no file; also asset-quality floor violation (`gallery_asset_quality`) | +| 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 6). +Smoke does not pass `--output`. Its catalog falsifier is `--no-caps` (expects exit 7). diff --git a/examples/curve-bevel-arc/curve_bevel_arc.py b/examples/curve-bevel-arc/curve_bevel_arc.py index 87b0f11e..63a76088 100644 --- a/examples/curve-bevel-arc/curve_bevel_arc.py +++ b/examples/curve-bevel-arc/curve_bevel_arc.py @@ -9,8 +9,9 @@ has no simple closed form, so those two constants pin today's behavior (see EXPECT_VERTS below for how to re-measure if a future Blender retessellates). -``--no-caps`` leaves ``use_fill_caps`` False and still asserts the ends are -capped. That is the falsifier (``--same-axis`` in export-preset-axis). +``--no-caps`` leaves ``use_fill_caps`` False. That is the falsifier: the check +never re-reads the flag, so the missing cap faces fail the evaluated +tessellation count (exit 7). The still stages the checked curve as a round-bar horseshoe magnet: its two filled caps are the ground-steel pole faces, turned to the camera, with @@ -83,9 +84,9 @@ def check(obj): if abs(curve.bevel_depth - BEVEL) > 1e-6: print(f"ERROR: bevel_depth {curve.bevel_depth} != {BEVEL}", file=sys.stderr) return 5 - if not curve.use_fill_caps: - print("ERROR: use_fill_caps is False — ends should be capped", file=sys.stderr) - return 6 + # No `use_fill_caps` read-back: `--no-caps` must be caught by the + # evaluated tessellation below (the caps are faces), not by re-reading the + # flag it just wrote (#469). bpy.context.view_layer.update() dg = bpy.context.evaluated_depsgraph_get() @@ -120,7 +121,7 @@ def check(obj): expect_x_span = 2 * RADIUS + 2 * BEVEL if abs(x_span - expect_x_span) > 0.05: print(f"ERROR: x span {x_span:.4f} != {expect_x_span:.4f}", file=sys.stderr) - return 10 + return 12 print(f"points={n} bevel={BEVEL} caps=True eval_verts={got_v} " f"eval_faces={got_f} z={z_lo:.3f}..{z_hi:.3f} x_span={x_span:.3f}") @@ -508,7 +509,7 @@ def light(name, loc, energy, size, col): bpy.ops.render.render(write_still=True) if not (os.path.exists(path) and os.path.getsize(path) > 0): print("ERROR: render produced no file", file=sys.stderr) - return 11 + return 13 return 0 @@ -519,7 +520,7 @@ def main(): p.add_argument("--engine", default="eevee", choices=("eevee", "cycles"), help="render engine for --output (cycles for GPU-less hosts)") p.add_argument("--no-caps", action="store_true", - help="falsifier: use_fill_caps=False, still assert caps") + help="falsifier: use_fill_caps=False; the evaluated tessellation must fail") args = p.parse_args(argv) obj = build(no_caps=args.no_caps) diff --git a/examples/damped-track-aim/README.md b/examples/damped-track-aim/README.md index dbe8430a..ec8f1658 100644 --- a/examples/damped-track-aim/README.md +++ b/examples/damped-track-aim/README.md @@ -50,12 +50,12 @@ against it. | 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) | +| 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 | +| 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 7). +Smoke does not pass `--output`. Its catalog falsifier is `--mute` (expects exit 9). diff --git a/examples/damped-track-aim/damped_track_aim.py b/examples/damped-track-aim/damped_track_aim.py index 7f627487..1c028c3c 100644 --- a/examples/damped-track-aim/damped_track_aim.py +++ b/examples/damped-track-aim/damped_track_aim.py @@ -15,8 +15,8 @@ tight angular epsilon. If the constraint is missing, muted, mistyped as TRACK_TO, or the axis is flipped, the dot product fails. -``--mute`` mutes every DAMPED_TRACK and still asserts unmute. That is the -falsifier (``--same-axis`` in export-preset-axis). +``--mute`` mutes every DAMPED_TRACK. That is the falsifier: the evaluated +aim dot catches it (exit 9), not a read-back of the ``mute`` flag. By default it runs only the correctness check (no render) — the CI smoke check. Pass --output to also render a still: @@ -243,12 +243,6 @@ def check(orb, lamps): file=sys.stderr, ) return 6 - if con.mute or con.influence < 0.999: - print( - f"ERROR: {ob.name} mute={con.mute} influence={con.influence}", - file=sys.stderr, - ) - return 7 if any(c.type == "TRACK_TO" for c in ob.constraints): print( f"ERROR: {ob.name} still has TRACK_TO — use DAMPED_TRACK for aim", @@ -271,13 +265,28 @@ def check(orb, lamps): worst_name = ob.name if dot < MIN_AIM_DOT: angle = math.degrees(math.acos(max(-1.0, min(1.0, dot)))) + con = [c for c in ob.constraints if c.type == "DAMPED_TRACK"][0] print( f"ERROR: {ob.name} aim dot={dot:.6f} ({angle:.2f}°) " - f"< {MIN_AIM_DOT} — constraint not evaluated or axis flipped", + f"< {MIN_AIM_DOT} — constraint not evaluated or axis flipped " + f"(mute={con.mute} influence={con.influence})", file=sys.stderr, ) return 9 + # Diagnostic after the behavioral check: a muted or partial constraint + # whose lamps still aim means something other than the constraint did it. + # `--mute` never reaches this line; the evaluated aim above catches it + # (#469), so deleting the aim check makes the falsifier fail. + for ob in lamps: + con = [c for c in ob.constraints if c.type == "DAMPED_TRACK"][0] + if con.mute or con.influence < 0.999: + print( + f"ERROR: {ob.name} mute={con.mute} influence={con.influence}", + file=sys.stderr, + ) + return 7 + print( f"OK: {N_LAMPS} DAMPED_TRACK lamp heads → Orb on TRACK_Z; " f"worst aim dot={worst:.6f} ({worst_name})" diff --git a/examples/exit-pre-sidecar/README.md b/examples/exit-pre-sidecar/README.md index 07b50876..30fc9b19 100644 --- a/examples/exit-pre-sidecar/README.md +++ b/examples/exit-pre-sidecar/README.md @@ -45,8 +45,10 @@ FAIL, so the skip cannot quietly spread to a version that should run. so a repo-wide skip cannot pass either. `--force-run` bypasses the skip so 4.5 proves the absence rather than -assuming it: the run exits 2 on `AttributeError`, and also exits 2 if -`exit_pre` turns out to exist on a version that should not have it. +assuming it: the run exits 3 on `AttributeError` (the expected 4.5 +outcome) and exits 4 if `exit_pre` turns out to exist on a version that +should not have it, so the two outcomes are told apart by code rather +than both collapsing onto argparse's 2. ## Falsifiers @@ -63,7 +65,7 @@ a post-exit contract can be falsified. | `--wrong-text` | handler writes `nope` | 0 | FAIL, contents mismatch | | `--write-in-main` | `from-main` written in `main`, no handler | 0 | FAIL, contents mismatch | | `--atexit-instead` | `atexit` writes `atexit-ok` instead of `exit_pre` | 0 | FAIL, contents mismatch | -| `--force-run` on 4.5 | asserts `exit_pre` is genuinely absent | 2 | n/a | +| `--force-run` on 4.5 | asserts `exit_pre` is genuinely absent | 3 (4 if it is present) | n/a | `--write-in-main` and `--atexit-instead` are the two that matter: both produce a file, so an existence-only check would pass them. They are why @@ -90,8 +92,11 @@ against it. `77` is the smoke skip protocol, not a product check. | Code | Meaning | | --- | --- | | 0 | Success (including `--silent-handler` / `--no-handler` / `--wrong-text` / `--write-in-main` / `--atexit-instead`, which the harness then fails) | -| 1 | Uncaught exception (FATAL wrapper); also `$BDT_SMOKE_SIDECAR` unset | -| 2 | argparse / usage; also `--force-run` on Blender < 5.1 (`exit_pre` missing or unexpectedly present) | +| 1 | Uncaught exception (FATAL wrapper) | +| 2 | argparse / usage | +| 3 | `--force-run` below 5.1: `exit_pre` is absent (`AttributeError`), the expected 4.5 outcome | +| 4 | `--force-run` below 5.1: `exit_pre` unexpectedly present, so the 5.1 floor and the skip are wrong | +| 5 | `$BDT_SMOKE_SIDECAR` unset (run outside the harness) | | 77 | `SMOKE_SKIP:` `exit_pre` requires Blender 5.1+ | The `blender-smoke` workflow runs the check on Blender 5.2 LTS (5.1 on the diff --git a/examples/exit-pre-sidecar/exit_pre_sidecar.py b/examples/exit-pre-sidecar/exit_pre_sidecar.py index 38f7a41d..528947c9 100644 --- a/examples/exit-pre-sidecar/exit_pre_sidecar.py +++ b/examples/exit-pre-sidecar/exit_pre_sidecar.py @@ -128,12 +128,12 @@ def main(): bpy.app.handlers.exit_pre.append(on_exit_write) except AttributeError as exc: print(f"ERROR: {type(exc).__name__}: {exc}", file=sys.stderr) - return 2 + return 3 print("ERROR: exit_pre existed on this Blender; 4.5 should AttributeError", file=sys.stderr) - return 2 + return 4 if sidecar_path() is None: - return 1 + return 5 if args.write_in_main: write_sidecar(MAIN_MARKER) diff --git a/examples/export-preset-axis/README.md b/examples/export-preset-axis/README.md index f75f2488..e9096909 100644 --- a/examples/export-preset-axis/README.md +++ b/examples/export-preset-axis/README.md @@ -76,17 +76,19 @@ the shared asset-quality helper. | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | -| 2 | argparse / usage; also exporter RNA missing expected glTF kwargs | +| 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; also `--output` produced no file | +| 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) | -| 10 | Gallery framing violation | -| 11 | `--same-axis` did not collapse the axis difference; also asset-quality floor violation (render path) | +| 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). diff --git a/examples/export-preset-axis/export_preset_axis.py b/examples/export-preset-axis/export_preset_axis.py index 9f93ead2..62cc331e 100644 --- a/examples/export-preset-axis/export_preset_axis.py +++ b/examples/export-preset-axis/export_preset_axis.py @@ -24,8 +24,9 @@ so the preset round-trips to `(x, y, z)` and the Z-up file becomes `(x, -z, y)`. -`--same-axis` exports both with `export_yup=True`. Both reimports stand, the -"orientations differ" check exits 9. That is the falsifier. +`--same-axis` exports both with `export_yup=True`. Both reimports stand, so +the measured "orientations differ" check exits 9. That is the falsifier; the +check does not branch on the flag. By default it runs only the correctness check (no render) - the CI smoke check. Pass --output to also render a still: @@ -363,7 +364,7 @@ def check(src, same_axis): missing = [k for k in PRESET_KWARGS if k not in exp_props] if missing: print(f"ERROR: exporter RNA drifted, missing {missing}", file=sys.stderr) - return 2, None, None + return 13, None, None pts = world_points(src) sx, sy, sz = aabb_of(pts) @@ -418,15 +419,6 @@ def check(src, same_axis): print(f"ERROR: preset node has rotation {u_node.get('rotation')}", file=sys.stderr) return 5, None, None - if not same_axis: - if not (near(g_min[2], sz[0]) and near(g_max[2], sz[1])): - print( - f"ERROR: Z-up disk POSITION is not raw Z-up " - f"g_min={g_min} g_max={g_max} source_z={sz}", - file=sys.stderr, - ) - return 6, None, None - preset_objs = import_gltf_meshes(preset_path) zup_objs = import_gltf_meshes(zup_path) if preset_objs is None or zup_objs is None: @@ -467,14 +459,15 @@ def check(src, same_axis): span(uy) - span(gy) ) > SPAN_GAP - if same_axis: - if orientations_differ: - print( - "ERROR: --same-axis did not collapse the axis difference", - file=sys.stderr, - ) - return 11, preset_objs, zup_objs - print("ERROR: orientations did not differ", file=sys.stderr) + # The behavioral contract first: the two re-imports must stand differently. + # `--same-axis` lands here on the measured re-import spans; the check knows + # nothing about the flag, so deleting it makes the falsifier fail (#469). + if not orientations_differ: + print( + f"ERROR: reimported orientations did not differ " + f"preset_z={span(uz):.4f} zup_z={span(gz):.4f}", + file=sys.stderr, + ) return 9, preset_objs, zup_objs if not zup_lying: @@ -497,13 +490,13 @@ def check(src, same_axis): ) return 8, preset_objs, zup_objs - if not orientations_differ: + if not (near(g_min[2], sz[0]) and near(g_max[2], sz[1])): print( - f"ERROR: reimported orientations did not differ " - f"preset_z={span(uz):.4f} zup_z={span(gz):.4f}", + f"ERROR: Z-up disk POSITION is not raw Z-up " + f"g_min={g_min} g_max={g_max} source_z={sz}", file=sys.stderr, ) - return 9, preset_objs, zup_objs + return 6, preset_objs, zup_objs return 0, preset_objs, zup_objs @@ -713,7 +706,7 @@ def render_still(source, preset_objs, zup_objs, path, engine): bpy.ops.render.render(write_still=True) if not (os.path.exists(path) and os.path.getsize(path) > 0): print("ERROR: render produced no file", file=sys.stderr) - return 6 + return 14 return 0 diff --git a/examples/gltf-export-roundtrip/README.md b/examples/gltf-export-roundtrip/README.md index a67e5bec..cc2e4dbd 100644 --- a/examples/gltf-export-roundtrip/README.md +++ b/examples/gltf-export-roundtrip/README.md @@ -70,8 +70,8 @@ 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. +authored crate. Both return their reserved codes (10, 11) directly; the +POSITION-count and UV V-flip checks use 22 and 23. ## Run @@ -104,8 +104,8 @@ against it. | 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 | +| 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 | @@ -116,8 +116,8 @@ against it. | 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) | +| 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). diff --git a/examples/gltf-export-roundtrip/gltf_export_roundtrip.py b/examples/gltf-export-roundtrip/gltf_export_roundtrip.py index 8c73ac8b..655a432a 100644 --- a/examples/gltf-export-roundtrip/gltf_export_roundtrip.py +++ b/examples/gltf-export-roundtrip/gltf_export_roundtrip.py @@ -331,7 +331,7 @@ def check(crate, export_kwargs): f"count {len(snap['loops'])} — the file's vertex split drifted " "from the evaluated mesh (export_apply / export_normals contract)", file=sys.stderr) - return 10 + return 22 xs = [v[0] for v in snap["verts"]] ys = [v[1] for v in snap["verts"]] @@ -367,11 +367,11 @@ def check(crate, export_kwargs): print(f"ERROR: prim0 TEXCOORD_0 count {len(disk_uv)} != POSITION " f"count {len(disk_pos)} - the UV attribute does not cover " "the primitive", file=sys.stderr) - return 11 + return 23 if len(disk_pos) < UV_SAMPLE: print(f"ERROR: prim0 carries only {len(disk_pos)} loops, fewer than " f"the {UV_SAMPLE} the V-flip check samples", file=sys.stderr) - return 11 + return 23 flip_err = 0.0 sampled = 0 for pd, (u, v) in zip(disk_pos[:UV_SAMPLE], disk_uv[:UV_SAMPLE]): @@ -383,11 +383,11 @@ def check(crate, export_kwargs): if sampled != UV_SAMPLE: print(f"ERROR: V-flip check compared {sampled} loops, expected " f"{UV_SAMPLE}", file=sys.stderr) - return 11 + return 23 if flip_err > UV_TOL: print(f"ERROR: on-disk UVs deviate {flip_err:.3e} from the V-flipped " "authored layout (glTF texture origin is top-left)", file=sys.stderr) - return 11 + return 23 # wipe the file, then re-import: names must survive exactly bpy.ops.wm.read_factory_settings(use_empty=True) @@ -667,18 +667,18 @@ def light(name, loc, energy, size, col, rot): scene.view_settings.view_transform = 'Standard' bpy.context.view_layer.update() # Layer 1 framing gate (silhouette matte), before the beauty render so a - # defective composition ships no artifact. The helper returns 10, which is - # a check code here (POSITION count), so the call site remaps it to 22. + # defective composition ships no artifact. Exit 10 is reserved for this + # gate in every example, so the POSITION-count check above uses 22. fcode = gallery_framing.check_framing( scene, cam, hero=[authored], elements=[authored, roundtrip], stage=[floor, wall]) if fcode: - return 22 - # asset-quality floors on the authored crate; its 11 is also a check - # code here (UV V-flip), so the call site remaps it to 23 + return gallery_framing.EXIT_FRAMING + # asset-quality floors on the authored crate (exit 11, reserved; the UV + # V-flip check above uses 23) if gallery_asset_quality.check_asset_quality(scene, cam, hero=[authored], stage=[floor, wall]): - return 23 + return gallery_asset_quality.EXIT_ASSET_QUALITY bpy.ops.render.render(write_still=True) return 0 if os.path.exists(path) and os.path.getsize(path) > 0 else 21 diff --git a/examples/gltf-skin-roundtrip/README.md b/examples/gltf-skin-roundtrip/README.md index 1fc1c122..79274ee1 100644 --- a/examples/gltf-skin-roundtrip/README.md +++ b/examples/gltf-skin-roundtrip/README.md @@ -108,8 +108,8 @@ against it. | 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 | +| 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 | @@ -119,6 +119,8 @@ against it. | 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` diff --git a/examples/gltf-skin-roundtrip/gltf_skin_roundtrip.py b/examples/gltf-skin-roundtrip/gltf_skin_roundtrip.py index 9fe7660d..4f3497ee 100644 --- a/examples/gltf-skin-roundtrip/gltf_skin_roundtrip.py +++ b/examples/gltf-skin-roundtrip/gltf_skin_roundtrip.py @@ -692,12 +692,12 @@ def weight_map(o): if len(arms) != 1: print(f"ERROR: {len(arms)} armatures after import, expected 1", file=sys.stderr) - return 10 + return 21 ri_arm = arms[0] if len(ri_arm.data.bones) != len(BONES): print(f"ERROR: {len(ri_arm.data.bones)} bones != {len(BONES)}", file=sys.stderr) - return 11 + return 22 rest_err = 0.0 for name in BONES: b1 = ri_arm.data.bones.get(name) diff --git a/examples/gn-instance-grid/gn_instance_grid.py b/examples/gn-instance-grid/gn_instance_grid.py index 965c4e41..d3d96bf4 100644 --- a/examples/gn-instance-grid/gn_instance_grid.py +++ b/examples/gn-instance-grid/gn_instance_grid.py @@ -135,10 +135,15 @@ def build_keycap_mesh(): bm.free() # the hidden underside must not bend the skirt's smooth normals: the foot # ring (the first RING_VERTS verts created) is marked sharp - for e in me.edges: - a, b = e.vertices - if a < RING_VERTS and b < RING_VERTS: - e.use_edge_sharp = True + n_edges = len(me.edges) + ev = [0] * (2 * n_edges) + me.edges.foreach_get("vertices", ev) + sharp = [False] * n_edges + me.edges.foreach_get("use_edge_sharp", sharp) + for i in range(n_edges): + if ev[2 * i] < RING_VERTS and ev[2 * i + 1] < RING_VERTS: + sharp[i] = True + me.edges.foreach_set("use_edge_sharp", sharp) return me diff --git a/examples/gn-modifier-inputs/README.md b/examples/gn-modifier-inputs/README.md index 921532a4..8229d3a4 100644 --- a/examples/gn-modifier-inputs/README.md +++ b/examples/gn-modifier-inputs/README.md @@ -66,16 +66,16 @@ against it. `10` is the shared framing helper. | 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 | +| 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 | -| 11 | Evaluated extents not distinct | | 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 7) and `--api dict` (expects exit 5; 5.2+ only, where the dict write raises; SKIP on older Blender, where it exits 0). +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). ## Falsification @@ -85,6 +85,6 @@ Smoke does not pass `--output`. Forcing the RNA path is not smoke-tested (it rai | `--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` | +| `--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 | diff --git a/examples/gn-modifier-inputs/gn_modifier_inputs.py b/examples/gn-modifier-inputs/gn_modifier_inputs.py index 9ae46d76..fee0c22a 100644 --- a/examples/gn-modifier-inputs/gn_modifier_inputs.py +++ b/examples/gn-modifier-inputs/gn_modifier_inputs.py @@ -17,9 +17,9 @@ ``--api dict`` / ``--api rna`` force one side of the 5.1/5.2 split — they fail on the *other* series, not on every binary. ``--same-height`` writes 1.0 to every modifier and still asserts 1 / 2 / 3, so the second -staircase's readback fails on all three. That is the portable falsifier -(``--same-axis`` in export-preset-axis). ``--same-scale`` is kept as an -alias for the pre-staircase flag name. +staircase's evaluated Z-extent fails on all three (exit 8). That is the +portable falsifier. ``--same-scale`` is kept as an alias for the +pre-staircase flag name. blender --background --python gn_modifier_inputs.py -- blender --background --python gn_modifier_inputs.py -- --same-height @@ -408,13 +408,10 @@ def check(tree, objs, mods, api, same_height=False): file=sys.stderr, ) return 6 - if abs(got - height) > READBACK_EPS: - print( - f"ERROR: readback {got} != written {height} on {obj.name}", - file=sys.stderr, - ) - return 7 + # The evaluated geometry is the contract; `--same-height` lands on it + # (exit 8). The readback comparison runs after it as a diagnostic, so + # deleting the extent check makes the falsifier fail (#469). zmin, zmax, nverts = evaluated_z_extent(obj) extent = zmax - zmin if abs(extent - height) > EXTENT_EPS: @@ -431,6 +428,12 @@ def check(tree, objs, mods, api, same_height=False): file=sys.stderr, ) return 9 + if abs(got - height) > READBACK_EPS: + print( + f"ERROR: readback {got} != written {height} on {obj.name}", + file=sys.stderr, + ) + return 7 print( f"{obj.name} height={height} readback={got:.6f} " f"z_extent={extent:.6f} zmin={zmin:.6f} verts={nverts}" @@ -446,7 +449,7 @@ def check(tree, objs, mods, api, same_height=False): "per-modifier copies did not land", file=sys.stderr, ) - return 11 + return 13 return 0 diff --git a/examples/gn-sdf-remesh/README.md b/examples/gn-sdf-remesh/README.md index fe821e0f..c5359738 100644 --- a/examples/gn-sdf-remesh/README.md +++ b/examples/gn-sdf-remesh/README.md @@ -36,6 +36,9 @@ blender --background --python gn_sdf_remesh.py -- # Falsifier: no SDF modifier. Must exit non-zero (evaluated == base). blender --background --python gn_sdf_remesh.py -- --no-sdf +# Falsifier: no Set Material node in the tree. Must exit non-zero (exit 6). +blender --background --python gn_sdf_remesh.py -- --no-set-material + # Also render the remeshed result (EEVEE on a GPU host; --engine cycles on GPU-less hosts): blender --background --python gn_sdf_remesh.py -- --output remesh.png blender --background --python gn_sdf_remesh.py -- --output remesh.png --engine cycles @@ -54,9 +57,9 @@ against it. | 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 | +| 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 falsifier is `--no-sdf` (expects exit 3). +Smoke does not pass `--output`. Its catalog falsifiers are `--no-sdf` (expects exit 3) and `--no-set-material` (expects exit 6). diff --git a/examples/gn-sdf-remesh/gn_sdf_remesh.py b/examples/gn-sdf-remesh/gn_sdf_remesh.py index b1abb347..d2f21e3f 100644 --- a/examples/gn-sdf-remesh/gn_sdf_remesh.py +++ b/examples/gn-sdf-remesh/gn_sdf_remesh.py @@ -12,6 +12,7 @@ blender --background --python gn_sdf_remesh.py -- # correctness check only blender --background --python gn_sdf_remesh.py -- --no-sdf # must fail + blender --background --python gn_sdf_remesh.py -- --no-set-material # must fail (exit 6) blender --background --python gn_sdf_remesh.py -- --output r.png # also render the result blender --background --python gn_sdf_remesh.py -- --output r.png --engine cycles # GPU-less """ @@ -250,6 +251,8 @@ def main(): p.add_argument("--engine", choices=["auto", "cycles"], default="auto") p.add_argument("--no-sdf", action="store_true", help="skip attaching the SDF remesh modifier (must fail)") + p.add_argument("--no-set-material", action="store_true", + help="build the tree without the Set Material node (must fail, exit 6)") args = p.parse_args(argv) # EEVEE-id inversion witnessed for real: the OTHER era's id must be @@ -266,7 +269,8 @@ def main(): obj = build() base = len(obj.data.vertices) src_mat = obj.data.materials[0] if obj.data.materials else None - tree, link_valid = build_remesh_via_sdf(material=src_mat) + tree, link_valid = build_remesh_via_sdf( + material=None if args.no_set_material else src_mat) if not args.no_sdf: obj.modifiers.new("sdf", 'NODES').node_group = tree dg = bpy.context.evaluated_depsgraph_get(); ev = obj.evaluated_get(dg) diff --git a/examples/gn-zone-iterate/README.md b/examples/gn-zone-iterate/README.md index 4289cfb4..befca4cd 100644 --- a/examples/gn-zone-iterate/README.md +++ b/examples/gn-zone-iterate/README.md @@ -59,11 +59,12 @@ against it. `10` is the shared framing helper. | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | -| 2 | argparse / usage; also carrier mesh was rewritten | +| 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 | diff --git a/examples/gn-zone-iterate/gn_zone_iterate.py b/examples/gn-zone-iterate/gn_zone_iterate.py index eb25aedf..b16f1945 100644 --- a/examples/gn-zone-iterate/gn_zone_iterate.py +++ b/examples/gn-zone-iterate/gn_zone_iterate.py @@ -238,7 +238,7 @@ def build(repeat_n, foreach_p, pair_repeat, pair_foreach, offset_repeat, foreach def check(rpt, fee, expect_n, expect_p): if len(rpt.data.vertices) != 1 or len(fee.data.vertices) != 1: print("ERROR: carrier mesh was rewritten; zones must not apply in-place", file=sys.stderr) - return 2 + return 7 r_coords, r_faces = eval_mesh(rpt) f_coords, f_faces = eval_mesh(fee) diff --git a/examples/gp-lineart-contour/README.md b/examples/gp-lineart-contour/README.md index b80063a4..b555d7b2 100644 --- a/examples/gp-lineart-contour/README.md +++ b/examples/gp-lineart-contour/README.md @@ -29,10 +29,10 @@ often gets wrong. - **GPv3 address** matches `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), +**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 or `use_contour` (exit 4), drawing below the gates -(exit 5), source clear not zeroing strokes (exit 6), flags-off not zeroing +`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 @@ -58,7 +58,7 @@ which is EEVEE-only, so `--engine cycles` does not render the look. # 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: @@ -74,15 +74,16 @@ against it. `10` is the shared framing helper. | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | -| 2 | argparse / usage; also GPv3 collection address contract | +| 2 | argparse / usage | | 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 | +| 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 4). +Smoke does not pass `--output`. Its catalog falsifier is `--no-contour` (expects exit 5). diff --git a/examples/gp-lineart-contour/gp_lineart_contour.py b/examples/gp-lineart-contour/gp_lineart_contour.py index e0d89f31..56256932 100644 --- a/examples/gp-lineart-contour/gp_lineart_contour.py +++ b/examples/gp-lineart-contour/gp_lineart_contour.py @@ -20,9 +20,9 @@ (no bake required) from the same camera the still renders from and asserts stroke/point lower bounds against the known failure modes above. -``--no-contour`` clears ``use_contour`` after the modifier is built and -still asserts it is True. That is the falsifier (``--same-axis`` in -export-preset-axis). The GPv3 address shim and the thickness/radius trap +``--no-contour`` clears ``use_contour`` after the modifier is built. That is +the falsifier: the check never re-reads the flag, so it is caught by the +evaluated stroke counts. The GPv3 address shim and the thickness/radius trap are untouched. By default it runs the correctness check only. Pass --output to render: @@ -78,20 +78,20 @@ def check_gp_version_gate(): if bpy.app.version >= (5, 0, 0): if not hasattr(bpy.data, "grease_pencils"): print("ERROR: grease_pencils missing on 5.x", file=sys.stderr) - return 2 + return 9 if hasattr(bpy.data, "grease_pencils_v3"): print("ERROR: grease_pencils_v3 should be gone on 5.x", file=sys.stderr) - return 2 + return 9 print("5.x contract: grease_pencils is GPv3; _v3 alias gone") else: if not hasattr(bpy.data, "grease_pencils_v3"): print("ERROR: grease_pencils_v3 missing on 4.5", file=sys.stderr) - return 2 + return 9 legacy = bpy.data.grease_pencils.new("LegacyProbe") lframe = legacy.layers.new("L").frames.new(1) if hasattr(lframe, "drawing") or not hasattr(lframe, "strokes"): print("ERROR: 4.5 grease_pencils is not legacy GPencil", file=sys.stderr) - return 2 + return 9 bpy.data.grease_pencils.remove(legacy) print("4.5 contract: grease_pencils is legacy; GPv3 lives at _v3") return 0 @@ -519,9 +519,8 @@ def check(sc, source, la_ob, mod): if mod.source_object != source: print("ERROR: source_object round-trip failed", file=sys.stderr) return 4 - if not mod.use_contour: - print("ERROR: use_contour should be True", file=sys.stderr) - return 4 + # No `use_contour` read-back here: `--no-contour` must be caught by the + # evaluated strokes below, not by re-reading the flag it just wrote (#469). # Happy path: contours present n_s, n_p = eval_stroke_counts(la_ob) diff --git a/examples/index.json b/examples/index.json index 772c7d3f..9ed173eb 100644 --- a/examples/index.json +++ b/examples/index.json @@ -196,15 +196,15 @@ "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", + "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": "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)" + "17": "Mirror partner deviation above tolerance", + "18": "Evaluated bbox not symmetric about X" } }, { @@ -235,9 +235,10 @@ "exit_codes": { "0": "Success", "1": "Uncaught exception (FATAL wrapper)", - "2": "argparse / usage; also exporter RNA missing expected glTF kwargs", + "2": "argparse / usage", "3": "Pathology missing: coincident shells (`--no-duplicate` lands here)", - "4": "glTF export handling failed (`--weld` lands here)" + "4": "glTF export handling failed (`--weld` lands here)", + "5": "Exporter RNA missing expected glTF kwargs (checked first)" } }, { @@ -291,7 +292,7 @@ "args": [ "--point-domain" ], - "expect_exit": 5 + "expect_exit": 6 } ], "exit_codes": { @@ -300,8 +301,8 @@ "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", + "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", @@ -322,15 +323,15 @@ "args": [ "--threshold-high" ], - "expect_exit": 3 + "expect_exit": 4 } ], "exit_codes": { "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", + "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)" @@ -371,13 +372,14 @@ "exit_codes": { "0": "Success", "1": "Uncaught exception (FATAL wrapper)", - "2": "argparse / usage; also an active object existed after the data-API build", + "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" } @@ -396,7 +398,7 @@ "args": [ "--no-caps" ], - "expect_exit": 6 + "expect_exit": 7 } ], "exit_codes": { @@ -406,12 +408,13 @@ "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", + "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": "X span off closed form; also gallery framing violation", - "11": "`--output` produced no file; also asset-quality floor violation (`gallery_asset_quality`)" + "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" } }, { @@ -462,7 +465,7 @@ "args": [ "--mute" ], - "expect_exit": 7 + "expect_exit": 9 } ], "exit_codes": { @@ -473,9 +476,9 @@ "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)", + "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", + "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" } @@ -671,8 +674,11 @@ ], "exit_codes": { "0": "Success (including `--silent-handler` / `--no-handler` / `--wrong-text` / `--write-in-main` / `--atexit-instead`, which the harness then fails)", - "1": "Uncaught exception (FATAL wrapper); also `$BDT_SMOKE_SIDECAR` unset", - "2": "argparse / usage; also `--force-run` on Blender < 5.1 (`exit_pre` missing or unexpectedly present)", + "1": "Uncaught exception (FATAL wrapper)", + "2": "argparse / usage", + "3": "`--force-run` below 5.1: `exit_pre` is absent (`AttributeError`), the expected 4.5 outcome", + "4": "`--force-run` below 5.1: `exit_pre` unexpectedly present, so the 5.1 floor and the skip are wrong", + "5": "`$BDT_SMOKE_SIDECAR` unset (run outside the harness)", "77": "`SMOKE_SKIP:` `exit_pre` requires Blender 5.1+" } }, @@ -698,17 +704,19 @@ "exit_codes": { "0": "Success", "1": "Uncaught exception (FATAL wrapper)", - "2": "argparse / usage; also exporter RNA missing expected glTF kwargs", + "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; also `--output` produced no file", + "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)", - "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" + "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" } }, { @@ -781,8 +789,8 @@ "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", + "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", @@ -793,8 +801,8 @@ "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)" + "22": "On-disk POSITION count ≠ evaluated loop count", + "23": "On-disk UV V-flip failed" } }, { @@ -828,8 +836,8 @@ "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", + "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", @@ -838,7 +846,9 @@ "17": "Vertex group names drifted", "18": "Weight round-trip drifted", "19": "Deformation round-trip drifted", - "20": "`--output` produced no file" + "20": "`--output` produced no file", + "21": "Armature count after import ≠ 1", + "22": "Bone count drifted" } }, { @@ -945,7 +955,7 @@ "args": [ "--same-height" ], - "expect_exit": 7 + "expect_exit": 8 }, { "args": [ @@ -964,12 +974,12 @@ "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", + "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", - "11": "Evaluated extents not distinct", - "12": "`--output` produced no file" + "12": "`--output` produced no file", + "13": "Evaluated extents not distinct" } }, { @@ -989,6 +999,12 @@ "--no-sdf" ], "expect_exit": 3 + }, + { + "args": [ + "--no-set-material" + ], + "expect_exit": 6 } ], "exit_codes": { @@ -998,7 +1014,7 @@ "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", + "6": "Input material dropped by remesh (`--no-set-material` lands here)", "10": "Framing gate violation on the `--output` path (`gallery_framing`)" } }, @@ -1125,11 +1141,12 @@ "exit_codes": { "0": "Success", "1": "Uncaught exception (FATAL wrapper)", - "2": "argparse / usage; also carrier mesh was rewritten", + "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" } @@ -1148,19 +1165,20 @@ "args": [ "--no-contour" ], - "expect_exit": 4 + "expect_exit": 5 } ], "exit_codes": { "0": "Success", "1": "Uncaught exception (FATAL wrapper)", - "2": "argparse / usage; also GPv3 collection address contract", + "2": "argparse / usage", "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", + "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" } }, @@ -1273,7 +1291,7 @@ "args": [ "--skip-link" ], - "expect_exit": 5 + "expect_exit": 6 } ], "exit_codes": { @@ -1282,8 +1300,7 @@ "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", + "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", @@ -1320,8 +1337,9 @@ "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" + "10": "Gallery framing violation (`--output` only; `gallery_framing`)", + "11": "Gallery asset-quality violation", + "12": "`--output` produced no file" } }, { @@ -1457,8 +1475,9 @@ "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" + "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" } }, { @@ -1545,7 +1564,7 @@ "exit_codes": { "0": "Success", "1": "Uncaught exception (FATAL wrapper)", - "2": "argparse / usage; also closed-form palette no longer stresses unpremul", + "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)", @@ -1553,10 +1572,12 @@ "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", + "10": "Gallery framing violation (`--output` only; `gallery_framing`)", "12": "EXR `color_mode=RGB` alpha-drop contract", - "13": "`--output` produced no file" + "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" } }, { @@ -1640,7 +1661,7 @@ "args": [ "--same-tint" ], - "expect_exit": 6 + "expect_exit": 8 } ], "exit_codes": { @@ -1650,8 +1671,9 @@ "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)", + "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" } }, @@ -1706,7 +1728,7 @@ "args": [ "--unlink-sky" ], - "expect_exit": 6 + "expect_exit": 7 } ], "exit_codes": { @@ -1716,8 +1738,8 @@ "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", + "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)", @@ -1755,11 +1777,11 @@ "7": "Non-manifold edges", "8": "Edge lengths not uniform", "9": "Face planarity off", - "10": "Centroid off origin", - "11": "Circumradius not uniform", + "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": "Framing gate (`--output` path only; `gallery_framing` returns 10, remapped)" + "14": "Centroid off origin", + "15": "Circumradius not uniform" } }, { @@ -1842,16 +1864,30 @@ "args": [ "--same-base" ], - "expect_exit": 3 + "expect_exit": 6 + }, + { + "args": [ + "--invert-engine-id" + ], + "expect_exit": 5 + }, + { + "args": [ + "--no-specular-shim" + ], + "expect_exit": 7 } ], "exit_codes": { "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", + "3": "Distinct swatch material colors ≠ 6 (check-only path; with `--output` a diagnostic after check 6)", "4": "`--output` produced no file", - "5": "Wrong-era EEVEE engine id was accepted", + "5": "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" } }, @@ -1920,14 +1956,15 @@ "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)", + "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" + "13": "`--output`: bundled `datafiles/fonts/Inter.woff2` not found", + "14": "`version_string` does not start with the dotted `bpy.app.version` tuple" } }, { @@ -2035,8 +2072,9 @@ "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)" + "10": "Gallery framing violation (`--output` only; `gallery_framing`)", + "11": "Asset-quality floor violation (`--output` only; shared helper)", + "12": "`--output` produced no file" } }, { @@ -2097,9 +2135,10 @@ "exit_codes": { "0": "Success", "1": "Uncaught exception (FATAL wrapper)", - "2": "argparse / usage; also exporter RNA missing expected glTF kwargs", + "2": "argparse / usage", "3": "Pathology missing: unapplied non-uniform scale (`--identity` lands here)", - "4": "Export handling failed (`--bake` lands here)" + "4": "Export handling failed (`--bake` lands here)", + "5": "Exporter RNA missing expected glTF kwargs (checked first)" } }, { @@ -2133,11 +2172,12 @@ "exit_codes": { "0": "Success", "1": "Uncaught exception (FATAL wrapper)", - "2": "argparse / usage; also base cube verts ≠ 8", + "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" } @@ -2173,11 +2213,11 @@ "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" + "15": "`--output` produced no file", + "16": "Broken panel is not flat" } }, { @@ -2244,8 +2284,9 @@ "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)" + "10": "Gallery framing violation (`--output` only; `gallery_framing`)", + "11": "Asset-quality floor violation (`--output` only)", + "12": "`--output` produced no file" } }, { @@ -2278,9 +2319,10 @@ "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" + "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" } }, { diff --git a/examples/light-link-studio/README.md b/examples/light-link-studio/README.md index 88ee8d08..ff7fc41b 100644 --- a/examples/light-link-studio/README.md +++ b/examples/light-link-studio/README.md @@ -58,7 +58,7 @@ matte check spheres do. The still's render path gates framing through # 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): @@ -77,8 +77,7 @@ against it. | 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 | +| 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 | @@ -86,4 +85,4 @@ against it. 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). +Smoke does not pass `--output`. Its catalog falsifier is `--skip-link` (expects exit 6). diff --git a/examples/light-link-studio/light_link_studio.py b/examples/light-link-studio/light_link_studio.py index 3b44a3b7..8cd30090 100644 --- a/examples/light-link-studio/light_link_studio.py +++ b/examples/light-link-studio/light_link_studio.py @@ -20,8 +20,9 @@ 4.5 LTS and 5.1 (ObjectLightLinking with receiver_collection/blocker_collection on both). -``--skip-link`` leaves ``receiver_collection`` unset and still asserts the -hero collection. That is the falsifier (``--same-axis`` in export-preset-axis). +``--skip-link`` leaves ``receiver_collection`` unset. That is the falsifier: +nothing re-reads the property, so the linked render lights the decoy and the +hero/decoy ratio check exits 6. By default it runs the two-render correctness check (no gallery still) — the CI smoke check. Pass --output to also render a still: @@ -306,12 +307,9 @@ def check(sc, key, hero_c, hero, decoy, skip_link=False): print("ERROR: light objects lost light_linking", file=sys.stderr) return 4 - # contract 1 (assignment round-trip through the API itself) + # contract 1: the assignment. No read-back of the value just written: + # `--skip-link` must be caught by the rendered pixels below (#469). key.light_linking.receiver_collection = None if skip_link else hero_c - if key.light_linking.receiver_collection != hero_c: - print("ERROR: receiver_collection assignment did not read back", - file=sys.stderr) - return 5 setup_render(sc, PXW, PXH, 8) tmp = tempfile.mkdtemp(prefix="light_link_") @@ -494,7 +492,7 @@ def main(): p = argparse.ArgumentParser() p.add_argument("--output", default=None, help="optional: render a still PNG here") p.add_argument("--skip-link", action="store_true", - help="falsifier: leave receiver_collection unset, still assert hero_c") + help="falsifier: leave receiver_collection unset; the linked render must fail") args = p.parse_args(argv) bpy.ops.wm.read_factory_settings(use_empty=True) diff --git a/examples/lightmap-uv-channel/README.md b/examples/lightmap-uv-channel/README.md index fa1db390..19687eda 100644 --- a/examples/lightmap-uv-channel/README.md +++ b/examples/lightmap-uv-channel/README.md @@ -116,8 +116,9 @@ helper. | 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 | +| 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). diff --git a/examples/lightmap-uv-channel/lightmap_uv_channel.py b/examples/lightmap-uv-channel/lightmap_uv_channel.py index 7a16f17d..c58e2aa3 100644 --- a/examples/lightmap-uv-channel/lightmap_uv_channel.py +++ b/examples/lightmap-uv-channel/lightmap_uv_channel.py @@ -979,7 +979,7 @@ def render_still(path, engine, falsify=False): bpy.ops.render.render(write_still=True) if not (os.path.exists(path) and os.path.getsize(path) > 0): print("ERROR: render produced no file", file=sys.stderr) - return 10 + return 12 return 0 diff --git a/examples/mesh-hygiene-audit/mesh_hygiene_audit.py b/examples/mesh-hygiene-audit/mesh_hygiene_audit.py index b924a85b..d8b93abb 100644 --- a/examples/mesh-hygiene-audit/mesh_hygiene_audit.py +++ b/examples/mesh-hygiene-audit/mesh_hygiene_audit.py @@ -700,8 +700,7 @@ def main(): if args.inject: target = bpy.data.objects.get(args.inject_part) if target is None or target not in parts: - print(f"ERROR: no part named {args.inject_part!r}", file=sys.stderr) - return 2 + p.error(f"no part named {args.inject_part!r}") # argparse's usage exit, 2 inject_defect(target.data, args.inject) print(f"injected {args.inject} into {args.inject_part}") code = check_valve(parts) diff --git a/examples/modular-kit-snap/README.md b/examples/modular-kit-snap/README.md index dc6afd53..3341babe 100644 --- a/examples/modular-kit-snap/README.md +++ b/examples/modular-kit-snap/README.md @@ -49,7 +49,7 @@ y-nudge on two verts): boundary verts off the end planes, worst **3.000e-03 m** 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). +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. @@ -108,8 +108,9 @@ the shared Layer 1 helper). `11` is the shared asset-quality helper. | 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 | +| 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). diff --git a/examples/modular-kit-snap/modular_kit_snap.py b/examples/modular-kit-snap/modular_kit_snap.py index 97ba42e3..1a9ac0cb 100644 --- a/examples/modular-kit-snap/modular_kit_snap.py +++ b/examples/modular-kit-snap/modular_kit_snap.py @@ -268,7 +268,7 @@ def fail(code, msg): fails.append(code) if len(meshes) != EXPECT_PARTS: - fail(11, f"kit built {len(meshes)} parts, expected {EXPECT_PARTS} - the " + fail(12, f"kit built {len(meshes)} parts, expected {EXPECT_PARTS} - the " f"per-part loops below would iterate over the wrong set") return fails[0] @@ -382,11 +382,11 @@ def fail(code, msg): bpy.context.view_layer.update() for ob in parts_obs: if max(abs(s - 1.0) for s in ob.scale) > 0.0: - fail(11, f"{ob.name} scale {tuple(ob.scale)} not applied") + fail(12, f"{ob.name} scale {tuple(ob.scale)} not applied") if not ob.name.startswith("Kit.CorridorSeg."): - fail(11, f"datablock name {ob.name!r} outside the kit namespace") + fail(12, f"datablock name {ob.name!r} outside the kit namespace") if max(abs(c) for c in ob.location) > 0.0: - fail(11, f"{ob.name} origin off the pivot {tuple(ob.location)}") + fail(12, f"{ob.name} origin off the pivot {tuple(ob.location)}") print(f"hygiene parts={len(parts_obs)} identity_scale=True origins_at_pivot=True") if fails: @@ -718,7 +718,7 @@ def render_still(path, engine, falsify=False, close_camera=False): "(--close-camera is the designed fail)", file=sys.stderr, ) - return 10 + return gallery_framing.EXIT_FRAMING aqcode = gallery_asset_quality.check_asset_quality( sc, cam, hero=aq_hero, stage=[floor, wall]) if aqcode: diff --git a/examples/png-exr-alpha/README.md b/examples/png-exr-alpha/README.md index 2dc83e7f..4792eecf 100644 --- a/examples/png-exr-alpha/README.md +++ b/examples/png-exr-alpha/README.md @@ -82,7 +82,7 @@ against it. `10` is also the shared framing helper. | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | -| 2 | argparse / usage; also closed-form palette no longer stresses unpremul | +| 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) | @@ -90,10 +90,12 @@ against it. `10` is also the shared framing helper. | 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 | +| 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). diff --git a/examples/png-exr-alpha/png_exr_alpha.py b/examples/png-exr-alpha/png_exr_alpha.py index f9cc70f7..8670deef 100644 --- a/examples/png-exr-alpha/png_exr_alpha.py +++ b/examples/png-exr-alpha/png_exr_alpha.py @@ -191,7 +191,7 @@ def check(opaque_alpha=False): return fail( f"closed-form palette worst {model_worst:.7f} < floor " f"{PNG_ERR_FLOOR} — probe colors no longer stress false unpremul", - 2, + 14, ) print( f"closed-form float→PNG worst RGB err {model_worst:.7f} " @@ -237,25 +237,13 @@ def check(opaque_alpha=False): ) # --- 2. Same float buffer → OpenEXR: float-precision round-trip -------- + # Image.save() writes in the IMAGE's own `file_format`; it ignores the + # scene's render.image_settings entirely, so none are set here. To write + # with explicit settings (color_mode, color_depth), use save_render with a + # scene whose image_settings carry them — section 4 does exactly that. img_f2 = new_float_image("FloatExrSrc") fill_pattern(img_f2) - scene = bpy.context.scene - scene.render.image_settings.file_format = "OPEN_EXR" - scene.render.image_settings.color_mode = "RGBA" - try: - scene.render.image_settings.color_depth = "32" - except TypeError: - pass - - td = tempfile.mkdtemp(prefix="png_exr_alpha_exr_") - exr_path = os.path.join(td, "probe.exr") - img_f2.filepath_raw = exr_path - img_f2.file_format = "OPEN_EXR" - img_f2.save() - exr_loaded = bpy.data.images.load(exr_path) - exr_loaded.colorspace_settings.name = "Non-Color" - exr_got = [0.0] * (W * H * 4) - exr_loaded.pixels.foreach_get(exr_got) + exr_got, _exr_path = save_and_reload(img_f2, "OPEN_EXR", "exr") exr_err = max_channel_err(orig, exr_got, channels=4) if exr_err > EXR_TOL: @@ -284,7 +272,7 @@ def check(opaque_alpha=False): return fail( f"byte→PNG disagrees with straight-alpha 8-bit model by " f"{byte_model_err:.7f} (tol {BYTE_TOL:.7f}); worst={byte_worst}", - 10, + 15, ) # Stress cell: authored (0.02,0.02,0.02,a=1/255). False unpremul → ~1.0; # byte straight path must stay near 0.02. @@ -294,7 +282,7 @@ def check(opaque_alpha=False): f"byte→PNG stress cell RGB=({stress[0]:.5f},{stress[1]:.5f}," f"{stress[2]:.5f}) looks false-unpremul-mangled (expected ~0.02) " f"— byte/float storage contract flipped", - 11, + 16, ) print( f"byte→PNG IHDR bit_depth={bit_b}; model residual {byte_model_err:.7f} " @@ -303,8 +291,10 @@ def check(opaque_alpha=False): ) # --- 4. OPEN_EXR color_mode='RGB' drops alpha -------------------------- + # save_render (unlike save) DOES honor the scene's image_settings. img_f3 = new_float_image("FloatExrRgb") fill_pattern(img_f3) + scene = bpy.context.scene scene.render.image_settings.file_format = "OPEN_EXR" scene.render.image_settings.color_mode = "RGB" td3 = tempfile.mkdtemp(prefix="png_exr_alpha_rgb_") diff --git a/examples/shader-node-group/README.md b/examples/shader-node-group/README.md index 9ec324a8..94871c5b 100644 --- a/examples/shader-node-group/README.md +++ b/examples/shader-node-group/README.md @@ -9,7 +9,9 @@ and the [`shader-node-group`](../../snippets/shader-node-group.py) 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. +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 @@ -56,10 +58,11 @@ against it. `10` is the shared framing helper. | 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) | +| 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 6). +Smoke does not pass `--output`. Its catalog falsifier is `--same-tint` (expects exit 8). diff --git a/examples/shader-node-group/shader_node_group.py b/examples/shader-node-group/shader_node_group.py index aa4b9536..00c90e59 100644 --- a/examples/shader-node-group/shader_node_group.py +++ b/examples/shader-node-group/shader_node_group.py @@ -18,9 +18,10 @@ points at that tree, and the instance-level Tint values are pairwise distinct — the whole point of grouping. -``--same-tint`` copies the first mug's Tint onto every instance and still -asserts the values differ. That is the falsifier (``--same-axis`` in -export-preset-axis). +``--same-tint`` copies the first mug's Tint onto every instance. That is the +falsifier: a tiny Cycles probe render of the five mugs (a throwaway scene, +so the still is untouched) finds the glazes no longer distinct and exits 8, +before the socket-value comparison runs. By default it runs only the correctness check (no render) — the CI smoke check. Pass --output to also render a still: @@ -29,8 +30,9 @@ blender --background --python shader_node_group.py -- --same-tint # must fail blender --background --python shader_node_group.py -- --output m.png # + render """ -import bpy, bmesh, sys, os, math, argparse +import bpy, bmesh, sys, os, math, argparse, shutil, tempfile from mathutils import Vector +from bpy_extras.object_utils import world_to_camera_view # Shared Layer 1 framing measurement (render path only) — see gallery_framing.py sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir)) @@ -255,6 +257,14 @@ def check(tree, objs): print(f"ERROR: {obj.name} instance points at a different tree", file=sys.stderr) return 5 tints.append(tuple(round(c, 3) for c in node.inputs["Tint"].default_value)) + + # The behavioral contract: the shared group renders a different glaze per + # instance. `--same-tint` lands here on rendered pixels (exit 8); the + # socket read-back below runs after it as a diagnostic (#469). + code = check_rendered_tints(objs) + if code: + return code + if len(set(tints)) != len(tints): print(f"ERROR: instance Tint values not pairwise distinct {tints} — parameters " "leaked into the group instead of the instance", file=sys.stderr) @@ -264,6 +274,89 @@ def check(tree, objs): return 0 +# Rendered-tint probe: a throwaway scene holding the same mug objects, so the +# main scene (and the --output still) is untouched. Flat white world, ortho +# front camera, a tiny Cycles render; each mug's glazed upper body is sampled. +PROBE_W, PROBE_H = 240, 80 +PROBE_SAMPLES = 16 +PROBE_Z = 0.62 # local height on the glazed wall, below the rim break +PROBE_HALF = 2 # sample a (2*PROBE_HALF+1)^2 pixel patch +MIN_TINT_SEP = 0.04 # min pairwise max-channel distance between mugs + + +def check_rendered_tints(objs): + probe = bpy.data.scenes.new("TintProbe") + world = bpy.data.worlds.new("TintProbeWorld") + world.use_nodes = True + world.node_tree.nodes["Background"].inputs["Color"].default_value = (1.0, 1.0, 1.0, 1.0) + probe.world = world + for obj in objs: + probe.collection.objects.link(obj) + cam_data = bpy.data.cameras.new("TintProbeCam") + cam_data.type = 'ORTHO' + cam_data.ortho_scale = 5.6 + cam = bpy.data.objects.new("TintProbeCam", cam_data) + cam.location = (0.0, -8.0, 0.75) + cam.rotation_euler = (math.radians(90.0), 0.0, 0.0) + probe.collection.objects.link(cam) + probe.camera = cam + r = probe.render + r.engine = 'CYCLES' + probe.cycles.samples = PROBE_SAMPLES + probe.cycles.use_denoising = False + r.resolution_x, r.resolution_y, r.resolution_percentage = PROBE_W, PROBE_H, 100 + r.image_settings.file_format = 'PNG' + probe.view_settings.view_transform = 'Standard' + tmp = tempfile.mkdtemp(prefix="shader_node_group_") + path = os.path.join(tmp, "tints.png") + r.filepath = path + try: + bpy.ops.render.render(write_still=True, scene=probe.name) + probe.view_layers[0].update() # the probe camera's matrix_world is stale until evaluated + if not (os.path.exists(path) and os.path.getsize(path) > 0): + print("ERROR: tint probe render produced no file", file=sys.stderr) + return 8 + img = bpy.data.images.load(path) + try: + w, h = img.size + px = img.pixels[:] + means = [] + for obj in objs: + co = obj.matrix_basis @ Vector((0.0, 0.0, PROBE_Z)) + ndc = world_to_camera_view(probe, cam, co) + cx, cy = int(ndc.x * w), int(ndc.y * h) + acc = [0.0, 0.0, 0.0] + n = 0 + for y in range(cy - PROBE_HALF, cy + PROBE_HALF + 1): + for x in range(cx - PROBE_HALF, cx + PROBE_HALF + 1): + i = (max(0, min(h - 1, y)) * w + max(0, min(w - 1, x))) * 4 + for c in range(3): + acc[c] += px[i + c] + n += 1 + means.append(tuple(a / n for a in acc)) + finally: + bpy.data.images.remove(img) + finally: + bpy.data.scenes.remove(probe) + bpy.data.objects.remove(cam) + bpy.data.cameras.remove(cam_data) + bpy.data.worlds.remove(world) + shutil.rmtree(tmp, ignore_errors=True) + + sep = min( + max(abs(a[c] - b[c]) for c in range(3)) + for i, a in enumerate(means) for b in means[i + 1:] + ) + shown = ", ".join("(" + ",".join(f"{v:.3f}" for v in m) + ")" for m in means) + print(f"rendered_tints={shown} min_pairwise_sep={sep:.3f} (gate>={MIN_TINT_SEP})") + if sep < MIN_TINT_SEP: + print(f"ERROR: rendered mug glazes not distinct (min pairwise {sep:.3f} < " + f"{MIN_TINT_SEP}) — the instances do not carry their own Tint", + file=sys.stderr) + return 8 + return 0 + + def eevee_engine_id(): return 'BLENDER_EEVEE' if bpy.app.version >= (5, 0, 0) else 'BLENDER_EEVEE_NEXT' @@ -383,7 +476,7 @@ def main(): p.add_argument("--engine", default="eevee", choices=("eevee", "cycles"), help="render engine for --output (cycles for GPU-less hosts)") p.add_argument("--same-tint", action="store_true", - help="falsifier: identical instance Tints, still assert they differ") + help="falsifier: identical instance Tints; the rendered probe must fail") args = p.parse_args(argv) tree, objs = build_scene(same_tint=args.same_tint) diff --git a/examples/sky-texture-sun-elevation/README.md b/examples/sky-texture-sun-elevation/README.md index fb0ff678..0acc4746 100644 --- a/examples/sky-texture-sun-elevation/README.md +++ b/examples/sky-texture-sun-elevation/README.md @@ -22,10 +22,10 @@ often gets wrong across the 4.5 LTS → 5.1 window. 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), +**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), and an elevation that +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; @@ -75,7 +75,7 @@ bottom 0.044, top 0.161. The ground, plaza and meridian inlay are stage. # 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): @@ -95,8 +95,8 @@ against it. | 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 | +| 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) | @@ -105,4 +105,4 @@ against it. 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). +Its catalog falsifier is `--unlink-sky` (expects exit 7). diff --git a/examples/sky-texture-sun-elevation/sky_texture_sun_elevation.py b/examples/sky-texture-sun-elevation/sky_texture_sun_elevation.py index 1fbff911..bef202ec 100644 --- a/examples/sky-texture-sun-elevation/sky_texture_sun_elevation.py +++ b/examples/sky-texture-sun-elevation/sky_texture_sun_elevation.py @@ -12,8 +12,9 @@ 4. ``sun_elevation`` is load-bearing: raising it brightens zenith luminance — proven with two tiny Cycles EXR probes (straight-up camera) in one check. -``--unlink-sky`` drops the Sky → Background Color link and still asserts -the chain. That is the falsifier (``--same-axis`` in export-preset-axis). +``--unlink-sky`` drops the Sky → Background Color link. That is the +falsifier: the zenith probe renders catch it (the world stops being a sky), +before the link check, which now runs last as a diagnostic. The sky_type / dust_density version traps are untouched. By default it runs the correctness check (tiny Cycles CPU renders, no gallery @@ -486,9 +487,9 @@ def check(sc, world, sky, bg, unlink_sky=False): for link in list(nt.links): if link.from_node == sky and link.to_node.name == "Background": nt.links.remove(link) - code = check_links(world) - if code: - return code + # The link check runs after the zenith probe renders, as a diagnostic: + # `--unlink-sky` must be caught by the rendered sky pixels, not by + # re-reading the link it just removed (#469). code = check_api(sky) if code: return code @@ -531,6 +532,10 @@ def check(sc, world, sky, bg, unlink_sky=False): ) return 8 + code = check_links(world) + if code: + return code + print( f"sun_elevation low={math.degrees(ELEV_LOW):.1f}deg zenith_L={z_lo:.4f}" ) diff --git a/examples/soccer-ball-goldberg/README.md b/examples/soccer-ball-goldberg/README.md index f5cae6df..b1fe983d 100644 --- a/examples/soccer-ball-goldberg/README.md +++ b/examples/soccer-ball-goldberg/README.md @@ -83,8 +83,8 @@ None of this touches the checked mesh, its binding, or any check. turf, so the spot centres under the ball's origin. - **Framing gate.** The render path calls [`gallery_framing.check_framing`](../gallery_framing.py) 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, + 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. @@ -126,11 +126,11 @@ against it. | 7 | Non-manifold edges | | 8 | Edge lengths not uniform | | 9 | Face planarity off | -| 10 | Centroid off origin | -| 11 | Circumradius not uniform | +| 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 | Framing gate (`--output` path only; `gallery_framing` returns 10, remapped) | +| 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). diff --git a/examples/soccer-ball-goldberg/soccer_ball_goldberg.py b/examples/soccer-ball-goldberg/soccer_ball_goldberg.py index 4c909bb0..ceb6fb6b 100644 --- a/examples/soccer-ball-goldberg/soccer_ball_goldberg.py +++ b/examples/soccer-ball-goldberg/soccer_ball_goldberg.py @@ -40,7 +40,6 @@ TOL_CENTER = 1.0e-6 # centroid at origin SEAM_WIDTH = 0.018 # render only: stitched seam width on the ball SEAM_LIFT = 0.002 # render only: seam sits this far proud of the ball -FRAMING_EXIT = 14 # render only: gallery_framing returns 10, taken here TURF_SIZE = (16.0, 14.0) # render only: pitch turf footprint (x, y) TURF_H = 0.22 # render only: sod sample thickness TURF_STRIPE = 0.6 # render only: mowing stripe width @@ -254,7 +253,7 @@ def check(obj): if center_off > TOL_CENTER: print(f"ERROR: centroid off origin by {center_off:.3e} (tol {TOL_CENTER:.1e})", file=sys.stderr) - return 10 + return 14 radii = [math.sqrt((v.co.x - cx) ** 2 + (v.co.y - cy) ** 2 + (v.co.z - cz) ** 2) for v in me.vertices] r_mean = sum(radii) / len(radii) @@ -262,7 +261,7 @@ def check(obj): if r_dev > TOL_RADIUS: print(f"ERROR: circumradius deviates {r_dev:.3e} from {r_mean:.6f} " f"(tol {TOL_RADIUS:.1e}) — vertices not on one sphere", file=sys.stderr) - return 11 + return 15 if len(me.materials) != 2: print(f"ERROR: expected 2 panel materials, found {len(me.materials)}", @@ -567,7 +566,7 @@ def light(name, loc, energy, size, col, rot): scene.view_settings.view_transform = 'Standard' if gallery_framing.check_framing(scene, cam, hero=[obj], elements=[obj], stage=[floor, wall, turf]): - return FRAMING_EXIT + return gallery_framing.EXIT_FRAMING bpy.ops.render.render(write_still=True) if not (os.path.exists(path) and os.path.getsize(path) > 0): print("ERROR: render produced no file", file=sys.stderr) diff --git a/examples/swatch-grid/README.md b/examples/swatch-grid/README.md index 2d502e57..ccc417dc 100644 --- a/examples/swatch-grid/README.md +++ b/examples/swatch-grid/README.md @@ -24,8 +24,10 @@ that mapping fails the example, not just the docs. # 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 @@ -77,9 +79,11 @@ against it. `10` is the shared framing helper. | 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 | +| 3 | Distinct swatch material colors ≠ 6 (check-only path; with `--output` a diagnostic after check 6) | | 4 | `--output` produced no file | -| 5 | Wrong-era EEVEE engine id was accepted | +| 5 | 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 @@ -87,7 +91,7 @@ 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). +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 diff --git a/examples/swatch-grid/swatch_grid.py b/examples/swatch-grid/swatch_grid.py index e76d97b8..1f3e290c 100644 --- a/examples/swatch-grid/swatch_grid.py +++ b/examples/swatch-grid/swatch_grid.py @@ -9,8 +9,11 @@ By default it runs only the correctness check (no render) — the CI smoke check. Pass --output to also render and pixel-verify a still. ``--same-base`` writes -the same RGB to every swatch and still asserts six distinct colors, so the -count fails. That is the falsifier (``--same-axis`` in export-preset-axis). +the same RGB to every swatch. That is the falsifier: with --output (as smoke +runs it) the rendered six-region check fails (exit 6) before any read-back of +the colors; without --output the material-value check is all there is (exit 3). +``--invert-engine-id`` (exit 5) and ``--no-specular-shim`` (exit 7) falsify the +engine-id and set_specular witnesses. blender --background --python swatch_grid.py -- # check only blender --background --python swatch_grid.py -- --same-base # must fail @@ -56,7 +59,17 @@ def set_specular(bsdf, value): return None -def make_principled(name, base_color, metallic, roughness, specular=None): +def naive_set_specular(bsdf, value): + """The pre-4.0 habit ``--no-specular-shim`` models: write the legacy + ``Specular`` socket when it exists, otherwise silently do nothing.""" + if 'Specular' in bsdf.inputs: + bsdf.inputs['Specular'].default_value = value + return 'Specular' + return None + + +def make_principled(name, base_color, metallic, roughness, specular=None, + setter=set_specular): mat = bpy.data.materials.new(name) mat.use_nodes = True nt = mat.node_tree @@ -65,7 +78,7 @@ def make_principled(name, base_color, metallic, roughness, specular=None): bsdf.inputs['Base Color'].default_value = base_color bsdf.inputs['Metallic'].default_value = metallic bsdf.inputs['Roughness'].default_value = roughness - resolved = set_specular(bsdf, specular) if specular is not None else None + resolved = setter(bsdf, specular) if specular is not None else None out = nt.nodes.new('ShaderNodeOutputMaterial') nt.links.new(bsdf.outputs['BSDF'], out.inputs['Surface']) return mat, resolved @@ -105,7 +118,7 @@ def make_emissive(name, color, strength, shell_color=None): return mat -def build_materials(): +def build_materials(setter=set_specular): """Return a list of (material, label) covering metal, dielectric, emissive, and the set_specular shim. The list order maps left-to-right, top-to-bottom across the grid.""" mats, specular_socket = [], None @@ -116,17 +129,20 @@ def build_materials(): mats.append(m) m, _ = make_principled("Copper", (0.92, 0.47, 0.36, 1), 1.0, 0.62) mats.append(m) - m, sr = make_principled("RedPlastic", (0.80, 0.05, 0.05, 1), 0.0, 0.40, specular=0.5) + m, sr = make_principled("RedPlastic", (0.80, 0.05, 0.05, 1), 0.0, 0.40, specular=0.5, + setter=setter) mats.append(m) specular_socket = specular_socket or sr - m, _ = make_principled("BluePlastic", (0.05, 0.20, 0.80, 1), 0.0, 0.30, specular=0.5) + m, _ = make_principled("BluePlastic", (0.05, 0.20, 0.80, 1), 0.0, 0.30, specular=0.5, + setter=setter) mats.append(m) # Standard does not compress highlights, so the core radiance stays under # 1.0 in every channel: at 1.4 the red channel clipped across the whole # face and the swatch read as a flat orange disk with no form mats.append(make_emissive("EmissiveOrange", (1.0, 0.35, 0.05, 1), 0.95, shell_color=(0.22, 0.035, 0.006, 1))) - m, _ = make_principled("WhiteRough", (0.90, 0.90, 0.92, 1), 0.0, 0.70, specular=0.3) + m, _ = make_principled("WhiteRough", (0.90, 0.90, 0.92, 1), 0.0, 0.70, specular=0.3, + setter=setter) mats.append(m) return mats, specular_socket @@ -411,6 +427,25 @@ def flatten_swatch_colors(mats): node.inputs["Color"].default_value = gray +SPECULAR_SOCKET = 'Specular IOR Level' # the 4.0+ name; 4.5 LTS is the floor +WHITE_SPECULAR = 0.3 # off the socket default (0.5) + + +def check_specular(mats, specular_socket): + """The set_specular shim resolved the 4.0+ socket, and the value landed on + it: WhiteRough's 0.3 differs from the socket default, so a shim that wrote + nothing (``--no-specular-shim``) is caught on the material itself.""" + white = next(m for m in mats if m.name == "WhiteRough") + bsdf = next(n for n in white.node_tree.nodes if n.type == "BSDF_PRINCIPLED") + got = bsdf.inputs[SPECULAR_SOCKET].default_value + if specular_socket != SPECULAR_SOCKET or abs(got - WHITE_SPECULAR) > 1e-6: + print(f"ERROR: set_specular resolved {specular_socket!r} (want " + f"{SPECULAR_SOCKET!r}); WhiteRough {SPECULAR_SOCKET}={got:.3f} " + f"(want {WHITE_SPECULAR})", file=sys.stderr) + return 7 + return 0 + + def check_distinct_swatches(mats): colors = [swatch_rgb(m) for m in mats] if len(set(colors)) != MATERIAL_COUNT: @@ -462,16 +497,22 @@ def main(): p.add_argument("--width", type=int, default=1280) p.add_argument("--same-base", action="store_true", help="write the same RGB to every swatch (must fail)") + p.add_argument("--invert-engine-id", action="store_true", + help="falsifier: swap the version branch, so the 'wrong-era' id " + "under test is this build's real one (must fail, exit 5)") + p.add_argument("--no-specular-shim", action="store_true", + help="falsifier: write only the legacy 'Specular' socket (must fail, exit 7)") args = p.parse_args(argv) # Empty the factory file FIRST so the materials we create below survive. bpy.ops.wm.read_factory_settings(use_empty=True) - mats, specular_socket = build_materials() + mats, specular_socket = build_materials( + naive_set_specular if args.no_specular_shim else set_specular) + scode = check_specular(mats, specular_socket) + if scode: + return scode if args.same_base: flatten_swatch_colors(mats) - dcode = check_distinct_swatches(mats) - if dcode: - return dcode swatches, stands, stage = build_scene(mats) sc = bpy.context.scene @@ -480,6 +521,8 @@ def main(): # by this build, and the helper's id must be accepted. eid = get_eevee_engine_id() wrong = 'BLENDER_EEVEE_NEXT' if bpy.app.version >= (5, 0, 0) else 'BLENDER_EEVEE' # engine-id-exempt: the wrong-era id this example asserts is rejected + if args.invert_engine_id: + eid, wrong = wrong, eid try: sc.render.engine = wrong print(f"ERROR: wrong-era EEVEE id '{wrong}' was accepted by this build — " @@ -492,6 +535,10 @@ def main(): f"set_specular resolved '{specular_socket}'") if not args.output: + # No render to measure: the material values are all there is. + dcode = check_distinct_swatches(mats) + if dcode: + return dcode print("swatch-grid OK") return 0 @@ -534,7 +581,12 @@ def main(): f"distinct_regions={regions} materials={MATERIAL_COUNT} ok={regions_ok}") if not (non_black and regions_ok): print("ERROR: render failed verification (black or wrong region count)", file=sys.stderr) - return 3 + return 6 + # Diagnostic after the pixels: `--same-base` lands on the rendered regions + # above (exit 6), not on this read-back of the colors it wrote (#469). + dcode = check_distinct_swatches(mats) + if dcode: + return dcode print("swatch-grid OK") return 0 diff --git a/examples/text-version-stamp/README.md b/examples/text-version-stamp/README.md index 1e0fa827..2f57f078 100644 --- a/examples/text-version-stamp/README.md +++ b/examples/text-version-stamp/README.md @@ -66,7 +66,7 @@ against it. `10` is the shared framing helper. | 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) | +| 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 | @@ -74,6 +74,7 @@ against it. `10` is the shared framing helper. | 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). diff --git a/examples/text-version-stamp/text_version_stamp.py b/examples/text-version-stamp/text_version_stamp.py index 98278455..0c702d69 100644 --- a/examples/text-version-stamp/text_version_stamp.py +++ b/examples/text-version-stamp/text_version_stamp.py @@ -15,9 +15,10 @@ `to_mesh_clear()` the returned Mesh reference is dead and any access raises ReferenceError. -``--wrong-body`` assigns a string that is not ``version_string`` and still -asserts the body is the live version. That is the falsifier (``--same-axis`` -in export-preset-axis). +``--wrong-body`` assigns a string that is not ``version_string``. That is the +falsifier: the stamp's evaluated glyph mesh no longer matches a reference +TextCurve built from ``version_string`` (exit 4). The check never re-reads +``body``. By default it runs only the correctness check (no render) — the CI smoke check. Pass --output to also render a still: @@ -76,13 +77,13 @@ def check(obj): f"type={obj.type} font={txt.font}", file=sys.stderr) return 3 - # body is the live version string, and version_string starts with the - # dotted version tuple on every supported release ("5.1.2", "4.5.11 LTS") + # version_string starts with the dotted version tuple on every supported + # release ("5.1.2", "4.5.11 LTS") — a fact about Blender, not the stamp dotted = "%d.%d.%d" % bpy.app.version - if txt.body != bpy.app.version_string or not bpy.app.version_string.startswith(dotted): - print(f"ERROR: body={txt.body!r} version_string={bpy.app.version_string!r} " - f"tuple={bpy.app.version}", file=sys.stderr) - return 4 + if not bpy.app.version_string.startswith(dotted): + print(f"ERROR: version_string={bpy.app.version_string!r} does not start " + f"with tuple {bpy.app.version}", file=sys.stderr) + return 14 deps = bpy.context.evaluated_depsgraph_get() @@ -93,6 +94,26 @@ def check(obj): f"z-extent={z_flat}", file=sys.stderr) return 5 + # the stamp's EVALUATED glyphs are the live version string: they match a + # fresh reference TextCurve built from version_string. `--wrong-body` lands + # here on evaluated geometry, not on a read-back of `body` (#469). + ref_cu = bpy.data.curves.new("VersionRef", type='FONT') + ref_cu.body = bpy.app.version_string + ref_cu.align_x, ref_cu.align_y = txt.align_x, txt.align_y + ref = bpy.data.objects.new("VersionRef", ref_cu) + bpy.context.collection.objects.link(ref) + try: + (rv, rf, rx, _), _ = eval_extents(ref, deps) + finally: + bpy.data.objects.remove(ref) + bpy.data.curves.remove(ref_cu) + if (nv, nf) != (rv, rf) or abs(x_flat - rx) > TOL: + print(f"ERROR: evaluated glyphs do not spell version_string " + f"{bpy.app.version_string!r}: verts/faces/x-extent " + f"{nv}/{nf}/{x_flat:.5f} != reference {rv}/{rf}/{rx:.5f}", + file=sys.stderr) + return 4 + # solidify: z-extent is exactly 2*(extrude+bevel), bevel widens x by 2*bevel txt.extrude = EXTRUDE txt.bevel_depth = BEVEL diff --git a/examples/triangulate-tangents/README.md b/examples/triangulate-tangents/README.md index 760dacee..6e949090 100644 --- a/examples/triangulate-tangents/README.md +++ b/examples/triangulate-tangents/README.md @@ -99,8 +99,9 @@ helper. | 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 | +| 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). diff --git a/examples/triangulate-tangents/triangulate_tangents.py b/examples/triangulate-tangents/triangulate_tangents.py index e034d24c..297af006 100644 --- a/examples/triangulate-tangents/triangulate_tangents.py +++ b/examples/triangulate-tangents/triangulate_tangents.py @@ -585,7 +585,7 @@ def light(name, loc, energy, size, col, rot): bpy.ops.render.render(write_still=True) if not (os.path.exists(path) and os.path.getsize(path) > 0): print("ERROR: render produced no file", file=sys.stderr) - return 10 + return 12 return 0 @@ -603,8 +603,7 @@ def main(): obj, n_dome = build_buckler() if args.zero_uv: uv = obj.data.uv_layers["UVMap"] - for loop in uv.data: - loop.uv = (0.0, 0.0) + uv.data.foreach_set("uv", [0.0] * (2 * len(uv.data))) code = check(obj, n_dome) if code: return code diff --git a/examples/unapplied-scale-gltf/README.md b/examples/unapplied-scale-gltf/README.md index 327ef111..7ac4b79f 100644 --- a/examples/unapplied-scale-gltf/README.md +++ b/examples/unapplied-scale-gltf/README.md @@ -86,9 +86,10 @@ against it. | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | -| 2 | argparse / usage; also exporter RNA missing expected glTF kwargs | +| 2 | argparse / usage | | 3 | Pathology missing: unapplied non-uniform scale (`--identity` lands here) | | 4 | Export handling failed (`--bake` lands here) | +| 5 | Exporter RNA missing expected glTF kwargs (checked first) | 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). diff --git a/examples/unapplied-scale-gltf/unapplied_scale_gltf.py b/examples/unapplied-scale-gltf/unapplied_scale_gltf.py index 62612344..584c086e 100644 --- a/examples/unapplied-scale-gltf/unapplied_scale_gltf.py +++ b/examples/unapplied-scale-gltf/unapplied_scale_gltf.py @@ -70,9 +70,10 @@ def accessor_floats(idx, ncomp): def local_extent(me): - xs = [v.co.x for v in me.vertices] - ys = [v.co.y for v in me.vertices] - zs = [v.co.z for v in me.vertices] + """Per-axis (min, max) of the mesh's local coordinates, read in bulk.""" + co = [0.0] * (3 * len(me.vertices)) + me.vertices.foreach_get("co", co) + xs, ys, zs = co[0::3], co[1::3], co[2::3] return ( (min(xs), max(xs)), (min(ys), max(ys)), @@ -117,7 +118,7 @@ def check(ob, bake): missing = [k for k in EXPORT_KWARGS if k not in props] if missing: print(f"ERROR: exporter RNA drifted, missing {missing}", file=sys.stderr) - return 2 + return 5 me = ob.data got = tuple(float(c) for c in ob.scale) non_uniform = len({round(c, 6) for c in got}) > 1 diff --git a/examples/usd-export-evaluation-mode/README.md b/examples/usd-export-evaluation-mode/README.md index 0bbf0ba6..bad01b53 100644 --- a/examples/usd-export-evaluation-mode/README.md +++ b/examples/usd-export-evaluation-mode/README.md @@ -61,11 +61,12 @@ against it. `10` is the shared framing helper. | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | -| 2 | argparse / usage; also base cube verts ≠ 8 | +| 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 | diff --git a/examples/usd-export-evaluation-mode/usd_export_evaluation_mode.py b/examples/usd-export-evaluation-mode/usd_export_evaluation_mode.py index 3df891bc..4b04061f 100644 --- a/examples/usd-export-evaluation-mode/usd_export_evaluation_mode.py +++ b/examples/usd-export-evaluation-mode/usd_export_evaluation_mode.py @@ -127,7 +127,7 @@ def check(obj, mod, out_dir, force_mode=None, force_subdiv=None): base = len(obj.data.vertices) if base != 8: print(f"ERROR: base cube verts {base} != 8", file=sys.stderr) - return 2 + return 7 mode = force_mode or "RENDER" subdiv = force_subdiv or "TESSELLATE" diff --git a/examples/uv-layer-grid/README.md b/examples/uv-layer-grid/README.md index 6ad6a52c..f23c0ce7 100644 --- a/examples/uv-layer-grid/README.md +++ b/examples/uv-layer-grid/README.md @@ -91,11 +91,11 @@ missing-render-file check. | 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 | +| 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). diff --git a/examples/uv-layer-grid/uv_layer_grid.py b/examples/uv-layer-grid/uv_layer_grid.py index 863f67f7..ad03439f 100644 --- a/examples/uv-layer-grid/uv_layer_grid.py +++ b/examples/uv-layer-grid/uv_layer_grid.py @@ -531,7 +531,7 @@ def verify_still(path): return fail( f"broken panel is not flat (spread {b_spread:.4f} > 0.02) — " f"the hazard did not render as one color", - 11, + 16, ) if not (b_mean[2] > b_mean[1] > b_mean[0]): return fail( diff --git a/examples/vertex-weight-limit/README.md b/examples/vertex-weight-limit/README.md index 29759081..73245994 100644 --- a/examples/vertex-weight-limit/README.md +++ b/examples/vertex-weight-limit/README.md @@ -95,8 +95,9 @@ code. `11` is the shared asset-quality helper. | 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 | +| 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). diff --git a/examples/vertex-weight-limit/vertex_weight_limit.py b/examples/vertex-weight-limit/vertex_weight_limit.py index 3e4e3edf..1d151326 100644 --- a/examples/vertex-weight-limit/vertex_weight_limit.py +++ b/examples/vertex-weight-limit/vertex_weight_limit.py @@ -729,7 +729,7 @@ def light(name, loc, energy, size, col, rot): bpy.ops.render.render(write_still=True) if not (os.path.exists(path) and os.path.getsize(path) > 0): print("ERROR: render produced no file", file=sys.stderr) - return 10 + return 12 return 0 diff --git a/examples/vse-cut-list/README.md b/examples/vse-cut-list/README.md index bade5ebd..56643de0 100644 --- a/examples/vse-cut-list/README.md +++ b/examples/vse-cut-list/README.md @@ -157,9 +157,10 @@ against it. `10` is also the shared framing helper. | 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 | +| 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 diff --git a/examples/vse-cut-list/vse_cut_list.py b/examples/vse-cut-list/vse_cut_list.py index c6cda4d3..777895a7 100644 --- a/examples/vse-cut-list/vse_cut_list.py +++ b/examples/vse-cut-list/vse_cut_list.py @@ -334,7 +334,7 @@ def run_checks(swap_inputs=False): return code code = check_roundtrip() if code != 0: - return fail(f"round-trip re-assert failed (exit {code})", 10) + return fail(f"round-trip re-assert failed (exit {code})", 13) print( f"accessor: {'strips only (.sequences removed)' if IS_5X else '.sequences bridged to .strips'}; " f"end kwarg: {'length' if IS_5X else 'frame_end'} (wrong kwarg TypeErrors); " @@ -683,7 +683,7 @@ def render_bay(sc, path, engine, w, h): bpy.ops.render.render(write_still=True, scene=sc.name) if not (os.path.exists(path) and os.path.getsize(path) > 0): print("ERROR: render produced no file", file=sys.stderr) - return 11 + return 14 return 0 @@ -702,7 +702,7 @@ def render_still(path, engine): try: frame_path = os.path.join(tmp, "program.png") if not render_frame(sc, frame_path, engine, RENDER_W, RENDER_H): - return 11 + return 14 bay = bpy.data.scenes.new("Bay") build_stage(bay) # the house dark studio, lights included build_bay(bay, frame_path, coll) @@ -828,8 +828,8 @@ def main(): # The round-trip reloaded a .blend; start clean for the still. bpy.ops.wm.read_factory_settings(use_empty=True) rcode = render_still(os.path.abspath(args.output), args.engine) - if rcode == 11: - return fail(f"render produced no file at {args.output}", 11) + if rcode == 14: + return fail(f"render produced no file at {args.output}", 14) if rcode: return rcode print(f"wrote {args.output}") diff --git a/skills/geometry-nodes-python/SKILL.md b/skills/geometry-nodes-python/SKILL.md index 7e370d84..3e92891a 100644 --- a/skills/geometry-nodes-python/SKILL.md +++ b/skills/geometry-nodes-python/SKILL.md @@ -397,7 +397,7 @@ Each example runs headless, asserts the contract, and exits non-zero when it bre - [`gn-bundle-roundtrip`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/gn-bundle-roundtrip): Combine Bundle / Separate Bundle packing a cube plus Scale, Offset, and Mark — witnessing `geometry-nodes-python` item-name round-trip, not tree structure. Blender 5.0+. Falsify: `--legacy-rna` (exit 7). - [`gn-instance-grid`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/gn-instance-grid): A generative Geometry Nodes tree — Mesh Grid → Instance on Points (a modeled keycap via Object Info) → Realize Instances → Set Shade Smooth → Set Material. Falsify: `--one-cell` (exit 4). -- [`gn-modifier-inputs`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/gn-modifier-inputs): Per-modifier Geometry Nodes Float inputs on a shared tree — 4.5/5.1 write mod[identifier], 5.2 writes mod.properties.inputs.Socket_1.value. Falsify: `--same-height` (exit 7). +- [`gn-modifier-inputs`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/gn-modifier-inputs): Per-modifier Geometry Nodes Float inputs on a shared tree — 4.5/5.1 write mod[identifier], 5.2 writes mod.properties.inputs.Socket_1.value. Falsify: `--same-height` (exit 8). - [`gn-sdf-remesh`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/gn-sdf-remesh): A Geometry Nodes SDF remesh (MeshToSDFGrid → GridToMesh at the SDF zero-level), with a Set Material node carrying the material through the remesh. Falsify: `--no-sdf` (exit 3). - [`gn-sim-fountain`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/gn-sim-fountain): A Simulation Zone only advances one step per consecutive frame_set. Falsify: `--unpair` (exit 3). - [`gn-socket-rename`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/gn-socket-rename): Compare and Random Value socket identifiers collapsed onto reused names in 5.2; enabled-name lookup wires on 4.5, 5.1, and 5.2 Falsify: `--legacy-ids` (exit 5). diff --git a/skills/mesh-editing-and-bmesh/SKILL.md b/skills/mesh-editing-and-bmesh/SKILL.md index 86dfbbbd..09bf1eb4 100644 --- a/skills/mesh-editing-and-bmesh/SKILL.md +++ b/skills/mesh-editing-and-bmesh/SKILL.md @@ -290,7 +290,7 @@ Each example runs headless, asserts the contract, and exits non-zero when it bre - [`bmesh-gear`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/bmesh-gear): A 14-tooth gear built entirely with bmesh — profile ring, face, extrude — with bm.free() in a try/finally, exactly as the ownership contract demands. Falsify: `--no-extrude` (exit 3). - [`boolean-exact-volume`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/boolean-exact-volume): The Boolean modifier on the EXACT solver. Falsify: `--float-solver` (exit 3). - [`collision-hull-proxy`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/collision-hull-proxy): A fire hydrant street prop inside its compound collision shell: four convex pieces hulled by bmesh.ops.convex_hull from a coarse inflated cage. Falsify: `--shrink-hull` (exit 3). -- [`color-attribute-wheel`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/color-attribute-wheel): The modern color-attributes API — mesh.color_attributes.new() on the CORNER domain. Falsify: `--point-domain` (exit 5). +- [`color-attribute-wheel`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/color-attribute-wheel): The modern color-attributes API — mesh.color_attributes.new() on the CORNER domain. Falsify: `--point-domain` (exit 6). - [`custom-normals-shade`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/custom-normals-shade): A jerry can prop shaded three ways to prove the post-4.1 shading contract: hard edges are mesh data, landing exactly where the dihedral crosses. Falsify: `--mismatch-angle` (exit 5). - [`degenerate-bevel-weld`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/degenerate-bevel-weld): Bevel offset >= half the min box dimension collapses the band into zero-area faces — and they ship: a stdlib GLB re-parse counts the degenerate triangles crossing the export boundary. Falsify: `--both-safe` (exit 4). - [`lattice-deform`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/lattice-deform): A 2x2x2 Lattice modifier set to KEY_LINEAR on all three axes, with two top control points moved through LatticePoint.co_deform, read back through the depsgraph (evaluated_get, to_mesh, to_mesh_clear). Falsify: `--bspline` (exit 4). diff --git a/skills/procedural-materials-and-shaders/SKILL.md b/skills/procedural-materials-and-shaders/SKILL.md index 2312fc26..62a5ccfd 100644 --- a/skills/procedural-materials-and-shaders/SKILL.md +++ b/skills/procedural-materials-and-shaders/SKILL.md @@ -205,9 +205,9 @@ Almost everything you want to do with materials has a `bpy.data` path. The two n Each example runs headless, asserts the contract, and exits non-zero when it breaks. Run one with `blender --background --python