Minimal Design System

A 100-component React kit where every design rule is a record.

Design systemsDesign tokensAgentic workflowAccessibilityCI gates
The kit's docs site home page in the neutral brand, light mode, with a brand toggle in the header
The docs site, neutral brand, light mode. The header toggle is the whole argument of the site: switch the brand and everything below re-skins from the token layer.

Every gate was made to fail before it was trusted, and debt is only allowed to shrink. Built over 98 days, with agents doing the volume and the gates doing the arguing.

Year
2026
Role
Designer and system architect. I own the token model, the rules, the contracts and every merge. Agents executed under the gates.
Type
Personal component kit, private package (@weeeha/ui)
Scale
100 components, 30 rules, 94 stories, 34 guidance pages, 327 commits in 98 days
Proof
5 CI gates with fixture suites, axe on every story in both themes, a consumer harness that installs the kit from its own tarball
Stack
React 19, TypeScript, Tailwind v4, Storybook 10, Vitest, Claude

The claim, proven

The README makes one promise: to rebrand, replace the Layer 1 ramps and the Layer 2 accent pair. Nothing else changes.

The docs site carries a second brand, warm, that is that promise executed. It overrides Layer 1 and the accent, touches no component, and the header toggle switches it live. A separate consumer app in CI does the same thing with a terracotta theme and runs axe on the result in both modes.

The same docs home page in the warm brand, light mode
Warm, light
The same docs home page in the warm brand, dark mode, with a lime accent
Warm, dark

The whole rebrand

The warm brand file, condensed. Two neutral ramps re-hued, one accent pair, three radii. The comment at the top of the real file reads: “this file is that claim executed”.

/* docs/src/brand-warm.css, condensed */
:root[data-brand="warm"] {
  /* Layer 1: both neutral ramps, hue ~85 */
  --base-50:  oklch(0.985 0.003 85);
  --base-100: oklch(0.966 0.004 88);
  /* ...ten more base steps... */
  --ink-50:   oklch(0.985 0.003 85);
  /* ...ten more ink steps... */

  /* Layer 2: the accent pair, lime fill, olive text */
  --accent-lime:      oklch(0.841 0.238 128.85);
  --accent-lime-ink:  oklch(0.32 0.09 131);
  --button-primary:   var(--accent-lime);
  --text-on-accent:   var(--accent-lime-ink);

  /* Radius: one step up, ratio preserved */
  --radius-button: 12px; --radius-card: 20px; --radius-popup: 28px;
}
/* Components touched: 0 */

Same composition, two brands

The Requests block on the showcase: Card, Stat, Tabs, Table, Badge and Chart, no bespoke styling. Rendered neutral light and warm dark.

A Requests dashboard block with stats, tabs, a table and a chart, neutral brand, light mode
Neutral, light
The same Requests block in the warm brand, dark mode
Warm, dark

The kit itself

77 primitives from shadcn, Radix and Base UI, 22 AI Elements components, and the kit’s own MessageParts renderer, all reading the same token layer. Every example on the docs site is the real kit: imported components, live tokens.

Feedback components: alerts and toasts that carry state with a word and an icon as well as colour
Feedback. State carried by words and icons as well as colour, so it survives a colour-blind reader and a glared screen.
AI Conversation component docs page in light mode: a message log above a composer
AI Conversation, light
The same AI Conversation page in dark mode
The same page, dark. The composer is a sibling of the log, not a child, because everything inside the log scrolls away.
Button docs page: six variants ordered by emphasis and four heights
Button. Six variants ordered by the emphasis they claim, and four heights that line up with the inputs.

Architecture: three layers, one inversion

In most shadcn projects the shadcn variables hold the values and the team’s own tokens alias them. Here it is reversed: the semantic layer owns the values, and shadcn’s names are the alias.

  1. Layer 1

    Primitives

    Raw ramps. Two neutral ramps on purpose, base for surfaces and ink for content, 12 steps each, so surfaces can warm or cool without dragging text along. Six hue ramps at 11 steps. No component reads these.

    90

  2. Layer 2

    Semantic

    Surface, text, icon, border, button, accent, status and chart roles. The only place where light and dark values are decided. The accent is one pair, and five other tokens derive from it.

    83

  3. Layer 3

    shadcn alias

    --background, --primary, --muted and the rest, mapped with var() onto Layer 2. A test asserts each alias is exactly var(--target), nothing more.

    23

Declaration counts from packages/tokens/src/tokens.css on main, 2026-10-30. Layer 2 counts light values; dark overrides sit in the same file.

Two neutral ramps, and what a brand does to them

Real values. The neutral brand’s ramps are hex; the warm brand’s are oklch around hue 85. Surfaces read base, content reads ink, so a brand can warm the page without warming the text, or the other way round.

base, neutral (surfaces)

  • 0
  • 50
  • 100
  • 200
  • 300
  • 400
  • 500
  • 600
  • 700
  • 800
  • 900
  • 950

ink, neutral (text, icons, borders)

  • 50
  • 100
  • 200
  • 300
  • 400
  • 450
  • 500
  • 600
  • 700
  • 800
  • 900
  • 950

base, warm brand

  • 50
  • 100
  • 200
  • 300
  • 400
  • 500
  • 600
  • 700
  • 800
  • 900
  • 950
  • accent

The ink-450 step was added on 2026-08-22 for one job: dark tertiary text.

Three real token chains

How a value reaches a component. The component never names Layer 1; it names Layer 3, which names Layer 2, which names the ramp step.

  1. Layer 1

    --base-0

    Layer 2, light

    --surface-card

    Layer 3

    --card

    Component reads

    bg-card

    Card, Dialog, Popover

  2. Layer 1

    --ink-950

    Layer 2, light

    --text-primary

    Layer 3

    --foreground

    Component reads

    text-foreground

    every body text

  3. Layer 1

    --base-900

    Layer 2, dark override

    --surface-card

    Layer 3

    --card, re-declared in .dark

    Component reads

    bg-card

    same class, no change

Any stock shadcn component drops in unmodified and inherits the theme. The rule the repo gives agents is one line: if a change needs to touch Layer 3, the semantic layer is missing a token.

Dark mode that nests

Dark values never live in a component. They live in Layer 2 only. Nested dark works because var() substitutes at the element that declares it, so every alias whose target flips between modes is re-declared inside the dark block with the same expression, as the third chain above shows. A section can go dark inside a light page.

The docs home page in the neutral brand, dark mode
Neutral, dark. Same page, same components, Layer 2 values only.

Dark made honest, 2026-08-22

Dark tertiary text before and after, with the kit’s own values. One new ramp step, zero component edits.

2.57:1Tertiary text on its surface

Before · --ink-600 on --ink-950

Tertiary text, as a caption or a timestamp reads. Fails AA at any size.

6.57:1Tertiary text on its surface

After · --ink-450 on --base-950

Same role, same component. The surface moved to the base ramp and the text got a step that did not exist.

The first full dark audit the next day passed 279 of 279 stories in both themes. About 130 axe violations cleared by changing roughly five Layer 2 values. None of the fixes were per component.

Figma as a name contract

There is no sync pipeline. tokens.ts holds metadata only: the CSS property, its byte-exact Figma variable name, and a usage line. A mirror gate checks that every documented name exists in CSS and every CSS token is documented. Drift the other way is recorded by hand with a status per record. Storybook renders swatches from the CSS and names from the metadata, so the docs cannot disagree with the code. Since 10-21 a second set of eight rules holds the Figma components themselves to the code contracts.

Storybook Design Tokens page with colour swatches and their Figma variable names
Storybook's Design Tokens page: swatches from the token CSS, names from tokens.ts. A token documented but not declared, or declared but not documented, fails the mirror test.

Rules as records

Every design rule in the kit is one typed record: an id, a severity, how it is detected, the fix, and why. There are 30: 22 for the code and, since 10-21, 8 for the Figma components.

One command emits those records to JSON, and that JSON is what the detector, the write-time hook, the unslop audit, CI and the Storybook Rules page all read. There is no second list to drift from. The repo’s own phrasing: a ban that exists only in a document does not exist.

One record, five readers

TOK-8, as it sits in rules/tokens.ts. The fix is a required field; a ban with no substitution is refused by the schema.

id
TOK-8 · blocker · detect: grep
title
No opacity modifier on a foreground. The dim belongs in Layer 2.
pattern
\b(?:text|fill|stroke|placeholder|decoration|caret)-(?!(?:xs|sm|base|lg|[2-9]?xl)/)[a-z][\w-]*/\d{1,3}\b
scope
src/components, src/lib, src/hooks · exempt: stories, tests, ai/sources.tsx
fix
Use the flat rung that owns the job (text-muted-foreground or text-text-secondary for quiet text, flat text-destructive, stroke-chart-grid for gridlines). If the dimmed value is genuinely new, declare the mix in Layer 2 the way --chart-grid does, and document it in tokens.ts. Never compose it in the component.
why
A composite resolves against whatever sits behind it, so its contrast is decided by the ground, not the token. Axe only measures the grounds stories happen to use, and never measures hover states. A /50 foreground shipping an AA failure under exactly this pattern is the recorded precedent.

npm run rules:emit writes rules/tokens.json, and five things read it:

  • Detectorcheck:tokens, check:rules
  • Write-time hookwarns in the agent session
  • unslop skillthe audit before done
  • CIstatic job, fails the PR
  • Storybook Rules pagerendered live, for people

The catalogue

22 blockers, 4 warnings, 2 at review, 2 notes. STA-7 needs a rendered page, SYS-3 needs judgment, and the Figma rules need the Figma file, so CI cannot run them. They print as unchecked on every run, so “I did not check this” never looks like “this passed”.

IdRuleSeverityDetect
COL-1Zero gradients in product UIblockergrep
COL-6Indigo, violet and purple exist only as chart-4warningheuristic
COL-9Backdrop blur earns its contentreviewheuristic
ICO-2Zero emoji in chromeblockergrep
MOT-1Motion only on state change, zero infinite loops outside progress indicatorsblockergrep
MOT-2Named transition properties, never transition-allblockergrep
MOT-9Motion reads tokens, never numeric stepsblockergrep
CPY-1Say the thing: no generic CTAsblockergrep
CPY-2Banned marketing registerblockergrep
CHT-1Chart colours are chart-1 to 5, never library defaultsblockergrep
CHT-3Flat marks, hairline gridlinesblockergrep
STA-3outline-none never ships without a focus-visible replacementreviewheuristic
STA-7The 375px pass: no horizontal scroll, tap targets at least 24pxblockerrendered · unchecked
SYS-3No decoration where hierarchy was the needblockerjudgment · unchecked
TOK-1No raw hex colourblockergrep
TOK-2No raw oklch()blockergrep
TOK-3No Tailwind palette classblockergrep
TOK-4Spacing only from the scale, off-scale literals ratchetblockergrep
TOK-5Motion only from the four durations, never a bracket literalblockergrep
TOK-6Radius only from the scale, off-scale literals ratchetblockergrep
TOK-7Type size only from the scale, off-scale literals ratchetblockergrep
TOK-8No opacity modifier on a foregroundblockergrep
CON-1Every component set matches a code componentblockerfigma
CON-2Every variant value in Figma is accepted by the contractblockerfigma
CON-3Contract values the set does not shownotefigma
CON-4Variant properties name a contract prop or a declared Figma-only axiswarningfigma
CON-5Paints bind a library variable where one resolves to the same valueblockerfigma
CON-6Layers bind the semantic token the contract names, not a primitivewarningfigma
CON-7The set's description carries the contract's purposewarningfigma
CON-8Contract tokens are bound by some layer in the setnotefigma
Storybook Rules page listing rule records with pattern, scope, exemptions, fix and reason
The Rules page in Storybook, rendered from the emitted JSON. The first record, COL-1, names its pattern, scope, one exempt file, the fix and the reason.

Eight rule bodies, one line each in the always-on file

The agent instructions file was 31 KB and re-injected into every session. It was split into eight rule bodies behind one-line stubs, with a byte ceiling that only ratchets down and a provenance test: a rule can leave the always-on file only if a gate or a written “ungated” reason stays behind.

  • Never hardcode. No raw hex, no oklch(), no palette classes, no arbitrary values, no opacity on a foreground.
  • Motion. Four durations chosen by what is moving. Because every transition reads a token, one reduced-motion block disables motion kit-wide, including components that do not exist yet.
  • Usage contracts. A per-component JSON with a purpose that must say when the component is the wrong choice, anti-patterns with a why, and relationships.
  • Gates and baselines. The ledger of every gate, when it landed, and what it carries.
  • Conformance ladder. A vendored checker with ten stages. Which claims are met, which are unchecked by design.
  • Review rounds and blockers. A round budget and a shared definition of blocker.
  • Traps. Seven bugs already paid for, so nobody rediscovers them by hand.
  • Worktrees and installs. One checkout per task, and a hygiene report for stranded work.

A trap, as recorded

The first entry in traps.md. Tailwind v4 has an --ease-* theme namespace and no --duration-* one, so the motion table compiled to nothing for three days.

Before

/* 2026-08-11 to 08-14: written everywhere,
   in effect nowhere */
@theme {
  --duration-fast: 100ms;
  --duration-base: 200ms;   /* compiles to NO utility */
  --duration-slow: 300ms;
}
/* every element wearing duration-base
   silently fell to the default duration */

After

/* PR #41, 2026-08-17 */
@utility duration-base {
  transition-duration: var(--duration-base);
}
/* ...and MOT-9: 22 numeric durations swept
   to tokens; rule record added so the hook
   and CI catch the next one */

The trap entry ends with the test: “compile globals.css and grep the output for the class before believing it exists.”

Four gate moments

A rule is only as real as the moment it runs. The kit checks at four points, each with a different cost and a different authority.

  1. 1 · Write time, cheapest

    Hook

    A pre-write hook lints what an agent is about to write against the blocker rules. Warn mode on purpose: it earns the right to block only after every grep rule has a false-positive fixture and two clean weeks.

  2. 2 · Definition of done

    Local suite

    Lint, typecheck, check, test. Four Vitest projects: unit, gates, Storybook light, Storybook dark. Nothing is done before all four are green on the author's machine.

  3. 3 · Every PR

    CI

    Five file-reading gates: tokens, stories, capabilities, liveness, rules. Axe on every story in real Chromium, once per theme, zero opt-outs. The consumer harness. The conformance ladder.

  4. 4 · Before merge, highest authority

    Second reader

    A review skill reads the diff, the touched contracts, the rule output over changed files, CI status and a report of test lines the PR removed, then writes a verdict with cited blockers. Advisory: a human approval outranks it.

The portability gate: install the kit the way a stranger would

The consumer harness, as it runs in CI. Every step is real; the screenshots are uploaded as artifacts.

  1. npm packthe kit as a tarball
  2. Installinto a standalone Vite + Tailwind v4 app, node_modules wiped first
  3. Re-themetheme.css: two neutral ramps, one brand ramp, one accent pair
  4. tsc + buildthe consumer's own TypeScript, not the kit's
  5. 9 probespaint, type, radius, focus ring, dark
  6. axe × 2light and dark
  7. Screenshotsbefore and after, uploaded as CI artifacts
/* harness/consumer/src/theme.css, condensed */
@import "@weeeha/ui/globals.css";
:root {
  /* two neutral ramps, warmed */
  --base-50: #fbf8f3; /* ... */ --base-950: #0f0d0a;
  --ink-50:  #faf7f2; /* ... */ --ink-950:  #0d0b09;
  /* one brand ramp */
  --accent-50: #fff7ed; /* ... */ --accent-950: #431407;
  /* the pair. Everything accent-coloured derives from it */
  --accent-solid:    var(--accent-800);
  --accent-on-solid: var(--accent-50);
}

What the harness found on its first day, 2026-08-18:

  • Tailwind was not declared as a peer dependency. A consumer's build failed on line 1 of globals.css.
  • The @source globs were too wide: 53 story-only utility classes shipped into consumer CSS. Narrowed to zero.
  • A same-named tarball reported up to date and never reinstalled, so every measurement after the first tested the old kit.
  • A half-rebrand: four of six accent tokens set by hand. The accent pair now makes that impossible, because the six derive from the two.

Two contracts a component can carry

A story declares what the component can do, and owes a story per state. A usage contract declares what the component is for, and when it is the wrong choice.

Capability contract: tags owe states

A story file declares capabilities. Each tag owes named stories, and CI derives the debt. The example is alert-dialog.stories.tsx on main.

// src/components/alert-dialog.stories.tsx
const meta = {
  parameters: {
    capabilities: ["interactive", "text-bearing"],
  },
}

// owed by "interactive"
export const Disabled: Story = { /* ... */ }      // line 66
// owed by "text-bearing"
export const LongContent: Story = { /* ... */ }   // line 101
text-bearing
LongContent
interactive
Disabled
validating
Invalid, Required
async
Loading, Failed
collection
Empty

Every story is also an axe test, in both themes. So “owes a Disabled story” means “the disabled state gets audited for contrast”, which is where disabled states usually fail.

Usage contract: what it is for, and when it is wrong

badge.meta.json, excerpted. The schema sets minimum lengths: an 80-character purpose, a 20-character intent per variant, a 20-character why per anti-pattern. The repo calls padding to pass “worse than failing”.

purpose
A small, non-interactive label for state, category or count, whose word carries the meaning while the variant only ranks its emphasis. It is the most over-used component in a kit: it looks decorative, so it accumulates on a screen until nothing is emphasised. It is the wrong choice for anything the reader is meant to click.
variant default
Solid accent fill, the strongest weight in the ramp. Reserve it for the one state that needs someone to act on it now.
variant ghost
No border and no fill at rest. For a state that has already resolved and no longer needs to be scanned for.
token
text-on-accent: kept as one pair with button-primary so the two cannot drift apart.
anti-pattern
aria-label on an icon-only badge. The badge is a bare span with no implicit role, and ARIA prohibits aria-label on a generic element. Assistive tech silently drops it and axe fails.

30 of 100 components carry one. The floor is 29 and can only rise. An agent placing a component is told to read the index first and at most two full contracts, never to survey the source folder.

Badge component docs page rendering its usage contract
The same contract, rendered as the Badge docs page.

Ratchets and floors

A gate that demands a clean slate never gets adopted. Each gate here binds on day one by carrying the debt that already existed, in a file that is only allowed to shrink.

A baseline lists the pre-existing violations, and the gate fails only on new ones. A floor is the inverse: an untagged component owes nothing, so coverage could decay to zero without a single violation unless a minimum count holds it. Growing a baseline, widening an exempt list, or lowering a floor is a review blocker.

Where the numbers moved

Light bar: when the gate landed. Dark bar: main on 2026-10-30. Bars share a scale within each row only.

  • Stories failing axe

    22 → 0

    22 on 08-17, 0 on 08-18, has stayed 0

  • Tokens with no consumer

    31 → 0

    31 found 08-21, 42 more found 09-04 and deleted, emptied 10-01

  • Owed capability states

    74 → 24

    74 states in 40 files on 08-23

  • Always-on agent file, bytes

    31,092 → 16,232

    ceiling 17,000, lowers only

  • Tagged story files (floor)

    1 → 26

    1 on 08-18, 18 on 09-08, 26 on 09-09

  • Components with a usage contract (floor)

    0 → 30

    schema landed 09-14, floor now 29

Still carriedNow
Off-scale values (spacing, radius, type)24 in 14 files
Components without a story8
Owed capability states24 in 15 files

Each of these is a baseline that can only shrink. Colour and motion rules carry no baseline: those are strict.

The loops

Gates stop the same mistake twice. Loops are what turn a mistake into a gate: an agent builds, a gate fails, the instance gets fixed, and the fix is encoded as a rule, a contract or a trap, so next time the gate catches it at write time.

One lap, as it happened

Build
In an earlier system, a sidebar shipped text-sidebar-foreground/50 for quiet text. The precedent is cited in the record.
Gate fails
Axe measured the composite on one ground and it failed AA. Hover grounds were never measured at all.
Fix
Replace the opacity with the flat rung that owns the job.
Encode
TOK-8, 2026-08-23: no opacity modifier on a foreground, blocker, grep, with the fix and the precedent written into the record.
Next lap
The hook warns at write time, CI fails on the pattern, the Rules page shows it, unslop audits for it.

Every gate is made to fail before it is trusted

Before a gate is adopted, someone breaks the code on purpose: append a raw hex to a component, watch the exit code, restore. The mutation is kept as a fixture test, and the fixture suites run in CI next to the gates. A gate that matched zero files used to pass. Since 2026-08-18, zero matched files is a failure.

How reviews are run

  • A round budget. Rounds one and two fix everything worth fixing. From round three only blockers get fixed in the PR, and everything else becomes a linked follow-up, because each fix invites a finding about the fix. Two blocker classes were written for the agent era: contract prose the change made false, and a weakened test or gate.
  • Findings are hypotheses. A review of a stale branch produced three findings. Rechecked against main, one reproduced and was repaired the same day with 23 new tests, each watched failing first. One was already fixed. One was stale. Reproduce before repairing is now a written rule.
  • Open decisions stay open. A new contrast failure is a design decision, so agents propose values with ratios and wait. Never darken tokens quietly to make CI green. The five failing pairs from August were settled in two reviewed PRs, not by a script.

What the loops caught

Three things that were green until a gate learned to look, beyond the motion trap, the dark audit and the harness above.

  1. 08-13 to 08-18

    Red for four days

    The accessibility job failed on five consecutive pushes while the fix sat on an unmerged branch, next to four gate scripts nobody had wired. The rescue took 22 failing stories to zero and adopted the gates the same day. The lesson became the one-checkout-per-task rule and a hygiene report for stranded branches.

  2. 08-21, 09-04

    Dead tokens

    The first liveness run found 31 tokens consumed by nothing, including an entire elevation family that was declared, themed and mirrored in Figma. Widening the check to every root token found 42 more dimension and weight roles. All deleted. The baseline is now empty.

  3. 08-23

    The phantom font

    The mirror gate's first run found font-mono documented, named in Figma and loaded by Storybook, read by nothing: one missing theme line. A token can be in every artifact except the one that matters.

What is not done

The repo states its own gaps. So does this page.

  • 30 of 100 components carry a usage contract. The schema refuses placeholders, so the other 70 are owed, not faked.
  • 10 components have no story. Two are declared to have no visual surface. Eight are carried, and two of those, sidebar and navigation menu, are the most accessibility-sensitive in the kit.
  • 24 capability states are still owed across 15 story files.
  • The write-time hook still warns. It blocks nothing until the promotion criterion is met.
  • No public release. @weeeha/ui and @weeeha/tokens are versioned 0.1.0 for a restricted GitHub Packages registry.
  • Figma sync is a name contract, not a pipeline. Twenty drift records are still marked undecided.

Conformance ladder

Ten stages in a vendored checker. The honest number is the highest contiguous stage, so a built stage 09 does not raise it.

00Architecture mapmet, gated
01Token contractmet, gated
02Rule cataloguepending
03Gate momentspending
04Component metapending
05Derivationpending
06Story coveragepending
07Docs generationpending
08Design-tool syncpending
09Loops and conventionsprobed, not adopted
10Runtime identity and stream renderingmet, gated
11Component syncmet, gated

Stages 02 and 03 have no probe yet, though their substance has landed as the rule records and the four gate moments above.

98 days

  1. 07-24

    Extracted: the token architecture, the primitives and the AI Elements set, from the Background IP of an earlier engagement. Everything client-specific stayed behind.

  2. 08-01

    16 guidance pages, with a review commit after nearly every page.

  3. 08-11

    First conformance gate wired: axe on every story, CI, motion tokens.

  4. 08-18

    The rescue: accessibility green, gates adopted with fixtures, consumer harness built the same day.

  5. 08-22

    Dark made honest. The token check becomes rule records.

  6. 09-04

    Hue ramps added to Layer 1, one accent pair. Liveness widened, 42 dead roles deleted.

  7. 09-14

    Agent file split into eight rule bodies under a byte ceiling. Usage-contract schema lands.

  8. 10-03

    Every token read or deleted. Review round budget and blocker taxonomy. Branch hygiene report.

  9. 10-19

    Stage 10 met: runtime identity, and MessageParts, the kit's own renderer for streamed AI SDK message parts.

  10. 10-21

    Stage 11 met: Figma components synced against the code contracts, with eight Figma rules.

What I would do differently

Wire the gates before the components, not a month after. The kit had 95 documented components on 08-10 and its first real gate on 08-11, and the week after that went to rescuing fixes stranded on branches. Gates on day one would have cost a day and saved a week.

One checkout per task from the start. Stranded work was the biggest loss: 31 unmerged branches and 20 live worktrees when I finally measured it on 08-17.

What surprised me

How often green was a lie. The harness that never reinstalled, the gates that matched zero files, the tokens documented in three places and read by none. Each of those passed every check that existed. The useful work turned out to be breaking each check on purpose before trusting it.

What I learned

The semantic layer is where accessibility gets paid for. About 130 dark-mode violations disappeared by changing roughly five values in Layer 2, and not one component.

A rule that lives only in prose does not exist. The next agent reads stale prose as fact, so the rule has to be a record with a detector, or it has to say out loud that it is unchecked.

See it

The repository is private. The method that moved between this kit, a public registry and a client rebuild is written up separately in the Agentic Design Systems case.