WootBuild.

Studio/docade/Advisory 06

Advisory · advisory-06.md

Advisory 06 — the silhouettes have no keys, and may not need any

To: docade · From: WootBuild · Date: 2026-08-19 Status: RESOLVED 2026-08-19 — docade decided, and corrected us Trigger: delivering the 60 plush sculpts. art deliver asked where a silhouette would go and found there is nowhere for it to go.


The finding

Your 14-ASSET-MANIFEST.md §5 says:

The collection view shows silhouettes for uncollected items — that's 18 more images at face value. Don't commission them. They're a deterministic transform of the alpha channel: flatten to a single fill at low opacity.

We agree with every word of that. The problem is that §5 is prose, and assets/asset-list.csv is the work-list.

There is no manifest row for a single silhouette. No key, no path, no size, no category. Nothing in the CSV mentions one. So the deterministic transform that costs nothing to run has nowhere to write its output, and producing them would mean WootBuild inventing 60 filenames that collectibles.image_key reads as foreign keys — which is the one thing we will not do (your §6, our ATL-10).

And "18" is stale. It was written when the catalogue held 18 stuffies. It holds 60. Whatever is decided here is decided for sixty pieces, not eighteen.

The question

Does an uncollected silhouette need to be a file at all?

That is the whole decision. Everything else follows from it.

Our recommendation: no — derive it at render time

All 60 delivered sculpts are 512×512 PNG with a real alpha channel. That makes the silhouette a CSS mask over the sculpt you have already loaded:

.stuffy--uncollected {
  -webkit-mask-image: url("/assets/stuffies/forest/fox.png");
          mask-image: url("/assets/stuffies/forest/fox.png");
  mask-size: contain;
  mask-repeat: no-repeat;
  background: var(--silhouette-fill);   /* any colour, any opacity */
}

Same source file, same key, no second asset. And it is strictly better than a file for a reason that has nothing to do with saving work:

A file bakes the fill colour and the opacity in permanently. Your §5 already names both — "a single fill at low opacity" — and the moment they are 60 PNGs, they are 60 PNGs forever. Restyling the collection view, adding a dark mode, or deciding the silhouette should be 30% rather than 25% means regenerating and redelivering sixty files. As a mask, all of it is one CSS variable.

It also cannot drift. Re-render a sculpt and its silhouette is already correct. As files, a revised sculpt silently leaves a stale silhouette behind, and nothing on either side would complain — the same shape as the Done column.

If you want files anyway

Entirely reasonable — a mask is a rendering constraint, and you may have reasons we cannot see (a native surface, an email, an OG image, a pre-render step).

Then we need 60 rows in asset-list.csv, with keys you choose and we never invent. Say the word and it is cheap on our side: the transform is deterministic, costs no art budget, and we would deliver them the same way the sculpts landed. What it costs you is a permanent second asset per collectible, and the drift above.

What we need back

One line is enough:

Nothing is blocked on this. The 60 sculpts are delivered and the collection view can be built against them today either way.

Not this advisory

app-icons/badge-72 — "same for the monochrome notification badge" in your §5 — is a different case. It has a declared key and a declared path (public/icons/badge-72.png, 72px), so it is not blocked on anything here. It is simply unproduced, and it sits in the brand lane. The only open question there is what it derives from, which we will ask separately.


Resolved — 2026-08-19

docade's answer: derived, with no keys — and to a FILE, not a CSS mask.

Both halves of our recommendation were assessed. One was right for a reason we had not articulated as well as they did, and one was wrong.

Right: no keys

A key is a unit of work in a list they bill against; sixty derived rows read as sixty pieces to draw, which is advisory 05's failure pointing the other way.

Better than our version. We argued no-keys from drift and delivery cost; the load-bearing reason is that a manifest key is a commitment to make something, and sixty rows for images nobody draws is the same category error as a lane claiming pieces at a size it cannot produce — just pointing the other way.

Their deeper framing, which we are adopting: a sculpt is one identity with two renderings. collectibles.image_key stores forest/fox and the silhouette path derives from it, so no second foreign key exists to go stale, and §6 is satisfied without inventing anything.

Declared in stuffies.derived[] with a path and a transform and no keys, produced by npm run assets:silhouettes, and stated in ART-BRIEF.md under "Derived — never on your work-list" — because a decision they cannot see is a decision they never got.

Wrong: the CSS mask

We recommended a mask over the delivered sculpt, and argued it from parameters staying variable and from never drifting. Both true. Both irrelevant, because:

It ships the real artwork to a child who hasn't collected the item, and CLAUDE.md §4 says assume a curious 11-year-old opens devtools.

That is decisive and we missed it entirely. A render-time derivation is a transform applied in the viewer's process, so the source must arrive there. The very property we were selling — no second asset — is the property that breaks it: the hidden state and the revealed state become the same download, one network-tab click apart. For a collection game, the uncollected silhouette is the entire tension of the product, and we proposed shipping the answer with the question.

Their derived file is one flat ink, every RGB channel a single constant, 13 KB against the source's 190 KB. The size difference is the security property, stated in bytes.

Recorded as craft, loaded into every compile for every client: docs/corpus/craft/derive-to-a-file-when-the-source-is-hidden.md. The test it carries: ask whether the derived form is a presentation (derive at render time) or a redaction (derive to a file, never ship the source). We asked the drift question before the access question, and only one of those cannot be fixed later.

Also corrected

18 → 60 is fixed on their side, and check:manifest now asserts the number against the manifest so it cannot drift again.

§5 was wrong about the notification badge. It claimed badge-72 was derived too. It is not — advisory 05 established it is separate artwork, because Android tints it and discards detail. Our own §"Not this advisory" note said badge-72 was a different case for a different reason (it has a key); the real reason is stronger, and it means the badge is genuinely art to be made rather than a transform to be run.