---
phase: 02-client-engine-documentation
plan: 01b
type: execute
wave: 0
depends_on:
  - 02-01
files_modified:
  - tools/asset-catalog/tests/fixtures/build-mini-extract.ts
  - tools/asset-catalog/tests/fixtures/mini-extract/settings.json
  - tools/asset-catalog/tests/fixtures/mini-extract/MANIFEST.sha256
  - tools/asset-catalog/tests/fixtures/mini-extract/sprites/0000-Foo/meta.json
  - tools/asset-catalog/tests/fixtures/mini-extract/sprites/0000-Foo/frames/img_000.bmp
  - tools/asset-catalog/tests/fixtures/mini-extract/sprites/0001-FooBar/meta.json
  - tools/asset-catalog/tests/fixtures/mini-extract/sprites/0001-FooBar/frames/img_000.bmp
  - tools/asset-catalog/tests/fixtures/mini-extract/sprites/0002-FooBaz/meta.json
  - tools/asset-catalog/tests/fixtures/mini-extract/sprites/0002-FooBaz/frames/img_000.bmp
  - tools/asset-catalog/tests/fixtures/mini-extract/sprites/0003-Hexport/meta.json
  - tools/asset-catalog/tests/fixtures/mini-extract/sprites/0003-Hexport/frames/img_000.bmp
  - tools/asset-catalog/tests/fixtures/mini-extract/sprites/0004-HexportIn/meta.json
  - tools/asset-catalog/tests/fixtures/mini-extract/sprites/0004-HexportIn/frames/img_000.bmp
  - tools/asset-catalog/tests/fixtures/mini-extract/sprites/0005-HexportOut/meta.json
  - tools/asset-catalog/tests/fixtures/mini-extract/sprites/0005-HexportOut/frames/img_000.bmp
  - tools/asset-catalog/tests/fixtures/mini-extract/sprites/0006-HexportMask/meta.json
  - tools/asset-catalog/tests/fixtures/mini-extract/sprites/0006-HexportMask/frames/img_000.bmp
  - tools/asset-catalog/tests/fixtures/mini-extract/scripts/0000-uses_foobar_only.gml
  - tools/asset-catalog/tests/fixtures/mini-extract/scripts/0001-uses_hexportin_only.gml
  - tools/asset-catalog/tests/fixtures/mini-extract/scripts/0002-comments_only.gml
  - tools/asset-catalog/tests/fixtures/mini-extract/objects/0042-player/meta.json
  - tools/asset-catalog/tests/fixtures/mini-extract/objects/0042-player/events/Step.dnd.json
  - tools/asset-catalog/tests/fixtures/mini-extract/rooms/0000-room1/meta.json
  - tools/asset-catalog/tests/fixtures/mini-extract/rooms/0000-room1/instances.json
  - tools/asset-catalog/tests/load.test.ts
  - tools/asset-catalog/tests/crossref.fixture.test.ts
  - tools/asset-catalog/tests/emit-determinism.test.ts
autonomous: true
requirements:
  - CDOC-02
tags:
  - tools
  - asset-catalog
  - fixtures
  - pitfall-regression
  - tdd-scaffolding

must_haves:
  truths:
    - "Synthetic mini-extract fixture exists with Hexport/HexportIn/HexportOut/HexportMask + Foo/FooBar/FooBaz collision sprites (Pitfalls 1, 4 fixtures present on disk)"
    - "tests/load.test.ts contains it.todo placeholders for the load.ts behaviour Plan 02-03 will implement"
    - "tests/crossref.fixture.test.ts contains it.todo placeholders covering Pitfalls 1, 2, 3, 4 (named comments per pitfall)"
    - "tests/emit-determinism.test.ts contains 4 PASSING tests against the working writeJsonDeterministic + stringifySortedJson from Plan 02-01"
    - "`cd tools/asset-catalog && pnpm test` exits 0; output reports 5+ passing + 13+ `todo` placeholders"
  artifacts:
    - path: "tools/asset-catalog/tests/fixtures/mini-extract/sprites/0003-Hexport/meta.json"
      provides: "Pitfall 4 regression fixture (sprite name overlaps with HexportIn/HexportOut/HexportMask)"
    - path: "tools/asset-catalog/tests/fixtures/mini-extract/sprites/0001-FooBar/meta.json"
      provides: "Pitfall 1 regression fixture (Foo/FooBar/FooBaz prefix-collision sprites)"
    - path: "tools/asset-catalog/tests/fixtures/mini-extract/objects/0042-player/events/Step.dnd.json"
      provides: "Pitfall 3 regression fixture (DnD action with typed Resource argument referencing sprite 3 = Hexport)"
    - path: "tools/asset-catalog/tests/load.test.ts"
      provides: "5+ it.todo placeholders covering loadExtracted contract for Plan 02-03 RED phase"
    - path: "tools/asset-catalog/tests/crossref.fixture.test.ts"
      provides: "8+ it.todo placeholders covering Pitfall 1/2/3/4 mitigations for Plan 02-03 RED phase"
    - path: "tools/asset-catalog/tests/emit-determinism.test.ts"
      provides: "4 passing tests proving Plan 02-01's writeJsonDeterministic + stringifySortedJson satisfy D-16 (round-trip, no-CR, sorted-keys, no-Date)"
    - path: "tools/asset-catalog/tests/fixtures/build-mini-extract.ts"
      provides: "Regen helper for the committed fixture tree"
  key_links:
    - from: "tools/asset-catalog/tests/emit-determinism.test.ts"
      to: "tools/asset-catalog/src/emit.ts"
      via: "import writeJsonDeterministic + stringifySortedJson + source-text negative-grep"
      pattern: "from\\s+['\"]\\.\\./src/emit\\.js['\"]"
    - from: "tools/asset-catalog/tests/fixtures/mini-extract/objects/0042-player/meta.json"
      to: "tools/asset-catalog/tests/fixtures/mini-extract/sprites/0003-Hexport/"
      via: "spriteId === 3 references the Hexport fixture sprite"
      pattern: "\"spriteId\": 3"
    - from: "tools/asset-catalog/tests/fixtures/mini-extract/objects/0042-player/meta.json"
      to: "tools/asset-catalog/tests/fixtures/mini-extract/sprites/0006-HexportMask/"
      via: "maskId === 6 references the HexportMask fixture sprite"
      pattern: "\"maskId\": 6"
---

<objective>
Land the synthetic `mini-extract/` fixture tree on disk plus the three test files (`load.test.ts`, `crossref.fixture.test.ts`, `emit-determinism.test.ts`) that Plans 02-03 and 02-04 will turn green.

Purpose: Wave 0 deliverable B. Plan 02-01 locked the source contracts and wrote the working determinism helpers; this plan provides the regression fixtures and `it.todo` test scaffolds so Plan 02-03's RED phase has explicit named targets for Pitfalls 1-4. Splitting fixture work out of 02-01 keeps each Wave 0 plan inside the 50% context budget while preserving the same dependency graph (Wave 1 plans 02-03/02-04 still only need both 02-01 and 02-01b complete).

Output: 1 fixture builder script + 23 fixture files (committed binary BMPs + meta.json/instances.json/script.gml) + 3 test files (4+ passing emit-determinism tests; 13+ `it.todo` placeholders for Plans 02-03/02-04). `pnpm --filter asset-catalog test` exits 0.
</objective>

<execution_context>
@$HOME/.ccs/instances/bigscreen/get-shit-done/workflows/execute-plan.md
@$HOME/.ccs/instances/bigscreen/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/02-client-engine-documentation/02-CONTEXT.md
@.planning/phases/02-client-engine-documentation/02-RESEARCH.md
@.planning/phases/02-client-engine-documentation/02-PATTERNS.md
@.planning/phases/02-client-engine-documentation/02-VALIDATION.md
@./CLAUDE.md
@tools/asset-catalog/src/types.ts
@tools/asset-catalog/src/emit.ts
@tools/asset-catalog/cli.ts
@tools/extract-gmd/tests/fixtures/build-fixtures.ts

<interfaces>
<!-- Plan 02-01 already committed working emit.ts implementations of writeJsonDeterministic +
     stringifySortedJson (the CatalogedSprite types live in src/types.ts). This plan consumes those
     contracts; it does NOT redefine or re-implement them. -->

From tools/asset-catalog/src/emit.ts (Plan 02-01 — already on disk):
```typescript
export function writeJsonDeterministic(path: string, value: unknown): void;  // working impl
export function stringifySortedJson(value: unknown): string;                  // working impl
export async function runCatalog(extractedDir: string, outDir: string): Promise<void>;  // STUB until Plan 04
export async function runRegenAutogen(docsDir: string): Promise<void>;        // STUB until Plan 05
export async function runVerify(docsDir: string): Promise<void>;              // STUB until Plan 04
```

From tools/extract-gmd/src/types.ts (re-exported by tools/asset-catalog/src/types.ts):
```typescript
export interface DnDAction { actionId: number; argTypes: number[]; argValues: string[]; }
// argTypes encoding: 0=Real, 1=String, 2=Boolean, 3=Menu, 4=Resource sprite, 5=sound,
//                    6=background, 7=path, 8=script, 9=object, 10=room, 11=font, 12=timeline.
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Synthetic mini-extract fixture + Pitfall 1/4 regression test scaffolds</name>
  <files>
    tools/asset-catalog/tests/fixtures/build-mini-extract.ts,
    tools/asset-catalog/tests/fixtures/mini-extract/settings.json,
    tools/asset-catalog/tests/fixtures/mini-extract/MANIFEST.sha256,
    tools/asset-catalog/tests/fixtures/mini-extract/sprites/0000-Foo/meta.json,
    tools/asset-catalog/tests/fixtures/mini-extract/sprites/0000-Foo/frames/img_000.bmp,
    tools/asset-catalog/tests/fixtures/mini-extract/sprites/0001-FooBar/meta.json,
    tools/asset-catalog/tests/fixtures/mini-extract/sprites/0001-FooBar/frames/img_000.bmp,
    tools/asset-catalog/tests/fixtures/mini-extract/sprites/0002-FooBaz/meta.json,
    tools/asset-catalog/tests/fixtures/mini-extract/sprites/0002-FooBaz/frames/img_000.bmp,
    tools/asset-catalog/tests/fixtures/mini-extract/sprites/0003-Hexport/meta.json,
    tools/asset-catalog/tests/fixtures/mini-extract/sprites/0003-Hexport/frames/img_000.bmp,
    tools/asset-catalog/tests/fixtures/mini-extract/sprites/0004-HexportIn/meta.json,
    tools/asset-catalog/tests/fixtures/mini-extract/sprites/0004-HexportIn/frames/img_000.bmp,
    tools/asset-catalog/tests/fixtures/mini-extract/sprites/0005-HexportOut/meta.json,
    tools/asset-catalog/tests/fixtures/mini-extract/sprites/0005-HexportOut/frames/img_000.bmp,
    tools/asset-catalog/tests/fixtures/mini-extract/sprites/0006-HexportMask/meta.json,
    tools/asset-catalog/tests/fixtures/mini-extract/sprites/0006-HexportMask/frames/img_000.bmp,
    tools/asset-catalog/tests/fixtures/mini-extract/scripts/0000-uses_foobar_only.gml,
    tools/asset-catalog/tests/fixtures/mini-extract/scripts/0001-uses_hexportin_only.gml,
    tools/asset-catalog/tests/fixtures/mini-extract/scripts/0002-comments_only.gml,
    tools/asset-catalog/tests/fixtures/mini-extract/objects/0042-player/meta.json,
    tools/asset-catalog/tests/fixtures/mini-extract/objects/0042-player/events/Step.dnd.json,
    tools/asset-catalog/tests/fixtures/mini-extract/rooms/0000-room1/meta.json,
    tools/asset-catalog/tests/fixtures/mini-extract/rooms/0000-room1/instances.json,
    tools/asset-catalog/tests/load.test.ts,
    tools/asset-catalog/tests/crossref.fixture.test.ts,
    tools/asset-catalog/tests/emit-determinism.test.ts
  </files>
  <read_first>
    tools/extract-gmd/tests/fixtures/build-fixtures.ts,
    tools/extract-gmd/src/types.ts,
    tools/extract-gmd/src/emit/tree.ts,
    extracted/client-5-8/sprites/0000-NaviStandD/meta.json (sample real meta to mirror schema),
    extracted/client-5-8/objects/0042-player/meta.json (sample real object meta),
    .planning/phases/02-client-engine-documentation/02-PATTERNS.md (sections "test/fixtures/mini-extract/", "Pattern S-1 Determinism"),
    .planning/phases/02-client-engine-documentation/02-RESEARCH.md (Pitfalls 1-4, lines 405-443)
  </read_first>
  <behavior>
    - Fixture tree exists on disk under `tools/asset-catalog/tests/fixtures/mini-extract/`. Tests read from this committed tree (NOT from a build step).
    - `tests/load.test.ts`: skipped/pending tests targeting Plan 03 (each test marked `it.todo(...)` or `it.skip(...)` with a clear description). At minimum: "load.todo: returns ExtractedProject with 7 sprites from mini-extract", "load.todo: handles missing sounds/ dir without throwing (Audio Caveat)", "load.todo: embeds MANIFEST.sha256 verbatim".
    - `tests/crossref.fixture.test.ts`: skipped/pending tests targeting Plan 03. Tests express the Pitfall 1/4 regression assertions (verbatim from PATTERNS.md lines 500-503): (a) `Foo.used_by.scripts` does NOT include script 0; (b) `FooBar.used_by.scripts` DOES include script 0; (c) `Hexport.used_by.objects` includes 42 (player.spriteId === 3); (d) `HexportMask.used_by.masks_for` includes 42 (player.maskId === 6); (e) script 2 (comments-only) — no sprite mentions despite mentioning `FooBar` inside `// FooBar` and inside a `/* FooBar */` block.
    - `tests/emit-determinism.test.ts`: passing test for `writeJsonDeterministic` (already implemented in Plan 01 task 1). Test: write same value twice → byte-identical files, no `\r` bytes in output, no `Date.now()` references in src/emit.ts source (negative test from PATTERNS.md §"Determinism — emit/json contains zero Date.now references"). Plus a `it.todo` for the integration determinism test that Plan 04 lands.
  </behavior>
  <action>
    Create the synthetic `mini-extract/` tree on disk (committed verbatim — tests read it, not regenerated). Then write three test files (one fully passing for emit-determinism, two with `it.todo`/`it.skip` placeholders for Plans 03/04 to fill in).

    **Step 1: Build script `tools/asset-catalog/tests/fixtures/build-mini-extract.ts`** — programmatic builder for reproducibility. Mirrors `tools/extract-gmd/tests/fixtures/build-fixtures.ts` shape. Header comment notes "Run `pnpm tsx tests/fixtures/build-mini-extract.ts` if you need to regenerate; the fixture tree is committed and tests read it directly." This script is documentation/helper — it writes the same bytes to disk that you commit. Body: for each sprite/script/object/room, mkdirSync recursive + writeFileSync.

    **Step 2: Write the synthetic tree on disk directly** (use Write tool for each file; the build-mini-extract.ts is the canonical source if regen needed).

    **settings.json** — minimal valid GameMaker settings: `{"id":0,"name":"settings","blockVersion":800,"width":800,"height":600,"speed":30}`. Format with deterministic 2-space indent + trailing LF.

    **MANIFEST.sha256** — synthetic placeholder: `# mini-extract synthetic fixture\nfixture-hash-not-real-d0a4b8c2e1f7\n`. Line endings: LF only.

    **Sprite meta.json schema** (mirror real sprite at `extracted/client-5-8/sprites/0000-NaviStandD/meta.json` — read this first to get exact field names). Each `meta.json` carries (per Phase 1 types): `id`, `name`, `blockVersion: 542`, `width: 32`, `height: 32`, `bboxLeft: 0`, `bboxRight: 31`, `bboxTop: 0`, `bboxBottom: 31`, `bboxMode: 0`, `precise: false`, `transparent: true`, `smoothEdges: false`, `preload: false`, `originX: 0`, `originY: 0`. ID matches dir prefix (0000 → id 0, 0001 → id 1, etc.). Name matches dir suffix. Vary only id+name across the 7 sprites.

    **Sprite frames** — `frames/img_000.bmp` for each — minimal valid BMP header (14-byte BMP file header + 40-byte DIB header for a 1×1 BMP). Concrete bytes for a 1×1 24-bit BMP (from BMP spec):
    ```
    42 4D 3A 00 00 00 00 00 00 00 36 00 00 00 28 00
    00 00 01 00 00 00 01 00 00 00 01 00 18 00 00 00
    00 00 04 00 00 00 13 0B 00 00 13 0B 00 00 00 00
    00 00 00 00 00 00 00 00 FF 00
    ```
    Total 58 bytes. First two bytes `0x42 0x4D = "BM"` → `detectImageFormat` returns `'bmp'`. Use Bash `printf` or Node `Buffer.from([...]).writeFileSync` if helpful, or commit the raw 58 bytes via Write tool with binary-safe hex string converted to bytes (recommended: write a tiny helper inline in build-mini-extract.ts). All 7 sprites share the same 58-byte BMP (content is irrelevant to crossref tests; only magic bytes matter).

    **Sprites listing** (7 total, IDs 0-6):
    - 0000-Foo, 0001-FooBar, 0002-FooBaz (Pitfall 1 prefix-collision)
    - 0003-Hexport, 0004-HexportIn, 0005-HexportOut, 0006-HexportMask (Pitfall 4 + RESEARCH cite of objects/0042-player/Step.gml)

    **Script files:**
    - `scripts/0000-uses_foobar_only.gml`:
      ```
      // Test fixture: mentions FooBar exactly once at code level.
      // Foo and FooBaz appear ONLY in this comment, not in code.
      if (sprite_index == FooBar) {
        sprite_index = FooBar;
      }
      ```
    - `scripts/0001-uses_hexportin_only.gml`:
      ```
      // Test fixture: mentions HexportIn but not Hexport, HexportOut, HexportMask.
      sprite_index = HexportIn;
      ```
    - `scripts/0002-comments_only.gml`:
      ```
      // FooBar is referenced here but only in a single-line comment
      /* FooBar is also referenced here but only in a block comment */
      var x = 1;
      ```

    **Object: `objects/0042-player/meta.json`** (mirror real BNO object; read `extracted/client-5-8/objects/0042-player/meta.json` first for exact schema). Required fields:
    ```json
    {
      "id": 42,
      "name": "player",
      "blockVersion": 800,
      "spriteId": 3,
      "maskId": 6,
      "parentId": -1,
      "visible": true,
      "solid": false,
      "persistent": false,
      "depth": 0
    }
    ```
    spriteId=3 → references sprite "Hexport"; maskId=6 → references "HexportMask". This anchors regression assertions (c) and (d).

    **Object: `objects/0042-player/events/Step.dnd.json`** — DnD event with one action that references a sprite via Resource argType (Pitfall 3 mitigation target). Format:
    ```json
    {
      "actions": [
        {
          "actionId": 102,
          "argTypes": [4, 0],
          "argValues": ["3", "0"]
        }
      ]
    }
    ```
    argTypes[0] === 4 (Resource sprite); argValues[0] === "3" (string form of sprite ID 3 = Hexport). When Plan 03's `dndReferencesSprite` runs, this MUST be detected.

    **Room: `rooms/0000-room1/meta.json`** — minimal room: `{"id":0,"name":"room1","width":640,"height":480,"speed":30}`.

    **Room: `rooms/0000-room1/instances.json`** — single instance referencing object 42:
    ```json
    {
      "instances": [
        {"id": 100000, "objectId": 42, "x": 32, "y": 32}
      ]
    }
    ```
    This anchors a future room→sprite transitive assertion (Hexport is used by room 0 because room 0 contains object 42 which uses Hexport).

    **Step 3: tests/load.test.ts** — pending suite with `it.todo()` placeholders (vitest's `it.todo` shows them in the report without failing):
    ```typescript
    import { describe, it } from 'vitest';
    describe('loadExtracted (mini-extract fixture) — implementation in Plan 03', () => {
      it.todo('returns ExtractedProject with 7 sprites from mini-extract');
      it.todo('handles missing sounds/ dir without throwing (Audio Caveat per RESEARCH §989-1006)');
      it.todo('embeds MANIFEST.sha256 verbatim into result.manifestSha256');
      it.todo('sorts sprites by directory name (numeric prefix) — 0000-Foo before 0001-FooBar');
      it.todo('reads frame count by listing frames/ directory');
    });
    ```

    **Step 4: tests/crossref.fixture.test.ts** — pending suite with `it.todo` placeholders citing exact Pitfall mitigations:
    ```typescript
    import { describe, it } from 'vitest';
    describe('crossref (mini-extract fixture) — implementation in Plan 03', () => {
      // Pitfall 1: word-boundary, not substring (RESEARCH lines 405-413)
      it.todo('Foo.used_by.scripts does NOT include script 0 (script mentions only FooBar; Foo is a prefix substring)');
      it.todo('FooBar.used_by.scripts DOES include script 0');
      it.todo('FooBaz.used_by.scripts does NOT include script 0');
      // Pitfall 2: block-comment stripping (RESEARCH lines 416-423)
      it.todo('FooBar.used_by.scripts does NOT include script 2 (FooBar appears only in // and /* */ comments)');
      // Pitfall 3: DnD argument values are typed strings (RESEARCH lines 425-433)
      it.todo('Hexport.used_by.objects includes 42 (object 42 spriteId=3 references Hexport via meta.json AND via DnD action with argType=4)');
      // Pitfall 4: multiple GML reference idioms (RESEARCH lines 435-443)
      it.todo('HexportMask.used_by.masks_for includes 42 (object 42 maskId=6 references HexportMask)');
      it.todo('HexportIn.used_by.scripts includes 1 (script 1 contains `sprite_index = HexportIn`)');
      // Room transitivity (RESEARCH line 712)
      it.todo('Hexport.used_by.rooms includes 0 (room 0 contains object 42 which uses sprite 3 = Hexport)');
    });
    ```

    **Step 5: tests/emit-determinism.test.ts** — passing tests (writeJsonDeterministic + stringifySortedJson are already implemented in Plan 01 task 1):
    ```typescript
    import { describe, it, expect } from 'vitest';
    import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
    import { tmpdir } from 'node:os';
    import { join } from 'node:path';
    import { writeJsonDeterministic, stringifySortedJson } from '../src/emit.js';
    import { fileURLToPath } from 'node:url';

    describe('emit determinism (Pattern S-1, mirrors Phase 1 D-15)', () => {
      it('round-trip: same input → byte-identical output (TR-10 mirror)', () => {
        const dir = mkdtempSync(join(tmpdir(), 'asset-catalog-emit-'));
        try {
          const value = { z: 1, a: 2, nested: { y: 'hello', x: 'world' } };
          const p = join(dir, 'out.json');
          writeJsonDeterministic(p, value);
          const buf1 = readFileSync(p);
          writeJsonDeterministic(p, value);
          const buf2 = readFileSync(p);
          expect(Buffer.compare(buf1, buf2)).toBe(0);
        } finally { rmSync(dir, { recursive: true }); }
      });

      it('output contains no \\r bytes (LF-only)', () => {
        const dir = mkdtempSync(join(tmpdir(), 'asset-catalog-emit-'));
        try {
          const p = join(dir, 'out.json');
          writeJsonDeterministic(p, { a: 'line1\nline2' });
          const buf = readFileSync(p);
          expect(buf.includes(0x0d)).toBe(false);
        } finally { rmSync(dir, { recursive: true }); }
      });

      it('keys are recursively sorted', () => {
        const json = stringifySortedJson({ z: 1, a: 2, m: { z: 3, a: 4 } });
        // 'a' before 'z' at every depth
        expect(json.indexOf('"a"')).toBeLessThan(json.indexOf('"z"'));
        expect(json.indexOf('"m"')).toBeLessThan(json.indexOf('"z"'));
      });

      it('src/emit.ts source contains zero Date.now / new Date references (D-16 lint)', () => {
        const src = readFileSync(fileURLToPath(new URL('../src/emit.ts', import.meta.url)), 'utf8');
        const noComments = src.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
        expect(noComments).not.toMatch(/Date\.now/);
        expect(noComments).not.toMatch(/new\s+Date\b/);
      });

      it.todo('integration: catalog over real extracted/client-5-8 produces byte-identical output across two runs (Plan 04)');
    });
    ```

    **Verification:** `cd tools/asset-catalog && pnpm test` — structure.test.ts passes; emit-determinism.test.ts has 4 passing + 1 todo; load.test.ts has 5 todos; crossref.fixture.test.ts has 8 todos. Total: 5+ passing tests, 14+ todos. Vitest exits 0 (todos do not fail).

    **CRITICAL: BMP file write technique.** Use the build-mini-extract.ts script as the canonical writer for the binary BMP files. Run it once locally to materialize the .bmp files, then commit the resulting fixture tree (the script remains committed alongside as the regen helper). Alternative if Write tool cannot emit binary: the build-mini-extract.ts script must run before any test depending on .bmp files. Recommended approach: write build-mini-extract.ts, run it once via `pnpm tsx tools/asset-catalog/tests/fixtures/build-mini-extract.ts`, commit the materialized tree.
  </action>
  <verify>
    <automated>cd tools/asset-catalog && pnpm test 2>&1 | tee /tmp/p2-01-task2.log && grep -E "Tests:|passed|todo" /tmp/p2-01-task2.log</automated>
  </verify>
  <acceptance_criteria>
    - Directory `tools/asset-catalog/tests/fixtures/mini-extract/sprites/` contains exactly 7 subdirectories (0000-Foo, 0001-FooBar, 0002-FooBaz, 0003-Hexport, 0004-HexportIn, 0005-HexportOut, 0006-HexportMask)
    - Each sprite dir has `meta.json` and `frames/img_000.bmp`; `od -An -tx1 -N2 tools/asset-catalog/tests/fixtures/mini-extract/sprites/0000-Foo/frames/img_000.bmp` returns `42 4d` (or use `head -c 2 ... | xxd` returning `4242` "BM")
    - File `tools/asset-catalog/tests/fixtures/mini-extract/sprites/0000-Foo/meta.json` parses as JSON; `.id === 0`; `.name === "Foo"`; `.blockVersion === 542`
    - File `tools/asset-catalog/tests/fixtures/mini-extract/objects/0042-player/meta.json` parses as JSON; `.spriteId === 3` (→ Hexport); `.maskId === 6` (→ HexportMask)
    - File `tools/asset-catalog/tests/fixtures/mini-extract/objects/0042-player/events/Step.dnd.json` parses as JSON; `.actions[0].argTypes[0] === 4` and `.actions[0].argValues[0] === "3"`
    - Files `tools/asset-catalog/tests/fixtures/mini-extract/scripts/0000-uses_foobar_only.gml`, `0001-uses_hexportin_only.gml`, `0002-comments_only.gml` exist
    - File `tools/asset-catalog/tests/fixtures/mini-extract/MANIFEST.sha256` exists; `od -c <file> | grep -c '\\r'` returns 0 (no CR bytes)
    - File `tools/asset-catalog/tests/load.test.ts` exists; `grep -c "it.todo"` returns at least 5
    - File `tools/asset-catalog/tests/crossref.fixture.test.ts` exists; `grep -c "it.todo"` returns at least 8; `grep -c "Pitfall"` returns at least 4 (one comment per pitfall mitigation)
    - File `tools/asset-catalog/tests/emit-determinism.test.ts` exists with at least 4 PASSING tests (round-trip, no-CR, sorted-keys, no-Date-references)
    - Command `cd tools/asset-catalog && pnpm test` exits 0; output shows "passed" count ≥ 5 and "todo" count ≥ 13
    - File `tools/asset-catalog/tests/fixtures/build-mini-extract.ts` exists (the regen helper)
  </acceptance_criteria>
  <done>Synthetic mini-extract fixture is committed and immediately useful: emit-determinism tests already prove writeJsonDeterministic works (Plan 01 task 1's inline implementation is validated); load + crossref tests are pending placeholders that Plan 03 will turn green. Fixture covers Pitfalls 1 (Foo/FooBar/FooBaz prefix collision), 2 (comments-only mention), 3 (DnD typed argument), 4 (Hexport/HexportIn/HexportMask via sprite_index assignment).</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| (none in this plan) | All fixture content is hand-authored synthetic ASCII data; no auth, no network, no user input. |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-02-01b-01 | Tampering | mini-extract fixture path injection (sprite name `../../../etc/passwd`) | accept | Fixture is hand-authored; all sprite/object/script/room names are ASCII alphanumeric (Foo, FooBar, Hexport, player, room1...). No filesystem write under runtime control. |

</threat_model>

<verification>
- `cd tools/asset-catalog && pnpm test` exits 0 with 5+ passing + 13+ `todo` placeholders
- `cd tools/asset-catalog && pnpm test -- tests/emit-determinism.test.ts` exits 0 (4 passing)
- All 7 fixture sprite directories exist with `meta.json` + `frames/img_000.bmp` (2-byte magic = `BM`)
- `git status` shows fixture tree + 3 test files staged for commit
</verification>

<success_criteria>
- Mini-extract fixture committed with Pitfall 1, 2, 3, 4 named regression cases on disk
- emit-determinism test file proves Plan 02-01's writeJsonDeterministic + stringifySortedJson satisfy D-16
- load + crossref test files exist as `it.todo` placeholders so Plan 02-03's RED phase has named targets
- Plans 02-03 + 02-04 can begin work with both source contracts (02-01) and regression fixtures (02-01b) in place
</success_criteria>

<output>
After completion, create `.planning/phases/02-client-engine-documentation/02-01b-SUMMARY.md` with:
- Fixture file count + total bytes on disk
- Test counts (passing / todo / total)
- Pitfall coverage map: which fixture file backs which Pitfall (1/2/3/4)
- Deviations from the original combined Task 2 spec (none expected)
</output>
