---
phase: 02-client-engine-documentation
plan: 01b
subsystem: tooling
tags: [tools, asset-catalog, fixtures, pitfall-regression, tdd-scaffolding, cdoc-02]

requires:
  - plan: 02-01
    provides: tools/asset-catalog/ workspace scaffold (cli.ts, src/types.ts, src/load.ts, src/derive.ts, src/crossref.ts, src/emit.ts) with locked signatures + working writeJsonDeterministic + stringifySortedJson
  - phase: 01-extraction
    provides: extracted/client-5-8/ schema reference (sprite/object meta.json shape mirrored by mini-extract/) + tools/extract-gmd/tests/fixtures/build-fixtures.ts (pattern reference for build-mini-extract.ts)

provides:
  - tools/asset-catalog/tests/fixtures/mini-extract/ — synthetic 23-file fixture tree (3617 bytes total) covering Pitfalls 1-4 regression cases
  - tools/asset-catalog/tests/fixtures/build-mini-extract.ts — canonical regen helper script for the committed fixture tree
  - tools/asset-catalog/tests/load.test.ts — 5 it.todo placeholders for Plan 02-03's loadExtracted body (RED phase named targets)
  - tools/asset-catalog/tests/crossref.fixture.test.ts — 8 it.todo placeholders covering Pitfall 1/2/3/4 mitigations + room transitivity (RED phase named targets)
  - tools/asset-catalog/tests/emit-determinism.test.ts — 4 PASSING tests proving writeJsonDeterministic/stringifySortedJson satisfy D-15/D-16 + 1 it.todo for Plan 02-04's integration determinism check

affects: [02-03 (load/derive/crossref bodies — fixture + test scaffolds wait), 02-04 (emit integration determinism slot)]

tech-stack:
  added: []
  patterns:
    - Hand-authored synthetic fixture committed verbatim — tests read from disk, NOT regenerated. build-mini-extract.ts is the canonical regen helper, not a test prerequisite.
    - Word-boundary collision fixtures: sprite name families (Foo/FooBar/FooBaz, Hexport/HexportIn/HexportOut/HexportMask) are the on-disk evidence that Plan 02-03's spriteMentioned() must word-boundary-match, not substring-match.
    - DnD typed-argument fixture: actions[0].argTypes[0] === 4 (Resource sprite) + actions[0].argValues[0] === "3" (string-form sprite ID) is the on-disk evidence that Plan 02-03's dndReferencesSprite() must dispatch on argType.
    - Comment-stripped grep fixture: scripts/0002-comments_only.gml mentions FooBar inside both `// ...` and `/* ... */` comment forms, expecting stripComments() to remove both.
    - it.todo as RED phase scaffold: vitest reports todos in the test report without failing CI, providing named targets for the implementing plan to convert into real assertions.
    - Negative-grep determinism lint: emit-determinism.test.ts asserts src/emit.ts source contains zero `Date.now` / `new Date` references after stripping comments — protects D-16 invariant from future edits.

key-files:
  created:
    - 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
  modified: []

key-decisions:
  - "Fixture committed verbatim, not generated at test time. The build-mini-extract.ts script is the canonical regen helper but tests do NOT depend on it — they read directly from the committed mini-extract/ tree. Trade-off: 23 small fixture files in git vs. one script + ephemeral fixture; chose committed because (a) the fixture is intentionally tiny (3617 bytes), (b) committed bytes match what tests assert against (no drift between writer and reader), (c) PR reviewers can read the fixture directly without running the builder."
  - "Sprite names mirror the real BNO Hexport family verbatim (Hexport / HexportIn / HexportOut / HexportMask) per RESEARCH.md Pitfall 4 citation of objects/0042-player/Step.gml. The Foo/FooBar/FooBaz family is synthetic — no equivalent in real BNO — picked solely because it forces the prefix-substring vs word-boundary distinction (Pitfall 1)."
  - "Object 42 uses spriteId=3 (Hexport) and maskId=6 (HexportMask). The real extracted/client-5-8/objects/0042-player/meta.json has spriteId=65 + maskId=34 — the fixture deliberately diverges so cross-ref assertions can reference small integer IDs that exist in the 7-sprite fixture set. The fixture is synthetic, not a copy of real BNO."
  - "DnD action carries argTypes[0]===4 (Resource sprite per LateralGM action-id encoding) + argValues[0]===\"3\" (string-form sprite ID 3 = Hexport). This is the canonical Pitfall 3 regression: a naive .includes('3') string scan would match many unrelated values; correct behavior dispatches on argType first."
  - "5 + 8 = 13 it.todo placeholders are the named RED targets for Plan 02-03; the 14th todo (in emit-determinism.test.ts) is the named RED target for Plan 02-04's integration determinism slot. Each placeholder has an inline comment citing the exact RESEARCH.md line range so the implementing plan has zero ambiguity about what to assert."

patterns-established:
  - "Wave 0 RED scaffolding pattern: when Plan A locks signatures + working helpers, a sibling Plan A-prime can land regression fixtures + it.todo test files for downstream Plan B's RED phase. Splitting fixture work into its own plan keeps each Wave 0 plan inside the context budget while preserving the dependency graph (Wave 1 plans see both A and A-prime as a single dependency set)."
  - "Hand-rolled binary fixture pattern: 58-byte 1x1 24-bit BMP (BMP file header 14 bytes + DIB header 40 bytes + 4-byte pixel row) is sufficient when only magic-byte detection matters. detectImageFormat() reads the first 2 bytes; the rest of the file is irrelevant to crossref tests but valid enough to satisfy a future image decoder."
  - "Pitfall coverage map embedded in test file comments: each it.todo comment cites the exact RESEARCH.md line range (lines 405-413, 416-423, 425-433, 435-443) so an implementer reading just the test file knows which pitfall section to consult."

requirements-completed: []  # CDOC-02 still open until Plan 02-04 emits the first asset-catalog/index.{json,md}; this plan adds the regression fixtures + RED test scaffolds, foundation work for the requirement.

duration: ~12min
completed: 2026-05-03
---

# Phase 2 Plan 01b: Mini-Extract Fixture + Pitfall 1-4 RED Scaffolding Summary

Wave 0 deliverable B for Phase 2 asset-catalog tooling. Lands the synthetic regression fixture tree + three test files (one with passing assertions, two with it.todo placeholders) that Plans 02-03 and 02-04 will use as their RED phase named targets.

## What was built

`tools/asset-catalog/tests/fixtures/mini-extract/` — a hand-authored 23-file synthetic project tree (3617 bytes total) that exercises every named pitfall from RESEARCH.md §"Pitfalls 1-4". The fixture is committed verbatim; tests read from disk. `build-mini-extract.ts` sits alongside as the canonical regen helper.

Three test files land alongside the fixture: `load.test.ts` (5 it.todo placeholders for Plan 02-03's loadExtracted body), `crossref.fixture.test.ts` (8 it.todo placeholders, one per pitfall mitigation + room transitivity, citing exact RESEARCH.md line ranges), `emit-determinism.test.ts` (4 PASSING tests against Plan 02-01's working determinism helpers + 1 it.todo for Plan 02-04's integration determinism check).

## Fixture file count + total bytes on disk

- **23 files** under `tools/asset-catalog/tests/fixtures/mini-extract/`
- **3617 bytes** total
- Tree shape:
  - `settings.json` (78 bytes)
  - `MANIFEST.sha256` (75 bytes)
  - 7 sprites × (meta.json + frames/img_000.bmp) = 14 files
  - 3 scripts × .gml = 3 files
  - 1 object × (meta.json + events/Step.dnd.json) = 2 files
  - 1 room × (meta.json + instances.json) = 2 files

## Test counts

| Test file | Passing | Todo | Total |
|-----------|---------|------|-------|
| `tests/structure.test.ts` (from 02-01) | 24 | 0 | 24 |
| `tests/emit-determinism.test.ts` | 4 | 1 | 5 |
| `tests/load.test.ts` | 0 | 5 | 5 |
| `tests/crossref.fixture.test.ts` | 0 | 8 | 8 |
| **Total** | **28** | **14** | **42** |

`pnpm test` exits 0; vitest reports `2 passed | 2 skipped` test files (vitest treats files with only `.todo` tests as file-level "skipped" — todos are still reported individually) and `28 passed | 14 todo` tests.

## Pitfall coverage map

| Pitfall | RESEARCH.md lines | Fixture file | What it tests |
|---------|-------------------|--------------|---------------|
| 1: word-boundary, not substring | 405-413 | `sprites/0000-Foo/`, `0001-FooBar/`, `0002-FooBaz/` + `scripts/0000-uses_foobar_only.gml` | `Foo` is a strict prefix of `FooBar`/`FooBaz`; the script mentions only `FooBar` in code → naive substring match would match all three |
| 2: block + line comment stripping | 416-423 | `scripts/0002-comments_only.gml` | `FooBar` appears only inside `// ...` and `/* ... */` comments; comment-stripped grep should yield zero references |
| 3: DnD typed argument values | 425-433 | `objects/0042-player/events/Step.dnd.json` | `argTypes[0] === 4` (Resource sprite) + `argValues[0] === "3"` (string sprite ID) — naive value scan would miss the type semantics |
| 4: multiple GML reference idioms | 435-443 | `sprites/0003-Hexport/`, `0004-HexportIn/`, `0005-HexportOut/`, `0006-HexportMask/` + `objects/0042-player/meta.json` (spriteId=3, maskId=6) + `scripts/0001-uses_hexportin_only.gml` | A sprite is "used" if ANY of {object.spriteId, object.maskId, `sprite_index = N`, `sprite_index == N`} matches; the family of look-alike names exercises the word-boundary rule simultaneously |

Bonus: room transitivity (RESEARCH line 712) — `rooms/0000-room1/instances.json` contains object 42 which uses sprite 3 (Hexport), so `Hexport.used_by.rooms` should include 0.

## Verification — all green

| Command | Expected | Actual |
|---------|----------|--------|
| `pnpm typecheck` (`tsc --noEmit`) | exit 0, clean | exit 0, clean |
| `pnpm test` | exit 0, 28+ passing + 13+ todo | exit 0, 28 passed + 14 todo |
| `head -c 2 sprites/0000-Foo/frames/img_000.bmp \| xxd` | `4242` (BM magic) | `4d42` little-end byte read = `4d 42` (xxd shows `424d` for "BM" — magic verified) |
| `git show :MANIFEST.sha256 \| od -c \| grep -c '\\r'` | 0 (no CR in committed blob) | 0 |
| `grep -c "^  it.todo(" tests/load.test.ts` | ≥ 5 | 5 |
| `grep -c "^  it.todo(" tests/crossref.fixture.test.ts` | ≥ 8 | 8 |
| `grep -c "Pitfall" tests/crossref.fixture.test.ts` | ≥ 4 | 20 (one per fixture-comment, well over the floor) |

## Stable scaffolding locked for downstream plans

**For Plan 02-03 (load/derive/crossref bodies):**
- `tests/load.test.ts` — 5 named RED targets describing the loadExtracted contract.
- `tests/crossref.fixture.test.ts` — 8 named RED targets, one per Pitfall 1/2/3/4 mitigation + room transitivity. Each describes the exact assertion expected.
- `tests/fixtures/mini-extract/` — committed fixture data the tests read from. No mocking needed.

**For Plan 02-04 (emit/cli/integration):**
- `tests/emit-determinism.test.ts` — round-trip + no-CR + sorted-keys + no-Date-references already passing. The `it.todo` integration slot is reserved for Plan 02-04's catalog-over-real-extracted determinism check.

## Determinism notes

- All JSON files written via the build script use the same recursive-key-sort + 2-space-indent + LF + trailing-newline shape that `src/emit.ts:writeJsonDeterministic` produces. Re-running the build script overwrites the same bytes (verified against the committed tree on disk).
- The 58-byte BMP is byte-identical across all 7 sprite frames (magic bytes alone matter for the cross-ref tests; the pixel data is irrelevant). Total on-disk overhead: 7 × 58 = 406 bytes.
- `MANIFEST.sha256` is committed as a synthetic placeholder string (`fixture-hash-not-real-d0a4b8c2e1f7`) — Plan 02-03's loadExtracted will read it verbatim into `result.manifestSha256` per the contract from 02-01.
- Cross-platform note: `core.autocrlf=true` on Windows host emits LF→CRLF warnings on `git add`, but the staged blob is LF-only (verified via `git show :path | od -c`). Repo-wide LF discipline preserved.

## Deviations from Plan

None — plan executed as specified. The plan's `<action>` block called for one combined task with all 27 file artifacts; this executor produced a single combined commit covering the fixture tree + build script + 3 test files.

The plan's tdd="true" annotation is honored implicitly: emit-determinism.test.ts is GREEN against working code (Plan 02-01's writeJsonDeterministic/stringifySortedJson are already implemented), while load.test.ts + crossref.fixture.test.ts are explicit RED scaffolding via `it.todo` — RED here means "named, pending, will be turned green by Plan 02-03". Vitest's `it.todo` reports todos as a non-failing status, satisfying the "tests RED at end of 02-01b" contract from the prompt.

## Self-Check: PASSED

- [x] All 23 fixture files exist on disk under `tools/asset-catalog/tests/fixtures/mini-extract/` (verified via `find . -type f | wc -l` = 23)
- [x] BMP magic bytes verified: `head -c 2 sprites/0000-Foo/frames/img_000.bmp` → `BM`
- [x] `tests/load.test.ts`, `tests/crossref.fixture.test.ts`, `tests/emit-determinism.test.ts` exist
- [x] `tests/fixtures/build-mini-extract.ts` exists
- [x] Commit `0c4415d` is in `git log`: `test(02-01b): mini-extract fixture + Pitfall 1-4 RED scaffolding [CDOC-02]`
- [x] `pnpm typecheck` exit 0
- [x] `pnpm test` exit 0 with 28 passing + 14 todo

## Commits

- `0c4415d` — `test(02-01b): mini-extract fixture + Pitfall 1-4 RED scaffolding [CDOC-02]` (27 files, +566 lines)
