feat(design-systems): declare what to write instead, rather than what not to
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 16s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / Python tests (push) Successful in 43s
CI & Build / Build & push image (push) Successful in 38s

Milestone #254 step 6, first half (#2295) — and this reframes the task rather
than answering it. The operator's call:

  "in this case we should declare what should be used in place of pure white,
   it's not a prohibition it's what should be used in its place."

None of the three options on the table (a constraints record / the panel reads
both sources / negative token rows) was right, because all three kept the
prohibition as a KIND OF THING. It isn't one. "Pure white is never text" is the
shadow cast by a positive fact — text is Parchment — and a design system that
stores what things ARE has no row for a ban because it never needed one.

So `design_tokens` gains `supersedes`: the literal values this token should be
written instead of. `--color-text-on-action` supersedes `#fff` / `#ffffff`. Same
fact as the rule, stated forwards, and now actionable — a finding can say what
to write rather than only objecting.

It has to be DECLARED, not derived, and that is the crux: `#fff` and Parchment
`#E8E4D8` are different colours, so no value-matching check could ever have
connected them. That mismatch is precisely why the prohibition looked
unrepresentable until it was turned around.

`supersedes` cascades on EMPTINESS rather than on None. A child overriding a
colour says nothing about which literals it replaces, and blanking the family's
declaration there would silently disarm the check for every app that customises
the token — while a child that states its own list replaces it wholesale.

Two things this deliberately does NOT do:

  - It does not feed the drift panel. Superseded literals live in component CSS,
    which `designDrift.ts` cannot see and already documents as a blind spot.
    This is input for the source lint (#2277). Declaring it with nothing
    consuming it yet is honest; wiring it to a panel that cannot check it would
    not be.
  - It does not remove the panel's `prohibited_color` arm yet — that happens
    when the panel is repointed at a resolved system, which needs #2288 first.

The declaration also exposes a missing token: most of the 67 hardcoded
`color: #fff` (#2275) are text on a coloured action button, and the system has
no token for that role at all. Every view hardcodes it. Declaring the token that
was never there is the first real output of the operator's framing.
This commit is contained in:
2026-07-30 21:16:45 -04:00
parent 78489308b8
commit 3da40abcb8
11 changed files with 305 additions and 8 deletions
@@ -0,0 +1,55 @@
"""design_tokens.supersedes — the literals a token should be used instead of
Revision ID: 0073
Revises: 0072
Create Date: 2026-07-30
Records what a prohibition was actually trying to say.
A design rulebook writes "pure white #FFFFFF is NEVER used as text color". That
sentence has no row in a table of tokens, because a design system stores what
things ARE — which looked like a gap in the model and was really a sentence
written backwards. The positive fact is "text is Parchment", and the useful
record is the mapping from the literal someone would otherwise write to the
token they should write instead.
So `supersedes` is a JSONB array of literal values, e.g. `["#fff", "#ffffff"]`
on a text-on-action token. A finding built from it can say what to write, not
merely what not to.
It must be DECLARED rather than derived. `#fff` and Parchment `#E8E4D8` are
different colours, so no value-matching rule could ever have connected them —
which is exactly why the prohibition felt unrepresentable until it was turned
around.
Consumed by the source lint that reads component CSS, not by the drift panel:
these literals are in the components, which the panel cannot see.
NOT NULL with a `'[]'` default, matching `value_by_mode` — a nullable JSONB
column has two empty states and every reader has to test for both.
Downgrade drops the column; the declarations are lost, which costs the lint its
input and nothing else.
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import JSONB
revision = "0073"
down_revision = "0072"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.add_column(
"design_tokens",
sa.Column(
"supersedes", JSONB, nullable=False, server_default=sa.text("'[]'::jsonb")
),
)
def downgrade() -> None:
op.drop_column("design_tokens", "supersedes")