> **This file is a ready-to-use prompt.** Open this repo and say > "do dopamine-ux-02-structural-check". It is self-contained -- it needs no context from > any other session. > > Canonical copy: `~/src/utils/dopamine-ux/02-structural-check.md` (source of truth; this is a > distributed copy). Running order and cross-prompt rules: `~/src/utils/dopamine-ux/00-INDEX.md`. > Generated 2026-08-16 from a survey of this repo -- line numbers are accurate > as of that date; **anchor on the symbol names, not the line numbers.** ## ⛔ WHEN YOU FINISH THIS PROMPT Do **both**, in the same session that completes the work: 1. **Delete this file** and commit that deletion together with the implementation: ```bash git rm prompts/dopamine-ux-02-structural-check.md ``` Do not leave a finished prompt lying in the repo -- a stale prompt is indistinguishable from a pending one, and the next session will re-run it. 2. **Log completion in the canonical index**, in `~/src/utils` (a *different* repo, so it needs its own commit): ```bash cd ~/src/utils # append to the "Completion log" table in dopamine-ux/00-INDEX.md: # | 02-structural-check.md | DONE | | | git add dopamine-ux/00-INDEX.md git commit -m "dopamine-ux: mark 02-structural-check.md done" git push ``` If you finish only part of the prompt, do **neither** -- instead edit this file to strike out what is done and state what remains, so the next session starts from the true state. Partial completion must never be recorded as DONE. --- Add a structural-token drift checker to CI ## findings (2, ranked by value over effort) 1. **Spacing, radius and type have never been drift-checked** — `palette_check.py` parses hex only, so three whole token categories can diverge across stacks while CI stays green. Motion inherits the same hole. 2. **`palette_map.py` points at a "scale check" that does not exist** — a comment describing a guarantee nobody wrote. ## what `palette_check.py` guarantees the colour palette agrees across all stack copies. It does **not** check anything else — verified 2026-08-16 by reading its four parsers, every one of which requires a `#RRGGBB` value: ```python parse_css : r"(--[a-z0-9-]+)\s*:\s*(#[0-9A-Fa-f]{6})\s*;" parse_dart: r"static const Color (\w+) = Color\(0x[fF]{2}([0-9A-Fa-f]{6})\)" parse_tk : r'^\s{4}(\w+):\s*str\s*=\s*"(#[0-9A-Fa-f]{6})"' parse_md : r"^\|\s*`([a-z0-9-]+)`\s*\|\s*`(#[0-9A-Fa-f]{6})`" ``` So **spacing, radius and type have been silently unprotected all along**, and the motion tokens added by prompt 01 inherit the same hole. A `--duration-fast: 120ms` is invisible to every parser: CI passes while the stacks drift apart. Worse, `scripts/palette_map.py:159` claims structural values are "checked by the scale check" — **no such script exists anywhere in `~/src/utils`**. That comment describes a check that was never written. This prompt writes it. ## where Repo: `~/src/utils`. **Create** `~/src/utils/unified-design-system/scripts/structural_check.py`. **Edit** `~/src/utils/.github/workflows/palette-drift.yml` — add a step invoking it. The workflow deliberately has no `paths:` filter; keep it that way. **Fix** the stale comment at `~/src/utils/unified-design-system/scripts/palette_map.py` (~:159) to name the real script. Sources to compare (three code stacks + the prose freeze): | Role | Path | |---|---| | prose freeze | `~/src/utils/unified-design-system/tokens.md` (spacing/radius/type) | | motion freeze | `~/src/utils/unified-design-system/motion.md` (**created by prompt 01** — run it first) | | web/React | `~/src/utils/web_ui/src/tokens.css` | | Flutter/Dart | `~/src/utils/design_system/lib/src/tokens.dart` | | Python/Tkinter | `~/src/utils/gatelock/gatelock/_window.py` (`class LockConfig`) | ## must - Cover **motion** (durations, easing) **and** the already-unprotected **spacing, radius and type** scales, in one checker. - Mirror `palette_check.py`'s two-halves design, because it is the part that actually works: 1. **Agreement** — for each mapped token, resolve it in every non-`n/a` stack and fail if values disagree. 2. **Completeness, bidirectional** — every structural value parsed out of any source must be mapped, or explicitly exempted. This is what catches "added it to one stack, forgot the others". - Support an explicit **`n/a` per stack**, and require a reason string. Tkinter has no animation framework; a duration legitimately does not exist there. An honest `n/a` with a reason is correct — a fabricated constant is not. - **Adjudicate with exit codes.** Exit non-zero on drift with a message naming the token and the disagreeing files. No warnings-only mode. - Keep every file ≤ **250 lines** (`~/src/utils/file_length/config.py`). Note `palette_check.py` is 231 and `palette_map.py` is 162 — that pair was split precisely to stay under the cap. **Split yours the same way from the start** (`structural_check.py` + `structural_map.py`) rather than discovering the problem at 251 lines. - Ruff-clean under the repo's config. No `# noqa`, no `type: ignore`. - **Expect it to fail on first run.** Spacing/radius/type have never been compared; there may be real pre-existing drift. That is the point of writing it. Fix whatever it surfaces, in the stack copy that is wrong, and say in the session summary what drifted. - must not: weaken the checker, add an exemption, or narrow its scope to make it green. If a token genuinely differs per stack for a good reason, encode it as an `n/a`/variant **with the reason recorded**, not as a silent skip. - must not: modify `palette_check.py`. It works; it is simply colour-only. The two scripts run side by side. - must not: change any token *value* to resolve a disagreement without saying so explicitly — that is a design change, and `tokens.md` is a freeze. Surface it and let kuhy decide which value is canonical. - optional: teach it the shadow tier too, if that falls out cheaply. ## done 1. `python3 ~/src/utils/unified-design-system/scripts/structural_check.py` exits 0, and exits non-zero when you deliberately break one stack copy (**test both directions** — a checker that never fails is not a checker). 2. It is wired into `~/src/utils/.github/workflows/palette-drift.yml` and the workflow passes. 3. `palette_map.py`'s "scale check" comment names the real script. 4. `~/src/utils/scripts/check_file_length.sh --all` reports no new violations. 5. The session summary states what drift, if any, the first run found. ## verify Desktop. Run the script, paste the output. Then prove it fails: change one spacing or duration value in **one** stack copy, re-run, paste the non-zero exit and the message, and revert. Both halves of that demonstration are required — the passing run alone proves nothing. ## read first - `~/src/utils/unified-design-system/scripts/palette_check.py` — the design to mirror. Read `NON_COLOUR_CSS`: it is the regex that currently *excludes* spacing/radius/type/shadow from the colour check, and therefore the precise list of what you are now covering. - `~/src/utils/unified-design-system/scripts/palette_map.py` — the `PALETTE` tuple of `Token(canonical, md, css, dart, tk, why)` rows. Your map wants the same shape, including the `why` field. - `~/src/utils/.github/workflows/palette-drift.yml` — how `palette_check.py` and `ramp_check.py` are invoked; add your step alongside. - `~/src/utils/unified-design-system/motion.md` — what prompt 01 created. - `~/src/utils/unified-design-system/tokens.md` — the spacing/radius/type tables. **Do not edit it**; it is 260 lines and already over the cap. ## context you would otherwise rediscover - The four "stack copies" are really **three code stacks plus a prose freeze**. There is no fourth code stack; do not hunt for one. - `~/src/dufs-cloud/app/lib/ui/theme.dart` is a **fifth, uncompared copy** of the palette (the Flutter app has no `design_system` dependency and hand-restates every hex). It is out of scope here — prompt 07 wires it up properly. Do not add it as a source to this checker; wait until 07 has removed it. - Three Flutter apps (diet-guard, workout_app, wake_alarm) similarly hand-transcribe tokens into local `lib/ui/theme.dart` files. Those are deliberately outside the checker's scope for now — bringing them in is a much larger job than this prompt. - `~/src/utils/gatelock/` is a package inside this monorepo, not a separate repo. REMOVE ME AFTER FINISH