You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(theme): generate the compiled seed from the bundled JSON (#4582)
#3184 item 2 — *"the high-leverage one; kills the drift class
permanently."*
> ⚠ **Stacked on #4570** (`6c599007`), whose checker this repurposes
into the freshness gate rather
> than superseding. Review `7250974f` alone, or merge #4570 first.
## What was wrong
`seedBuiltinDefaults()` was a hand-maintained copy of
`default-dark.json`. Both failure modes its
own comment warned about had already happened:
- **9 tokens had drifted** — `color.accent.dim` and all eight
`color.slice.a..h`. The seed said
slice A was `#ff4040` (red); both bundled themes say `#00d4ff` (cyan).
- **25 tokens were never seeded at all** — `slice.dim.a..h`,
`highlight.*`, `button.*.disabled`,
and the six `waterfall.colormap.*` gradients. These resolved
**transparent** (alpha 0) on any
theme predating them, which is strictly worse than a stale value.
## What this does
`tools/gen_theme_seed.py` emits `src/core/ThemeSeedGenerated.cpp` from
the resource, resolving
`{primitive}` aliases to concrete values (the seed runs before any
primitives palette exists).
**209 hand-written lines become one call. All 128 tokens emitted, up
from 104.**
Per the design agreed on the issue: **commit-time, not build-time.** The
generated file is checked
in and greppable — `seedBuiltinDefaults()` is code people demonstrably
read, and its *"Preliminary
values — a dedicated slice-colour audit may tune these"* comment is
**how the drift was
diagnosed**. `tools/check_theme_seed.py` becomes a staleness gate; its
`KNOWN_SEED_DRIFT` list is
deleted because the class of bug is gone, not because the nine were
individually patched.
One structural note: the generated TU can't see `ThemeScope` (private to
`ThemeManager.cpp`), so
scoped tokens go through a new private `seedScopedToken()` helper.
## On testing — the honest version
**The seed is not observable through the public API.** The constructor
loads Default Dark
immediately after seeding, so by the time `instance()` returns the JSON
has overwritten everything.
That is precisely the invisibility that let the drift go unnoticed.
I wrote C++ assertions for it, discovered **they passed identically
against the old hand-written
seed**, and removed them rather than ship a test that proves nothing.
`theme_manager_test` carries
a note explaining why, so the next person doesn't repeat the attempt.
Verification lives where the property *is* observable — `--check` now
asserts **completeness**
(every JSON token reaches the emitted table) independently of
byte-equality, because byte-equality
alone would happily pass a generator that silently dropped a family:
| injected fault | result |
|---|---|
| drop the waterfall gradients | **FAIL** — "emitted 120 tokens but the
theme defines 128" |
| edit the theme, don't regenerate | **FAIL** `--strict`, exit 1 |
| neither | pass |
`ctest -R "theme\|s_meter\|colour\|color\|style\|applet"` **5/5**; full
tree builds on Windows/MSVC.
## Behaviour change worth calling out
A user whose theme predates the slice tokens currently renders slice A
**red** from the seed and
will flip to **cyan**. That is the correct value — both bundled themes
agree — and the blast radius
is limited to pre-v2 themes. Per the issue discussion this wants a
`CHANGELOG.md` line under
whichever release carries it; happy to add that here if you'd prefer it
in-PR.
Refs #3184.
---
## Review round (maintainer, 2026-07-31 — commit `eb4df8f5`)
Reviewed, and the follow-ups were pushed to this branch rather than
handed back.
The generator design was right; what was missing was the scaffolding
around it —
four of the five blockers were in files this PR didn't touch.
**Corrections to the description above:** the count of never-seeded
tokens is
**24**, not 25 (104 → 128); it was wrong in three source comments too,
and is now
fixed everywhere. The "stacked on #4570" note is obsolete — #4570
landed, and this
branch has since been merged with current `main`. The CHANGELOG offer is
taken up:
the entry is in this PR. The injected-fault table's "emitted 120 tokens"
is
actually 121 (there are seven gradient tokens, not six —
`color.meter.bar.fillGradient`
is one), and that assertion has been replaced outright, see below.
**Fixed in `eb4df8f5`:**
- **`tests/theme_seed_test.cpp` (new).** The premise that "the seed is
not
observable through the public API" holds only for `theme_manager_test`,
which
links `resources.qrc`. `container_widget_test` already links
ThemeManager
*without* the theme resource — there the seed **is** the rendered
palette, which
is the resource-missing fallback path #3184 named. The new target pins
the nine
formerly-drifted values, alias resolution, one token from each
previously
unseeded family, all seven gradients, the `seedScopedToken()` scope
tree, and the
non-colour token types. Mutation-tested: reverting only the nine values
→ 9
failures; deleting only the six colormap gradients → 6 failures;
unmodified →
PASS. The CI gate proves the *generator* agrees with the JSON; it cannot
prove
the emitted C++ reaches `m_tokens` at runtime — a target that omits
`ThemeSeedGenerated.cpp` from its source list passes the gate and ships
a
transparent UI.
- **`theme-seed-check.yml` `paths:`** did not include
`ThemeSeedGenerated.cpp` or
`gen_theme_seed.py` — the two files the gate exists to protect. A
hand-edit of
the "DO NOT EDIT" file never ran the check. Both added; the header
comment
(still describing the hand-maintained seed and `KNOWN_SEED_DRIFT`)
rewritten.
- **`docs/theming/slider-knob-tokens.md` and `toggle-button-tokens.md`**
are the
"adding the next control namespace" playbooks, and both still instructed
contributors to hand-add entries to `seedBuiltinDefaults()` —
re-creating the
dual source of truth this PR removes. Both now say: edit the JSON,
regenerate.
- **`CHANGELOG.md`** under `[Unreleased]`, per the ruling on #3184 —
covering both
the corrected slice colours and the 24 tokens that previously resolved
transparent.
- **Generator hardening.** An alias naming a missing primitive was
passed through
and emitted as `QString("{color.teal.500}")` — an invalid QColor, i.e.
transparent — with the gate exiting 0; now a hard error. `resolve()`
didn't
descend into lists, so an alias in a gradient stop survived to
`QColor("{...}")`; lists are now walked. `--check` compared token
*counts* with
`<`, so a fault duplicating one token and dropping another passed; it
now
compares the `(scope, token)` sets and names what's missing. Output is
byte-identical on the current tree.
- **Merged current `main`** (settings sources moved to
`${AETHER_SETTINGS_SOURCES}` under RFC #4603) — resolved across all nine
targets, which also removed the duplicated `ThemeSeedGenerated.cpp` line
in the
`ENABLE_ASR` block.
- **Stale ctor comment** claiming the seed "lacks the
`waterfall.colormap`
gradients" — it no longer does; rewritten to the reason that still
holds.
**Verified:** full tree builds (2563/2563, zero errors, Linux/GCC/Qt6);
`ctest -R "theme|s_meter|colour|color|style|applet|container"` 7/7;
`gen_theme_seed.py --check` and `check_theme_seed.py --strict` pass, and
fail on
each injected fault above.
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Jeremy [KK7GWY] <kk7gwy@aethersdr.com>
0 commit comments