DESIGN: derived interaction-state tests assume a per-product CSS var scheme the color.json files explicitly reject #72

Closed
opened 2026-07-28 17:17:38 +00:00 by peterswimm · 2 comments
Owner

While working design issue #51, python -m pytest generator/tests/ surfaced 2 pre-existing failures unrelated to that work:

  • generator/tests/unit/test_derived_states.py::test_bespoke_canonical_palettes_are_covered
  • generator/tests/unit/test_derived_states.py::test_missing_primary_role_skips_primary_states

Both assert that spelwork/tctd/fux-capacity's derived interaction-state CSS vars (the color-mix() hover/active/focus-glow states added by extract.py:_derived_state_lines) use product-prefixed names — e.g. --spelwork-color-primary-hover, --tctd-accent-primary-hover, --fux-capacity-accent-secondary-hover.

That doesn't match the actual, current Brand/UDG/products/{spelwork,tctd,fux-capacity}/dtcg/color.json files:

  • All three $description fields explicitly say the opposite of what the tests expect: "self-defined on canonical --udg-color-* identities... keeps the standard contract so agnostic surfaces and clients can read/inherit it." Renaming to per-product-prefixed vars would break that documented contract (this repo's components//templates/ are var(--udg-color-*)-only consumers per CLAUDE.md).
  • The tests' own premises don't match the data shape: test_bespoke_canonical_palettes_are_covered expects tctd to use an accent.* color grouping — tctd has no accent.* path at all, only brand.*/bg.*/text.*/border.*/safety.*. test_missing_primary_role_skips_primary_states asserts "fux-capacity has no primary token" — fux-capacity has a real brand.primary (#E63535, udg.dart-key: brandPrimary).

The code comment directly above _derived_state_lines in udg/generators/extract.py (around line 99) says: "PROVISIONAL (2026-07-25): the mix ratios, the base-role candidate lists, and the derived-name convention... await ville's design review." So this looks like the tests were written against a planned retokenization (probably the per-product CSS-var namespacing that would matter once multiple WP theme.json style presets — spelwork/tctd/fux-capacity/peterswimm all load under toilville-fse — coexist in one site and need non-colliding var names) that either never landed or was superseded by the "keep the standard contract" decision recorded in the color.json files themselves.

Ask: @ville — which is current intent?

  1. The --udg-color-* shared contract is correct as documented in the color.json files → the two tests are stale and should be rewritten to assert the actual (shared-var) behavior, or deleted if the scenario they're pinning no longer applies.
  2. Product-prefixed derived-state names were actually intended (e.g. for WP multi-style-preset collision avoidance) → then the full color.json retokenization needs scoping (not just 3 roles — _common_var_prefix's focus-glow naming only works if every emitted color var for that product shares the prefix, so this would mean renaming every token in these 3 files, which is a bigger change than these two tests alone suggest).

Not fixing this in design issue #51 / its PR — that work is a documented space/radius/border override, unrelated to this color-derived-states gap. Left _derived_state_lines/the color.json files untouched pending this decision.

While working design issue #51, `python -m pytest generator/tests/` surfaced 2 pre-existing failures unrelated to that work: - `generator/tests/unit/test_derived_states.py::test_bespoke_canonical_palettes_are_covered` - `generator/tests/unit/test_derived_states.py::test_missing_primary_role_skips_primary_states` Both assert that spelwork/tctd/fux-capacity's derived interaction-state CSS vars (the color-mix() hover/active/focus-glow states added by `extract.py:_derived_state_lines`) use **product-prefixed** names — e.g. `--spelwork-color-primary-hover`, `--tctd-accent-primary-hover`, `--fux-capacity-accent-secondary-hover`. That doesn't match the actual, current `Brand/UDG/products/{spelwork,tctd,fux-capacity}/dtcg/color.json` files: - All three `$description` fields explicitly say the opposite of what the tests expect: **"self-defined on canonical `--udg-color-*` identities... keeps the standard contract so agnostic surfaces and clients can read/inherit it."** Renaming to per-product-prefixed vars would break that documented contract (this repo's `components/`/`templates/` are `var(--udg-color-*)`-only consumers per CLAUDE.md). - The tests' own premises don't match the data shape: `test_bespoke_canonical_palettes_are_covered` expects tctd to use an `accent.*` color grouping — tctd has no `accent.*` path at all, only `brand.*`/`bg.*`/`text.*`/`border.*`/`safety.*`. `test_missing_primary_role_skips_primary_states` asserts "fux-capacity has no primary token" — fux-capacity has a real `brand.primary` (`#E63535`, `udg.dart-key: brandPrimary`). The code comment directly above `_derived_state_lines` in `udg/generators/extract.py` (around line 99) says: *"PROVISIONAL (2026-07-25): the mix ratios, the base-role candidate lists, and the derived-name convention... await ville's design review."* So this looks like the tests were written against a planned retokenization (probably the per-product CSS-var namespacing that would matter once multiple WP theme.json style presets — spelwork/tctd/fux-capacity/peterswimm all load under `toilville-fse` — coexist in one site and need non-colliding var names) that either never landed or was superseded by the "keep the standard contract" decision recorded in the color.json files themselves. **Ask:** @ville — which is current intent? 1. The `--udg-color-*` shared contract is correct as documented in the color.json files → the two tests are stale and should be rewritten to assert the actual (shared-var) behavior, or deleted if the scenario they're pinning no longer applies. 2. Product-prefixed derived-state names were actually intended (e.g. for WP multi-style-preset collision avoidance) → then the full color.json retokenization needs scoping (not just 3 roles — `_common_var_prefix`'s focus-glow naming only works if *every* emitted color var for that product shares the prefix, so this would mean renaming every token in these 3 files, which is a bigger change than these two tests alone suggest). Not fixing this in design issue #51 / its PR — that work is a documented space/radius/border override, unrelated to this color-derived-states gap. Left `_derived_state_lines`/the color.json files untouched pending this decision.
Member

Option 1 — the --udg-color-* shared contract is correct as documented. The two tests are stale and should be rewritten against actual behaviour.

Verified your diagnosis by running the suite and reading the data. All of it holds:

  • python -m pytest generator/tests/unit/test_derived_states.py → 2 failed, 6 passed, exactly the two you named.
  • All three $description fields say "self-defined on canonical --udg-color-* identities … keeps the standard contract so agnostic surfaces and clients can read/inherit it."
  • tctd has no accent.* grouping at all — only brand.* / bg.* / text.* / border.* / safety.*.
  • fux-capacity has a real brand.primary = #E63535, udg.dart-key: brandPrimary. The test's "no primary token" premise is simply false.

Two things worth adding, because they close the question rather than leaving it a judgement call.

The WP collision scenario that would motivate prefixing does not exist. I checked tools/wp_theme_ingestor/build_surfaces.py and the presets it reads. udg/products/wp-themes/toilville-fse/styles/{fux-capacity,pardner,spelwork,tctd,peterswimm}.json are theme.json style variations carrying a settings.color.palette of slug/colour pairs — they emit --wp--preset--color--<slug>, never --udg-color-*, and WordPress applies exactly one variation at a time. So the five brands never coexist as CSS custom properties in one document, and there is nothing for a product prefix to disambiguate. The hypothesis in option 2 was reasonable but the WP path doesn't work that way.

_common_var_prefix cannot produce a per-product prefix today, by construction. All four canonical products emit byte-identical derived-state names:

--udg-color-brand-primary-hover
--udg-color-brand-primary-active
--udg-color-brand-secondary-hover
--udg-color-safety-block-hover
--udg-color-focus-glow

spelwork, tctd, fux-capacity and rituals all produce that exact block. The longest common prefix of a set of --udg-color-* vars is always --udg-color-, whatever the product — so the function can only ever yield a product namespace if the base vars were already product-prefixed. That is your point about scope, arrived at from the other direction: prefixing the derived states is not a small change that happens to touch three roles, it is unreachable without renaming every token in these files first. Which the $description fields rule out.

Note this also means the derived states inherit precisely the collision profile the base contract already has — if two canonical products' skins ever did load into one document they would already collide on --udg-color-brand-primary itself. That is a scoping problem (:root[data-theme=…], or a wrapper selector), not a naming problem, and it is orthogonal to this issue.

Suggested follow-ups when the tests get rewritten:

  1. test_bespoke_canonical_palettes_are_covered — assert the real behaviour: a product with color_inherits_baseline: false still resolves all three roles from its own brand.* / safety.* tokens and emits the canonical names. Worth noting that as written this would then duplicate test_canonical_grammar_derives_from_canonical_base, so the thing actually worth pinning is the no-baseline-inheritance path, not the naming.
  2. test_missing_primary_role_skips_primary_states — the scenario no longer has a fixture. No shipping product lacks a primary role. test_absent_base_token_is_skipped already covers the skip logic with a synthetic palette and does it better. I'd delete this one rather than re-point it at a product that will acquire a primary the moment someone edits it.
  3. While in there: _common_var_prefix is dead logic for every current product. Either drop it and hardcode the canonical prefix, or leave it with a comment saying it is a hook for a namespacing scheme that was considered and rejected here — otherwise the next reader will assume it namespaces when it cannot.

The PROVISIONAL marker stays on the mix ratios. That half of the comment is a separate question from the naming one, and it is not settled — see #75. Short version: one global derivation rule does not carry across brands, so 88/78/90/85/22-toward-#000 is accepted as a placeholder default, not as a house rule. Nothing blocks on it today; the tests in this issue can be rewritten against current behaviour regardless.

Option **1** — the `--udg-color-*` shared contract is correct as documented. The two tests are stale and should be rewritten against actual behaviour. Verified your diagnosis by running the suite and reading the data. All of it holds: - `python -m pytest generator/tests/unit/test_derived_states.py` → 2 failed, 6 passed, exactly the two you named. - All three `$description` fields say "self-defined on canonical `--udg-color-*` identities … keeps the standard contract so agnostic surfaces and clients can read/inherit it." - tctd has no `accent.*` grouping at all — only `brand.*` / `bg.*` / `text.*` / `border.*` / `safety.*`. - fux-capacity has a real `brand.primary` = `#E63535`, `udg.dart-key: brandPrimary`. The test's "no primary token" premise is simply false. Two things worth adding, because they close the question rather than leaving it a judgement call. **The WP collision scenario that would motivate prefixing does not exist.** I checked `tools/wp_theme_ingestor/build_surfaces.py` and the presets it reads. `udg/products/wp-themes/toilville-fse/styles/{fux-capacity,pardner,spelwork,tctd,peterswimm}.json` are theme.json **style variations** carrying a `settings.color.palette` of slug/colour pairs — they emit `--wp--preset--color--<slug>`, never `--udg-color-*`, and WordPress applies exactly one variation at a time. So the five brands never coexist as CSS custom properties in one document, and there is nothing for a product prefix to disambiguate. The hypothesis in option 2 was reasonable but the WP path doesn't work that way. **`_common_var_prefix` cannot produce a per-product prefix today, by construction.** All four canonical products emit byte-identical derived-state names: ``` --udg-color-brand-primary-hover --udg-color-brand-primary-active --udg-color-brand-secondary-hover --udg-color-safety-block-hover --udg-color-focus-glow ``` spelwork, tctd, fux-capacity and rituals all produce that exact block. The longest common prefix of a set of `--udg-color-*` vars is always `--udg-color-`, whatever the product — so the function can only ever yield a product namespace if the base vars were already product-prefixed. That is your point about scope, arrived at from the other direction: prefixing the derived states is not a small change that happens to touch three roles, it is unreachable without renaming every token in these files first. Which the `$description` fields rule out. Note this also means the derived states inherit precisely the collision profile the base contract already has — if two canonical products' skins ever did load into one document they would already collide on `--udg-color-brand-primary` itself. That is a scoping problem (`:root[data-theme=…]`, or a wrapper selector), not a naming problem, and it is orthogonal to this issue. Suggested follow-ups when the tests get rewritten: 1. `test_bespoke_canonical_palettes_are_covered` — assert the real behaviour: a product with `color_inherits_baseline: false` still resolves all three roles from its own `brand.*` / `safety.*` tokens and emits the canonical names. Worth noting that as written this would then duplicate `test_canonical_grammar_derives_from_canonical_base`, so the thing actually worth pinning is the no-baseline-inheritance path, not the naming. 2. `test_missing_primary_role_skips_primary_states` — the scenario no longer has a fixture. No shipping product lacks a primary role. `test_absent_base_token_is_skipped` already covers the skip logic with a synthetic palette and does it better. I'd delete this one rather than re-point it at a product that will acquire a primary the moment someone edits it. 3. While in there: `_common_var_prefix` is dead logic for every current product. Either drop it and hardcode the canonical prefix, or leave it with a comment saying it is a hook for a namespacing scheme that was considered and rejected here — otherwise the next reader will assume it namespaces when it cannot. **The PROVISIONAL marker stays on the mix ratios.** That half of the comment is a separate question from the naming one, and it is not settled — see #75. Short version: one global derivation rule does not carry across brands, so 88/78/90/85/22-toward-`#000` is accepted as a *placeholder default*, not as a house rule. Nothing blocks on it today; the tests in this issue can be rewritten against current behaviour regardless.
Member

Wish completed — all requirements satisfied.

Wish completed — all requirements satisfied.
Sign in to join this conversation.
No description provided.