Repository navigation
feat(design): DS-H-01 sprite format, generator and pixel scale - #1637
Conversation
Five failing tests for the probe hall sprite pipeline (ASSETS.md §1-§3): integer device pixel scale at 125/150 %, committed atlas equals the generator output, 32 x 32 figure frames with palette characters only, delta frames, and one MIT provenance entry per raster source. The stubs only make them compile. Test-First: src/design/hall/pixelScale.test.ts::picks an integer device pixel scale at 125 and 150 percent Test-First: src/design/hall/sprites.test.ts::generated atlas equals the committed atlas Test-First: src/design/hall/sprites.test.ts::every frame is 32 by 32 and uses only palette entries Test-First: src/design/hall/sprites.test.ts::a delta frame replaces only its rows of the base frame Test-First: src/design/hall/sprites.test.ts::provenance lists every raster source once under MIT Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Text rasters in src/design/hall/<sheet>/*.px.ts (one palette character per pixel, delta frames for poses) become indexed PNG atlases per theme plus one frame map per sheet. scripts/dev/build-sprites.mjs uses Node built-ins only, resolves the palette from tokens.css (:root and :root[data-theme="dark"]) and enforces the ASSETS.md frame sizes, id form, colour count and size budget. usePixelScale picks a whole number of device pixels per art pixel at any devicePixelRatio. One placeholder Planung frame with its MIT provenance entry exercises the pipeline. Test-First: src/design/hall/pixelScale.test.ts::picks an integer device pixel scale at 125 and 150 percent Test-First: src/design/hall/sprites.test.ts::generated atlas equals the committed atlas Test-First: src/design/hall/sprites.test.ts::every frame is 32 by 32 and uses only palette entries Test-First: src/design/hall/sprites.test.ts::a delta frame replaces only its rows of the base frame Test-First: src/design/hall/sprites.test.ts::provenance lists every raster source once under MIT Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0c47fe55b3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // §1 and §5: frame size (figures and tiles exactly, things in whole multiples) and atlas limit. | ||
| const SHEETS = { figures: [32, true, 256, 192], tiles: [16, true, 128, 128], things: [16, false, 128, 128] }; |
There was a problem hiding this comment.
Enforce 16×16 work-piece frames
When a piece-* source is added under things, this sheet-wide exact: false rule accepts any multiple of 16, so a 32×32 work piece passes generation even though docs/design/hall/ASSETS.md:18-19 requires work pieces to be exactly 16×16 and reserves larger dimensions for stations, gates, and props. Derive the size rule from the frame-id kind rather than applying one rule to every entry in the sheet.
Useful? React with 👍 / 👎.
| // A `from` that is unknown or comes later yields an empty frame, which the size checks reject. | ||
| out[id] = "from" in frame ? (out[frame.from] ?? []).map((row, y) => frame.rows[y] ?? row) : frame; |
There was a problem hiding this comment.
Reject delta rows outside the base raster
If a 32-row pose accidentally specifies a replacement such as { 32: "..." } (or a negative index), this map only visits base rows 0–31 and silently discards the replacement. The expanded frame still has valid dimensions and palette characters, so the generator and committed-atlas test pass while the pose renders identically to its base; validate every replacement index against the referenced raster before expanding it.
Useful? React with 👍 / 👎.
| // §1 and §5: frame size (figures and tiles exactly, things in whole multiples) and atlas limit. | ||
| const SHEETS = { figures: [32, true, 256, 192], tiles: [16, true, 128, 128], things: [16, false, 128, 128] }; | ||
| const MAX_PNG = 24 * 1024; | ||
| const ID = /^(tile|fig|piece|station|gate|prop)-[a-z0-9]+(-[a-z]+)?-\d+$/; |
There was a problem hiding this comment.
Validate figure roles and poses
When DS-H-03 adds the real figures, IDs such as fig-plna-run-0 or fig-plan-running-0 satisfy this regex and are emitted into the atlas even though docs/design/hall/ASSETS.md:122-125 restricts roles to plan|build|check|ops and poses to the six StateForm values plus unknown|ready. Such a typo produces a frame that the state-to-pose mapping cannot address while the generator remains green, so parse fig IDs and reject unknown roles or poses.
Useful? React with 👍 / 👎.
| const entries: { file: string; license: string }[] = JSON.parse(readFileSync(join(atlasDir, "provenance.json"), "utf8")); | ||
| // Equality with the glob also proves every listed file exists. | ||
| expect(entries.map((e) => e.file).sort()).toEqual(Object.keys(sources).map((p) => `src/design/hall/${p.slice(2)}`).sort()); | ||
| for (const e of entries) expect(e.license, e.file).toBe("MIT"); |
There was a problem hiding this comment.
Validate all required provenance fields
When later art packages add a provenance object containing only file and license, this test passes even though docs/design/hall/ASSETS.md:97-115 also requires package, date, drawnBy, method, and references, with constrained values for the latter fields. That leaves the original-art and attribution record incomplete while the provenance gate is green; validate the complete object shape and the documented value constraints for every entry.
Useful? React with 👍 / 👎.
|
Root auto-queue: head 0c47fe5 unchanged since review-ok, all checks green. @Mergifyio queue |
|
Root queue (head 0c47fe5, review-ok, required checks pass): @Mergifyio queue |
Merge Queue Status
This pull request spent 12 minutes 7 seconds in the queue, including 11 minutes 23 seconds running CI. Required conditions to merge
|
Die Probe-Halle hat jetzt ein Sprite-Format: Figuren und Kacheln werden als Textraster im Repo gezeichnet, und ein Skript macht daraus pro Farbschema (hell und dunkel) ein Bild mit den Farben aus den Design-Tokens. Dazu kommt eine kleine Funktion, die die Pixelgröße so wählt, dass jedes Kunstpixel bei 100, 125 und 150 % Windows-Skalierung eine ganze Zahl an Bildschirmpixeln breit ist und nichts verschwimmt. Du musst nichts tun; die echten Figuren und Kacheln zeichnen erst DS-H-02 und DS-H-03, hier gibt es nur eine Platzhalterfigur zum Testen.
Report
Plan-ID: DS-H-01
Package: DS-H-01 · Review tier: B (frontend TS + dev script, no seam; AGENTS.md rule 5)
Goal / benefit: a sprite format and deterministic generator that turn code-drawn rasters into committed atlases, plus a hook for an integer device-pixel scale, so DS-H-02/03 can draw tiles and figures · Parent / ticket / owner: DS-FABRIK-ZIEL, cut report §3.4 row DS-H-01, PLAN row DS-H-01, Root dispatches
Native evidence: this server worker has no harness goal function (no goal tool offered); ticket goal = the DS-H-01 brief and the PLAN row.
Scope / limits / dependencies / acceptance: contract
docs/design/hall/ASSETS.md(DS-H-00, #1633); no seam, no screen, no route, no new dependency (Node built-ins only); ≤ 300 lines incl. tests and generated files.What changed
src/design/hall/sprites/format.ts: the format. A raster is one string per row, one character per pixel; its position inPIXELS(.123456789abcdef) is the palette index,.= 0 = transparent.PALETTEbinds index → token exactly as ASSETS §2 (--g-ink…--g-sw-off; no state token). A frame is a full raster or a delta{ from, rows }that replaces whole rows of an earlier frame (ASSETS §5: poses stored againstready).expand()resolves deltas.scripts/dev/build-sprites.mjs(Nodefs/path/url/zlibonly): reads everysrc/design/hall/<sheet>/*.px.ts(figures,tiles,things) via Node type stripping, resolves the palette fromtokens.cssfor:root(light) and:root[data-theme="dark"]and rejects any token that is not opaque hex. It validates id form (§4), frame size (figures 32×32, tiles 16×16, things multiples of 16; §1), palette characters, ≤ 8 colours per figure frame (§2), atlas limits and ≤ 24 KiB per PNG (§5). It writeshall-<sheet>-light.png,hall-<sheet>-dark.png(PNG type 3, 4 bit, palette in §2 order,tRNSmakes only index 0 transparent) and one frame maphall-<sheet>.json(id: [x, y, w, h], one line per frame). Deflate usesZ_RLE, so the bytes do not depend on zlib's hash-chain search;--out <dir>writes elsewhere (used by the test).src/design/hall/usePixelScale.ts:pixelScale(dpr, target = 2)returnsdevice=round(target × dpr)whole device pixels per art pixel andcss=device / dpr(100 % → 2 / 2 px, 125 % → 3 / 2.4 px, 150 % → 3 / 2 px).usePixelScale()followsdevicePixelRatiovia a(resolution:…dppx)media query that re-arms on each change.src/design/hall/figures/plan.px.ts, one placeholder framefig-plan-ready-0(5 colours; DS-H-03 redraws it), its entry insrc/design/hall/sprites/provenance.json(agent:claude, hand-placed text raster, references none, MIT), and the generatedhall-figures-light.png,hall-figures-dark.png(279 bytes each, 32×32) andhall-figures.json.docs/design/hall/ASSETS.md(rule change, said here as §0 of that file requires): §4 names the raster source locationsrc/design/hall/<sheet>/<name>.px.ts; the §3 provenance example path becameplan.px.ts. Reason: a fixed suffix lets generator, frame test and provenance test find exactly the raster files without importing components or helpers that DS-H-03 may put next to them.scripts/contrast-check.mjsunchanged: the pair--g-ink/--g-groundis already checked (light 15.09:1, dark 15.85:1, above the 3:1 that §2 asks for).docs/PLAN.md: only the DS-H-01 row status → "Erledigt: feat(design): DS-H-01 sprite format, generator and pixel scale #1637; …" (commit0c47fe55).Red → green (red commit
82748406, tests plus compiling stubs; green commita633d084):src/design/hall/pixelScale.test.ts::picks an integer device pixel scale at 125 and 150 percentsrc/design/hall/sprites.test.ts::generated atlas equals the committed atlassrc/design/hall/sprites.test.ts::every frame is 32 by 32 and uses only palette entriesexpand)src/design/hall/sprites.test.ts::a delta frame replaces only its rows of the base frameexpand)src/design/hall/sprites.test.ts::provenance lists every raster source once under MITVisual check: the light and dark atlases were decoded with an independent decoder (Python
zlib, no shared code), compared pixel by pixel with the source raster (match in both themes) and viewed at 8× on the floor colour of each theme. Light reads as intended. Observation for DS-H-03 / DS-H-07: in the dark theme the §2 binding inverts tones:--g-av3(face) becomes dark brown (#3D3122) and--g-ink2(hair) light grey, so figures drawn for light look like negatives in dark. That follows from the contract, not from this generator; whoever draws the real figures (or adds--g-px-*tokens) decides.Prompt-Log
src/designtrippedsrc/i18n/hardcoded.test.ts; the delta error became an empty frame that the size check rejects, and the media query lost its space. Fixed before the first push (amend of the unpushed commit).Evidence
Base
6342a9fa(origin/main after #1633 at start), head0c47fe55(git ls-remote origin claude/ds-h-01→0c47fe55b3330e7e22194933769bda8e8ae9fa64). Diff: 12 files, +272/−2 (274 changed lines ≤ 300; the two PNGs are binary, 279 bytes each).npm run dev:start-check(RAM 12.9 GB free, 0 cargo builds)npx vitest run src/design/hallat red commit82748406(5 failed)npx vitest run src/design/hall src/i18n/hardcoded.test.ts(20 passed)npm run typechecknode scripts/contrast-check.mjs(740 pairs)node scripts/dev/build-sprites.mjs, thengit status --short(0 lines: atlas reproduced byte-identically)BASE_SHA=$(git merge-base origin/main HEAD) HEAD_SHA=$(git rev-parse HEAD) bash scripts/ci/red-first.sh(5 tests missing at base, green at head)bash scripts/ci/secret-scan.shbefore each commitbash scripts/ci/gates.sh lane prepushata633d084(28 gates green)bash scripts/ci/gates.sh lane prepushat final head0c47fe55(28 gates green)node scripts/plan-check.mjs --offline(0 Befunde)PA_PREPUSH=light git pushblocked once at the first attempt (fe-test red:hardcoded.test.ts, see Prompt-Log); nothing reached the remotePA_PREPUSH=light git push -u origin claude/ds-h-01after the fix, verified withgit ls-remoteNICHT ABGEDECKT
#[cfg(windows)]Rust tests and the Windows arms of clippy did not run (Linux server); the Windows lane runs in the merge queue. This package has no Rust change.sprites.test.tsruns there (Linux CI lane, Windows merge-queue lane).Z_RLEavoids the match-hash search; that this makes zlib output identical across builds is an expectation, not a measurement.usePixelScalere-arming on a real monitor or scale change is not tested in a browser (jsdom has nomatchMedia); the test covers the scale choice at 100/125/150 %. Real 125/150 % rendering is a DS-H-07 screenshot case (--dpr 1.25,--dpr 1.5).linuxlane: browser smoke, frontend build and workflow gates run in CI.Reviews and disposition
Open points
main; next owner Root (reviews, ready, labels, queue). DS-H-02a and DS-H-03 can start on this format after merge.🤖 Generated with Claude Code on the ProjectA server worker
Root acceptance (16.10.2026 ~15:10 MESZ)
scripts/dev/build-sprites.mjs:18imports.tsfrom Node (type stripping needs Node >= 23.6)package.jsonengines.node ">=24"and every CI workflow usesnode-version: 24getServerSnapshot; brittle:root {lookup; nomkdirSyncfor--out; dynamic import of committed*.px.ts--outwithout value; DPR change subscription untested