fix(udg): one fit mapping, emitted — and a review artifact that self-checks #153

Merged
peterswimm merged 1 commit from fix/udg-press-recipe-integrity into main 2026-09-16 20:25:03 +00:00
Owner

Three defects in the press tier I shipped in #148 / #149 and had not noticed. All three are the same failure mode: something that looks right and is not.

Scope

1 · The review artifact ignored fit. scratchpad/build_press_review.py hardcoded mask-size:100% 100% and never forwarded --pN-size, so every pull rendered as fit: stretch whatever its token said — in the very artifact ville reviews, which #148 described as "rendered from the real tokens, no mockups". It did not bite only because the one non-frame pull happens to be stretch.

Fixed at the root, not the symptom: the fit → mask-size/repeat mapping was being re-derived independently by each of three consumers. It now lives in extract.py and is emitted as --press-*-N-mask-size / -mask-repeat. A consumer can only read it; artifact.py's local _FIT_MASK is gone.

The three CSS recipes stay three. The design board, the review page and the doc's copy-paste block serve different audiences (board chrome, review chrome, teaching with full var names) and a shared constant would wreck the third. Consolidating the mapping was the fix; consolidating the CSS was a trap.

2 · native.py dropped the slice fields. _press_rows stopped at fit, so a Flutter/SwiftUI consumer received fit: 'slice' with none of the slice geometry — an unrenderable pull. Both emitters now carry every key the row holds (Dart formats by type; Swift gains optionals), resolved from the plate in assets.yml, where that geometry is declared: "the corner size belongs to the drawing, not to each press".

3 · No test read any recipe. Which is precisely why they drifted.

Testing

just check green · 231 tests · design board probed directly.

  • Each new drift test was verified to fail when the original bug is reintroduced, not trusted because it passed. Two had to be rewritten to get there: the first matched prose that merely mentions mask-size, and the second was line-based — so a line carrying both a var-driven and a hardcoded declaration was excluded wholesale, and it passed while the bug was still present. It now examines each declaration.
  • The design board was probed on its own, via its own recipe copy and its own plate-inlining path — the copy the review page does not exercise, and the one most likely to break. Masks resolve, geometry comes from the emitted var, boxes are non-zero.
  • The presses render byte-identically: of four stills, only the fit-comparison changed.

The review artifact now self-checks

A short script walks every rendered pull box and prints its size and mask state into the page, flagging any box that lost its mask or collapsed to zero.

Every press bug in this whole effort was invisible — a dropped mask floods the panel with solid pigment and just check stays green through it. This turns that class of failure into text in the committed still.

It found a failure on its first run: the substrate-only degradation panel, which has no mask by design. Now marked data-nomask and reported as "ok (no mask by design)" rather than flagged — the distinction between lost its mask and never had one is exactly what the check needs to know.

Impact Statement

Behaviour-preserving for the shipped presses. The two spelwork presses render identically; skin.css gains two vars per pull. dist/ churn is spelwork plus the review artifact. Versions bumped in meta.yml and sync-manifest.yml.

One real behaviour change: a fit: tile, contain or cover pull now renders correctly in the review artifact instead of being flattened to stretch. Nothing in the repo authors one today, so nothing visibly moves — the fix is for the next press, and for the trust the artifact is supposed to carry.

Native consumers gain fields, lose none. udgPressMap is emitted for no consumer today (spelwork has the press; rituals has the consumers), so no shipped dist/ bytes change for Rituals.

Reversibility: high. The mapping move is the only structural change and it is one constant plus three read sites.

Not addressed here: scratchpad/press-review.html is committed but not gated by just check, so it can still drift from dist/. Worth deciding separately — either gate it or state in the script that it is a point-in-time artifact.

Three defects in the press tier I shipped in #148 / #149 and had not noticed. All three are the same failure mode: something that looks right and is not. ## Scope **1 · The review artifact ignored `fit`.** `scratchpad/build_press_review.py` hardcoded `mask-size:100% 100%` and never forwarded `--pN-size`, so **every pull rendered as `fit: stretch`** whatever its token said — in the very artifact ville reviews, which #148 described as *"rendered from the real tokens, no mockups"*. It did not bite only because the one non-frame pull happens to be `stretch`. Fixed at the root, not the symptom: the `fit` → mask-size/repeat mapping was being **re-derived independently by each of three consumers**. It now lives in `extract.py` and is *emitted* as `--press-*-N-mask-size` / `-mask-repeat`. A consumer can only read it; `artifact.py`'s local `_FIT_MASK` is gone. The three CSS recipes stay three. The design board, the review page and the doc's copy-paste block serve different audiences (board chrome, review chrome, teaching with full var names) and a shared constant would wreck the third. **Consolidating the mapping was the fix; consolidating the CSS was a trap.** **2 · `native.py` dropped the slice fields.** `_press_rows` stopped at `fit`, so a Flutter/SwiftUI consumer received `fit: 'slice'` with none of the slice geometry — an unrenderable pull. Both emitters now carry every key the row holds (Dart formats by type; Swift gains optionals), resolved from the **plate** in `assets.yml`, where that geometry is declared: *"the corner size belongs to the drawing, not to each press"*. **3 · No test read any recipe.** Which is precisely why they drifted. ## Testing `just check` green · **231 tests** · design board probed directly. - Each new drift test was verified to **fail when the original bug is reintroduced**, not trusted because it passed. Two had to be rewritten to get there: the first matched *prose* that merely mentions `mask-size`, and the second was line-based — so a line carrying both a var-driven and a hardcoded declaration was excluded wholesale, and **it passed while the bug was still present**. It now examines each declaration. - **The design board was probed on its own**, via its own recipe copy and its own plate-inlining path — the copy the review page does not exercise, and the one most likely to break. Masks resolve, geometry comes from the emitted var, boxes are non-zero. - The presses render **byte-identically**: of four stills, only the fit-comparison changed. ### The review artifact now self-checks A short script walks every rendered pull box and prints its size and mask state into the page, flagging any box that lost its mask or collapsed to zero. Every press bug in this whole effort was **invisible** — a dropped mask floods the panel with solid pigment and `just check` stays green through it. This turns that class of failure into text in the committed still. It found a failure on its first run: the substrate-only degradation panel, which has no mask **by design**. Now marked `data-nomask` and reported as *"ok (no mask by design)"* rather than flagged — the distinction between *lost its mask* and *never had one* is exactly what the check needs to know. ## Impact Statement **Behaviour-preserving for the shipped presses.** The two spelwork presses render identically; `skin.css` gains two vars per pull. `dist/` churn is spelwork plus the review artifact. Versions bumped in `meta.yml` and `sync-manifest.yml`. **One real behaviour change:** a `fit: tile`, `contain` or `cover` pull now renders correctly in the review artifact instead of being flattened to `stretch`. Nothing in the repo authors one today, so nothing visibly moves — the fix is for the next press, and for the trust the artifact is supposed to carry. **Native consumers gain fields, lose none.** `udgPressMap` is emitted for no consumer today (spelwork has the press; rituals has the consumers), so no shipped `dist/` bytes change for Rituals. **Reversibility:** high. The mapping move is the only structural change and it is one constant plus three read sites. **Not addressed here:** `scratchpad/press-review.html` is committed but not gated by `just check`, so it can still drift from `dist/`. Worth deciding separately — either gate it or state in the script that it is a point-in-time artifact.
fix(udg): one fit mapping, emitted — and a review artifact that self-checks
All checks were successful
PR policy check / policy-gate (pull_request) Successful in 0s
Security Scans / Gitleaks secret scan (pull_request) Successful in 27s
Security Scans / Trivy filesystem scan (pull_request) Successful in 31s
Test Generator / lint-workflows (pull_request) Successful in 16s
Test Generator / lint-prose (pull_request) Successful in 21s
UDG dist drift gate / dist/ matches dtcg + udg sources (pull_request) Successful in 14s
Block Validation / validate (pull_request) Successful in 54s
Copy coverage / Design copy reaching rendered HTML (pull_request) Successful in 28s
Test Generator / test (pull_request) Successful in 3m21s
6a914faf40
Three defects in the press tier I shipped in #148/#149 and had not noticed.
All three are the same failure: something that looks right and is not.

1. The review artifact ignored `fit`. scratchpad/build_press_review.py
   hardcoded `mask-size:100% 100%` and never forwarded --pN-size, so EVERY
   pull rendered as `fit: stretch` whatever its token said — in the very
   artifact ville reviews, which #148 described as "rendered from the real
   tokens, no mockups". It did not bite only because the one non-frame pull
   happens to be stretch.

   Root cause, not symptom: the fit -> mask-size/repeat mapping was re-derived
   independently by each of three consumers. It now lives in extract.py and is
   EMITTED as --press-*-N-mask-size / -mask-repeat. A consumer can only read
   it. artifact.py's local _FIT_MASK is gone.

   The three CSS recipes stay three — the design board, the review page and the
   doc's copy-paste block serve different audiences (board chrome, review
   chrome, teaching with full var names), and a shared constant would wreck the
   third. Consolidating the MAPPING was the fix; consolidating the CSS was a
   trap.

2. native.py dropped the slice fields. _press_rows stopped at `fit`, so a
   Flutter/SwiftUI consumer received fit: 'slice' with none of the slice
   geometry — an unrenderable pull. Both emitters now carry every key the row
   holds (Dart formats by type, Swift gains optionals), resolved from the PLATE
   in assets.yml, where slice geometry is declared: "the corner size belongs to
   the drawing, not to each press".

3. No test read any recipe. That is precisely why they drifted. Added, and each
   was verified to FAIL when the original bug is reintroduced rather than
   trusted because it passed. Two of them had to be rewritten to get there: the
   first matched prose that merely mentions mask-size, and the second was
   line-based, so a line carrying both a var-driven and a hardcoded declaration
   was excluded wholesale — it passed while the bug was still present.

Also: the review artifact now SELF-CHECKS. A short script walks every rendered
pull box and prints its size and mask state into the page, flagging any box
that lost its mask or collapsed to zero. Every press bug this session was
invisible — a dropped mask floods the panel with solid pigment and `just check`
stays green through it. It found a failure on its first run (the substrate-only
degradation panel, which has no mask BY DESIGN — now marked data-nomask and
reported as such rather than flagged).

Verified: just check green; 231 tests; the design board probed directly via its
own recipe and plate-inlining path (the copy least exercised by the review
page); presses render byte-identically — only the fit-comparison still changed.
peterswimm deleted branch fix/udg-press-recipe-integrity 2026-09-16 20:25:03 +00:00
Sign in to join this conversation.
No description provided.