17 — Standards: naming, numbering, labelling, state
Justin, 2026-08-19: "standards about process, tagging, naming, numbering, labeling, states, et cetera, having that be consistently integrated on each and every build."
Every identifier this studio produces is listed here, with one owner in code and, wherever it is possible, one check that fails if it drifts. A standard that lives only in a document is a preference; the column that matters below is "enforced by."
The rule behind all of them: an identifier that changes when the thing it names has not changed is not an identifier. Everything mutable — verdict, state, rarity, style version, file path — is a field, never part of a name.
The identifiers
| Thing | Form | Example | Owner | Enforced by |
|---|---|---|---|---|
| Client | lowercase slug, no spaces | docade | directory name under clients/ | art client sync |
| Lane | lowercase slug | plush | key in client.yaml lanes: | unknown lane fails loudly and names the real ones |
| Style | <id>@<version> | docade-plush@1 | lib/style.mjs | every committed style.yaml parses |
| Asset id | CLI-LAN-NNNN | DOC-PLU-0042 | lib/assetid.mjs | well-formed + globally unique |
| Manifest key | <set>/<piece> | frostline/narwhal | the client's own CSV | it is a foreign key — see below |
| Run id | r-YYYYMMDD-xxxx | r-20260819-b3b6 | lib/pipeline/run.mjs | date-sortable by construction |
| Candidate | NNN-<subject>-<variant> | 005-narwhal-a | lib/pipeline/generate.mjs | index-ordered, stable within a run |
| Lesson | L-NNN per style | L-010 | styles/<s>@<v>/lessons.md | append-only; numbers never reused |
| Craft entry | topic slug | set-coherence | docs/corpus/craft/ | loaded into every compile |
| Model corpus | router key, not vendor id | gemini-flash-image.md | docs/corpus/models/ | routed model must have an entry whose model_id matches the router |
| Issue | ATL-N | ATL-24 | Linear team ATL | Linear is a view; files win any disagreement |
| Canon doc | NN-TITLE.md | 15-LANE-WORKFLOW.md | docs/ | numbers are permanent; a superseded doc is rewritten, not renumbered |
| Contact sheet | contact-<lane>-at-<px>.png | contact-plush-at-80.png | art contact | regenerable — never hand-made |
| Set | lowercase slug, first segment of the manifest key | sunbaked | the client's own CSV | a unit of judgement and a section of the board — never its own file |
| Board | one per lane | docade/plush | lib/pipeline/board.mjs | art review <target>; sets are sections on it |
| Target | <client>/<lane>[/<set>], or a run id | docade/plush | lib/pipeline/sets.mjs | what art review and art verdict both take |
| Piece name | leaf, or full manifest key | jackalope · sunbaked/jackalope | resolvePiece in lib/pipeline/sets.mjs | ambiguity is an error naming both, never a guess |
Asset id
DOC-PLU-0042
└┬┘ └┬┘ └─┬┘
│ │ └── sequence, per client + lane, never reused
│ └─────── lane, three letters
└─────────── client, three letters
Assigned before the image exists, so a slot that fails to generate still has a name — a retryable slot with no identity is indistinguishable from one nobody asked for. Derived by scanning sidecars rather than from a counter file, because a counter is a second source of truth that can disagree with the files.
Never encoded: style version, rarity, set, verdict, file path. All of those change over an asset's life.
Manifest key — treat it as a foreign key
<set>/<piece> is not a label, it is collectibles.image_key in the client's own database. A subject typed by hand can drift from the key by one character and deliver art the app cannot see. Always brief from the manifest (art brief --from-manifest), which pulls the key, the path, the rarity and the client's own brief text together.
Target — what a board is a board of, and what a verdict addresses
A run id addresses a billing event. A target addresses what a person sits down to review. They are different things and not interchangeable: one set routinely spans several runs, because a re-run of one piece is its own run.
art review docade/plush art verdict docade/plush --approve gecko,pika
art review docade/plush/sunbaked art verdict docade/plush/sunbaked 111411
art review r-20260819-b3b6 art verdict r-20260819-b3b6 1123
One board per lane. A set is a unit of judgement (16-PRODUCTION-DOCTRINE.md level two) and a section of that board — it is not its own artifact. The rule, because it was got wrong in both directions inside one session:
The judging unit decides the layout. The reviewer's sitting decides the file.
Verdicts address a piece by name, not by position. A name is the piece's leaf when that is unique in the lane, or its full manifest key always. A positional string survives for one set or one run — a small grid you can count, and a run's candidates have no manifest name to be called by.
Positions were the original grammar and they do not scale past a screen: sixty characters of which fifty-eight mean "leave alone" is not a command anyone can check, and every surface that indexes into a position has to agree on an ordering forever. Names are order-independent, readable in a commit a year later, and the same words a person says out loud.
Vocabulary that is one letter apart
approve is a verdict. approved is an asset state. They are different vocabularies and the difference has already shipped a bug: the console's board page compared piece.state === 'approve' and rendered 0/6 for every set while the header above it correctly read 20 of 60.
- A verdict is what a human said:
approve·reject·revise·promote. - A state is what an image is:
approved·rejected·revise·promoted·delivered·machine-approved·in-review·qa-failed·superseded·failed. Decided only inlib/states.mjs.
Never compare a state to a literal. Ask stateMeta(state).ships. npm run check greps the console for .state === '<verdict>' because nothing else would have caught it.
Labelling: every image carries a state, always
lib/states.mjs is the only place an image's state is decided, and every surface calls it. There is no unlabelled asset: a candidate with no verdict is in-review, which is a state, not an absence.
| State | Ships | Means |
|---|---|---|
delivered | ✅ | Written into the client's repo at its real key path |
promoted | ✅ | Human-approved and promoted to canon — the compiler reads it later |
approved | ✅ | A human said yes |
machine-approved | ❌ | The grader passed it. Not seen by a human, does not ship on this alone |
in-review | ❌ | Generated, waiting on a verdict |
revise | ❌ | A human asked for a change. Re-running costs money, so never automatic |
qa-failed | ❌ | Failed mechanical QA |
rejected | ❌ | A human said no. Kept with the reason |
superseded | ❌ | Replaced by a later candidate for the same piece |
failed | ❌ | The provider returned nothing. A retryable slot, not an asset |
The four rules, each closing a bug that actually happened
- State is recorded, never inferred. Not from a directory, not from which step ran last. A normalized file once appeared under a heading reading ready to ship purely because normalize had run on it.
- A step that changes what an asset IS must update its state.
normalizeonce wrote the cut-out file without touching the sidecar, so a generation-time "no alpha channel" verdict outlived the step that supplies the alpha and 28 finished assets reportedqa-failed. - One candidate is current; everything else is history — on the board, in Approved, and in every queue. Filtering a queue on "the key is settled" is not the same rule, and it let three attempts at one piece all ask to be judged.
- A grader verdict is not a human verdict. Every verdict records
verdict_by, andmachine-approvedis its own non-shipping state. Autonomy 2 and 3 let the grader write through the same code path — that is the design — but a machine pass must never be indistinguishable from a person's.
Tagging: attribution and reasons
| Field | On | Means |
|---|---|---|
verdict | candidate sidecar | approve · reject · revise · promote |
verdict_by | candidate sidecar | human or the grader. Never omitted |
critique | candidate sidecar | why — and History groups by this, because the reason is the reusable part |
critique_by | candidate sidecar | who wrote the reason |
qa | candidate sidecar | the check that reflects the file as it is now |
qa_at_generation | candidate sidecar | the original check, kept because "the model cannot emit alpha" stays true |
asset_id | candidate sidecar | assigned before the image exists |
hue / hue_name | brief subject | computed by lib/hueplan.mjs, validated before the estimate |
A superseded candidate without a critique is a lost lesson. Ten pieces redone for one reason is one lesson, not ten events — which is why History groups by reason rather than by date.
Piece kind — what a lane assumes is uniform, and what is not
Owner: lib/piecekind.mjs. Declared per piece in the style sheet as kind:.
A lane means one production method. It does not mean one file shape, and it took two failures on the same lane in one day to establish that:
| Assumed uniform | Actually per piece | Fixed by |
|---|---|---|
| Size | docade/crane is one visual family at 1024, 512, 256 and 128 | ATL-39 — spec: from-manifest, one run per size |
| Ground | the claw keys out; the cabinet carries its own lit interior | kind: — one run, per-piece mechanics |
| Kind | The runtime | Alpha | Cutout | Ground asserted in the prompt |
|---|---|---|---|---|
moving (default) | moves it independently | yes | yes | drawn alone on the lane's flat keyable colour |
scene | composites over it | no | no | drawn as a lit scene carrying its own background and depth |
Three rules make it a standard rather than a field:
- A kind decides mechanics only — alpha, cutout, ground clause. Never register, palette, construction or light: those are what make fifteen files belong to one machine, and a kind that started deciding them would be a second style sheet wearing a mechanical name.
- The default is
moving, and it is the safe default rather than the common one. An unnecessary cutout is visible in review; a missing one delivers a welded background into a client repo. - A mixed-kind lane states no lane-wide ground rule.
qa.must,normalizeandspec_defaultshold the base; the kind swaps its own in. Enforced —npm run checkfails on a ground assertion in a mixed lane's lane-widemust.
Why the check exists. docade-crane@4 split the ground rule per piece in prose and left @3's "flat #262c4a ground, uniform across the whole frame" in the lane-wide qa.must. The first probe came back correct against the new register and failed mechanical QA against the old declaration — "spec requires transparency and the file has no alpha channel". Prose moved; three declarations did not. That is docs/16's line, and this is it enforced.
Unlike size, transparency does not split the run: no routed model emits alpha, normalize supplies it, so it is a downstream stage rather than a routing constraint. One run, one model, one estimate, two contracts.
The architectural half of this — whether cabinet-frame should be a component at all or a composed plate — is clients/docade/advisory-13.md, and only the client can answer it.
Process standards
| Standard | |
|---|---|
| Verdict grammar | one keystroke per candidate in grid order — 1 approve · 2 reject · 3 revise · 4 approve+exemplar · . undecided |
| Batch size | six, matching a set. Big enough to judge coherence, small enough that a bad batch costs one set |
| Spend | every generating command estimates first. art brief prices for free |
| Commits | say what changed and what was wrong — the corrections are the valuable part |
| Linear | maintained continuously, not at session end. Close when work lands; file when work is discovered |
| Lessons | never speculative. Every entry names the run that produced it |
| Canon | docs/ outranks anything inferred from code. If they disagree, the code is wrong or the doc is stale — say which |
| Piece kind | a lane may hold more than one mechanical contract. Declared per piece, never inferred from a name — see above |
Adding a standard
- Give it exactly one owner in code.
- Add a row above.
- Add a check to
scripts/check.mjsthat fails when it drifts.
If step 3 is impossible, say so in the row. An unenforceable standard is worth having and worth being honest about; an unenforceable standard presented as enforced is how the studio ends up believing things that stopped being true.