Skip to content

Commit c4cd7b4

Browse files
TMHSDigitalclaude
andauthored
fix: correct the inverted EEVEE engine id and close the coverage gap that hid it (#204)
* fix: correct the inverted EEVEE engine id in shipping-crate eevee_engine_id() keyed on >= (4, 2, 0) and returned 'BLENDER_EEVEE_NEXT' for every version at or above it. Blender 5.x reclaimed the plain 'BLENDER_EEVEE' id and no longer offers 'BLENDER_EEVEE_NEXT', so the --output path died on 5.1 and 5.2: TypeError: bpy_struct: item.attr = val: enum "BLENDER_EEVEE_NEXT" not found in ('BLENDER_EEVEE', 'BLENDER_WORKBENCH', 'CYCLES') Smoke stayed green because smoke never passes --output. The correct mapping is already witnessed in the tree: examples/swatch-grid asserts both the chosen id and the other era's id against the running build. Use its form and name it as the source, per the copied-not-imported convention the hygiene helpers already follow. shipping-crate is the pilot piece the other 27 were modelled on, which is how one inverted ternary propagated. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> * fix: correct the inverted EEVEE engine id in iron-cauldron Second copy of the same inversion, found by auditing every engine-id mapping in the tree rather than by grepping for the one known bad file. Audit method: extract every expression that yields a BLENDER_EEVEE* id and evaluate it twice, against a stubbed bpy.app.version of (4, 5, 11) and (5, 2, 1), then compare both results against the canonical mapping. 83 expressions across 85 files; two returned 'BLENDER_EEVEE_NEXT' for 5.2 — shipping-crate and this one. Everything else was already correct. Like shipping-crate, its --output path raised TypeError on 5.1 and 5.2 and smoke could not see it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> * test: assert every EEVEE engine-id mapping matches the canonical one The render path is invisible to CI by construction: smoke runs the default check path, and --output is exercised only at authoring time, on one version, by the author. That is how an inverted engine id shipped twice. tests/smoke/run_smoke.py already asserts the mapping against the running build, but against its own copy of the helper. It cannot see the other copies, and showcase and example scripts are standalone by convention, so each carries its own. Eighty-four mappings across eighty-three files. This checker closes that gap at zero render cost. It walks examples/, showcase/, templates/, snippets/, scripts/ and tests/, and for every function that resolves an EEVEE id it compiles and *calls* the function twice against a stubbed bpy.app.version of (4, 5, 11) and (5, 2, 1); conditional expressions yielding an id are evaluated the same way. AST rather than regex, deliberately. A line-oriented regex misreads the if/return form as two independent branches and flags snippets/version-branch-skeleton.py, which is correct. Calling the code is the only reading that cannot be fooled by formatting. Resolver detection is narrow on purpose: only a body of returns and version branches qualifies, so a main() that merely mentions the id is not compiled and does not need the module's imports. Three deliberately inverted mappings exist — swatch-grid, turntable and gn-sdf-remesh each assert that the *other* era's id is rejected by the build. Those now carry an explicit `# engine-id-exempt:` marker, so the intent is stated in the source rather than inferred by the checker. Canary-proven. With the shipping-crate inversion reintroduced: ERROR: showcase/shipping-crate/shipping_crate.py:103 eevee_engine_id() 5.2.1->BLENDER_EEVEE_NEXT (want BLENDER_EEVEE) RC=1 Reverted: engine-id checks passed: 84 mapping(s) across 83 file(s), 3 exempt. RC=0 Runs in validate.yml beside the other tests/ checkers. No smoke cost, no blender-smoke.yml change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> * docs: require contact sheets and asset sheets for showcase pieces showcase/README.md said nothing about either gate, and no showcase piece has ever shipped one — all 44 contact sheets and all 4 asset sheets in docs/gallery/ belong to examples. The gates live under CLAUDE.md "Quality Gates for Example Runs", so the next author had to guess. Both are now required, each on its own evidence rather than on symmetry. Contact sheets have caught a real defect in showcase work: the first sheets for crate-stack and stone-archway showed both wedge pools reading as cool grey bands instead of the warm pool the house style calls for, and both pieces were relit. Nothing else in the pipeline puts the still beside its peers. Asset sheets cover what nothing else here covers. Showcase is entirely game props, which is exactly the scope docs/VISUAL-STYLE.md names for the gate. Budgets measure geometry conformance; the contact sheet measures staged presentation; neither removes the scene. The recorded evidence is socket-attach-points, which passed every measurable floor on its first draft (edge90 0.000, ten materials, no default names) and was still judged bad by eye and rebuilt — "the floors scored the bevels, not the design". That is precisely the failure a showcase budget cannot see. Also records the exit-code collision: check_asset_quality returns 11, which showcase numbering already spends on the collider ceiling, so call sites remap rather than sharing a code. Existing pieces are not retrofitted here; the backlog is filed separately. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> * test: assert falsifiers fail the budget they target Five falsifiers in the previous showcase run had to be retuned because they tripped an earlier check instead of the budget they were built for. A falsifier that exits non-zero on an earlier budget is red for the wrong reason and proves nothing about its target, and the exit code is right there to catch it. Documents the rule in CONTRIBUTING.md and showcase/README.md, with --off-circle replacing --flat-arch as the worked example: a lintel is 0.62 m shorter than the arch, so it failed the bounding box (exit 8) and never reached the intrados circle fit (exit 19). Widening the bbox tolerance would have destroyed a real budget to rescue a bad falsifier. The rule is explicit that the fix is the model or the falsifier, never the band. tests/check_falsifier_targets.py enforces it in two modes, one checker rather than a parallel harness: - static (default, no Blender, runs in validate.yml): reads each piece's falsifier table and asserts the flags are real argparse flags, the declared exit codes appear in that piece's exit-code table, every falsifier names a target budget, and no falsifier-shaped flag in the script is undocumented. 160 falsifiers across 25 pieces. - --run BLENDER: executes each falsifier and asserts the observed exit equals the declared one. This is the mode that catches an ill-aimed falsifier. One Blender launch per falsifier, about 150 s for the tree on one version, so it is an authoring and cron tool rather than a per-PR smoke step. No blender-smoke.yml change. Table parsing is header-aware, not positional: wooden-ladder's four-column "Falsifier | Budget violated | Exit | Measured failure" is as valid as the three-column form, and an earlier positional regex silently read it as having no exit column at all. The bidirectional variant — requiring each exit-code row to name its falsifier — was tried and dropped. It would have forced a format change across 23 READMEs to state something the falsifier table's own target column already carries. cart, hay-bale and stone-well predate the convention and have no table. They are listed explicitly in the checker and reported on every run; a new piece without a table is an error rather than an entry in that list. Canary-proven at runtime. Declaring --off-circle as targeting the bbox budget: MISMATCH stone-archway --off-circle: want 8, got 19 RC=1 Reverted: ok stone-archway --off-circle: want 19, got 19 falsifier-target checks passed: 167 falsifier(s) across 25 pieces. RC=0 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> * docs: record the measured runtime sweep cost The estimate said roughly 150 s. Running the full sweep measured 276 s for 160 falsifiers across 25 showcase pieces on Blender 5.2.1, with zero mismatches — every declared exit code is the one the falsifier actually produces. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> --------- Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 48684a9 commit c4cd7b4

10 files changed

Lines changed: 669 additions & 5 deletions

File tree

‎.github/workflows/validate.yml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,12 @@ jobs:
136136
- name: Check product exit codes are in README tables
137137
run: python3 tests/check_exit_code_readme.py
138138

139+
- name: Check EEVEE engine-id mappings
140+
run: python3 tests/check_engine_id.py
141+
142+
- name: Check falsifiers declare the budget they target
143+
run: python3 tests/check_falsifier_targets.py
144+
139145
- name: Validate template Python syntax
140146
run: |
141147
echo "Checking template Python syntax..."

‎CONTRIBUTING.md‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,39 @@ Three checks that appear on PRs are deliberately **excluded**:
223223
The two `Socket Security` checks come from a third-party GitHub App. An
224224
outage or an uninstall would deadlock merges, so they are advisory.
225225

226+
## Falsifiers must fail the budget they target
227+
228+
Every falsifier declares the budget it aims at, and it must fail **that**
229+
budget. A falsifier that exits non-zero on some earlier check proves
230+
nothing about its target: the run is red for the wrong reason, and the
231+
budget it was built for was never reached.
232+
233+
Fix a collision by changing the model or the falsifier. **Never widen a
234+
band so an ill-aimed falsifier lands.**
235+
236+
Worked example. `showcase/stone-archway`'s arch falsifier began as
237+
`--flat-arch`, laying the voussoirs as a flat lintel. A lintel is 0.62 m
238+
shorter than the arch, so it tripped the bounding-box budget (exit 8) and
239+
never reached the intrados circle fit (exit 19) it existed to break.
240+
Widening the bbox tolerance to let it through would have destroyed a real
241+
budget to rescue a bad falsifier. It was replaced by `--off-circle`, which
242+
keeps the angles, joints, materials, triangle count and envelope identical
243+
and wanders only the intrados radius. Four other falsifiers in the same run
244+
needed the same treatment: a stray vertex moved inside the silhouette, a
245+
`--short-skids` that floats one runner of three instead of all of them, a
246+
`--same-seed` split into design and placement RNG streams, and a
247+
`--sink-keystone` that no longer changes the Y envelope.
248+
249+
`tests/check_falsifier_targets.py` enforces the declaration. Its default
250+
static mode reads each showcase piece's falsifier table and asserts the
251+
flags are real argparse flags, the declared exit codes appear in that
252+
piece's exit-code table, and every falsifier names a target budget. Its
253+
`--run BLENDER` mode executes each falsifier and asserts the **observed**
254+
exit equals the declared one — the mode that catches an ill-aimed
255+
falsifier. Runtime costs one Blender launch per falsifier, measured at
256+
276 s for the whole showcase tree on one version, so it is an authoring
257+
and cron tool rather than a per-PR smoke step.
258+
226259
## Exit codes
227260

228261
Three roles, not one global table. Do not copy a code from one script into

‎examples/gn-sdf-remesh/gn_sdf_remesh.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ def main():
116116
# EEVEE-id inversion witnessed for real: the OTHER era's id must be
117117
# rejected by this build, the helper's accepted
118118
eid = get_eevee_engine_id()
119-
wrong = 'BLENDER_EEVEE_NEXT' if bpy.app.version >= (5, 0, 0) else 'BLENDER_EEVEE'
119+
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
120120
try:
121121
bpy.context.scene.render.engine = wrong
122122
print(f"ERROR: wrong-era EEVEE id '{wrong}' was accepted", file=sys.stderr); return 5

‎examples/swatch-grid/swatch_grid.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ def main():
265265
# Cycles. Witness the inversion for real: the OTHER era's id must be rejected
266266
# by this build, and the helper's id must be accepted.
267267
eid = get_eevee_engine_id()
268-
wrong = 'BLENDER_EEVEE_NEXT' if bpy.app.version >= (5, 0, 0) else 'BLENDER_EEVEE'
268+
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
269269
try:
270270
sc.render.engine = wrong
271271
print(f"ERROR: wrong-era EEVEE id '{wrong}' was accepted by this build — "

‎examples/turntable/turntable.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ def main():
135135
# the EEVEE-id mapping is asserted regardless of whether we render: the
136136
# OTHER era's id must be rejected by this build, the helper's accepted
137137
eid = get_eevee_engine_id()
138-
wrong = 'BLENDER_EEVEE_NEXT' if bpy.app.version >= (5, 0, 0) else 'BLENDER_EEVEE'
138+
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
139139
try:
140140
bpy.context.scene.render.engine = wrong
141141
print(f"ERROR: wrong-era EEVEE id '{wrong}' was accepted", file=sys.stderr); return 5

‎showcase/README.md‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,18 @@ entry in `showcase/gallery.json`, and a rendered still.
4545
A budget with no falsifier witnesses nothing:
4646
prove each one fails once, and check the exit code, not just
4747
non-zero.
48+
- **A falsifier must fail the budget it targets.** Declare the target in
49+
the piece README's falsifier table — flag, target budget, exit code —
50+
and make the run exit *that* code. A falsifier that trips an earlier
51+
check is red for the wrong reason and proves nothing about its target.
52+
Fix a collision by changing the model or the falsifier; never widen a
53+
band so an ill-aimed falsifier lands. `stone-archway`'s `--flat-arch`
54+
laid the voussoirs as a lintel, which is 0.62 m shorter and so failed
55+
the bounding box (exit 8) instead of the intrados circle fit (exit 19);
56+
`--off-circle` replaced it by keeping the whole envelope and wandering
57+
only the radius. `tests/check_falsifier_targets.py` checks the
58+
declaration statically, and with `--run BLENDER` executes each falsifier
59+
and asserts the observed exit matches.
4860
- **Hygiene budgets.** Copied combinatorics from
4961
`examples/mesh-hygiene-audit` (do not import the example). Every piece
5062
asserts on the generated mesh: non-manifold edges 0, loose verts 0,
@@ -164,6 +176,46 @@ entry in `showcase/gallery.json`, and a rendered still.
164176
`measure_framing_deviation` and assert at the call site. Do not move
165177
or modify `gallery_framing.py` — import it by resolving the repo root
166178
(see the shipping-crate script).
179+
- **Contact sheet (required).** Composite the candidate hero beside the
180+
pinned calibration set — canonical membership is in `CLAUDE.md`
181+
§ Quality Gates — commit it as
182+
`docs/gallery/contact-sheets/<name>-contact-sheet.webp`, link it in the
183+
PR body, and report a per-criterion verdict: stage darkness, wedge
184+
warmth, subject fill, saturation, thumbnail legibility, plus mean
185+
luminance against the calibration band. A claim without the committed
186+
composite is not evidence.
187+
188+
Required because it has caught a real defect in showcase work. The
189+
first sheets for `crate-stack` and `stone-archway` showed both wedge
190+
pools reading as cool grey bands rather than the warm pool the house
191+
style calls for; both were relit as a result. Nothing else in the
192+
pipeline looks at the still beside its peers, so nothing else could
193+
have seen it.
194+
195+
- **Asset sheet (required).** Render the hero alone — neutral
196+
three-quarter view, plain studio lighting, no staging tricks, no
197+
labels, no comparison props — composite it beside the pinned
198+
asset-quality reference set rendered the same way, commit under
199+
`docs/gallery/asset-sheets/`, and report a verdict. The piece ships
200+
only if it is not identifiable as the least-designed object in that
201+
lineup.
202+
203+
Required because showcase is *entirely* game props, which is exactly
204+
the scope `docs/VISUAL-STYLE.md` § Asset quality names, and because it
205+
covers something no other gate here does. Budgets measure geometry
206+
conformance; the contact sheet measures staged presentation. Neither
207+
removes the scene, and a strong scene carries a weak model. The
208+
recorded evidence is `socket-attach-points`: it passed every
209+
measurable floor on its first draft — `edge90` 0.000, ten materials,
210+
no default datablock names — and was then judged bad by eye and
211+
rebuilt from scratch. The floors scored the bevels, not the design.
212+
213+
`examples/gallery_asset_quality.check_asset_quality` returns **11** on
214+
violation, the same call pattern as `gallery_framing`. Showcase
215+
numbering already spends 11 on the collider-triangle ceiling, so remap
216+
the return at the call site rather than letting two budgets share a
217+
code.
218+
167219
- **Composition.** The README names which shipped skills and snippets the
168220
piece composes. Duplicated helpers stay inlined or copied; showcase
169221
scripts do not import snippets as a package.

‎showcase/iron-cauldron/iron_cauldron.py‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,14 @@
115115

116116

117117
def eevee_engine_id():
118-
return "BLENDER_EEVEE_NEXT" if bpy.app.version >= (4, 2, 0) else "BLENDER_EEVEE"
118+
"""EEVEE id: 'BLENDER_EEVEE' on 5.0+, 'BLENDER_EEVEE_NEXT' on 4.2-4.5.
119+
120+
Mapping matches examples/swatch-grid.get_eevee_engine_id, which asserts it
121+
against the running build (copied, not imported). Inherited the inverted
122+
form from showcase/shipping-crate; it returned 'BLENDER_EEVEE_NEXT' on 5.x,
123+
where that id does not exist.
124+
"""
125+
return "BLENDER_EEVEE" if bpy.app.version >= (5, 0, 0) else "BLENDER_EEVEE_NEXT"
119126

120127

121128
def fail(msg, code):

‎showcase/shipping-crate/shipping_crate.py‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,14 @@
101101

102102

103103
def eevee_engine_id():
104-
return "BLENDER_EEVEE_NEXT" if bpy.app.version >= (4, 2, 0) else "BLENDER_EEVEE"
104+
"""EEVEE id: 'BLENDER_EEVEE' on 5.0+, 'BLENDER_EEVEE_NEXT' on 4.2-4.5.
105+
106+
Mapping matches examples/swatch-grid.get_eevee_engine_id, which asserts it
107+
against the running build (copied, not imported). The old form here keyed
108+
on >= (4, 2, 0) and so returned 'BLENDER_EEVEE_NEXT' on 5.x, where that id
109+
does not exist — the render path raised TypeError on 5.1 and 5.2.
110+
"""
111+
return "BLENDER_EEVEE" if bpy.app.version >= (5, 0, 0) else "BLENDER_EEVEE_NEXT"
105112

106113

107114
def fail(msg, code):

0 commit comments

Comments
 (0)