design-RFC-007 — Token hierarchy (seed / map / alias)
Surveys whether Verge should evolve from its current FLAT token model (`bg`, `surface`, `text`, `accent`, `border`, `focus` declared directly per preset) to a 3-tier hierarchy modeled on Ant Design — **seed tokens** (small input set), **map tokens** (derived by algorithm: light / dark / compact), **alias tokens** (semantic names consumed by components). Owner sign-off required to ratify; this RFC LAYS OUT the three options and trade-offs without picking one.
Status: ratified 2026-06-23 (owner). Decision: Option B (3-tier seed → map → alias), HCT-primary / OKLCH-fallback, WCAG 2 AA hard + APCA advisory dual gate. See R9 for the rationale and R10 for the ratified parameters. Implementation sliced from
#093into Phase 0→3 (R6 path, incremental strangler). v0.1.0 Draft (2026-05-22) superseded.
R1 — Problem
Verge v0 uses flat tokens — every preset (canonicalPresets in internal/kinds/verge.go) declares the same handful of names (bg, surface, text, accent, border, focus) directly. The 44 presets × 2 modes (light / dark) = ~88 independent token sets, each hand-curated.
Properties:
| Property | Status |
|---|---|
| Easy to read a single preset | ✅ |
| Easy to add a new preset | ✅ — copy/paste then tune |
| Algorithmic theming (one seed → full palette) | ❌ |
| Compact / spacious density swap | ❌ |
| Cross-preset systematic invariants | ❌ — manual discipline only |
| Drift detection between consumer products | ❌ |
The flat model shipped Verge v0 intentionally — minimalism (v0 = Adwaita 1:1). The trade-off was that systematic theming, density modes, and consumer-extension ergonomics were deferred. This RFC re-opens that trade-off for Verge v1.
R2 — Prior art
Ant Design — explicit 3-tier
Seed: colorPrimary = #1677ff, borderRadius = 6, fontSize = 14
↓ (algorithm: defaultAlgorithm | darkAlgorithm | compactAlgorithm)
Map: colorBgContainer, colorBgElevated, colorBorderSecondary, ...
↓ (rename for semantics)
Alias: colorBgLayout, colorTextDescription, colorBgSpotlight, ...Consumers consume alias tokens. Theming = swap a few seeds. Dark mode = swap the algorithm. Compact = swap the algorithm. 5,000+ alias tokens generated from ~15 seeds.
Material 3 — partial hierarchy
Source HCT hue → Tonal palette (13 tones) → Role tokens
(primary, on-primary, primary-container, on-primary-container, ...)Less explicit than Ant — Material bakes the algorithm into the spec. Theming uses source color as the single seed; tonal palette is implicit.
Base Web / Polaris — flat
Base Web (~80 components) uses flat tokens like Verge. Polaris ships flat with tone:* semantic names. Neither offers algorithmic derivation; consumers extend via Provider props.
Adwaita (Verge v0 reference) — flat
GNOME Adwaita uses flat library colors (accent_bg_color, card_bg_color, etc.). No seed/algorithm layer. Verge v0 inherited this directly.
R3 — Options
Option A — Keep flat (status quo)
Mechanics: 44 presets × 2 modes continue as the contract. Each preset declares all alias tokens directly. New presets remain "copy/paste + tune".
Pros:
- Zero migration cost.
- Each preset is fully auditable as a single block of values
(no derivation hides surprises).
- Designers and devs can read any preset and see the literal
rendered values.
- Aligns with Verge v0 = Adwaita 1:1 philosophy.
Cons:
- Cross-preset invariants enforced only by audit (
verge-contrast-audit,koder-spec-audit themes) — no structural guarantee. - Compact / spacious density mode is a separate preset family,
doubling the set.
- A consumer wanting a single-color brand override (e.g. red accent
for an emergency-services product) must fork a full preset.
Option B — Adopt 3-tier (Ant model)
Mechanics:
- Seed tokens declared in a new
internal/kinds/verge_seeds.go:seedAccent,seedNeutral,seedDanger,seedSurface,seedRadius,seedFontSize,seedSpacingUnit. - Algorithms in
internal/tokens/algorithms.go:lightAlgorithm,darkAlgorithm,compactAlgorithm. Each takes seeds and emits a map of derived tokens (color-bg-container,color-text-secondary,border-radius-sm, …). - Alias tokens are renamed map outputs with semantic names; this
is what components consume.
Pros:
- Compact / spacious modes are trivial — swap algorithm.
- Brand override = swap one seed, propagation is automatic.
- 5–10× compression of preset definitions (44 presets become
44 seed sets).
- Cross-preset invariants are structural, not policed by audit.
- Theme drift detection across consumer products becomes
"does seed
Xderive to aliasYconsistently?" — trivially testable.
Cons:
- Migration cost — every existing preset must be re-expressed as a
seed set, then the derivation re-validated against the current literal values.
- A token's literal value is no longer readable directly — must
trace seed → algorithm → map → alias. Higher cognitive load.
- The algorithm becomes part of the contract; changing it changes
every consumer.
- More moving parts in
tools/design-genbuild chain.
Option C — Hybrid (seed → alias, no algorithm)
Mechanics:
- Seeds decompose alias values into "seed-derived" relations
(e.g.
--kdr-surface = lighten($seed-neutral, 8%)) but no algorithm layer sits between them. - Each preset declares seeds; alias tokens are computed via a small
fixed set of SCSS-like helpers (
lighten,darken,with-alpha). - No compact / spacious algorithm — density still ships as separate
presets.
Pros:
- Compresses preset definitions (~3–4× less code than flat).
- Still readable — every alias has a one-step derivation.
- Cross-preset invariants are structural (any seed change propagates).
- Migration risk lower than Option B (algorithm layer absent).
Cons:
- No compact / spacious benefit — density still copy/paste.
- Helper functions become part of the contract.
- Hybrid sits between "fully readable flat" and "fully algorithmic"
without the strengths of either.
R4 — Decision criteria
| Criterion | A — flat | B — 3-tier | C — hybrid |
|---|---|---|---|
| Migration cost | none | high (88 presets × derivation tuning) | medium |
| Readability of a single preset | ✅ literal | ❌ trace 3 layers | ◑ one-step derive |
| Brand-override ergonomics | ❌ fork preset | ✅ swap seed | ✅ swap seed |
| Density mode (compact/spacious) | ❌ separate presets | ✅ algorithm swap | ❌ separate presets |
| Cross-preset invariants | audit-policed | structural | structural |
| Consumer-extension cost | high (fork) | low (override seed) | low (override seed) |
| Risk of breaking existing consumers | none | high (every alias may shift) | medium |
| Adwaita 1:1 alignment (v0 philosophy) | ✅ | ❌ | ◑ |
R5 — Recommendation (advisory — pending owner sign-off)
Defer the decision. Verge v0 is shipping under flat tokens with 44 presets and the audit pipeline holding cross-preset invariants ("good enough"). Re-opening the model now risks consumer breakage during the homologation acceleration phase.
When the trigger materializes — first product that needs a custom brand-override beyond preset swap OR first request for compact / spacious density mode — re-evaluate. At that point Option B is the strong default; Option C is a fallback if migration risk is too high.
Superseded 2026-06-23: the trigger materialized (owner's design-evolution directive) and the derivation engine now exists (#190 HCT). Owner ratified Option B — see R9 (rationale) and R10 (ratified parameters).
R6 — Migration path (when triggered)
Whichever option ratifies, the migration MUST:
- Ship the new model alongside flat —
verge.gokeeps emittingthe flat alias map for one Verge minor version (backwards-compat).
- Generate a delta report — "preset
X: seed derivation divergesfrom current literal by ΔE = 1.4 in dark mode" — for designer review before flipping.
- Update
koder_kit/koder_web_kitcross-language tokendistribution (RFC-006) to emit the new seedmapalias triple.
- Open per-consumer tickets to migrate hardcoded alias references
to the new names (if any drift).
- Remove the flat shim one Verge minor after the new model ratifies.
R7 — Test contract (when triggered)
- T1: For every flat preset, the new model derives values within
ΔE ≤ 2.0 of the original (visual equivalence).
- T2: All cross-preset invariants previously audit-policed
(WCAG AA / AAA contrast ratios, focus-ring contrast, etc.) hold structurally — i.e. the algorithm cannot emit a non-compliant map.
- T3: Compact algorithm swap halves the spacing unit consistently
across every alias that references it.
- T4: Dark algorithm swap inverts neutral lightness while
preserving accent saturation.
R8 — Open questions
- If Option B ratifies: is
compactAlgorithmshipped together withlightAlgorithm/darkAlgorithm, or is it a Verge v1.1 follow-up? - If Option B ratifies: does
internal/tokens/algorithms.goship aspure Go (one binary, design-gen owns it) or as a published library (multi-language consumers run the algorithm client-side via
koder_kitSDK)? - If Option C ratifies: which helper set is closed (
lighten,darken,mix,with-alpha, …)? OKLCH-based or sRGB-based?
R9 — Recommendation update (2026-06-23 — AI, pending owner ratification)
Added after a deep UXUIcolour/design-system research pass (2024–2026 best practice) + a
/k-archdeliberation, requested by the owner ("evolve Koder Design so the experience is the best possible"). This section does not change R5 or flip status — it argues the deferral's trigger has now materialized and gives a ratifiable recommendation for owner sign-off. The owner decides.
The R5 trigger has materialized. R5 deferred until "first product needing a brand-override beyond preset swap OR first compact/spacious request". The owner's 2026-06-23 directive to evolve the design system for a memorable, praiseworthy experience is a stronger, explicit trigger — and two of R3-Option-B's main cons have since evaporated:
- "The algorithm is unbuilt / more moving parts" — *190 already shipped a
verified Go HCT engine (
internal/hct, CAM16 forward +SolveToInt), and #196 ports it to the live themer with OKLCH as the no-JS fallback. The derivation algorithm exists and is goldens-verified* Option B no longer means "build the math", only "wire presets through it". - "Cross-language distribution churn" — design-RFC-006 + #059 already
emit
tokens.{css,scss,json(DTCG),dart,kt,swift,d.ts}fromdesign-gen. The multi-platform pipeline is in place.
Recommendation: ratify Option B (3-tier seed → map → alias). It is the only option that delivers algorithmic theming, structural cross-preset invariants (contrast can't regress by construction — D6/D8), trivial density modes, and one-seed brand/tenant override — and the build cost that made it risky is now mostly paid. Resolving R8:
- R8#3 (colour space): HCT primary, OKLCH fallback — reuse #190/#196
(reuse-first / D10), do not introduce a third space. HCT gives Material-grade tonal palettes (0–100) the Verge specs already reference; OKLCH stays the perf/no-JS path. (My generic research leaned OKLCH-primary; the Stack-specific reality — a verified HCT engine already in flight — makes HCT the conformant choice.)
- R8#2 (algorithm location): pure Go in
design-gen(self-hosted-first;190 already is) + the JS port (#196) for the live themer only.
- R8#1 (compact): ship
compactAlgorithmwithlight/dark— thedensity win is a primary reason to adopt; splitting it loses half the value.
Add to the contract (gaps this surfaces):
- APCA as an advisory contrast gate alongside WCAG 2.x AA (which stays
the hard gate for compliance) —
contrast-checker.kmdis already APCA-aware; wire it intoverge-contrast-auditso the map algorithm is tuned by perceptual Lc, not just WCAG ratios. (T2 then becomes "algorithm cannot emit a sub-threshold map" on both metrics.) - Retire the known v0 accent AA debt structurally: the flat model let
accent #3584E4ship at 3.77 on light (just hand-fixed per-surface in the Koder ID login, 2026-06-23). Under Option B the seed→map algorithm derives the interactive accent to meet the gate by construction — closing the debt Stack-wide instead of per-component.
Sequencing = the R6 migration path, executed as an incremental strangler (the /k-arch conclusion): Phase 0 — emit current literal values from the seed/algorithm path byte-for-byte (flat shim alongside, zero consumer change); Phase 1 — flip presets to derived maps, each gated by the ΔE≤2.0 delta report (R7-T1) + contrast gate; Phase 2 — CI gates block merge (contrast WCAG-hard + APCA-advisory + token-drift); Phase 3 — migrate consumers to generated Dart/ CLI, retire hand-transcription, and emit the missing `color-schemes