design-RFC-007 — Token hierarchy (seed / map / alias)

ratified

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 #093 into 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 X derive to alias Y consistently?" — 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-gen build 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:

  1. Ship the new model alongside flat — verge.go keeps emitting

    the flat alias map for one Verge minor version (backwards-compat).

  2. Generate a delta report — "preset X: seed derivation diverges

    from current literal by ΔE = 1.4 in dark mode" — for designer review before flipping.

  3. Update koder_kit / koder_web_kit cross-language token

    distribution (RFC-006) to emit the new seedmapalias triple.

  4. Open per-consumer tickets to migrate hardcoded alias references

    to the new names (if any drift).

  5. 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

  1. If Option B ratifies: is compactAlgorithm shipped together with

    lightAlgorithm / darkAlgorithm, or is it a Verge v1.1 follow-up?

  2. If Option B ratifies: does internal/tokens/algorithms.go ship as

    pure Go (one binary, design-gen owns it) or as a published library (multi-language consumers run the algorithm client-side via koder_kit SDK)?

  3. 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-arch deliberation, 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} from design-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 fallbackreuse #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 compactAlgorithm with light/dark — the

    density 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.kmd is already APCA-aware; wire it into verge-contrast-audit so 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 #3584E4 ship 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