Android Token Pipeline — making the design system real (AND-1)
The android-design-system feature shipped 2026-04-04 as research-only — an iOS→Material-Design-3 mapping doc, an adaptation strategy, and a Style Dictionary config that generated nothing. Two and a half months later the config was broken (referenced non-existent transforms, written against SD v3 while the repo had migrated to v5) and the mapping doc was stale (92 tokens against a source restructured to 108). This ships the real token layer: mirror the working iOS pipeline's hand-rolled-format philosophy for Android, generating committed, drift-gated Kotlin + XML artifacts. 49 colors + 34 dimens + 8 Compose objects, byte-identical across builds, with a drift gate that fails on any token edit. Honest limit — the Kotlin is structurally valid and drift-gated but NOT compiled (no Android toolchain in-repo).
What do T1 / T2 / T3 mean?▾
Android Token Pipeline — making the design system real (AND-1)
The problem: a design system that was "Android" in name only
The android-design-system feature shipped 2026-04-04 as a research-only deliverable — an iOS→Material-Design-3 mapping doc, an adaptation strategy, and a Style Dictionary config. But it generated nothing. Two and a half months later the reality (T1, audited 2026-06-17):
design-tokens/config-android.jsonwas broken — it referenced Style Dictionary transformssize/compose/dpandsize/compose/spthat don't exist (the real compose group ships rem-input transforms), and it was written against Style Dictionary v3 while the repo had since migrated to v5 (2026-06-08).npm run tokenshad no Android target. No.ktor.xmlexisted anywhere.docs/design-system/android-token-mapping.mdwas stale — documented 92 tokens against a source that had restructured to 108 (66 actionable).
This is precisely the silent-gap class the FitMe framework exists to kill: a config that looks like a pipeline but produces no artifact, with a doc that looks current but tracks a two-month-old schema. No gate caught it because nothing consumed it — the classic "no consumer, no signal" blind spot.
What shipped
Mirror the working iOS pipeline (sd.config.mjs → DesignTokens.swift, gated by make tokens-check) for Android, reusing its hand-rolled-format philosophy so the output depends on no built-in Style Dictionary transform group (the thing that broke the original config across the v3→v5 migration).
sd.config.android.mjs(SD v5, ESM) — custom Compose + XML formats. Colors: hex andrgba(r,g,b,a)→Color(0xAARRGGBB)/#AARRGGBB(e.g.rgba(0,0,0,0.84)→ alpha0xD6). Spacing/radius/size/layout/shadow px →Dp/<dimen>Ndp. Opacity →Float. Motion{easing,duration}→ duration-ms + easing comment. The 16typography.*tokens are semantic style strings (largeTitle/bold/rounded), not numeric sizes — so there's nothing to emit assp; they're a reference comment block pointing at the MD3 type-scale mapping.- Generated + committed artifacts:
android/FitMeDesignTokens.kt(FitMeColors/FitMeSpacing/FitMeRadius/FitMeSize/FitMeLayout/FitMeElevation/FitMeOpacity/FitMeMotion),android/res/values/colors.xml(49),android/res/values/dimens.xml(34). - Drift gate:
npm run tokens:android+tokens:android:check(scripts/check-tokens-android.js— regenerate, strip date lines, diff, restore;execFileSyncwith fixed argv, no shell). - Removed the broken
design-tokens/config-android.json(superseded). - Refreshed
android-token-mapping.md(§0a "AND-1 SHIPPED") +android-adaptation.md(generated token layer; fixed a staleFocus.ringrow removed in audit DS-015).
Decisions
- Compose + XML, not XML-only (operator choice) — the complete, modern-Android-consumable output. Marginal cost over XML-only was ~1h.
- Raw-pixel
Dp, not rem-scaled — cross-platform parity with iOSCGFloatdirect values; the original rem-input assumption was the root of the broken transforms. - Hand-rolled formats — like the iOS config; immune to Style-Dictionary built-in-format drift, which is exactly what broke v1.
Verification (T1)
npm run tokens:android→ 3 artifacts, 0 errors.- Two consecutive builds → byte-identical (
diffclean). tokens:android:check→ exits 0 in-sync; non-zero after a token edit (verified mutate-then-revert).- 49 colors + 34 dimens, 0 duplicate resource names; Kotlin val-name "collisions" (
small,medium) are across different objects — valid Kotlin.
What this is NOT
This builds the token layer, not an app. No Gradle, no Compose screens, no AndroidManifest.xml. android-app-implementation (AND-3) remains deferred. The value here: the Android design system is now a real, generated, drift-gated artifact instead of a broken config and a stale doc — ready to consume the day an app is warranted.
Follow-ups
make tokens-android-check+verify-localwiring (infra-glob → isolated worktree, separate PR).- This showcase MDX +
related_prsbackfill (post-merge close-feature).
Cross-references
- Source case study (FT2):
docs/case-studies/android-token-pipeline-case-study.md - Predecessor:
docs/case-studies/android-design-system-case-study.md(the 2026-04-04 research-only deliverable)
- •The generated Kotlin is NOT compiled — there is no Android toolchain in-repo — so verification is determinism + structural validity + the drift gate, not
gradlew assemble; the kill criterion "Kotlin won't compile in a real Android module" therefore cannot yet be evaluated. - •This ships the token layer only, not an app: no Gradle, no Compose screens, no
AndroidManifest.xml. The native Android app (AND-3) remains deferred indefinitely (re-eval 2027-05-26). - •The pipeline emits no typography sizes — the 16
typography.*tokens are semantic style strings (e.g.largeTitle/bold/rounded), not numericspvalues, so they ship as a reference comment block pointing at the MD3 type-scale mapping rather than generated dimens.
- Generated Kotlin won't compile in a real Android module
- Drift gate false-positives on a clean tree
- Style Dictionary v6 removes the v5 format API