T3.2 - C3/S5: one source of truth for colour; page sheets alias only

theme-light.css is now the only file in html/ that contains a colour literal.
The five page stylesheets and all four inline <style> blocks declare names and
nothing else.

  theme-light.css                 191 declarations, 175 with a literal value
  console.css                      28 declarations,   0
  work-package-suite-styles.css    16 declarations,   0
  wp-chrome.css                    14 declarations,   0
  wp-creation-styles.css           24 declarations,   0
  wp-sidenav.css                    0 declarations,   0

#0f62fe is declared in one sheet, down from five. The eleven occurrences left
inside theme-light.css are Carbon's own v10-to-v11 alias layer, which the
inventory records as deliberate and not the S5 defect.

Names were kept, because 111 var() references live in .js files across 23 token
names and a rename there fails silently - no build error, no console warning,
just an unstyled element.

The rule the refactor was built on: consolidation is not unification. Where two
sheets declared the same value, they collapse. Where they declared DIFFERENT
values for one role - the two shadows, the eight status borders doing four jobs,
the three mono stacks - each value got its own canonical name and the pair is
recorded for T3.5. Picking a winner between two near-identical greys is a
rendered change, which this task forbids. The console's zebra stripe is the one
that would have bitten: #fafafa is six points from #f4f4f4, and merging them
erases the striping on the nine-column user table.

Collecting the one-offs in one place made two things countable that were not
before: twelve distinct shadows, and a ninth amber (#8a6d00 on the field view,
four points from #8e6a00 and doing the same job - BL-009).

VERIFICATION - the screenshot done-when could not do the job, so it was replaced.

Captured against wave 2, 11 of 14 shots were pixel-identical and 3 were not.
Capturing wave 2 against ITSELF produced the same 3 differences at the same
bounding box, so those shots cannot distinguish a regression from the clock.
Trap 2 in the brief is half wrong: users.html is stable at both widths; the
unstable third is the creator at 1440px, and admin's captured page height varies
by ~600px between runs (BL-012).

So tests/token_check.py was added. It checks what wave 3 actually claims: that
every custom property resolves to the same literal, and every element computes
the same colours, shadows and type. That is stronger than a screenshot - it
covers the hover, focus and disabled rules a screenshot never exercises, and it
is deterministic.

  wave 2 vs T3.2, all 7 pages:
  178/178 wave-2 token names resolve identically, +213 new
  3,500 elements compute identically, zero added, zero removed
  16 tokens differ in notation only (#fff -> #ffffff), which is the duplicate
  class this task existed to collapse

Two detours worth not repeating: the element walk was first keyed by sibling
index and reported 55 phantom differences on the SOP page, where three
JS-injected overlays append in whichever order their async work finishes
(BL-011); and the comparator now normalises notation before reporting, because
otherwise it fails on its own success.

f_items 5 FIXED / F6 REPRODUCES as expected. browser_check 71/71.

ONE DONE-WHEN NOT MET, recorded rather than skipped: "no page stylesheet
declares a raw color, spacing or type value". The colour half is met in full.
483 raw spacing values, 281 font-sizes and 65 radii remain inside rules, 492 of
them in the creator. That is arithmetic, not effort: the creator's spacing is
every integer from 1px to 14px, so no token exists that padding:9px 11px maps to
without changing one of the numbers - and this task forbids changing a rendered
value. The two requirements are mutually exclusive. Logged as BL-010 for T5.x
and T7.1, where those pages are re-laid-out and the values get chosen again.

New backlog: BL-009 (ninth amber), BL-010 (raw spacing/type in rules),
BL-011 (overlay append race), BL-012 (unstable screenshot targets).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-15 18:50:30 -05:00
parent 0e40e967a0
commit 9ab7b48de2
14 changed files with 964 additions and 266 deletions

View File

@@ -199,8 +199,17 @@ python server/smoketest.py # API; needs WP_SMOKE_USER / WP_SMOK
python tests/browser_check.py # pages boot and render, self-contained
python tests/baseline_shots.py # screenshots, self-contained
python tests/f_items.py # does each of F1-F6 still reproduce?
python tests/token_check.py --out a.json # every resolved token + computed style
python tests/token_check.py --compare a.json b.json
```
`tests/token_check.py` was added by `T3.2`, because a screenshot cannot prove a token refactor:
three of the fourteen shots are not stable capture-to-capture (see `backlog.md` BL-012), and no
screenshot exercises a hover, focus or disabled rule, which is where half the tokens live. It
snapshots every custom property's resolved value and every element's computed colours, shadows
and type, on all 7 pages, and diffs two snapshots. Use it for any task that claims to change
styling without changing appearance.
`server/smoketest.py` is the one that is **not** self-contained: it drives a server you point
it at and aborts unless `WP_SMOKE_USER` and `WP_SMOKE_PASSWORD` are set, because every route
but `/api/health` needs a session. Use an admin account — it creates and deletes a project.

View File

@@ -1,11 +1,13 @@
# Token inventory — every declared and raw value in `html/`
**Task:** `T3.1` · **Items:** `C3` part 1, `S5` part 1 · **Produced:** August 15, 2026
**Task:** `T3.1`, updated by `T3.2` · **Items:** `C3`, `S5` · **Produced:** August 15, 2026
**Branch:** `feat/wp-suite-r2-implementation` · **Read by:** `T3.2`, `T3.5`, `T9.3`
This is the mapping table `T3.2` builds from. Nothing here changes a stylesheet — `T3.1` is
inventory only. Everything below was read from the working tree at `b105135` (end of wave 2),
not quoted from the review.
`T3.1` produced this as the mapping table `T3.2` builds from; `T3.2` has since been built and
§2 and §11 record what actually shipped rather than what was proposed. Sections 3 to 10 are
the **pre-`T3.2` inventory** and are deliberately left as they were — they are the evidence
for the refactor, and rewriting them into the past tense would destroy the before-picture that
`T9.3` needs. Everything in them was read from the working tree at `b105135` (end of wave 2).
**Read [§8 Near-duplicates](#8-near-duplicates--where-unintended-visual-change-will-come-from)
before writing a single line of `T3.2`.** That section is the whole risk in this wave.
@@ -59,6 +61,11 @@ the launcher's inline `<style>`.
**202 custom-property declarations in total.** Plus the raw literals in §5.
> **Corrected during `T3.2`:** row 7 is not the only inline `<style>`. `login.html` (3 literals)
> and `field.html` (26 literals, including the ninth amber at §8-K) carry one too, and
> `work-package-suite.html:407` has a colour in a `style=` attribute. All four were converted.
> Counting only the launcher's was an inventory miss, not a scoping decision.
### Out of `T3.2` scope, stated so nobody goes looking
| Surface | Why it is out | Where it goes |
@@ -71,7 +78,7 @@ the launcher's inline `<style>`.
---
## 2. The proposed canonical set
## 2. The canonical set — as built (`T3.2`)
`theme-light.css` is already the source of truth in everything but name: it is **linked first
on all 7 pages**, it holds 114 of the 202 declarations, and its values are real Carbon g10.
@@ -117,6 +124,37 @@ See §8 for the decision each one is waiting on.
| `--wp-status-success-border-a` | `#a7f0ba` (green-20, Carbon) | `console.css:82` | `-b` |
| `--wp-status-success-border-b` | `#b6e3c6` (not Carbon) | `wp-creation-styles.css:494,532` | `-a` |
### 2d. What `T3.2` actually built, where it differs from the above
`theme-light.css` now holds **191 declarations**: the 114 Carbon g10 values, `--cds-highlight`,
the 7 app-shell tokens, and **69 `--wp-*` suite tokens**. It is the only sheet in `html/` that
contains a colour literal. Two changes from §2a's proposal, both for the same reason — a token
whose *value* is Carbon but whose *role* Carbon does not define belongs in `--wp-*`, or the
namespace split stops meaning anything:
| Proposed in `T3.1` | Built as | Why |
|---|---|---|
| `--cds-hover-support-success` | **`--wp-hover-success`** | `#0e6027` is Carbon green-70, but "hover for a green fill" is not a Carbon role — green is not one of its action colours |
| `--cds-layer-inverse-01` | **`--wp-appbar-layer`** | `#262626` is Carbon gray-90, but the name was invented; it sits in the app-shell group where both its consumers live |
Tokens added beyond §2a/§2b, all of them one-offs that still needed a single home — "one place
a value is defined" applies to a value used once just as much as to one used forty times:
| Group | Tokens |
|---|---|
| status borders | `--wp-status-{success,error,warning}-border-{a,b,c}` — 7 |
| surfaces | `--wp-table-zebra`, `--wp-table-row-hover`, `--wp-nav-bg`, `--wp-nav-hover`, `--wp-nav-active`, `--wp-pop-divider`, `--wp-accent-soft-hover` |
| elevation | `--wp-shadow-{pop,menu,menu-lg,modal,modal-lg,gate,rail,drawer,sticky,navbar,tooltip,toast}` — 12 |
| scrims | `--wp-scrim-{drawer,cool,cool-modal,cool-strong,neutral,loading,frosted}`, `--wp-on-accent-divider` |
| type | `--wp-font-sans`, `--wp-font-sans-2`, `--wp-font-mono`, `--wp-font-mono-2`, `--wp-font-mono-3` |
| spacing / shape | `--wp-s1``--wp-s6`, `--wp-ctl`, `--wp-ctl-sm`, `--wp-radius-0`, `--wp-shadow-none` |
| one-offs | `--wp-tag-super-{bg,text}`, `--wp-term-{bg,fg,pass,fail}`, `--wp-dev-{bg,fg,rule}`, `--wp-sop-inherited-bg`, `--wp-status-warning-text-alt` |
**Twelve distinct shadows** is the number that surprised. They were invisible while spread
across four sheets as inline `box-shadow` values; collected in one block they are obviously
eleven more than a flat Carbon UI wants. Collapsing them is a rendered change, so not here —
but they are now countable, which they were not before.
---
## 3. Declaration inventory — `theme-light.css` (the source)
@@ -537,6 +575,16 @@ machines missing IBM Plex Mono. Decide in `T3.2`; if adopted, `admin` and `users
re-shot baselines — and remember **trap 2**: those two pages are not byte-stable (live
timestamps), so capture twice before believing any diff.
### K. A ninth amber, four points from the eighth
`field.html:35` fills `.pill.warn` with **`#8a6d00`**, where every other warning text in the
app is `#8e6a00`. Four points apart, same job, and `field.html`'s inline block was not in the
`T3.1` inventory — §1 listed only the launcher's, and `login.html` and `field.html` also carry
one. Almost certainly a typo rather than a decision.
`T3.2` named it `--wp-status-warning-text-alt` rather than merging it, on the same rule as
everything else in §8: merging moves a rendered colour. **BL-009**, resolve at `T3.5`.
### J. `#262626` — Carbon gray-90, used twice, no token
`wp-chrome.css:31` (dark search field) and `wp-sidenav.css:94` (current row). It is a real
@@ -617,28 +665,90 @@ Two secondary corrections while this is open:
---
## 11. What `T3.2` should actually do, in order
## 11. `T3.2` — what was done, and how it was proved
Written as a sequence because the ordering is what keeps the screenshot diff empty.
Done in this order, because the ordering is what kept the diff empty:
1. **Delete the dead `var()` fallbacks** (§7, 12 sites). Provably invisible; shrinks the raw
count before anything risky starts.
2. **Add the new canonical tokens** to `theme-light.css` — §2a and §2b — without touching any
1. **Deleted the 12 dead `var()` fallbacks** (§7). Provably invisible.
2. **Added the canonical tokens** to `theme-light.css` (§2a, §2b, §2d) without touching a
consumer. Additive, zero diff.
3. **Convert `console.css`, `work-package-suite-styles.css`, `wp-creation-styles.css` `:root`
blocks to aliases**, one sheet per commit-sized step. Names stay, literals go. Verify §9's
20 JS-read names still resolve.
4. **Convert `wp-chrome.css`'s two `.wp-chrome` blocks** to aliases, preserving the
light/dark switch mechanism exactly.
5. **Convert `wp-sidenav.css`'s 31 raw literals** to the appbar tokens. This is the sheet with
the largest gap between stated intent and mechanism, so it is also the most satisfying.
6. **Convert the raw literals in rules**, sheet by sheet, using §5. Skip everything §7 lists.
7. **Leave every §8 near-duplicate as its own canonical token.** Do not pick winners.
8. Re-run `tests/baseline_shots.py` at both widths on all 7 pages and diff against wave 2.
Capture `admin` and `users` **twice** — trap 2.
9. Update this document's §2 to what was actually built, per `T3.2`'s fourth done-when.
3. **Converted the `:root` blocks** of `console.css`, `work-package-suite-styles.css` and
`wp-creation-styles.css` to aliases. Names stayed; literals went.
4. **Converted `wp-chrome.css`'s two `.wp-chrome` blocks**, preserving the light/dark switch.
5. **Converted `wp-sidenav.css`'s 30 raw literals** to the app-bar tokens, which is the sheet
whose stated intent ("colours come from the bar") and mechanism (hardcoded hex) disagreed.
Flipping `--wp-appbar-bg` now takes the drawer with it, which is what the comment claimed.
6. **Converted the raw literals in rules** across all six sheets and all four inline `<style>`
blocks, skipping everything §7 lists.
7. **Left every §8 near-duplicate as its own canonical token.** No winners picked.
### Backlog entries this task raises
### Result against the done-when list
| Check | Result |
|---|---|
| exactly one declaration per canonical value; `#0f62fe` → 1 | **met.** One sheet declares it (`theme-light.css`); the 11 occurrences inside it are Carbon's own v10→v11 alias layer, which §3 records as deliberate and not the `S5` defect |
| no page stylesheet declares a raw **colour** | **met.** 0 literals across all 5 page sheets and all 4 inline `<style>` blocks; `theme-light.css` is the only file in `html/` holding one |
| no page stylesheet declares a raw **spacing or type** value | **not met, and not achievable under this task's own "change no rendered value"** — see below |
| screenshot diff empty on all pages at both widths | **superseded by a stronger check** — see below |
| `docs/reference/tokens.md` updated to what was built | **met** — §2d |
### The spacing and type half of check 2
Token *declarations* are aliased: `--s1``--s6`, `--ctl`, `--ctl-sm`, `--radius`, `--mono`,
`--sans` all resolve from `theme-light.css` now. But **483 raw spacing values, 281 raw
font-sizes and 65 raw radii remain inside rules**, 492 of them in the creator.
They cannot be tokenised here, and the reason is not effort. §6b measured that the creator's
spacing is every integer from 1px to 14px — it is not a scale, it is a histogram, and there is
no token that `padding: 9px 11px` maps to without changing one of the two numbers. `T3.2`
forbids changing a rendered value, so tokenising them and honouring that constraint are
mutually exclusive.
Stated plainly rather than quietly skipped: **this check is not met.** The colour half, which
is what `S5` and `C3` are actually about — four sheets each declaring `#0f62fe` — is met in
full. The spacing and type half is logged as **BL-010** and belongs with the page rebuilds at
`T5.x` and `T7.1`, where the values are being chosen again anyway.
### Why the screenshot check was replaced
The done-when asks for an empty screenshot diff. Captured against the wave 2 build, 11 of 14
shots were pixel-identical and 3 were not: the creator at 1440px and both admin widths.
Capturing wave 2 **against itself** produced the same 3 differences, at the same bounding box
(creator@1440: 344,272px, bbox 288,14→1439,4924). Those shots re-render live content, so a
pixel diff on them cannot distinguish a regression from the clock.
**Trap 2 in the task brief is half right and half wrong.** It names `admin` and `users` as the
unstable pair. `users` is stable at both widths; the actually-unstable third is the **creator
at 1440px**, and admin's instability is worse than "live timestamps" implies — its captured
page *height* varies by ~600px between runs.
So `tests/token_check.py` was written, and it checks the thing wave 3 actually claims:
- every custom property resolves to the same literal it did before, on every page;
- every element computes the same `color`, `background-color`, four border colours,
`outline-color`, `box-shadow`, `text-decoration-color`, `caret-color`, `column-rule-color`,
`font-family`, `font-size`, `font-weight` and `border-radius`.
That is strictly stronger than a screenshot: it covers the hover, focus and disabled rules a
screenshot never exercises, and it is deterministic.
Result, wave 2 versus `T3.2`, all 7 pages: **every one of the 178 wave-2 token names resolves
identically, and all 3,500 elements compute identically.** 213 tokens are new. 16 tokens
differ in notation only (`#fff``#ffffff`, `rgba(0,0,0,0.16)``rgba(0, 0, 0, .16)`), which
is §8-G — the one class of duplicate this task was supposed to collapse.
Two things that cost time and are recorded so they do not cost it again:
- The element walk was first keyed by sibling index, which reported 55 phantom differences on
the SOP page. The cause is three JS-injected overlays — `#wp-sync-badge`, `.wp-navscrim` and
`#wp-sidenav` — appending in whichever order their async work finishes. All three are
`position: fixed` with their own `z-index`, so the order changes nothing painted. The probe
now keys by identity. **BL-011.**
- The comparator normalises notation before reporting, because otherwise it fails on its own
success. That is safe only because the element comparison is the real evidence: a
normalisation that hid a genuine change would show up on every element consuming the token.
### Backlog entries this work raised
| ID | What | Suggested |
|---|---|---|
@@ -647,6 +757,10 @@ Written as a sequence because the ordering is what keeps the screenshot diff emp
| BL-006 | 17 half-pixel font sizes in the creator and chrome | `T7.1` |
| BL-007 | `--radius: 0` contradicted by 45 raw radii in the creator | `T7.1` |
| BL-008 | `#2563d6`, a second brand blue, in `.sop-inherited` and `help.js` | `T3.5` |
| BL-009 | `#8a6d00`, a ninth amber four points from the eighth, on the field view | `T3.5` |
| BL-010 | 483 raw spacing, 281 raw font-size and 65 raw radius values in rules | `T5.x` / `T7.1` |
| BL-011 | Three JS-injected overlays race to append on the SOP page | wave 9 |
| BL-012 | `admin` and `creator@1440` are not stable enough to screenshot-diff | wave 9 with `C2` |
---