Skip to content

Commit 6697af7

Browse files
TMHSDigitalclaude
andcommitted
refactor: define the gallery reference sets once in scripts/gallery_refs.py
asset_sheet.py regex-parsed its reference set out of CLAUDE.md prose (and its gate regex-extracted that regex from the script's source), so rewording one sentence broke the tool and silently emptied the gate's list; contact_sheet.py hard-coded its set while CLAUDE.md called itself canonical. All three now read CONTACT_SET / ASSET_SET from a stdlib module, and a new Validate check fails when the CLAUDE.md bullets name a different set (falsified: dropping or swapping a name, or removing the bullet, fails it; rewording does not). Closes #523 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LpkeXRvikhJZSVGBFM91Dv Signed-off-by: TM Hospitality Strategies <TMHospitalityStrategies@gmail.com>
1 parent 3ca08a3 commit 6697af7

8 files changed

Lines changed: 107 additions & 23 deletions

File tree

‎.github/workflows/validate.yml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ jobs:
9494
- name: Check asset_sheet.py can render every showcase piece and reference
9595
run: python3 tests/check_asset_sheet_select.py
9696

97+
- name: Check CLAUDE.md names the reference sets in scripts/gallery_refs.py
98+
run: python3 tests/check_reference_sets.py
99+
97100
- name: Check falsifiers declare the budget they target
98101
run: python3 tests/check_falsifier_targets.py
99102

‎CLAUDE.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,8 +173,8 @@ Stage with **explicit paths only** — never `git add -A` or `git add .`. Cursor
173173

174174
- `docs/VISUAL-STYLE.md` is the **binding** render standard; deviations are defects.
175175
- **Framing gate:** Layer 1 framing is measured, not eyeballed — the example's `--output` render path calls the shared helper `examples/gallery_framing.py` (`check_framing`, exit 10 on violation) before writing the still: hero fill 0.70–0.90 of frame in at least one axis, every element that matters clearing all four edges by ≥ 0.02. The check-only path never invokes it, so smoke runtimes are unaffected. The helper measures; call sites enforce. Bleed compositions call `measure_framing_deviation` and assert their own cap at the call site — `check_framing` has no `deviation=` flag.
176-
- **Contact-sheet gate:** composite the candidate hero beside the pinned calibration set — currently `armature-bend`, `damped-track-aim`, `bmesh-gear` — commit the composite under `docs/gallery/contact-sheets/`, link it in the PR body, and report per-criterion verdicts (stage darkness, wedge warmth, subject fill, saturation, thumbnail legibility) including mean luminance versus the calibration images. A claim without the committed composite is not acceptable evidence. Re-rendering a hero means regenerating its sheet: `tests/check_contact_sheets.py` (Validate) fails when a sheet's candidate tile no longer matches the hero that ships (#487), and an asset-type piece's asset sheet must be re-rendered in the same commit. **This list is the canonical home of the pinned set** — update it here when a new example outclasses a member; `docs/new-example-prompt.md` points here rather than naming members. The longer "calibration references" list in `docs/VISUAL-STYLE.md` is a style reference, not this contact-sheet set. Tooling: `python scripts/render_hero.py --blender PATH --only NAME` renders the README `--output` command and writes the hero, its 640 card variant, and the preview webps (the one committed PNG-to-webp path); `python scripts/contact_sheet.py NAME` builds the composite in the committed format and prints each tile's luma, stage, wedge warmth and saturation; `python scripts/measure_hero_drift.py` checks committed heroes against what the code renders now.
177-
- **Asset-sheet gate (asset-type examples — game props/kits):** composite the hero asset rendered alone (neutral three-quarter view, plain studio lighting, no staging tricks, no labels, no comparison props) beside the pinned asset-quality reference set — currently `collision-hull-proxy`, `socket-attach-points`, `vertex-color-ao`, `wheelbarrow`, `apothecary-shelf` — rendered the same way; commit under `docs/gallery/asset-sheets/`, link it in the PR body, and report a verdict. The asset ships only if it is not identifiable as the least-designed object in that lineup — a strong scene can carry a weak model; this gate removes the scene. **This list is the canonical home of the reference set** — update it here when a new asset outclasses a member. A reference may be an example or a showcase piece. Tooling: `python scripts/asset_sheet.py --blender PATH NAME` renders the candidate and this set (read from this line) through `scripts/asset_sheet_panel.py` and writes the composite. The measurable floors behind the gate (naming, material variation, edge treatment) live in `examples/gallery_asset_quality.py` — render path only, same call pattern as `gallery_framing`, exit 11 on violation — with the calibration table and dropped-floor evidence in `docs/VISUAL-STYLE.md` § Asset quality.
176+
- **Contact-sheet gate:** composite the candidate hero beside the pinned calibration set — currently `armature-bend`, `damped-track-aim`, `bmesh-gear` — commit the composite under `docs/gallery/contact-sheets/`, link it in the PR body, and report per-criterion verdicts (stage darkness, wedge warmth, subject fill, saturation, thumbnail legibility) including mean luminance versus the calibration images. A claim without the committed composite is not acceptable evidence. Re-rendering a hero means regenerating its sheet: `tests/check_contact_sheets.py` (Validate) fails when a sheet's candidate tile no longer matches the hero that ships (#487), and an asset-type piece's asset sheet must be re-rendered in the same commit. **The set lives in `scripts/gallery_refs.py` (`CONTACT_SET`), which the tooling reads; this bullet mirrors it** — when a new example outclasses a member, change both in one commit (`tests/check_reference_sets.py` fails on drift); `docs/new-example-prompt.md` points here rather than naming members. The longer "calibration references" list in `docs/VISUAL-STYLE.md` is a style reference, not this contact-sheet set. Tooling: `python scripts/render_hero.py --blender PATH --only NAME` renders the README `--output` command and writes the hero, its 640 card variant, and the preview webps (the one committed PNG-to-webp path); `python scripts/contact_sheet.py NAME` builds the composite in the committed format and prints each tile's luma, stage, wedge warmth and saturation; `python scripts/measure_hero_drift.py` checks committed heroes against what the code renders now.
177+
- **Asset-sheet gate (asset-type examples — game props/kits):** composite the hero asset rendered alone (neutral three-quarter view, plain studio lighting, no staging tricks, no labels, no comparison props) beside the pinned asset-quality reference set — currently `collision-hull-proxy`, `socket-attach-points`, `vertex-color-ao`, `wheelbarrow`, `apothecary-shelf` — rendered the same way; commit under `docs/gallery/asset-sheets/`, link it in the PR body, and report a verdict. The asset ships only if it is not identifiable as the least-designed object in that lineup — a strong scene can carry a weak model; this gate removes the scene. **The set lives in `scripts/gallery_refs.py` (`ASSET_SET`), which `scripts/asset_sheet.py` reads; this bullet mirrors it** — when a new asset outclasses a member, change both in one commit (`tests/check_reference_sets.py` fails on drift). A reference may be an example or a showcase piece. Tooling: `python scripts/asset_sheet.py --blender PATH NAME` renders the candidate and this set (read from `scripts/gallery_refs.py`) through `scripts/asset_sheet_panel.py` and writes the composite. The measurable floors behind the gate (naming, material variation, edge treatment) live in `examples/gallery_asset_quality.py` — render path only, same call pattern as `gallery_framing`, exit 11 on violation — with the calibration table and dropped-floor evidence in `docs/VISUAL-STYLE.md` § Asset quality.
178178
- **Falsification:** every check must be proven to fail once — break the contract, observe the non-zero exit, restore — with the probe and the measured error reported in the PR body. An assertion that cannot fail witnesses nothing.
179179
- **After gallery regeneration** (`python scripts/build_gallery.py`), read the **generated HTML** character by character — the `<img alt>` text and witnesses callouts in `docs/gallery/index.html` and `docs/gallery/<name>/index.html` — not just `examples/gallery.json`. Precedent: the `teaches.split(".")[0]` bug truncated 14/21 card alts at dotted API paths like `bmesh.ops` while the source JSON looked fine (fixed in PR #68).
180180
- **Playwright gallery captures:** gallery `<img>` tags lazy-load, so force them first (`document.querySelectorAll('img').forEach(i => i.loading = 'eager')`, then wait). **Scroll the target card into view and take a viewport capture** — `scrollIntoView({block:'center', behavior:'instant'})`, short wait, `browser_take_screenshot` with `fullPage` omitted. A `fullPage` capture is NOT a workaround: on a tall gallery page it renders every card image blank even when the images are verified loaded (`complete === true`, `naturalWidth === 1280`, `opacity === 1`) — measured on the 45-card grid at 1425x4516. Verify load state via `browser_evaluate` rather than trusting the pixels.

‎docs/new-example-prompt.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ per-criterion verdicts including mean luminance of the candidate versus each
153153
calibration image. The image ships only if it is not sortable as the odd one out in
154154
that lineup—not merely "looks fine alone." A claim without the committed composite
155155
is not acceptable evidence. When a new example outclasses a member, update the
156-
pinned set in `CLAUDE.md`, its canonical home. One successful render command is not
156+
pinned set in `scripts/gallery_refs.py` and the `CLAUDE.md` bullet that names it. One successful render command is not
157157
proof that the image is good, and neither is the second.
158158

159159
Asset-sheet gate (required when the subject is a game asset: a prop, kit piece or

‎scripts/asset_sheet.py‎

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,14 @@
3333

3434
import argparse
3535
import os
36-
import re
3736
import subprocess
3837
import sys
3938
import tempfile
4039
from pathlib import Path
4140

4241
from PIL import Image
4342

43+
from gallery_refs import ASSET_SET
4444
from measure_hero_drift import REPO, entries, render_args
4545

4646
PANEL = (640, 360)
@@ -159,16 +159,9 @@
159159
"wooden-bucket": (r"^BucketLow$", None),
160160
}
161161

162-
_REF_LINE = re.compile(r"Asset-sheet gate.*?reference set — currently (.+?) — rendered", re.S)
163-
164-
165162
def reference_set() -> list[str]:
166-
"""The pinned reference set, parsed from its canonical home in CLAUDE.md."""
167-
text = (REPO / "CLAUDE.md").read_text(encoding="utf-8")
168-
m = _REF_LINE.search(text)
169-
if not m:
170-
raise SystemExit("could not find the asset-sheet reference set in CLAUDE.md")
171-
return re.findall(r"`([a-z0-9-]+)`", m.group(1))
163+
"""The pinned reference set (scripts/gallery_refs.py; CLAUDE.md mirrors it, #523)."""
164+
return list(ASSET_SET)
172165

173166

174167
def render_panel(blender: str, entry: dict, png: Path) -> bool:

‎scripts/contact_sheet.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@
2727
import numpy as np
2828
from PIL import Image, ImageDraw
2929

30+
from gallery_refs import CONTACT_SET as CALIBRATION # the one pinned set (#523)
3031
from measure_hero_drift import REPO, entries
3132

32-
CALIBRATION = ("armature-bend", "damped-track-aim", "bmesh-gear")
3333
TILE = (640, 360)
3434
GAP, CAPTION = 10, 32
3535
BG = (22, 23, 26)

‎scripts/gallery_refs.py‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""The pinned gallery reference sets, defined once (#523). Stdlib only.
2+
3+
CONTACT_SET is the contact-sheet calibration set (scripts/contact_sheet.py
4+
composites a candidate hero beside it). ASSET_SET is the asset-quality
5+
reference lineup (scripts/asset_sheet.py renders the candidate beside it, and
6+
tests/check_asset_sheet_select.py requires a SELECT row for each name).
7+
8+
CLAUDE.md § Quality Gates names both sets for readers; it mirrors this file and
9+
tests/check_reference_sets.py fails when the two disagree. To change a set,
10+
edit it here and in that CLAUDE.md bullet in the same commit.
11+
"""
12+
13+
CONTACT_SET = ("armature-bend", "damped-track-aim", "bmesh-gear")
14+
15+
ASSET_SET = (
16+
"collision-hull-proxy",
17+
"socket-attach-points",
18+
"vertex-color-ao",
19+
"wheelbarrow",
20+
"apothecary-shelf",
21+
)

‎tests/check_asset_sheet_select.py‎

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,15 @@
77
that way. This reads SELECT with ast (no Pillow needed) and requires a row for:
88
99
- every piece in showcase/gallery.json, and
10-
- every name in the CLAUDE.md asset-quality reference set (read the same way
11-
asset_sheet.reference_set() reads it).
10+
- every name in the asset-quality reference set (scripts/gallery_refs.py, the
11+
same tuple asset_sheet.reference_set() returns).
1212
1313
python tests/check_asset_sheet_select.py (exit 0 ok, 1 on a gap)
1414
"""
1515
from __future__ import annotations
1616

1717
import ast
1818
import json
19-
import re
2019
import sys
2120
from pathlib import Path
2221

@@ -33,10 +32,9 @@ def select_keys(root: Path) -> set[str]:
3332

3433

3534
def reference_set(root: Path) -> list[str]:
36-
src = (root / "scripts" / "asset_sheet.py").read_text(encoding="utf-8")
37-
pattern = re.search(r'_REF_LINE = re\.compile\(r"(.+?)", re\.S\)', src).group(1)
38-
m = re.search(pattern, (root / "CLAUDE.md").read_text(encoding="utf-8"), re.S)
39-
return re.findall(r"`([a-z0-9-]+)`", m.group(1)) if m else []
35+
sys.path.insert(0, str(root / "scripts"))
36+
from gallery_refs import ASSET_SET
37+
return list(ASSET_SET)
4038

4139

4240
def check(root: Path = ROOT) -> list[str]:
@@ -46,10 +44,10 @@ def check(root: Path = ROOT) -> list[str]:
4644
refs = reference_set(root)
4745
errors = [f"showcase piece {n!r} has no SELECT row in scripts/asset_sheet.py"
4846
for n in pieces if n not in keys]
49-
errors += [f"asset-sheet reference {n!r} (CLAUDE.md) has no SELECT row"
47+
errors += [f"asset-sheet reference {n!r} (scripts/gallery_refs.py) has no SELECT row"
5048
for n in refs if n not in keys]
5149
if not refs:
52-
errors.append("could not read the asset-sheet reference set from CLAUDE.md")
50+
errors.append("scripts/gallery_refs.py ASSET_SET is empty")
5351
return errors
5452

5553

‎tests/check_reference_sets.py‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#!/usr/bin/env python3
2+
"""CLAUDE.md names the same gallery reference sets as scripts/gallery_refs.py (#523).
3+
4+
The sets used to be parsed out of CLAUDE.md prose by scripts/asset_sheet.py
5+
(rewording one sentence broke the tool and emptied a gate's list) or hard-coded
6+
in scripts/contact_sheet.py while CLAUDE.md claimed to be canonical. The code
7+
now reads scripts/gallery_refs.py; this check keeps the CLAUDE.md bullets an
8+
accurate mirror. It fails loudly when a bullet cannot be found, never passes on
9+
an empty read.
10+
11+
python tests/check_reference_sets.py (exit 0 ok, 1 on drift)
12+
"""
13+
from __future__ import annotations
14+
15+
import re
16+
import sys
17+
from pathlib import Path
18+
19+
ROOT = Path(__file__).resolve().parent.parent
20+
sys.path.insert(0, str(ROOT / "scripts"))
21+
from gallery_refs import ASSET_SET, CONTACT_SET # noqa: E402
22+
23+
# Bullet label in CLAUDE.md -> the set it must name.
24+
BULLETS = {
25+
"Contact-sheet gate": CONTACT_SET,
26+
"Asset-sheet gate": ASSET_SET,
27+
}
28+
29+
30+
def bullet(text: str, label: str) -> str | None:
31+
"""The CLAUDE.md list item whose bold label starts with `label`."""
32+
m = re.search(rf"^- \*\*{re.escape(label)}[^\n]*(?:\n(?!- |\n|#)[^\n]*)*", text, re.M)
33+
return m.group(0) if m else None
34+
35+
36+
def named_set(item: str) -> list[str]:
37+
"""Backticked names after 'currently', up to the end of that sentence."""
38+
m = re.search(r"currently(.+?)(?:\.\s|;|\s—\s(?![`]))", item, re.S)
39+
span = m.group(1) if m else ""
40+
return re.findall(r"`([a-z0-9-]+)`", span)
41+
42+
43+
def check(root: Path = ROOT) -> list[str]:
44+
text = (root / "CLAUDE.md").read_text(encoding="utf-8")
45+
errors = []
46+
for label, want in BULLETS.items():
47+
item = bullet(text, label)
48+
if item is None:
49+
errors.append(f"CLAUDE.md has no '- **{label}' bullet")
50+
continue
51+
got = named_set(item)
52+
if not got:
53+
errors.append(f"CLAUDE.md '{label}' bullet names no set after 'currently'")
54+
elif set(got) != set(want):
55+
errors.append(f"CLAUDE.md '{label}' names {got}, scripts/gallery_refs.py has {list(want)}")
56+
return errors
57+
58+
59+
def main() -> int:
60+
errors = check()
61+
for e in errors:
62+
print(f"::error::{e}", file=sys.stderr)
63+
if not errors:
64+
print(f"reference sets match CLAUDE.md: contact {len(CONTACT_SET)}, asset {len(ASSET_SET)}")
65+
return 1 if errors else 0
66+
67+
68+
if __name__ == "__main__":
69+
sys.exit(main())

0 commit comments

Comments
 (0)