Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AwtowrowDXx6ccmWVPkCaF
6.9 KiB
Add a structural-token drift checker to CI
findings (2, ranked by value over effort)
- Spacing, radius and type have never been drift-checked —
palette_check.pyparses hex only, so three whole token categories can diverge across stacks while CI stays green. Motion inherits the same hole. palette_map.pypoints 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:
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:- Agreement — for each mapped token, resolve it in every non-
n/astack and fail if values disagree. - 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".
- Agreement — for each mapped token, resolve it in every non-
-
Support an explicit
n/aper stack, and require a reason string. Tkinter has no animation framework; a duration legitimately does not exist there. An honestn/awith 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). Notepalette_check.pyis 231 andpalette_map.pyis 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, notype: 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.mdis 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
python3 ~/src/utils/unified-design-system/scripts/structural_check.pyexits 0, and exits non-zero when you deliberately break one stack copy (test both directions — a checker that never fails is not a checker).- It is wired into
~/src/utils/.github/workflows/palette-drift.ymland the workflow passes. palette_map.py's "scale check" comment names the real script.~/src/utils/scripts/check_file_length.sh --allreports no new violations.- 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. ReadNON_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— thePALETTEtuple ofToken(canonical, md, css, dart, tk, why)rows. Your map wants the same shape, including thewhyfield.~/src/utils/.github/workflows/palette-drift.yml— howpalette_check.pyandramp_check.pyare 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.dartis a fifth, uncompared copy of the palette (the Flutter app has nodesign_systemdependency 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.dartfiles. 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