Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,12 @@ jobs:
- name: Check product exit codes are in README tables
run: python3 tests/check_exit_code_readme.py

- name: Check EEVEE engine-id mappings
run: python3 tests/check_engine_id.py

- name: Check falsifiers declare the budget they target
run: python3 tests/check_falsifier_targets.py

- name: Validate template Python syntax
run: |
echo "Checking template Python syntax..."
Expand Down
33 changes: 33 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,39 @@ Three checks that appear on PRs are deliberately **excluded**:
The two `Socket Security` checks come from a third-party GitHub App. An
outage or an uninstall would deadlock merges, so they are advisory.

## Falsifiers must fail the budget they target

Every falsifier declares the budget it aims at, and it must fail **that**
budget. A falsifier that exits non-zero on some earlier check proves
nothing about its target: the run is red for the wrong reason, and the
budget it was built for was never reached.

Fix a collision by changing the model or the falsifier. **Never widen a
band so an ill-aimed falsifier lands.**

Worked example. `showcase/stone-archway`'s arch falsifier began as
`--flat-arch`, laying the voussoirs as a flat lintel. A lintel is 0.62 m
shorter than the arch, so it tripped the bounding-box budget (exit 8) and
never reached the intrados circle fit (exit 19) it existed to break.
Widening the bbox tolerance to let it through would have destroyed a real
budget to rescue a bad falsifier. It was replaced by `--off-circle`, which
keeps the angles, joints, materials, triangle count and envelope identical
and wanders only the intrados radius. Four other falsifiers in the same run
needed the same treatment: a stray vertex moved inside the silhouette, a
`--short-skids` that floats one runner of three instead of all of them, a
`--same-seed` split into design and placement RNG streams, and a
`--sink-keystone` that no longer changes the Y envelope.

`tests/check_falsifier_targets.py` enforces the declaration. Its default
static mode reads each showcase piece's falsifier table and asserts the
flags are real argparse flags, the declared exit codes appear in that
piece's exit-code table, and every falsifier names a target budget. Its
`--run BLENDER` mode executes each falsifier and asserts the **observed**
exit equals the declared one — the mode that catches an ill-aimed
falsifier. Runtime costs one Blender launch per falsifier, measured at
276 s for the whole showcase tree on one version, so it is an authoring
and cron tool rather than a per-PR smoke step.

## Exit codes

Three roles, not one global table. Do not copy a code from one script into
Expand Down
2 changes: 1 addition & 1 deletion examples/gn-sdf-remesh/gn_sdf_remesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ def main():
# EEVEE-id inversion witnessed for real: the OTHER era's id must be
# rejected by this build, the helper's accepted
eid = get_eevee_engine_id()
wrong = 'BLENDER_EEVEE_NEXT' if bpy.app.version >= (5, 0, 0) else 'BLENDER_EEVEE'
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
try:
bpy.context.scene.render.engine = wrong
print(f"ERROR: wrong-era EEVEE id '{wrong}' was accepted", file=sys.stderr); return 5
Expand Down
2 changes: 1 addition & 1 deletion examples/swatch-grid/swatch_grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ def main():
# Cycles. Witness the inversion for real: the OTHER era's id must be rejected
# 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'
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
try:
sc.render.engine = wrong
print(f"ERROR: wrong-era EEVEE id '{wrong}' was accepted by this build — "
Expand Down
2 changes: 1 addition & 1 deletion examples/turntable/turntable.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ def main():
# the EEVEE-id mapping is asserted regardless of whether we render: the
# OTHER era's id must be rejected by this build, the helper's accepted
eid = get_eevee_engine_id()
wrong = 'BLENDER_EEVEE_NEXT' if bpy.app.version >= (5, 0, 0) else 'BLENDER_EEVEE'
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
try:
bpy.context.scene.render.engine = wrong
print(f"ERROR: wrong-era EEVEE id '{wrong}' was accepted", file=sys.stderr); return 5
Expand Down
52 changes: 52 additions & 0 deletions showcase/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,18 @@ entry in `showcase/gallery.json`, and a rendered still.
A budget with no falsifier witnesses nothing:
prove each one fails once, and check the exit code, not just
non-zero.
- **A falsifier must fail the budget it targets.** Declare the target in
the piece README's falsifier table — flag, target budget, exit code —
and make the run exit *that* code. A falsifier that trips an earlier
check is red for the wrong reason and proves nothing about its target.
Fix a collision by changing the model or the falsifier; never widen a
band so an ill-aimed falsifier lands. `stone-archway`'s `--flat-arch`
laid the voussoirs as a lintel, which is 0.62 m shorter and so failed
the bounding box (exit 8) instead of the intrados circle fit (exit 19);
`--off-circle` replaced it by keeping the whole envelope and wandering
only the radius. `tests/check_falsifier_targets.py` checks the
declaration statically, and with `--run BLENDER` executes each falsifier
and asserts the observed exit matches.
- **Hygiene budgets.** Copied combinatorics from
`examples/mesh-hygiene-audit` (do not import the example). Every piece
asserts on the generated mesh: non-manifold edges 0, loose verts 0,
Expand Down Expand Up @@ -164,6 +176,46 @@ entry in `showcase/gallery.json`, and a rendered still.
`measure_framing_deviation` and assert at the call site. Do not move
or modify `gallery_framing.py` — import it by resolving the repo root
(see the shipping-crate script).
- **Contact sheet (required).** Composite the candidate hero beside the
pinned calibration set — canonical membership is in `CLAUDE.md`
§ Quality Gates — commit it as
`docs/gallery/contact-sheets/<name>-contact-sheet.webp`, link it in the
PR body, and report a per-criterion verdict: stage darkness, wedge
warmth, subject fill, saturation, thumbnail legibility, plus mean
luminance against the calibration band. A claim without the committed
composite is not evidence.

Required because it has 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 rather than the warm pool the house
style calls for; both were relit as a result. Nothing else in the
pipeline looks at the still beside its peers, so nothing else could
have seen it.

- **Asset sheet (required).** Render the hero alone — neutral
three-quarter view, plain studio lighting, no staging tricks, no
labels, no comparison props — composite it beside the pinned
asset-quality reference set rendered the same way, commit under
`docs/gallery/asset-sheets/`, and report a verdict. The piece ships
only if it is not identifiable as the least-designed object in that
lineup.

Required because showcase is *entirely* game props, which is exactly
the scope `docs/VISUAL-STYLE.md` § Asset quality names, and because it
covers something no other gate here does. Budgets measure geometry
conformance; the contact sheet measures staged presentation. Neither
removes the scene, and a strong scene carries a weak model. The
recorded evidence is `socket-attach-points`: it passed every
measurable floor on its first draft — `edge90` 0.000, ten materials,
no default datablock names — and was then judged bad by eye and
rebuilt from scratch. The floors scored the bevels, not the design.

`examples/gallery_asset_quality.check_asset_quality` returns **11** on
violation, the same call pattern as `gallery_framing`. Showcase
numbering already spends 11 on the collider-triangle ceiling, so remap
the return at the call site rather than letting two budgets share a
code.

- **Composition.** The README names which shipped skills and snippets the
piece composes. Duplicated helpers stay inlined or copied; showcase
scripts do not import snippets as a package.
Expand Down
9 changes: 8 additions & 1 deletion showcase/iron-cauldron/iron_cauldron.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,14 @@


def eevee_engine_id():
return "BLENDER_EEVEE_NEXT" if bpy.app.version >= (4, 2, 0) else "BLENDER_EEVEE"
"""EEVEE id: 'BLENDER_EEVEE' on 5.0+, 'BLENDER_EEVEE_NEXT' on 4.2-4.5.

Mapping matches examples/swatch-grid.get_eevee_engine_id, which asserts it
against the running build (copied, not imported). Inherited the inverted
form from showcase/shipping-crate; it returned 'BLENDER_EEVEE_NEXT' on 5.x,
where that id does not exist.
"""
return "BLENDER_EEVEE" if bpy.app.version >= (5, 0, 0) else "BLENDER_EEVEE_NEXT"


def fail(msg, code):
Expand Down
9 changes: 8 additions & 1 deletion showcase/shipping-crate/shipping_crate.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,14 @@


def eevee_engine_id():
return "BLENDER_EEVEE_NEXT" if bpy.app.version >= (4, 2, 0) else "BLENDER_EEVEE"
"""EEVEE id: 'BLENDER_EEVEE' on 5.0+, 'BLENDER_EEVEE_NEXT' on 4.2-4.5.

Mapping matches examples/swatch-grid.get_eevee_engine_id, which asserts it
against the running build (copied, not imported). The old form here keyed
on >= (4, 2, 0) and so returned 'BLENDER_EEVEE_NEXT' on 5.x, where that id
does not exist — the render path raised TypeError on 5.1 and 5.2.
"""
return "BLENDER_EEVEE" if bpy.app.version >= (5, 0, 0) else "BLENDER_EEVEE_NEXT"


def fail(msg, code):
Expand Down
Loading
Loading