ADR 0008 — Token tier strict separation
Status
Accepted, 2026-05-18.
Source: v1.0 architectural quiz outcomes
(docs-internal/architecture/v1-master-diff-merge.md §3).
Context
The v0.1.0 token system defined three tiers — Foundations, Semantic, Component-specific — and documented that foundations "should not" be consumed directly by components. The word "should" did the wrong work: two of the ten existing components reach into foundations for edge cases (a custom hover shade, a one-off radius). Each violation appears reasonable in isolation; collectively they erode the abstraction:
- Theming breaks silently. Foundations are theme-invariant by
design (raw palette, raw scale). A theme switch from light to dark
re-maps semantic tokens onto a new foundation set. A component that
reads
--color-neutral-50directly does not flip; it stays light. - Refactoring the palette becomes scary. If
--color-neutral-50is referenced from 47 component files, renaming or restructuring the palette requires updating all 47. The semantic tier exists precisely to absorb this kind of change. - The "primary consumption surface" sales pitch breaks. When the README claims "semantic tokens are the design system's public vocabulary," contributors who see foundation references in shipping code reasonably conclude semantic is optional.
v0.1.0 had no automated enforcement. The next 40+ components in v1.0 will compound the issue if we ship them under the soft rule.
Decision
The three-tier system gains a strict separation rule:
- Foundations are private to the semantic tier. No component, application, modifier, theme override, or example may reference a foundation token directly.
- Semantic is the only public token surface. External consumers read semantic tokens.
- Component-specific tokens reference semantic, never foundations. They are a refinement layer between semantic and a component's CSS, never a shortcut to the palette.
- Tier references flow downward only. A tier may reference the
tier immediately below; no skipping (
component-specific→foundationsdirect reference is forbidden), no upward references.
component CSS → component-specific → semantic → foundations
↑
(only path that crosses
this boundary)
Enforcement
Three mechanisms make the rule mechanical, not aspirational:
- Lint rule
no-foundation-direct-referencein@malevich/lintscans CSS files forvar(--{foundation-name})references in any file outsidepackages/core/tokens/semantic.json. Violations are build-blocking. - Lint rule
tier-respectvalidates that component-specific tokens (--button-bg) resolve through semantic (--color-accent), not through foundations (--color-neutral-50). v0.2.0 ships this as the strict variant of an advisory rule that existed in v0.1.0. - CSS variable naming pattern. Foundation custom properties live
under
--{tier1-category}-*(e.g.--color-neutral-50). Semantic custom properties use distinct role-based names (--color-surface-raised). The naming pattern alone makes violations visible in code review.
What to do when semantic is insufficient
A component author needs a value that semantic doesn't offer. The correct path:
- First: propose a new semantic token. If the need is general ("a 'success-subtle' background"), it belongs in semantic.
- Second: if the need is component-specific ("button hover state needs a different shade than card hover"), add a component-specific token that references semantic.
- Never: reach into foundations directly. The temptation is
highest here ("just use
--color-accent-hover"). The rule is firm regardless of how reasonable the local case feels.
If neither path produces an acceptable value, the gap is a semantic
tier deficiency. File an issue against @malevich/core; ship the
component blocked until semantic gains the token.
Modifiers and themes
Modifiers and theme overrides bind to semantic tokens only. Modifier-generated component-specific values may compose semantic references, but never foundations:
/* ✅ allowed — modifier overrides component-specific via semantic */
[data-surface="elevated"] {
--card-background: var(--color-surface-elevated);
}
/* ❌ forbidden — modifier references foundations */
[data-surface="elevated"] {
--card-background: var(--color-neutral-50);
}
Themes (packages/core/themes/<name>/) override foundations only.
A theme is a foundations replacement; the semantic layer above is
shared.
Existing v0.1.0 violations
Phase 2 audit identified handful of direct foundation references in v0.1.0 component CSS. v0.2.0 migration includes:
- Add the lint rule in advisory mode (warn, not error).
- Fix all violations as part of the token system migration.
- Flip the rule to error before tagging v0.2.0.
The migration codemod cannot automate this fix because choosing the correct semantic token requires design intent. It is hand-work.
Consequences
Positive:
- Themes work uniformly. A new theme overrides foundations and every component picks it up via the semantic layer.
- Palette refactors stay surgical. Renaming
neutral-50→neutral-100touches semantic.json and nothing else. - The semantic tier earns its "public vocabulary" claim — there is no legitimate reason to reach below it from outside.
- AGENTS.md and lint enforce the rule, removing the "but the docs said 'should not'" excuse.
- Audit becomes a single grep across the codebase for foundation variable names in component files.
Negative / tradeoffs:
- Some legitimate one-off needs require adding semantic tokens that feel narrow (e.g. a single component needs a specific shade). Mitigation: the component-specific tier is the right home for these, with semantic references.
- The lint rule has false-positive potential for legitimate cases
inside
packages/core/. We scope the rule to excludepackages/core/. - Hand-fixing existing v0.1.0 violations adds time to v0.2.0 migration (estimated 1-2 hours).
Alternatives considered
- Keep "should not" as advice. Rejected: v0.1.0 has shipped violations that the soft rule did not prevent. The model only works under mechanical enforcement.
- Allow foundation references with a justified annotation. Rejected: any escape hatch becomes the default path. Code reviewers cannot reliably judge "this case is truly justified."
- Collapse component-specific into semantic. Rejected: keeps the semantic tier clean by routing component-specific concerns through a dedicated tier 3, exactly as v0.1.0 intended.
- Two tiers (foundations + semantic, no component-specific). Rejected: per-component refinements are a real need (button hover vs. card hover); compressing them into semantic pollutes the public vocabulary with per-component intent.
References
docs-internal/architecture/v1-master-diff-merge.md§3 — full token system spec.docs-internal/architecture/v1-principles.md— principles P-3.x.llm-wiki/decisions/0005-token-group-lifecycle.md— establishes how new token groups are added; this ADR constrains how they are consumed.packages/lint/src/rules/no-foundation-direct-reference.ts— enforcement (added in v0.2.0).