Back to work

Building Parsimony, an Agentic Design System

A cross-site design system built to be used by agents as much as by people: one source of truth, from DTCG tokens through framework-agnostic components to an MCP server an agent can read and lint against before it writes any UI.

Design SystemsDesign TokensAgentic / AIWeb Components
Company
Personal
Year
2026
Role
Designer & Design Engineer
Timeline
Ongoing
Parsimony An agentic design system. One source of truth, read by both people and agents. shipped planned 01 Source: design tokens primitives → semantic · + 3 brand overrides · DTCG JSON 02 Build: Style Dictionary, validated one CSS file per brand · published as a versioned npm package 03 Components: 27 Lit web components each ships: .ts · meta.json contract 27/27 · Code Connect authored 22/27 04 Artifact: one machine-readable file design-system.json props · rules · anatomy · code↔Figma bindings read by HUMANS Figma (mappings authored) Component docs (generated) AGENTS MCP server 17 tools · get · check · lint 05 Consumers: every site installs the package portfolio · decisioning-table · 4 brand sites drift + parity audits → auto-fix PR
The whole system in one view: one source of truth, read by both people and agents. Green is shipped; dashed amber is planned.

Most design systems are documentation a person has to read. Parsimony is data an agent can query, build against, and lint itself with before it ships any UI.

Interfaces are increasingly assembled by agents, and design systems were never written for that reader. Most of my own work now happens through AI agents, so I built a system whose rules a machine can read, build against, and self-check.

Parsimony is a design system with an API for agents. One source of truth runs from DTCG tokens, through framework-agnostic Web Components, to an MCP server an agent can query and lint against before it writes any UI. It's also a sequel: OTKit was this discipline built for people, with docs humans read and governance by hand. Parsimony inverts it.

Note: A personal project, live across my own sites plus an enterprise UI sub-brand. Shipped vs deferred is marked throughout; the public roadmap tracks what's next.

Stack
DTCG · Style Dictionary · Lit · MCP · Figma Code Connect
Status
Live and evolving

Browse the live token catalog: this site consumes the system it documents.


Framework-agnostic components
27
Brands from one source
4
Governed agent runs ship clean, measured
95%

The Problem

Design systems are written for people: docs sites, Figma libraries, "use this, not that." But the thing building my UI now is an agent, and an agent doesn't read your docs site. The problem was never missing tokens. It was that nothing in the system was readable by a machine.

~ $ "Add a confirm button. Use the design system." same prompt · four ungoverned runs run 1 Confirm ✗ #22C55E close, but not a token run 2 ✓ CONFIRM 🎉 ✗ invented a gradient and an emoji run 3 Confirm! ✗ font-family: cursive no notes run 4 Submit → ✗ renamed the action now it's a link Not hallucinations: every agent had read the docs. Prose doesn't govern.
Same prompt, four ungoverned runs, four opinions. Drift, not hallucination — the failure mode this system targets.

The Architecture

Primitives hold values. Semantics name roles. Components render decisions. UI code never touches a hex. That's the whole layer model; the rest is enforcement. One repo, one direction of flow, so tokens and components version together and a token rename is a breaking change by design.

  • Author. DTCG tokens: primitives → semantic roles. Brands re-point the roles.
  • Build. Style Dictionary compiles every brand to CSS. A gate rejects hex, primitive refs, and dangling aliases — a bad rename fails the build, not production.
  • Components. 27 framework-agnostic Lit web components. Code Connect mappings are authored for 22 and held to each component's real prop unions by a build gate — unpublished, because Code Connect needs a Figma Enterprise plan.
  • Artifact. Hand-authored metadata + generated manifest merge into one design-system.json.
  • Interfaces. Humans read Figma and docs; agents read an MCP server.
  • Consumers. Every site pulls from one source, as an installable package.
Two tiers, one value UI writes against named semantic roles. Each role resolves to exactly one raw primitive — no third set of values in between. 01 Semantic tokens — the named roles UI writes against background.success-alt every component references this layer resolves to 02 Primitives — raw values, never referenced in UI color.green.chip → #0F2016 renders as Active UI references semantic roles, never --primitive-*. Brand overrides re-point the semantic role — the components never change.
One semantic role, one primitive, one rendered badge.

One Source of Truth, Four Brands

baseriverromney .com / .designAa TitleBody text in the brand'sown foreground tone.ActionSurfaceTOKENSbackground#0A0D0Abackground-alt#1E241Eforeground#C8CFC4background-action#4ADE6Eforeground-on-action#0A0D0Adark · phosphor accentdecision-engineenterprise data UIAa TitleBody text in the brand'sown foreground tone.ActionSurfaceTOKENSbackground#F5F8FCbackground-alt#EBF0F8foreground#1A1A2Ebackground-action#2456E4foreground-on-action#FFFFFFlight inversion · Geistdot-artriverromney .artAa TitleBody text in the brand'sown foreground tone.ActionSurfaceTOKENSbackground#000000background-alt#1E241Eforeground#C8CFC4background-action#4ADE6Eforeground-on-action#0A0D0Apure-black canvasdot-blogriverromney .blogAa TitleBody text in the brand'sown foreground tone.ActionSurfaceTOKENSbackground#0A0D0Abackground-alt#1E241Eforeground#C8CFC4background-action#4ADE6Eforeground-on-action#0A0D0A18px / 1.7 reading
Every brand is the same token graph with a thin override layer. No forks; the difference is data.

Decision-engine flips to a light enterprise theme with a blue primary; dot-art goes pure black for photos; dot-blog bumps the reading size. None of it forks a component — each brand is a small override file.


Components as Contracts

badge.meta.json{ "name": "rr-badge", "summary": "Status badge and accent chip. 9 variants…", "props": [{ "name":"variant", "type":"default | success…" }], "tokensUsed": [ "--color-background-success-alt", "--color-foreground-success", "--color-background-danger-alt", // semantic roles only "--font-label-small" ], "rules": [ "Never use hex values. Always use var(--color-*)", "Never reference --primitive-* tokens in component code", "All text/background pairings must pass WCAG AA (4.5:1)", "Variants use a subtle style: accent text, not a solid fill…" ], "anatomy": [{ "part":"label", "fg":"…foreground-success", "bg":"…success-alt" }],anatomythe fg/bg pair each part renders "accessibility": { "ariaPattern": "…/apg/patterns/badge/", "focusable": false, "wcag": ["1.4.3 Contrast (Minimum)", "1.4.11 Non-text Contrast"] }}tokensUsedexactly which tokens it may touchrulesthe constraints it must obeyaccessibilityARIA pattern + WCAG criteria,declared, not assumed→ get_component("rr-badge") returns this, verbatim.
badge.meta.json: the component's machine-readable rulebook. get_component() returns it verbatim.

Each component ships its own rulebook: the tokens it may touch, the rules that bind it, the ARIA pattern it implements. The spec and what the agent reads are the same file — prop descriptions come from the code's JSDoc, so the contract can't drift from the implementation. Docs generate from that same metadata: mechanical sections regenerate, hand-written prose is preserved, and CI fails the build if a change leaves its docs stale.

shipped planned 01 Source: the metadata already exists design-system.json · 27 meta.json · token store (resolved values) 02 Generate: one deterministic pass npm run docs:components → one portable .mdx per component docs/components/rr-badge.mdx --- front-matter: title · component · status · a11yPattern · sourceUrl --- GEN Properties · Design tokens · Accessibility · Examples · Guardrails regenerated every run — never hand-edited AUTHORED Overview · Usage (Do / Don't) human prose — preserved across regens released to CI freshness gate regenerate + git diff — a stale doc can't merge Human docs site Mintlify now · Next.js + MDX (live components)
GEN regions regenerate; AUTHORED prose survives; a CI gate blocks a stale doc from merging.

check_usage: Governance, Moved Upstream

Design-system checks usually happen after the fact. check_usage moves them earlier: hand the system a snippet before the code ships, get back every violation. The same rules run in three places — this check, the build gate, the drift scan — so the answer an agent gets here is the one the build enforces later.

check_usageplayground: edit the CSS below
5 violations
  • Ln 2:10no-hexHardcoded hex "#4ade6e" — use a semantic var(--color-*) token, never a raw color.
  • Ln 3:19no-primitivePrimitive token "--primitive-color-green-500" — UI must reference the semantic layer, not primitives.
  • Ln 4:3hardcoded-sizeHardcoded font-size — use a --font-* token or a semantic font shorthand.
  • Ln 5:3hardcoded-weightHardcoded font-weight — use a --font-weight-* token (display/title 300, body/label 400, strong 500).
  • Ln 6:3font-familyUnapproved font-family — use var(--font-family-sans|serif|mono). Approved: Space Grotesk, Spectral, JetBrains Mono.
Live: edit the snippet and watch the violations update. A faithful port of the static rules — the deployed MCP tool runs the same set against the live registry.
agent session · add a success badgeTASKAdd a success status badge to the page.→ get_component("rr-badge")contract returned props variant: default | success | warning | danger | info | accent-* tokensUsed 23 semantic roles (--color-* + --font-label-small) rules no hex · no --primitive-* · WCAG AA 4.5:1 · subtle style a11y role="status" · WCAG 1.4.3, 1.4.11DRAFT 1 · agent hand-rolls the markup<span style="background: #0F2016; color: #4ADE6E;">Active</span>→ check_usage(draft 1)✗ 2 violations no-hex "#0F2016" No hardcoded colors. Use var(--color-*) custom properties no-hex "#4ADE6E" No hardcoded colors. Use var(--color-*) custom propertiesREVISED · uses the component, not raw values<rr-badge variant="success">Active</rr-badge>→ check_usage(revised)✓ no violationsThe loop the system exists to enable: the agent asks what's allowed, gets it wrong,the system catches it, and it corrects itself before anything reaches a pull request.
The whole argument in one session: the agent asks what a badge is, drafts it with raw hex, gets back the exact violations the build would reject, and ships the fix instead. None of it is staged.

Governance, Measured

"Agents follow the system better when the system is data" was an assertion until I measured it. Twenty realistic UI prompts, two arms, a fresh agent per run. The governed arm got the compiled context packs; the ungoverned arm got "Use the design system." Every output scored by the same rule set that gates the build — no hand grading.

Governed runs clean (19 of 20)
95%
Ungoverned runs clean (14 of 20)
70%
Fewer violations per governed run
5.5×

Neither arm invented a token that doesn't exist — the failure mode was drift, not fabrication. The one dirty governed run exposed two gaps in the packs; both became issues the next day. The eval is committed and re-runnable, so the number gets measured again as the system grows.


The Contract Caught a Shipped Bug

The newest layer of the contract is anatomy: each component declares which foreground token sits on which background token, in every state. Contrast stops being a convention the system hopes holds and becomes a pairing the gate checks against what actually renders.

The first pass found a real defect. The badge's accent variants had shipped unreadable under the light enterprise brand — as low as 1.39:1 — because the brand re-tinted the fills without re-tinting the text. The gate had those pairings on an exclusion list, so nothing ever looked. Anatomy named the pair the badge actually renders, and the gate failed it immediately. The fix took one evening: three brand-side overrides, the exclusion list deleted, a patch release rolled to all three consumer sites the same day.

SHIPPED · ENTERPRISE LIGHT BRAND in production, unnoticed Pending review fill   #FFFBEB · amber tint label #FCD34D · base leak 1.39:1 requires 4.5:1 · FAIL The pairing sat on a brand exclusion list. No one had ever looked. "anatomy" part: label fg on bg · every state · every brand the pair the badge actually renders PATCHED · SAME DAY rolled to 3 production sites Pending review fill   #FFFBEB · unchanged label #92400E · brand override 6.84:1 WCAG AA · PASS Exclusion list deleted. The gate now holds this pairing against every brand, forever.
The amber accent variant, before and after — real values, not a mockup.

A docs site promises. A contract catches.


Decisions & Tradeoffs

Distribution: public npm over GitHub Packages

GitHub Packages needs a login even for public installs — a token just to download tokens. Public npm needs none, and tokens are CSS that ships to the browser anyway.

The agent interface: MCP over docs or a REST API

Docs don't work; a REST API needs a server, a login, discovery. MCP is what agents already speak — the session writing the code can just ask. The cost: it's new, and mine runs locally.

Components: Web Components over React

Lit is clunkier inside React — the real cost. But one version works everywhere instead of three synchronized buttons. One source beats the smoother React-only version.


Honest Status

Part of the work was deciding what to leave unbuilt. Shipped: a two-layer token architecture across four brands; 27 Lit components with build-gated Code Connect bindings; an MCP server with 17 tools; one rule set behind every checker, so the gates can't disagree; weekly automated drift and parity audits; and WCAG AA verified across every intended pairing, in every brand.

Deferred on purpose: publishing the component library and MCP server to npm (the token package ships today), migrating the remaining sites onto it, auto-fixing drift rather than just filing it, and generating Figma and component code from the contract — the committed end state.

Self-assessment is cheap, so the system is also graded against Brad Frost's ten-station design-system inspection. First pass: 71. Three re-inspections closed the gaps.

Across the 8 stations scored so far
76/80
Perfect stations: accessibility, machine-readable docs, agent access
3
Every remaining scored station
9/10

Still open: one borderline 4.38:1 pairing, and the two process stations unscored until they've run long enough to judge — re-score in October.


Reflection

Every system I'd built before made things clear to people. This one asks what a system looks like when its main reader is a machine. The answer is structure: tokens as data, components that carry their own rules, a system an agent can query. A component library tells you what exists. This one also tells an agent what it's allowed to use.