ec61ce77f8
The ETag shipped in fe9c4f49 is keyed on (source_hash, size, format), which is one term short: a thumbnail is a function of those PLUS the renderer. Change the encoder or a quality setting and identical inputs produce different bytes under an unchanged ETag — the same staleness class the commit fixed, one level down. It bites when an already-cached thumbnail is re-rendered after a renderer change. Keying on the derived blob's own hash removes the term entirely: the ETag IS the hash of the bytes, so any output change invalidates by construction. It is self-consistent for free, because store_derived_blob is ON CONFLICT DO NOTHING — a re-render never displaces the stored row, so the ETag always equals what the derived tier will serve. No renderer version constant to remember to bump. Records why it cannot land yet. The derived tier is read LAST by design, so an ETag naming the derived hash would describe a tier the response probably did not come from; sidecar and derived agree at creation but diverge if a sidecar is re-rendered while the derived row stays pinned by DO NOTHING. An ETag that lies about the body is worse than one that is merely coarse. Also the tier is WebP-only (variant is the size, with no format term) and empty for anything predating this work until derived_import backfills. So it lands at step 10 with the flip, keeping today's form as the fallback for ungenerated variants and formats the tier does not hold. The LEFT JOIN already planned for the read path returns the derived hash in the same query, so it costs no extra round-trip.