Skip to content

feat(design): DS-H-01 sprite format, generator and pixel scale - #1637

Merged
mergify[bot] merged 3 commits into
mainfrom
claude/ds-h-01
Oct 16, 2026
Merged

mergify[bot] merged 3 commits into
mainfrom
claude/ds-h-01

Conversation

@Cuarroc

@Cuarroc Cuarroc commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

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 in PIXELS (.123456789abcdef) is the palette index, . = 0 = transparent. PALETTE binds 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 against ready). expand() resolves deltas.
  • scripts/dev/build-sprites.mjs (Node fs/path/url/zlib only): reads every src/design/hall/<sheet>/*.px.ts (figures, tiles, things) via Node type stripping, resolves the palette from tokens.css for :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 writes hall-<sheet>-light.png, hall-<sheet>-dark.png (PNG type 3, 4 bit, palette in §2 order, tRNS makes only index 0 transparent) and one frame map hall-<sheet>.json (id: [x, y, w, h], one line per frame). Deflate uses Z_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) returns device = round(target × dpr) whole device pixels per art pixel and css = device / dpr (100 % → 2 / 2 px, 125 % → 3 / 2.4 px, 150 % → 3 / 2 px). usePixelScale() follows devicePixelRatio via a (resolution:…dppx) media query that re-arms on each change.
  • Test sprite: src/design/hall/figures/plan.px.ts, one placeholder frame fig-plan-ready-0 (5 colours; DS-H-03 redraws it), its entry in src/design/hall/sprites/provenance.json (agent:claude, hand-placed text raster, references none, MIT), and the generated hall-figures-light.png, hall-figures-dark.png (279 bytes each, 32×32) and hall-figures.json.
  • docs/design/hall/ASSETS.md (rule change, said here as §0 of that file requires): §4 names the raster source location src/design/hall/<sheet>/<name>.px.ts; the §3 provenance example path became plan.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.mjs unchanged: the pair --g-ink / --g-ground is 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; …" (commit 0c47fe55).

Red → green (red commit 82748406, tests plus compiling stubs; green commit a633d084):

Test at red commit at head
src/design/hall/pixelScale.test.ts::picks an integer device pixel scale at 125 and 150 percent red (naive scale 2.5 device px at 125 %) green
src/design/hall/sprites.test.ts::generated atlas equals the committed atlas red (no generator, no atlas) green
src/design/hall/sprites.test.ts::every frame is 32 by 32 and uses only palette entries red (stub expand) green
src/design/hall/sprites.test.ts::a delta frame replaces only its rows of the base frame red (stub expand) green
src/design/hall/sprites.test.ts::provenance lists every raster source once under MIT red (no provenance file) green

Visual 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

  • rounds: 1 · critic: self · findings: 0 high / 1 other · changed: two developer strings with spaces in src/design tripped src/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), head 0c47fe55 (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).

Command Exit code
npm run dev:start-check (RAM 12.9 GB free, 0 cargo builds) 0
npx vitest run src/design/hall at red commit 82748406 (5 failed) 1 (expected)
npx vitest run src/design/hall src/i18n/hardcoded.test.ts (20 passed) 0
npm run typecheck 0
node scripts/contrast-check.mjs (740 pairs) 0
node scripts/dev/build-sprites.mjs, then git status --short (0 lines: atlas reproduced byte-identically) 0
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) 0
bash scripts/ci/secret-scan.sh before each commit 0
bash scripts/ci/gates.sh lane prepush at a633d084 (28 gates green) 0
bash scripts/ci/gates.sh lane prepush at final head 0c47fe55 (28 gates green) 0
node scripts/plan-check.mjs --offline (0 Befunde) 0
PA_PREPUSH=light git push blocked once at the first attempt (fe-test red: hardcoded.test.ts, see Prompt-Log); nothing reached the remote 1
PA_PREPUSH=light git push -u origin claude/ds-h-01 after the fix, verified with git ls-remote 0

NICHT ABGEDECKT

  • Windows half: the #[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.
  • Byte identity of the atlas on Windows and on the CI runner is only proven when sprites.test.ts runs there (Linux CI lane, Windows merge-queue lane). Z_RLE avoids the match-hash search; that this makes zlib output identical across builds is an expectation, not a measurement.
  • usePixelScale re-arming on a real monitor or scale change is not tested in a browser (jsdom has no matchMedia); 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).
  • Delta frames, tiles and things sheets are exercised only by the unit test and the generator's checks, not by a committed sprite (none exists yet; DS-H-02a/03).
  • Prepush lane, not the full linux lane: browser smoke, frontend build and workflow gates run in CI.

Reviews and disposition

ID Source Severity Finding Disposition
– – – Review: ausstehend (Stufe B); pending, Root commissions two non-Anthropic reviewers –

Open points

  • State: implemented, tested (local gates); not reviewed, not merged.
  • Handover: candidate = head above, base 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)

ID Source Severity Finding Disposition
R1637-K1 OpenRouter moonshotai/kimi-k3 medium scripts/dev/build-sprites.mjs:18 imports .ts from Node (type stripping needs Node >= 23.6) rejected: package.json engines.node ">=24" and every CI workflow uses node-version: 24
R1637-K2 kimi-k3 medium no negative tests for the generator's contract checks (bad id, frame size, colours, overflow) follow-up: DS-H-02a adds the first real sheets and the negative generator tests
R1637-K3..K6 kimi-k3 low no getServerSnapshot; brittle :root { lookup; no mkdirSync for --out; dynamic import of committed *.px.ts follow-up hardening in DS-H-02a; dev-only script, committed sources
R1637-G1/G2 google/gemini-3.8-flash low --out without value; DPR change subscription untested follow-up
Reviews on head 0c47fe5: google/gemini-3.8-flash APPROVE, moonshotai/kimi-k3 APPROVE — two non-author families (Google, Moonshot). Red-first: five named Test-First trailers, red commit 8274840 first. Size 272 lines (incl. two 279-byte atlases). Tier B met.
Root accepts head 0c47fe5.

Cuarroc and others added 3 commits October 16, 2026 14:34
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>
@Cuarroc
Cuarroc marked this pull request as ready for review October 16, 2026 13:07
@Cuarroc Cuarroc added the review-ok Pipeline: review disposition complete, no open high finding label Oct 16, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-16T13:14:21.631877Z 0c47fe5 Draft marked ready
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +24 to +25
// §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] };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +23 to +24
// 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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+$/;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +39 to +42
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");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@Cuarroc

Cuarroc commented Oct 16, 2026

Copy link
Copy Markdown
Owner Author

Root auto-queue: head 0c47fe5 unchanged since review-ok, all checks green. @Mergifyio queue

@Cuarroc

Cuarroc commented Oct 16, 2026

Copy link
Copy Markdown
Owner Author

Root queue (head 0c47fe5, review-ok, required checks pass): @Mergifyio queue

@mergify mergify Bot added the queued label Oct 16, 2026
@mergify mergify Bot mentioned this pull request Oct 16, 2026
30 of 46 tasks
@mergify
mergify Bot merged commit 3571795 into main Oct 16, 2026
25 checks passed
@mergify

mergify Bot commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

Merge Queue Status

  • ✅ Entered queue — 2026-10-16 13:18 UTC · Rule: default · triggered by merge protections
  • ✅ Checks passed · on draft merge queue: checking #1637 on main (b6c36f2) #1639
  • ✅ Merged — 2026-10-16 13:30 UTC · at 3571795c55597043da3586393b430a59ecb417d3 · merge

This pull request spent 12 minutes 7 seconds in the queue, including 11 minutes 23 seconds running CI.

Required conditions to merge
  • check-success = gates (linux)
  • check-success = gates (windows)
  • check-success = red-first
  • github-review-approved [🛡 GitHub repository ruleset rule main-pr-required-checks]
  • label != do-not-merge
  • any of [🛡 GitHub repository ruleset rule main-pr-required-checks]:
    • check-success = @github-actions/gates (linux)
    • check-neutral = @github-actions/gates (linux)
    • check-skipped = @github-actions/gates (linux)
  • any of [🛡 GitHub repository ruleset rule main-pr-required-checks]:
    • check-success = @github-actions/gates (windows)
    • check-neutral = @github-actions/gates (windows)
    • check-skipped = @github-actions/gates (windows)
  • any of [🛡 GitHub repository ruleset rule main-pr-required-checks]:
    • check-success = @github-actions/red-first
    • check-neutral = @github-actions/red-first
    • check-skipped = @github-actions/red-first
  • any of [🛡 GitHub repository ruleset rule main-pr-required-checks]:
    • check-neutral = @mergify/Mergify Merge Protections
    • check-skipped = @mergify/Mergify Merge Protections
    • check-success = @mergify/Mergify Merge Protections

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review-ok Pipeline: review disposition complete, no open high finding

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant