utils/dopamine-ux/TODO-02-structural-check.md
Krzysztof kuhy Rudnicki 7096c102cd
Some checks are pending
crdt_sync_dart tests / test (push) Waiting to run
crdt_sync_flutter tests / test (push) Waiting to run
crdt-sync pre-commit / pre-commit (push) Waiting to run
crdt-sync tests / test (push) Waiting to run
dependency freshness / dependencies on newest stable (push) Waiting to run
design_system tests / test (push) Waiting to run
file length / file-length (push) Waiting to run
freedays pre-commit / pre-commit (push) Waiting to run
freedays tests / test (push) Waiting to run
gatelock pre-commit / pre-commit (push) Waiting to run
gatelock tests / test (push) Waiting to run
github_device_auth tests / test (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
music_theory tests / test (push) Waiting to run
palette drift / palette (push) Waiting to run
shared gate tests / gate unit tests (push) Waiting to run
sync_settings_ui tests / test (push) Waiting to run
web_ui tests / test (push) Waiting to run
Re-point paths for the ~ reorganisation (home-tidy)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AwtowrowDXx6ccmWVPkCaF
2026-09-11 18:45:55 +02:00

6.9 KiB

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:

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