A literal gets written into a stylesheet when there is no role to reach for.
This codebase demonstrated it: the house style had no "text on a filled colour"
role, so 76 call sites wrote a pure-white literal — not out of defiance, but
because nothing existed to write instead. The correction was not a better ban
list; it was declaring the missing role (#2275, #2349).
So the useful moment is creation. A system whose roles are named on day one
never presents the occasion.
Ten groups, ~40 roles: surface, text, action, semantic, border, accent, radius,
space, motion, state. Operator's call was one flat list, every group
individually skippable — presets keyed to app shape (web / CLI / docs) were
rejected because they need the product to hold opinions about app categories,
and a wrong category is worse than a list someone prunes once.
TWO BOUNDARIES THIS HAS TO HOLD, both rule #115:
- The ROLES ship; the VALUES never do. Every seeded token has an empty
value_by_mode, so a fresh system is a set of named, deliberately-unanswered
questions. A test asserts no hex appears anywhere in the module — not just
that tokens are blank, but that no palette hides in a comment waiting to be
pasted in.
- The PREFIX is the install's. `--fs-` is FabledSword's convention, not the
product's; the default is a neutral `--ds-` and callers pass their own.
Valueless roles are already legible downstream — render_stylesheet emits them
as commented-out declarations and stylesheet_for_system reports them under
`valueless` (#2299) — so "declared but undecided" reads correctly with nothing
new built.
Both surfaces, per rule #33: MCP gains starter_role_groups/token_prefix plus
list_starter_role_groups(); REST gains the same on POST plus
GET /api/design-systems/starter-roles. The parity enumeration is extended
rather than loosened.
Note create_design_system treats None and [] alike (seed nothing), while
starter_tokens treats None as "all". Deliberate: creation must never write 40
rows into a system whose caller never asked, and the everything-checked default
belongs in the UI where the operator can see it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs