(motir-core) Raw / Tier-0 RADIUS escapes from the shape-swap layer are repo-wide — route them, then GUARD the radius axis like `swapLayerLint` guards colour
Repo: motir-core. One PR. Surfaced while fixing MOTIR-2301 (the ChildList row's rounded-md px-2 py-2), which deliberately scoped the sweep out. Logged as its own card rather than absorbed (notes.html #27 / the drive-by-fix rule).
⚠️ RE-SCOPED to the RADIUS axis by THE REPLAN ACTION, 2026-08-07. This card originally also owned the own-box PADDING / HEIGHT axis and sized both from a one-directory sample. Measured on
origin/main, that half is 447 literals in 190 files and — more decisively — has nowhere to go:--spacing-card-paddingis 24px and 140 of 145 card surfaces pad at something else, so the instruction "route it to the tokenCLAUDE.mdmaps its surface to" is unsatisfiable until the token set grows a density scale. That axis now lives in MOTIR-2335, whose first subtask is that decision; the story isblocked_bythis card because it extends the guard this card creates. Full evidence:notes.html#232 ("a sweep card whose routing instruction pointed at a destination that could not hold the values" — locate by title, the numbering shifts on append collisions), planning bug MOTIR-2337.
The defect class
CLAUDE.md's shape rule: a surface's own radius MUST name an element-semantic shape token — --radius-{btn,card,input,modal,badge,control,kbd} — because only those are overridden by a [data-style] block. A raw rounded-md, a bare rounded, or a generic rounded-(--radius-sm) does not participate in the style swap.
Why it stayed invisible
Measured on packages/design-system/theme.css: of the ten bare [data-style] token blocks, nine also override the Tier-0 --radius-xs/sm/md/lg/xl scale. So an escaped surface still moves when the style changes — just to the wrong value (the container radius instead of the control radius, differing under most styles). Only soft-playful leaves the generic scale alone, which is the one style where the escape is plainly visible. A visual spot-check does not find this class; the wrong-but-moving value looks fine. That is what makes a mechanical guard the deliverable, not a one-off sweep.
Inventory (measured origin/main @ 7ffa2ba4 — a sample of the shape, not the assertion)
62 offenders in 34 files. By token: rounded-(--radius-sm) ×18 · rounded-md ×15 · bare rounded ×15 · rounded-(--radius-xs) ×9 · rounded-none ×1 · rounded-lg ×1 · rounded-xl ×1 · rounded-[calc(var(--radius-btn)-2px)] ×2. Concentrations: the app shell's menus and switchers (ProjectSwitcher, WorkspaceSwitcher, UserMenu, SidebarHeader, ThemeToggle, CommandPaletteTrigger, CreateIssueButton — rows and <kbd> chips on --radius-sm/-xs), the work-item detail panels (AddLinkControl, ContentSectionCard, RelationshipsPanel, RemoveLinkButton, FieldCard, ParentBreadcrumb), the skeletons (IssueTreeSkeleton, BoardSkeleton, BacklogSkeleton — bare rounded), WorkflowEditor, LinkAddForm, CreateIssueLinksField, MarkdownEditor, TreeTable.
Do not treat that list as the specification — it goes stale on every merge. The guard IS the specification: run it, and fix what it names (notes.html's site-count-goes-stale lesson).
Three known allowlist candidates, each to be judged on its merits, not inherited:
app/tokens/page.tsx— the design-system specimen route, whose job is to render the Tier-0 scale as labelled swatches. The same exemptionswapLayerLintgrants it for--color-*.packages/design-system/src/components/ui/Segmented.tsxand.../theme/AppearancePickers.tsx—rounded-[calc(var(--radius-btn)-2px)], a value DERIVED from a semantic token so the inner pill fits its shell. It swaps correctly; it is not an escape.app/(authed)/items/[key]/_components/AttachmentPreview.tsx—rounded-none, if the intent is a deliberately square full-bleed image. Verify before allowlisting; a wrong reason is worse than a fixed site.
Acceptance criteria
- A structural guard
tests/theme/shapeSwapLint.test.tsexists, mirroringtests/theme/swapLayerLint.test.ts's exact shape: the samegit ls-filestracked-source set, comments stripped before matching, and the offender list asserted EQUAL to an enumerated allowlist — so a fixed exception must be deleted from the test and a new violation cannot hide behind an old one. - Its source set is asserted non-empty and containing known files (
swapLayerLint'sSOURCES.length > 1000+ named-file checks), so it can never silently pass by scanning nothing (notes.html#195). - The radius check flags every radius utility that is NOT an element-semantic role: the raw scale (
rounded-none|sm|md|lg|xl|2xl|3xl), a barerounded, the genericrounded-(--radius-xs|sm|md|lg|xl), and an arbitraryrounded-[…]that does not derive from a--radius-*token.rounded-fullandrounded-(--radius-pill)pass — a genuinely circular thing is not style-dependent. - Every site the guard flags is either routed to the role
CLAUDE.md's radius-by-surface table maps its surface to, or carries an allowlist entry stating why it is legitimate. The allowlist entries are the ONLY exceptions; there is no third disposition. - The guard's comment states which axis it owns and which it does not — radius here, own-box padding / height deferred to MOTIR-2335 — so the next reader does not mistake its silence on padding for permission.
pnpm test tests/theme/shapeSwapLint.test.tsis green, and reverting any one migrated site turns it red (state the negative control you ran in the PR body).- The change is a token substitution, not a redesign: every migrated site keeps its surface's role, and any site whose default-style pixel value moves is named in the PR body with its before → after.
Context refs
motir-core/CLAUDE.md— "Shape (radius + spacing + sizing) flows through element-semantic shape tokens", with the radius-by-surface table this sweep routes to.tests/theme/swapLayerLint.test.ts— the colour-axis guard this mirrors: therenderedSources()git ls-filesset,stripComments, the exact-allowlist discipline, and the reason each is written that way.packages/design-system/theme.css:159-174— the Tier-0 radius scale and the seven element-semantic roles;:360+— the ten[data-style]blocks, nine of which override the generic scale.- MOTIR-2301 (merged, PR #1900) — the one-row instance and the token pair a list row takes.
- MOTIR-2335 — the own-box padding / height axis carved out of this card; it extends this card's guard file.