- Date: 2026-06-06
- Status: Accepted
- Phase / Feature: Foundation v1 operational policy
- Closes: ADR 0036 Q1 — "Foundation upgrade path"
- Builds on: ADR 0036 (Foundation v1 spec, semver versioning), ADR 0035 (paradigm)
Context
ADR 0036 §2.7 chose semver for the Foundation package and noted that each project pins a version at create time (brand.foundation.version). What ADR 0036 deliberately deferred to a sub-ADR is the operational rituals around moving a project across major Foundation releases:
- A greenfield project created today pins to
1.0.0. The Foundation package will eventually grow to1.1.0(additive primitives, new components, no breaking changes),2.0.0(re-tier of color semantics, breaking variant API, etc.). - Patch + minor upgrades are safe and should NOT require designer intervention — but the project still needs to know what version it's resolved to right now so a returning designer reads the right stories / sees the right defaults.
- Major upgrades are not safe by definition. The decision shape, the pre-upgrade preview, the rollback story, the compatibility shim window — all need to be settled before we ship v2 lest we paint ourselves into the "every project is stuck on the version it was created with" corner.
This ADR fixes the rituals before the first v2 ships, in the same spirit ADR 0037 + 0038 settled persistence before the editor surface arrived.
Decision
§1 — Where the version lives
A single foundation_version text column on project. NULL = not Foundation-bound (classic / brownfield rows that never imported Foundation). For greenfield rows it carries the SemVer string the project is currently resolved to ('1.0.0' at create time, can be bumped by the rituals below).
We do NOT store a separate "created with" vs "currently using" pair. Audit trail of upgrade events lives in a future foundation_upgrade_event table (deferred to whenever the first v2 actually ships — out of scope for this ADR). Currently-resolved is sufficient for read-time behavior.
§2 — Version semantics + auto-upgrade scope
Foundation follows standard SemVer:
- Patch (1.0.0 → 1.0.1): bug fixes, no API changes, no visual change. Auto-upgraded on every project read. Designer notified via a single-shot toast on next visit ("Foundation 1.0.1 fixes a Checkbox focus ring bug. No design changes.").
- Minor (1.0.x → 1.1.0): additive — new components, new primitives, new icons. Existing primitives, semantics, components unchanged. Auto-upgraded on next project read after the new minor ships. Toast lists what's new (one line per category).
- Major (1.x.x → 2.0.0): breaking. Removed primitives, retiered semantics, breaking component variant API, etc. Never auto-upgraded. Project stays pinned to its current major until the designer (or admin in multi-user mode) goes through the migration ritual below.
§3 — Major-upgrade ritual
When a project pinned to 1.x.x opens and a 2.x.x Foundation is available, the Design System tab surfaces a non-blocking banner. The banner offers:
- Preview migration. Opens a side-by-side comparison: current 1.x render of the project's component instances + tokens vs. a synthetic 2.x render. The synthetic render runs the migration rules (§4) against the existing project state — designer sees exactly what will change.
- Apply migration. Runs the migration rules, writes a
foundation_upgrade_eventrow (deferred table), bumpsproject.foundation_versionto the new major. Reversible by walking back to the previous version row for a 30-day window (§5). - Stay on 1.x. No change. The 1.x major receives security + patch fixes for N+1 minor cycles after 2.0.0 ships — i.e. when 2.0.0 lands, 1.x continues to receive patches at least until 2.1.0 (typically 3-6 months). Hard sunset date is communicated in the banner.
The banner is dismissible per major-version offer, not per session. Re-shows on the next project read.
§4 — Migration rules format
Each major upgrade ships a migrations/<from>-to-<to>.ts file in the Foundation package. The file exports:
defaultMigrations: BrandSeedMigration[]— token renames that auto-apply (e.g.text.subtle→text.mutedkeeps the binding pointed at the new name).componentMigrations: ComponentMigration[]— variant API rewrites (e.g. a Buttontoneprop split intotone+emphasis; the migration carries a deterministic mapping).manualReviewItems: ReviewItem[]— rules the designer must look at by hand (e.g. removed primitives that have no clean replacement; new accessibility constraints that fail on the current state).
Run shape:
const result = migrateProject(projectState, migrations);
// result.applied: changes the migration could auto-make
// result.reviewItems: items the designer must touch
// result.blockers: items that would require code-level intervention (rare)Apply emits the full diff to the audit table.
§5 — Rollback
Within 30 days of an Apply, a Rollback action restores project.foundation_version to the previous major and reverts the auto-applied changes that came from defaultMigrations / componentMigrations. After 30 days the rollback affordance disappears — manual ADR-driven recovery only.
This is identical in spirit to the WCAG override revoke flow (ADR 0037 §2): soft-delete + window, no destructive overwrites.
§6 — Multi-user / brand inheritance (parking)
The rituals above describe single-owner projects. Cross-project brand kits (parked in ADR 0038 §"Open questions") need separate thought when a shared brand kit is bumped to v2. Defer until shared kits exist.
Anti-patterns explicit
- Do not silently bump a project past a major. Designer trust is the only moat the rule-locked cascade has over Figma; silent breaking changes invalidate it.
- Do not keep the pre-migration snapshot forever. 30 days is enough for "I changed my mind"; longer is dead state that confuses the storage shape.
- Do not support partial migrations ("apply some rules, defer others"). The migration result is atomic — apply or don't. Mid-state is a third version no one tested.
- Do not ship a major without
manualReviewItemsif any are present. The banner's Apply CTA must be disabled until the designer has acknowledged each item.
Open questions / parking
- Cross-project brand kits. ADR 0038 parked sharing. Major migration on a shared kit cascades across multiple projects — needs ritual design when the shared-kit feature lands.
- Pre-release Foundation versions (
2.0.0-beta.1). v1 ships strict SemVer only. Pre-release tagging would let early designers opt into the next major on a staging project before it stabilizes. Out of scope for v1 ops. - Tooling for the Apply diff. The migration result is JSON-friendly; how the designer reviews 200 line items pre-Apply is a UX problem of its own. Lives with the future in-app token-editor surface.