---
phase: 02-client-engine-documentation
plan: 01
subsystem: tooling
tags: [tools, asset-catalog, scaffold, vitest, typescript, nodenext, cdoc-02]

requires:
  - phase: 01-extraction
    provides: tools/extract-gmd canonical types (Sprite/Background/Script/GmObject/Room/etc.) — re-exported verbatim by tools/asset-catalog/src/types.ts to guarantee zero drift (D-06 tier 1)
  - phase: 02-client-engine-documentation (planning artifacts)
    provides: 02-PATTERNS.md (file-by-file scaffold spec), 02-RESEARCH.md (three-tier catalog model + Pitfalls 1-4), 02-CONTEXT.md (conventions D-05/D-06/D-15/D-16)

provides:
  - tools/asset-catalog/ workspace scaffold (Node TS strict, NodeNext, exact-pinned deps mirroring Phase 1)
  - cli.ts dispatcher with subcommands (catalog | regen-autogen | verify | help) and exit-code matrix (0 success, 1 functional, 2 usage)
  - src/types.ts re-exports of Phase 1 canonical types + new CatalogedSprite/CatalogedBackground/CatalogedScript/CatalogedObject/CatalogedRoom/Catalog interfaces (D-06 three-tier shape)
  - src/load.ts loadExtracted signature + ExtractedProject interface + safeReadDir helper (Pattern S-4)
  - src/derive.ts detectImageFormat / sizeBucket / audioFormatFromMagic stubs with locked signatures
  - src/crossref.ts crossRefSprite/Background/Script/Object/Room stubs + helpers (escapeRegex, stripComments, spriteMentioned, dndReferencesSprite) with Pitfalls 1-4 documented inline
  - src/emit.ts WORKING writeJsonDeterministic + stringifySortedJson (D-15/D-16 deterministic JSON) + runCatalog/runRegenAutogen/runVerify stubs
  - scripts/lint-{docs,matrix,adr}.mjs stub linters (exit 2 no-arg, exit 0 --help, exit 0 with TODO note otherwise) — Plans 02-05 / 02-06 fill in real lint logic
  - tests/structure.test.ts — 24 passing structural assertions

affects: [02-01b (mini-extract fixture + Pitfall regression scaffolds), 02-03 (load/derive/crossref bodies), 02-04 (emit/cli/integration), 02-05 (autogen + lint-docs), 02-06 (matrix/ADR + lint-matrix/lint-adr)]

tech-stack:
  added:
    - typescript 5.6.3 (matches Phase 1)
    - tsx 4.21.0 (matches Phase 1)
    - vitest 4.1.5 (matches Phase 1)
    - "@types/node 25.6.0" (matches Phase 1)
  patterns:
    - exact-pin all deps (no ^/~ — D-15 byte-determinism)
    - strict TS with noUncheckedIndexedAccess + exactOptionalPropertyTypes
    - NodeNext module resolution → relative imports must carry .js extension even from .ts source (e.g. `from '../../extract-gmd/src/types.js'`)
    - Type re-export from Phase 1 (D-06 tier 1) — zero canonical-shape drift
    - Three-tier catalog interfaces (CatalogedSprite/Background/Script/Object/Room) carry tier 1 verbatim + tier 2 derived + tier 3 cross-ref `used_by` + (scripts) `mentions`
    - cli.ts exit-code matrix (0/1/2) with cross-platform `fileURLToPath(import.meta.url)` invokedDirectly guard (IN-05)
    - vitest single-worker (`maxWorkers: 1`, `pool: 'forks'`) for fs-determinism
    - Stub bodies use `throw new Error('TODO(plan-02-XX): ...')` so any accidental call surfaces as an obvious failure rather than silently returning undefined
    - `writeJsonDeterministic` + `stringifySortedJson` working (not stubbed) — needed by structural test today and Plan 02-04 tomorrow; copied from `tools/extract-gmd/src/emit/json.ts` verbatim

key-files:
  created:
    - tools/asset-catalog/.gitignore
    - tools/asset-catalog/README.md
    - tools/asset-catalog/package.json
    - tools/asset-catalog/pnpm-lock.yaml
    - tools/asset-catalog/tsconfig.json
    - tools/asset-catalog/vitest.config.ts
    - tools/asset-catalog/cli.ts
    - tools/asset-catalog/src/types.ts
    - tools/asset-catalog/src/load.ts
    - tools/asset-catalog/src/derive.ts
    - tools/asset-catalog/src/crossref.ts
    - tools/asset-catalog/src/emit.ts
    - tools/asset-catalog/scripts/lint-docs.mjs
    - tools/asset-catalog/scripts/lint-matrix.mjs
    - tools/asset-catalog/scripts/lint-adr.mjs
    - tools/asset-catalog/tests/structure.test.ts
  modified: []

key-decisions:
  - "tools/asset-catalog scaffold mirrors tools/extract-gmd verbatim — same TS/tsx/vitest/@types/node pins, same tsconfig flag set (strict + noUncheckedIndexedAccess + exactOptionalPropertyTypes + NodeNext), same vitest determinism config (maxWorkers: 1, pool: forks). Zero convention drift between Phase 1 and Phase 2 tooling (D-05)."
  - "Type re-export pattern: src/types.ts re-exports Sprite/Background/Script/GmObject/Room/etc. from `../../extract-gmd/src/types.js`. NodeNext requires the explicit .js extension on relative imports even from .ts source. Phase 4+ pnpm workspaces will replace these with `@rebno/extract-gmd` package imports."
  - "Three-tier CatalogedSprite shape locked: tier 1 (Sprite verbatim + frameCount derived from on-disk frames/ dir) + tier 2 (imageFormat, imageByteLength, sizeBucket, isAnimated) + tier 3 (used_by: { objects, masks_for, scripts, rooms }). Parallel CatalogedBackground/CatalogedScript/CatalogedObject/CatalogedRoom + top-level Catalog interface declared. Plans 02-03/02-04 implement bodies against these locked types (D-06)."
  - "writeJsonDeterministic/stringifySortedJson copied verbatim from tools/extract-gmd/src/emit/json.ts (22 lines, no deps) rather than stubbed — needed by both the structural smoke test today and Plan 02-04 implementation tomorrow. Approved per the plan's `<action>` File 11 spec."
  - "Three lint script stubs (lint-docs.mjs / lint-matrix.mjs / lint-adr.mjs) follow identical exit-code shape: 2 on missing arg, 0 on --help, 0 with TODO note otherwise. Plans 02-05 (lint-docs) and 02-06 (lint-matrix, lint-adr) fill in real lint logic. Stubs let CI wire the call today without failure."
  - "src/load.ts ExtractedProject interface defined here (not deferred) so crossref.ts and emit.ts type-import it cleanly. Body of loadExtracted is stubbed; signature is locked."

patterns-established:
  - "Phase 2 tooling inherits Phase 1 conventions verbatim — same package.json shape, same tsconfig, same vitest config. Copy-and-adapt is faster and lower-risk than re-deriving."
  - "Stub bodies throw `TODO(plan-02-XX)` errors with the implementing plan ID so accidental calls surface obviously."
  - "Working pure-helper code (writeJsonDeterministic/stringifySortedJson) lands in scaffold plan if it has no dependencies and is needed by the structural test."
  - "Lint .mjs scripts live OUTSIDE tsconfig include — they are ESM Node scripts, not TS. Plans 02-05/02-06 fill in real logic."
  - "Type re-export with NodeNext .js extension is the canonical cross-tool type-sharing pattern until pnpm workspace packages exist."

requirements-completed: []  # CDOC-02 stays open until plan 02-04 emits the first asset-catalog/index.{json,md}; this scaffold is the foundation but does not satisfy the requirement on its own.

duration: ~30min (recovery)
completed: 2026-05-03
---

# Phase 2 Plan 01: tools/asset-catalog Scaffold Summary

Wave 0 foundation for Phase 2 asset-catalog tooling. Stand up the workspace, lock public signatures, prove the test suite runs.

## What was built

`tools/asset-catalog/` mirrors `tools/extract-gmd/` conventions verbatim — same TypeScript strict + NodeNext config, same vitest determinism pins, same exit-code matrix. Subsequent Wave 1 plans (02-03, 02-04) implement load/derive/crossref/emit bodies against the locked signatures established here.

## Files created (16 total)

**Configuration trio (4)**
- `package.json` — name=`asset-catalog`, ESM, version pins matching Phase 1 (TS 5.6.3, tsx 4.21.0, vitest 4.1.5, @types/node 25.6.0)
- `tsconfig.json` — strict + noUncheckedIndexedAccess + exactOptionalPropertyTypes + NodeNext
- `vitest.config.ts` — maxWorkers: 1, pool: forks, 30s timeout
- `pnpm-lock.yaml` — committed for D-15 install-time reproducibility

**CLI + source modules (5)**
- `cli.ts` — async `main(argv)` dispatcher; cases: catalog | regen-autogen | verify | help/--help/-h | (default → unknown). Returns 0/1/2 per matrix. `fileURLToPath(import.meta.url)` invokedDirectly guard (cross-platform).
- `src/types.ts` — re-exports 17 Phase 1 types from `../../extract-gmd/src/types.js`; declares CatalogedSprite, CatalogedBackground, CatalogedScript, CatalogedObject, CatalogedRoom, top-level Catalog (D-06 three-tier shape)
- `src/load.ts` — ExtractedProject interface + safeReadDir helper (working) + loadExtracted stub
- `src/derive.ts` — detectImageFormat / sizeBucket / audioFormatFromMagic stubs with locked signatures
- `src/crossref.ts` — SpriteCrossRef/BackgroundCrossRef/ScriptCrossRef/ObjectCrossRef interfaces + crossRefSprite/Background/Script/Object/Room stubs + helpers (escapeRegex, stripComments, spriteMentioned, dndReferencesSprite). Pitfalls 1-4 documented inline as MANDATORY for Plan 02-03.
- `src/emit.ts` — writeJsonDeterministic + stringifySortedJson + sortKeysRecursive WORKING (verbatim from extract-gmd/src/emit/json.ts) + runCatalog/runRegenAutogen/runVerify stubs

**Lint script stubs (3)**
- `scripts/lint-docs.mjs` — exit 2 no-arg, exit 0 --help, exit 0 with TODO note (plan-02-05 fills in)
- `scripts/lint-matrix.mjs` — same shape (plan-02-06 fills in)
- `scripts/lint-adr.mjs` — same shape (plan-02-06 fills in)

**Test (1)**
- `tests/structure.test.ts` — 24 passing assertions covering all of: package.json version pins, tsconfig flags, vitest determinism config, cli.ts header + main signature + invokedDirectly guard, types.ts re-exports + CatalogedSprite + Catalog + compile-time type checks, emit.ts working helpers + locked signatures, src stubs (load/derive/crossref) export their public API, cli smoke (no-arg → 2, help → 0, --help → 0, unknown-cmd → 2, missing args → 2)

**Doc (1)**
- `README.md` — thin wrapper per Pattern S-5

**Misc (1)**
- `.gitignore` — node_modules/, dist/, coverage/, *.tsbuildinfo

## Test counts

- structure.test.ts: 24 passed / 0 todo / 24 total
- Cumulative tools/asset-catalog: 24/24 passing

## Verification — all green

| Command | Expected | Actual |
|---------|----------|--------|
| `pnpm install` | exit 0 | exit 0 |
| `pnpm typecheck` (`tsc --noEmit`) | exit 0, no errors | exit 0, clean |
| `pnpm test` | exit 0, all tests pass | 24/24 passed, 229ms |
| `node scripts/lint-docs.mjs --help` | exit 0 | exit 0 |
| `node scripts/lint-docs.mjs` (no arg) | exit 2 | exit 2 |
| `node scripts/lint-matrix.mjs` (no arg) | exit 2 | exit 2 |
| `node scripts/lint-adr.mjs` (no arg) | exit 2 | exit 2 |

## Stable signatures locked for downstream plans

**For Plan 02-03 (load/derive/crossref bodies):**
```typescript
// load.ts
export interface ExtractedProject { rootDir: string; manifestSha256: string; ... }
export function safeReadDir(dir: string): string[];                       // working
export function loadExtracted(rootDir: string): ExtractedProject;          // stub

// derive.ts
export function detectImageFormat(firstBytes: Buffer | null): 'bmp' | 'unknown';
export function sizeBucket(width: number, height: number): 'tiny' | 'small' | 'med' | 'large';
export function audioFormatFromMagic(firstBytes: Buffer): 'wav' | 'mid' | 'mp3' | 'unknown';

// crossref.ts
export function escapeRegex(s: string): string;
export function stripComments(code: string): string;
export function spriteMentioned(scriptCode: string, spriteName: string): boolean;
export function dndReferencesSprite(action, spriteId: number): boolean;
export function crossRefSprite(project, sprite): SpriteCrossRef;
export function crossRefBackground(project, bg): BackgroundCrossRef;
export function crossRefScript(project, script): ScriptCrossRef;
export function crossRefObject(project, obj): ObjectCrossRef;
export function crossRefRoom(project, room): { uniqueObjects: number[]; uniqueBackgrounds: number[] };
```

**For Plan 02-04 (emit/cli/integration):**
```typescript
// emit.ts
export function stringifySortedJson(value: unknown): string;               // working
export function writeJsonDeterministic(path: string, value: unknown): void; // working
export async function runCatalog(extractedDir: string, outDir: string): Promise<void>;  // stub
export async function runRegenAutogen(docsDir: string): Promise<void>;     // stub
export async function runVerify(docsDir: string): Promise<void>;           // stub
```

**For Plan 02-05 / 02-06 (lint scripts):** three .mjs entry points with the exit-code shell already wired; only the body needs to be filled in.

## Conventions inherited from Phase 1 (zero drift)

- Version pins: TS 5.6.3, tsx 4.21.0, vitest 4.1.5, @types/node 25.6.0 — exact-pin (no ^/~)
- TypeScript strictness: strict + noUncheckedIndexedAccess + exactOptionalPropertyTypes
- Module resolution: NodeNext (relative imports require .js extension)
- Test determinism: maxWorkers: 1, pool: forks
- CLI exit codes: 0 success / 1 functional / 2 usage
- Cross-platform invokedDirectly: `fileURLToPath(import.meta.url) === process.argv[1]`
- Deterministic JSON output: sorted keys (recursive), 2-space indent, trailing LF, CRLF stripped

## Deviations from Plan

None — plan executed as specified.

The plan's `<action>` block called for one combined task; this recovery executor produced a single combined commit covering both the structure test (RED) and the scaffold (GREEN), matching the plan's `tdd="true"` flag with the implementer's note that test+impl could ship together when the impl is purely structural (no behavior to drive). Acceptable per recovery instructions ("If RED+GREEN already commingled, ONE combined commit acceptable").

## Recovery context

Prior executor scaffolded all configuration + source + test files but ran out of usage limit before the three lint script stubs were created and before any commit was made. This recovery executor:
1. Verified all 13 already-on-disk files match the plan's locked behavior contracts (no fixes needed — prior agent's output was clean).
2. Created the three missing `scripts/lint-{docs,matrix,adr}.mjs` stubs per the plan's `<action>` File 12-14 spec.
3. Ran `pnpm typecheck` and `pnpm test` — both green (24/24 tests).
4. Smoke-tested all three lint scripts — all behave per spec (exit 2 no-arg, exit 0 --help).
5. Created a single combined commit (`9daa29b`) covering the entire scaffold.

## Commits

- `9daa29b` — `feat(02-01): scaffold tools/asset-catalog CLI with structure smoke test [CDOC-02]` (16 files, +2022 lines)

## Self-Check: PASSED

All claimed files exist on disk; commit `9daa29b` is in `git log`.
