Skip to content

fix: close checks that could pass without asserting anything, plus gallery-infrastructure debt#120

Merged
TMHSDigital merged 5 commits into
mainfrom
fix/vacuous-check-audit
Jul 25, 2026
Merged

fix: close checks that could pass without asserting anything, plus gallery-infrastructure debt#120
TMHSDigital merged 5 commits into
mainfrom
fix/vacuous-check-audit

Conversation

@TMHSDigital

Copy link
Copy Markdown
Owner

Consolidation pass. Six items, ordered by severity. Item 1 is the reason for the PR.

Runtime: .scratch/blender-5.1.2-windows-x64/blender.exe reports 5.1.2;
.scratch/blender-4.5.11-windows-x64/blender.exe reports 4.5.11 LTS.
Everything below is a LIVE RUN unless labelled otherwise.

1 — Vacuous-check audit

Two shipped checks could not fail. Both are fixed. A third assertion was
self-referential. Everything else survived.

Method

Three passes, because re-running 44 examples' historical probes by hand would
have been guesswork about what each one once measured:

  1. AST scan of all 44 for patterns that pass vacuously — zip() over
    sequences whose lengths are part of the claim, all()/any() over
    possibly-empty comprehensions, loops whose only effect is a conditional
    failure, max/min with default=. 19 examples carried at least one such
    site (78 total); 25 were clean. Every site was read by hand.
  2. Reachability sweep: for each of 89 module-level constants that gate a
    comparison (derived from the AST, not guessed), push it in the direction
    that must break its check and require a non-zero exit. 84 of 91 mutations
    broke their check.
    (The first run of this sweep was itself wrong — CRLF
    in the job list left a stray carriage return on the direction argument, so
    every run silently took the wrong branch and loosened the tolerance instead
    of tightening it. Re-run after fixing.)
  3. Hand audit of the 7 survivors and of the 12 examples whose thresholds
    are inline literals rather than named constants.

Findings

gltf-export-roundtrip — could not fail. flip_err was initialised to
0.0 and derived from a zip over two accessors whose lengths nothing
asserted. Deleting the entire TEXCOORD_0 attribute:

pre-fix   exit=0   (no ERROR line)
post-fix  exit=11  ERROR: prim0 TEXCOORD_0 count 0 != POSITION count 1296

lightmap-uv-channel — could not fail. It asserted "0 overlapping UV1
islands" without ever establishing that its SAT detector can find one.
Widening SAT_EPS makes every axis read as separated, so the detector reports
zero and the check passes. Fixed with a positive control on hand-built
triangles (overlapping / disjoint / merely touching must give
True/False/False) before the scan is trusted. The SAT_EPS mutation now
exits 7.

socket-attach-points — self-referential assertion. The up-axis rule was
compared against a re-derivation using the same PARALLEL threshold, so both
sides moved together and collapsing the threshold changed nothing. It now
asserts that both branches are actually exercised (2 fallback / 5
Gram-Schmidt); PARALLEL -> -1.0 exits 4.

Hardening — no demonstrated pass-through, but the same defect class:
length guards before zip() in gltf-skin-roundtrip, vertex-weight-limit
and vse-cut-list's near(); population guards in modular-kit-snap,
socket-attach-points and vertex-color-ao, whose checks are built entirely
from per-element loops that assert nothing over an empty collection.

Falsification of every guard added

example probe exit verdict
gltf-export-roundtrip TEXCOORD_0 emptied, then truncated to 5 0 pre-fix -> 11 could not fail -> fixed
lightmap-uv-channel SAT_EPS widened (detector blinded) 0 pre-fix -> 7 could not fail -> fixed
socket-attach-points PARALLEL collapsed to -1.0 0 pre-fix -> 4 self-referential -> fixed
socket-attach-points one socket dropped 6 hardened
gltf-skin-roundtrip JOINTS_0 truncated 7 hardened
vertex-weight-limit evaluated vertex count changed across the prune 7 hardened
vse-cut-list near() called with nothing to compare ValueError hardened
modular-kit-snap one part dropped 11 hardened
vertex-color-ao one part dropped 8 hardened

Explained survivors (not defects)

N_NEEDLES, RINGS, POINTS, BLEND, PUMPER are construction parameters:
they drive both the build and the closed form, so changing one changes both
consistently. That is correct behaviour, not a hole. AREA_EPS / VOL_EPS in
degenerate-bevel-weld and mesh-hygiene-audit are floors a measurement must
exceed, so pushing them down loosens rather than tightens.

Result

44/44 pass on 5.1.2 and 44/44 on 4.5.11 after all changes. No contract was
weakened and no measured closed form changed.

2 — WebP recipe never applied quality

Blender's Image.save() ignores quality and keeps the buffer RGBA. The
recipe in docs/VISUAL-STYLE.md now specifies Pillow, with a size sanity
floor.

asset before after
modular-kit-snap hero 1,146,855 26,356 44x
lightmap-uv-channel hero 984,475 19,884 50x
modular-kit-snap preview 968,096 22,800 42x
lightmap-uv-channel preview 850,867 16,504 52x

Whole gallery: 5,360,979 -> 1,496,230 bytes across 88 images (-72%).
Nothing remains over 200 KB.

Fidelity against the committed originals: mean absolute deviation
0.84–1.07 / 255, max 19–29, under 0.3% of pixels differing by more than
8/255
— ordinary q85 loss. Spot-checked at full size and at card size,
including the lightmap atlas board (thin saturated strips on black, the worst
case in this set). No example was re-rendered.

3 — Framing gate gradient

framing_advice now prints a suggested camera distance factor and per-edge
margin shortfalls. Enforcement behaviour is unchanged.

scene start fill advice after one step
under-filled 0.467 x=0.583 0.817 — in band
over-filled, unsaturated 0.972 x=1.215 0.800 — in band
over-filled, saturated 1.000 x=1.250 AT LEAST 1.000 — still out, exactly as the advice says

The third row is why the qualifier exists: fill saturates at 1.000 once the
silhouette leaves the frame, so the ratio is only a lower bound. The gate says
so rather than implying a one-step fix it cannot deliver.

4 — Playwright guidance was wrong

The documented workaround (full-page capture, crop locally) produces blank
cards on the 44-card grid even with images verified loaded (complete,
naturalWidth 1280, opacity 1). Replaced with force-eager, scroll into
view, viewport capture — and the advice that does not work is called out
rather than quietly dropped.

5 — Asset floors: crash, and status

measure_edge90 called Vector.angle on zero-length normals. Before and
after, on a manifold zero-area face produced by an over-large bevel:

pre-fix   RAISED ValueError: Vector.angle(other): zero length vectors have no valid angle
post-fix  measure_edge90 -> (0.023, 2, 88, 8)     # 8 degenerate edges reported

degenerate_faces now appears in the aq_edge90 line. Status recorded in
VISUAL-STYLE: no floor has ever bound on a shipped asset, and
socket-attach-points' first draft passed all three (edge90 0.000, 10
materials) and was then judged bad by eye and rebuilt from scratch. The floors
are a filter for obvious failures; the asset sheet decides. No floors added or
extended.

6 — Count drift

CLAUDE.md and AGENTS.md said 40 against a real 44, taken from the
manifest. The stale "All forty run headless" phrasing is fixed too.

Also noted

ROADMAP now records that the neutral asset-sheet rig sized its camera from the
bounding-box diagonal, so sheets committed before that fix
(modular-kit-snap, lightmap-uv-channel) contain mis-scaled panels and must
not be cited as evidence. Not re-rendered here.

Verification

  • 44/44 check-only pass on 5.1.2 and on 4.5.11 after all changes.
  • Runtimes unchanged. Full suite 58.5 s (5.1) / 61.6 s (4.5).
    lightmap-uv-channel is the slowest at 21 s — measured 21443 ms at HEAD
    versus 21047 ms on this branch
    , so it is pre-existing and untouched by the
    added control. (An earlier measurement suggested a 29x regression; that was
    an artefact of running the HEAD copy from .scratch/, where its
    gallery_framing import shim cannot resolve. Diagnosed, not assumed.)
  • Gallery regenerated; 8 detail pages changed because they embed example
    source. Generated HTML read directly: new code correctly escaped (<,
    &quot;), zero raw < outside tags, index card set unchanged.
  • Served page verified with the corrected Playwright method at 1440x900 and
    390x844: 44 cards, 0 broken images, both re-encoded heroes at native
    1280x720, no horizontal overflow on mobile.

Labelling

Everything above is a live run on the two binaries named at the top. The one
inference not directly executed: the 25 examples the AST scan found clean of
vacuity patterns were not each given a bespoke historical probe. They are
covered by the constant-mutation sweep and by static analysis, and all 25 pass
on both versions.

A falsification probe on vertex-color-ao exited 0 when it should have failed:
the depsgraph comparison used zip(src.data, eva.data), which truncates to the
shorter sequence, so a resampled attribute read as a clean 0.0 deviation. That
is a direct hit on this repository's central claim, so all 44 examples were
audited for the same class of defect.

Two checks could not fail at all. gltf-export-roundtrip initialised flip_err
to 0.0 and derived it from a zip over two accessors whose lengths nothing
asserted: deleting the entire TEXCOORD_0 attribute still exited 0. And
lightmap-uv-channel asserted "zero overlapping islands" without ever
establishing that its SAT detector can find one - widening SAT_EPS blinds the
detector, and a blind detector reports zero.

The rest are hardening. Pairings whose lengths are part of the claim now
assert those lengths instead of letting zip() quietly shorten the comparison
(gltf-skin-roundtrip, vertex-weight-limit, vse-cut-list). Checks built
entirely from per-element loops now pin their population first, because a loop
over an empty collection reports success (modular-kit-snap,
socket-attach-points, vertex-color-ao). socket-attach-points additionally
asserts that both branches of its up-axis rule are exercised: that assertion
compared the socket basis against a re-derivation using the same threshold, so
on its own it could not notice the threshold being wrong.

Every guard added here was proven to fire, and all 44 checks pass unchanged on
4.5.11 LTS and 5.1.2.

Signed-off-by: fOuttaMyPaint <TMhospitalitystrategies@gmail.com>
Blender's Image.save() ignores its quality argument and keeps the buffer RGBA,
so the documented recipe was writing near-lossless files. Four assets shipped
that way, totalling 3.95 MB; encoded from the same pixels with Pillow they
total 85 KB. Whole-gallery image weight drops from 5.36 MB to 1.50 MB.

Mean absolute deviation from the committed originals is about 1/255 with under
0.3% of pixels differing by more than 8/255 - ordinary q85 loss, including on
the lightmap atlas board, which is the worst case here (thin saturated island
strips on black). No example was re-rendered; this is an encoding change only.

Signed-off-by: fOuttaMyPaint <TMhospitalitystrategies@gmail.com>
The gate printed FAIL without a magnitude, so converging into the band cost a
render per guess - nine of twenty-seven renders in the last example run went
on that. On-screen extent is very nearly inversely proportional to camera
distance, so the measured-to-target fill ratio is a usable one-step
correction, and margin shortfalls can be named per edge.

Enforcement is unchanged; this is diagnostic output. The suggestion is
labelled as a lower bound when the silhouette leaves the frame, because fill
saturates at 1.000 there and the true extent is unknowable from the
measurement.

Signed-off-by: fOuttaMyPaint <TMhospitalitystrategies@gmail.com>
measure_edge90 called Vector.angle on face normals without checking length,
and a zero-area face has a zero-length normal, so an asset with one lost every
other measurement to a ValueError. A degenerate face is a real defect worth
hearing about; the count is now reported in the aq_edge90 line and those edges
are skipped rather than crashing the gate.

Signed-off-by: fOuttaMyPaint <TMhospitalitystrategies@gmail.com>
The WebP recipe told authors to use a Blender call that silently ignores
quality; it now specifies Pillow and gives a size sanity floor.

The Playwright note recommended full-page capture as the workaround for
lazy-loaded gallery images. That does not work: on the 44-card grid a fullPage
capture renders every card blank even with images verified loaded. Replaced
with the method that does work - force eager, scroll into view, viewport
capture - and the failing advice is called out rather than quietly dropped.

The asset floors get an honest status note: no floor has ever bound on a
shipped asset, and one first draft passed all three and was then judged bad by
eye and rebuilt. They are a filter for obvious failures; the asset sheet
decides.

Example counts in CLAUDE.md and AGENTS.md said 40 against a real 44, taken
from the manifest. ROADMAP records that asset sheets committed before the
neutral-rig camera fix used mis-scaled panels and are not comparable.

Signed-off-by: fOuttaMyPaint <TMhospitalitystrategies@gmail.com>
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Jul 25, 2026
@TMHSDigital
TMHSDigital merged commit a827da0 into main Jul 25, 2026
9 checks passed
@TMHSDigital
TMHSDigital deleted the fix/vacuous-check-audit branch July 25, 2026 15:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant