Appearance
ADR-0015 · Design governance & drift reconciliation
Status: 🟢 Accepted — adopted 2026-07-26 · Depends on: ADR-0011 (two UI idioms over one token package), ADR-0012 · Related: screen reviews, QRS-188
The one-line thesis
Governance is asymmetric, split on a decidable line — not on "how small is this change". A diff that touches packages/tokens or apps/mobile/src/ui is design-first, always; a diff that touches only a feature screen's composition is code-first, freely. Divergence is recorded at the moment it happens, never discovered by a later audit, and reconciled at the develop → uat promotion that already exists.
Context
CLAUDE.md states that the Claude Design MCP project is the source of truth and that "visual drift from the design is a bug". Read absolutely, that requires updating the design system before every visual refinement — including a 4 px spacing nudge found on a device. Product's position, and it is correct, is that this trades an unacceptable amount of delivery speed for governance that nobody asked for at that granularity.
The proposal put forward was: Claude Design as the baseline; small refinements implemented directly; periodic drift audits; batch sync back to the design system. Three of those four are adopted below. The audit is not, for a reason this repo has already proved twice.
Why "periodic audit + batch sync" fails here specifically
This is not a generic caution. Measured against this repo's own history, deferred-reconciliation debt has a base rate near zero:
| Deferred item | Outcome |
|---|---|
QRS-TBD — "assign real ids later, in the real tracker" | Survived ~170 items and several months, covering shipped features and two remediated security incidents. Fixed only when the user noticed (QRS-180) |
~56 files still call supabase.from()`` | Documented as transitional with intent to converge. Still open |
| Two caching layers; two "shared" locations | Same. Still open |
| CI lint gate | Believed enforced for months, was a green no-op (QRS-013) |
Any workflow whose correctness depends on a future, unscheduled, unowned audit must be assumed to be a workflow that never audits. The mechanism has to be forced by something that already happens.
Why "small vs large" is the wrong axis
The changes that actually emerged from the first real device-testing pass:
| Change | Layer | Blast radius |
|---|---|---|
| Line-height clipping (QRS-181) | token (typography) | Every string in the app |
| Haptics coverage (QRS-183) | primitive (PressableScale) | ~36 controls |
| Tab-bar active state (QRS-186) | primitive (app shell) | Whole app |
| Social brand colours (QRS-187) | primitive (BrandIcon) | Every social field |
| Dashboard grid, Social layout | screen | One screen each |
Four of six were token- or primitive-level. A workflow optimised for ad-hoc screen tweaks is optimised for the minority of what real testing surfaces. Worse, "is this small?" is not decidable by two people independently, so it cannot be a gate. "Did the diff touch packages/tokens or src/ui?" is decidable by a script.
The tab bar is the worked example, and it cuts both ways
Product asked for the tab bar's active-state background pill to be removed. Pulling components/app-shell/TabBar from the design project first showed the design already specified background: 'none' with active state carried by colour alone. So the "ad-hoc improvement" was in fact a drift correction.
Had it been free-handed: the same visual result, but recorded as a divergence from the design, and eventually "synced back" a change the design already contained. Wasted work in both directions, and a design file edited to match itself. One file read prevented that.
The risk that is specific to this architecture
Per ADR-0011, QRSETU implements components twice — shadcn/DOM for public + admin, RN/NativeWind for the merchant app — with the shared token package as the only thing binding them. If the RN side accepts an ad-hoc primitive change and the design is not updated, the DOM side receives no signal that anything moved. The seam CLAUDE.md explicitly names — a merchant previewing their card in-app versus the live public card — then breaks with no failing test and no reviewer positioned to see it, because the two halves live in different codebases. This is the reason token/primitive changes cannot be code-first, independent of how small they are.
Stale design is a poisoned input, not a neutral one
Already realised once here. CLAUDE.md carries the scar: the QR setu prototype-handoff/ files "predate the current design system … do not implement from it." And the screen reviews record what a drifted source produced — a prototype that "ended up inventing an Ad Manager and an in-house billing engine with no backend decision behind either."
Once the design source drifts, Claude Designs reasons from stale foundations and generates work that contradicts shipped reality. That is more expensive than having no design at all, because someone must adjudicate the contradiction before anything can proceed.
Decision
- Asymmetric governance on a mechanically decidable line.
- Systemic surface — design-first, no exceptions.
packages/tokens/**andapps/mobile/src/ui/**(plus the futureapps/web/src/ui/**). Few files, changed rarely, cheap to change in the design system, catastrophic to fork across two stacks. Pull the design, change it there if it is wrong, then implement. - Screen composition — code-first, freely. A feature screen's layout, grouping, ordering and copy placement. Build it, ship it, record it. This is where the velocity argument is wholly correct.
- Systemic surface — design-first, no exceptions.
- Record at the moment of divergence, never at audit time. One entry in
documentation/portal/design-system/design-drift-ledger.mdin the same PR that diverges: what changed, why, which design path is now stale. ~30 seconds while the decision is fresh, versus archaeology from a diff months later. This converts reconciliation from discovery (expensive, skippable, and the thing that never happens) into working through a list (cheap, and obviously incomplete if skipped). - Reconcile at the
develop → uatpromotion, not on a calendar. That gate already exists, is manual, and the user is personally the gate. "Periodic" has no owner and no forcing function; a ritual attached to an existing ritual survives. - Machine-enforced, because conventions here demonstrably decay.
tools/check-design-drift.js(npm run check:design) fails when a diff touches the systemic surface without a ledger entry in the same range. Same shape ascheck-sql-grants.jsandcheck-readmes.js— the enforcement layers that exist because review missed something once. It fails closed: if it cannot determine a base ref it errors rather than passing, since a gate that can silently pass is worse than no gate (the green-no-op lesson). - A drift-correction is still a ledger entry. When implementation is changed to match the design (as with the tab bar), record it as
correction. It needs no sync-back, and recording it is what stops someone later "syncing" the design to itself.
Options considered
A — Asymmetric split + record-at-divergence + release-boundary sync · [adopted]
- For: the gate is decidable by a script rather than by judgement; protects the surface where divergence is systemic and cross-stack while leaving screen work unblocked; the recording cost lands when context is free; the cadence is bound to an event that already reliably happens.
- Against: two rules instead of one, and the boundary is occasionally arguable (a primitive gaining a new variant for exactly one screen). Resolved by defaulting to design-first when in doubt — the cheaper error.
B — Design-first for everything (the literal reading of CLAUDE.md)
- For: the design file is never wrong; Claude Designs is always a safe input.
- Against: demonstrably too slow for device-testing feedback, and it would have forced a design round-trip for a line-height bug fix. Rejected as the default, retained for the systemic surface.
C — Code-first everywhere + periodic audit (as proposed)
- For: maximum velocity; matches how most teams actually behave.
- Against: the audit is the load-bearing element and has a measured near-zero completion rate here; leaves the two-stack token fork completely unguarded. Rejected — though its velocity intent is preserved for screens.
D — Abandon the design system as source of truth; treat code as canonical
- For: honest about where reality lives; no reconciliation cost at all.
- Against: forfeits the ability to design a screen before building it, which is the main reason the design project exists, and guarantees the two UI idioms diverge. Rejected.
Consequences
design-drift-ledger.mdis created and seeded with the real divergences to date, so it starts as a true record rather than an empty file nobody believes.npm run check:designis added and wired into CI. Expect it to fail PRs that touchsrc/uiwithout a ledger line; that is the intended behaviour, and the fix is one line, not a design round-trip.CLAUDE.md's design-system section is amended to point here, so the absolute reading ("pull the design before ANY UI work") is replaced by the asymmetric rule rather than being quietly ignored in practice — a standard that is routinely violated is worse than one that is accurate.- The
develop → uatpromotion checklist gains a design-sync step. If the ledger has open entries, they are either pushed to the design project (viaDesignSync) or explicitly deferred with a reason on the row. - Screen-level work no longer blocks on a design pull — but pulling first is still recommended when a design may already exist, for efficiency rather than governance. Re-inventing an existing design is wasted work even when it is procedurally allowed.
- Open risk, accepted: the ledger can be satisfied with a low-quality entry ("tweaked spacing"). No mechanism prevents that; it is caught, if at all, at the promotion review. Recorded here rather than pretended away.
Related
- ADR-0011 — the two-idiom frontend that makes token divergence a cross-stack fork.
- UI Quality Assurance — the testing tiers that detect unintended visual change; this ADR governs intended change. Both are needed.
- Design to Code Workflow · screen reviews