Authoring a matrix case
CLAUDE.md invariant 2: every decision rule in packages/playback-engine lands with matrix cases in the same PR. This page is what that means in practice.
What the matrix is
packages/playback-engine/matrix/ holds one YAML file per test case — currently 512 of them (docs/PLAYBACK.md §10 sets a Phase 3 exit bar of "≥ 500 cases"; the matrix already exceeds it) — plus a manifest file, burnup.json, that records every case's name and current status.
A case looks like this (trimmed from matrix/013-mp4-direct-play-hevc-sdr-main10.yaml):
name: direct-play — mp4 hevc main10@123 10-bit 2160p + aac 6ch, hevc-sdr (SDR source, no HDR involved)
why: "Stage A (docs/PLAYBACK.md §3): mp4 IS in hevc-sdr's directPlayContainers, and every SELECTED stream is copy-eligible under Stages B-F's current permissive stubs, so the decision is direct-play with reasons=[]. …"
input:
media: { ... }
device: { ... }
network: { ... }
policy: { ... }
caps: { ... }
selection: { ... }
mode: { ... }
expect:
decision: ...
reasons: [ ... ]Every case's why field cites the spec section it's proving — a matrix case is a claim about docs/PLAYBACK.md's decision algorithm, not just an arbitrary input/output pair.
The regression law
Two mechanisms enforce this, both running as part of the normal test suite:
matrix-meta.spec.tsasserts the case count matchesburnup.json's total, and that the set of on-disk case files exactly equals the set of filenamesburnup.jsonlists — no case can be silently added without a manifest entry, and no manifest entry can point at a file that doesn't exist.matrix.spec.tsre-runs every case's actual input throughplan()and checks the real output againstburnup.json's recorded status for that case. This is the regression law in force: if you change decision logic and a case's outcome flips from whatburnup.jsonsays it should be, this fails — you cannot "green" a case's behavior without updatingburnup.jsonto say so, in the same PR, which is exactly the point. This spec runs viapnpm test:matrix(a separate Vitest project from the defaulttestrun), and CI runs it explicitly as its own "playback matrix burn-up" step.
Adding a new decision rule + case
- Write the stage logic in
packages/playback-engine/src/stages/*.ts(orsrc/plan.tsfor top-level orchestration changes). - Add a new case file:
matrix/NNN-short-description.yaml, following thename/why/input/expectshape above. Pick the next unused number. - Update
matrix/burnup.jsonin the same PR to include the new case with its correct status —matrix-meta.spec.tsfails otherwise. - Run
pnpm test:matrixlocally before opening the PR — it's the same check CI runs as its dedicated step, so there's no reason to find out about a mismatch only after pushing.
Reason codes
Every decision carries one or more reason codes from a closed enum (docs/PLAYBACK.md §4, "Reason taxonomy") — adding a new reason code is a contract-level change to that spec section, not something to invent inline in a case file. If your new rule needs a reason code that doesn't exist yet, that's a spec change first, matrix case second.