# tools/asset-pipeline

[doc->REQ-AST-01] [doc->REQ-CLI-07]

Standalone Node CLI that turns extracted BMP frames into the canonical
PNG atlas the Phase 6 client `BootScene` loads at runtime. Phase 1 D-17
boundary — installs via `pnpm install --ignore-workspace`, no workspace
deps.

## Subcommands

```bash
asset-pipeline bootstrap   <extracted-dir> <source-out-dir>
asset-pipeline build       <source-dir> <atlas-out-dir> [--background <path>] [--include <id1,id2,...>]
asset-pipeline postprocess <vite-manifest.json> <pipeline-manifest.json> <out-path>
```

`--include` filters the source set to a comma-separated list of sprite
IDs. Useful locally after running bootstrap on the full extracted set
(854 sprites overflow the 1024² atlas budget) — verify-phase-6 itself
runs against a clean checkout where only the D-17 subset exists, so
the flag is a convenience, not a requirement.

| Subcommand    | Reads                                                             | Writes                                                                                |
| ------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `bootstrap`   | `extracted/<root>/sprites/<id>/{meta.json, frames/img_NNN.bmp}`   | `<source-out>/sprites/<id>.{png,json}` (Aseprite-friendly horizontal-strip)           |
| `build`       | `<source>/sprites/*.{png,json}`                                   | `<atlas-out>/{atlas-mvp.png, atlas-mvp.json, pipeline-manifest.json}`                 |
| `postprocess` | Vite `manifest.json` + the `build` step's `pipeline-manifest.json` | `<out-path>` — pipeline-manifest with Vite-hashed atlas paths                          |

## Pipeline order in `verify-phase-6.mjs` (plan 06-09)

1. `pnpm asset-pipeline:bootstrap` (one-shot, idempotent)
2. `pnpm asset-pipeline:build`
3. `pnpm build:client:staging` (Vite hashes + copies the atlas into `apps/server/public/`)
4. `pnpm asset-pipeline:postprocess` (rewrites manifest atlas paths to the Vite-hashed equivalents)
5. `pnpm lint:asset-pipeline` (T-06-02-02 — re-hashes referenced files; fails on mismatch)

If step 4 is skipped, step 5 silently degrades to a warning because the
manifest paths point at the unhashed names that Vite has already rewritten
on disk.

## `apps/server/public/` is BUILD-OUTPUT-ONLY (RESEARCH §Pitfall 8)

Vite's `emptyOutDir: true` deletes that directory before each build. NEVER
write runtime state into `apps/server/public/`. Runtime state lives in
`/data/` on the Fly.io volume. Only `.gitkeep` + `.gitignore` are committed
to the directory so it survives fresh checkouts.

## Determinism (D-15)

Every step pins:

- sharp@0.34.5 with `{ compressionLevel: 9, palette: false, effort: 10, progressive: false, adaptiveFiltering: false }` — bytes are stable across re-runs
- Lex-sorted directory + frame discovery
- `Object.fromEntries(... .sort())` for atlas frame keys + manifest sprite keys
- Trailing newline + LF-only line endings on every emitted JSON file

`pnpm test` covers all three guarantees end-to-end (`bootstrap.test.ts`,
`build.test.ts`, `determinism.test.ts`).

## BMP decode note

`sharp@0.34.5`'s prebuilt libvips on Windows ships without the BMP loader.
Phase 1 EXT-04 emits 24-bit uncompressed BMPs with the standard 40-byte
BITMAPINFOHEADER, so this tool ships an in-house ~90-line decoder
(`src/bmp-decoder.ts`). Anything outside that surface throws a clear
error so it can be added explicitly.
