Commit Graph
2 Commits
Author SHA1 Message Date
bvandeusenandClaude Opus 5 5b824c1626 feat(design): the panel now asks whether the app agrees with its own sheet
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 19s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / Python tests (push) Successful in 58s
CI & Build / Build & push image (push) Successful in 43s
Retiring rulebook #2 left the /design drift panel with no data source, and
because its empty state was well-written the feature read as working while it
could only ever render "nothing designated" (#2419). The original question is
genuinely gone: theme.css is generated from design system 2, so checking the
system against a sheet derived from it would be a tautology.

The question that survives is the one no server can answer. A generated sheet
still has to be LOADED and APPLIED, and nothing checked that it was:

  absent      the record declares a token the app doesn't have — the sheet was
              never regenerated after the record changed, or never loaded
  differs     the app has it with another value — a stale sheet, or a later
              rule that overrode it
  unrecorded  the app declares a token in the record's own family that the
              record has never heard of

Both sides go through the same engine so the comparison is honest: declared
values are set on an offscreen probe and read back, which performs the same
var() substitution the browser already did to the live values. Comparing raw
strings would mark every derived token as drift.

The designation moved with the feature — design_rulebook_id becomes
ui_design_system_id, with a migration deleting the retired key rather than
leaving an inert row. The prose extractor it fed goes too (#2288 said its
runtime role ended when the import landed).

Three orphans of the same shape, found alongside and fixed here:

- darkOverriddenNames hardcoded [data-theme="dark"]. The sheet went dark-first
  months ago, so it matched nothing and the "mode-aware" flag silently left the
  gallery. Now matches the SHAPE of a mode selector, which also holds for an
  install whose modes aren't light and dark.
- groupFor's prefix table never heard of --fs-, so 110 tokens sat under
  "other". Groups now come from the record where there is one; the table can
  only know families that shipped with the product (rule #115).
- The type scale was a hand-written table of nine sizes marked "no token",
  true when written and false since the scale was recorded. Now rendered from
  whatever size tokens the sheet declares, so it can't go stale twice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
2026-08-03 20:50:03 -04:00
bvandeusenandClaude Opus 5 4ca3ab02c4 feat(design-explorer): the drift panel — rulebook says X, tokens say Y
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / TypeScript typecheck (push) Successful in 11s
CI & Build / integration (push) Successful in 15s
CI & Build / Python tests (push) Successful in 43s
CI & Build / Build & push image (push) Successful in 40s
Milestone #251 step 5 (#2262), plus the Settings control that makes it reachable.

The comparison is deliberately thin — set arithmetic over live token values,
which is the one thing the browser knows and the server doesn't. The hard half
(prose to claims) already lives in Python where pytest can assert on it.

Three claim kinds, and the third inverts the test: a `token` claim asks whether a
custom property of that name exists; a `color` claim asks whether any token
resolves to that value; a `prohibited_color` claim FAILS when present.

normalizeColour is the client-side twin of normalize_hex and has one job the
server cannot do: getComputedStyle reports colours as rgb()/rgba() regardless of
how they were authored. So one colour has three spellings in play — #FFFFFF in
the rulebook, #fff in the stylesheet, rgb(255,255,255) from the browser — and a
comparison that misses any of them under-reports silently rather than erroring.

THE PANEL STATES ITS OWN BLIND SPOT, which matters more than it sounds. This
compares the rulebook against TOKENS. A literal hardcoded in a component, where
a token should have been referenced, is invisible to it — the drift isn't in the
tokens at all (#2275: 67 hardcoded whites against a rule forbidding pure white).
Reading those would mean bundling every SFC's source into the app; the check
belongs in CI and is tracked at #2277. A drift report that silently omitted a
whole category would invite the reader to conclude the category is clean, so the
panel says so in the panel rather than in a comment nobody reads.

Findings are ranked violated → missing → ok, and `ok` rows are hidden behind a
toggle. Same principle the auto-inject menu is built on: a short list that gets
read beats a complete one that doesn't.

Settings gains a rulebook picker. "None" is a first-class choice, not an unset
error — most installs have no rulebook describing their design system, and
saving empty DELETES the setting rather than storing a zero. The panel's empty
state points at Settings and Settings points back at the panel, so neither is a
dead end.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
2026-07-30 15:32:29 -04:00