◐ PRE-RELEASEDeveloper tool · open source planned

design-doctor

A policy-driven design-quality scanner for React 19, shadcn/ui, and Tailwind v4. Sixteen rules check tokens, Tailwind, shadcn, motion, accessibility, and polish; a design policy the team writes down decides what they allow. It reports in plain text, JSON, SARIF, or GitHub annotations.

TypeScript 5.9Node 24+Scans React 19Scans Tailwind v4
Zod 4pnpm + TurborepotsdownVitest 3SARIF 2.1.0GitHub Action

Policy over taste. design-doctor doesn't decide whether your cards may move on hover — your design policy does.

// Why it exists

Most tools catch broken code. Very few catch design entropy.

Linters tell you the code parses. They don't tell you a card hover is jittering your dashboard, that a status color skipped the token system, or that a dialog is missing an accessible title. AI coding assistants make it worse: they write plausible UI fast, and it drifts from the design system just as fast. That's the gap design-doctor lives in.

It started inside AlphaForge as a 13-rule checker in one 1,128-line file. In June 2026 it became a four-package TypeScript monorepo in about 26 hours of commits, borrowing React Doctor's shape: a CLI, a rules pack, an agent skill, and a CI Action. Its README calls it a scanner, which is the honest word. It reads files, not a syntax tree — yet.

// The terminal

Boring in CI. Boring in a TTY, too.

The real command and output format; the project is made up. No color, no spinner, no score.

design-doctor scan
$ design-doctor scan apps/web
Scanning apps/web...
Found 47 files to scan.

3 findings
16 rules scanned

By rule:
  MOTION_NO_CARD_HOVER_MOVEMENT: 1
  TW_001_CONFLICTING_CLASSES: 1
  SH_002_DIALOG_TITLE_MISSING: 1

Details:
  [WARNING] MOTION_NO_CARD_HOVER_MOVEMENT @ src/components/project-card.tsx
    Hover movement on card-like surface
    suggestion: Replace with hover:shadow-md / hover:border-accent (or set cardsMayMove: true in .design-doctor/motion-policy.json).
  [ERROR] TW_001_CONFLICTING_CLASSES @ src/components/project-card.tsx
    Conflicting Tailwind padding-all classes: p-2 is overridden by p-4
    suggestion: Remove p-2.
  [ERROR] SH_002_DIALOG_TITLE_MISSING @ src/features/settings/rename-dialog.tsx
    DialogContent is missing a DialogTitle
    suggestion: Import and add DialogTitle from the shadcn dialog primitive.
// The rule catalog

Sixteen rules. No score yet.

Each finding carries a score impact (a missing dialog title is worth −15), but nothing adds them up yet. The 100-point model is designed, not built.

Accessibility
4
Tailwind
3
Polish
3
Design tokens
2
shadcn/ui
2
Motion
2

Seven rules are errors, seven are warnings, and two are suggestions. The four that encode taste rather than fact are marked subjective, and none of them defaults to an error. Behind the catalog sits a 90-item backlog with acceptance criteria for every item: 16 built, 12 next, 51 planned, and the rest in research or deferred.

// What sets it apart

Opinions with receipts.

Every finding says why. Most also say what to change: the code, or the policy.

POLICY

A policy the team owns

design-doctor init asks six questions, from product type to whether cards may move on hover, and writes a config plus four policy files. There are six presets, from recommended to operator-console, and the schema is strict: an unknown key is an error, not a shrug.

MOTION

Motion as a system

The operator-console preset sets two duration bands: 150–250 ms for controls and 250–450 ms for content. Two motion rules exist so far; reduced-motion and motion-token drift are next. This site's own tokens, 180 ms and 320 ms, sit inside those bands.

OUTPUT

Four formats, eight lenses

--format prints plain text, versioned JSON, SARIF 2.1.0, or GitHub annotations. --category picks one of eight lenses, though Content and Security have no rules yet, and --delta shows only what's new since the last run.

CI

Private by default

The Action emits annotations and a findings count, and its fail-on input takes error, warning, none, or regression. PR comments are off by default and the scan workflow runs only by hand, because findings about an unreleased product shouldn't publish themselves.

AGENT

Written for agents

A SKILL.md of about 165 lines gives coding agents the loop: read the config and contracts, scan to JSON, make the smallest fix, scan again. No installer yet; the skill lives in the repo.

FIX

Fixes that can run twice

TW_001 is the one rule with a safe autofix. It deletes the losing class, editing from the end of the file backwards so earlier positions stay valid, and a test runs it twice to prove the second pass changes nothing. The CLI can't apply fixes yet.

// The rule contract

Every rule is typed. The policy gets a veto.

The real contract, condensed, with detection elided. The registry is generated from files like this one, and pnpm check fails when it goes stale.

packages/rules/src/motion/no-card-hover-movement.ts
export const noCardHoverMovement: DesignRule = {
  id: "MOTION_NO_CARD_HOVER_MOVEMENT",
  category: "motion",
  defaultSeverity: "warning",
  meta: {
    rationale: "cards may not translate, scale, or lift on hover …",
    subjective: true,
    autofix: "none",
  },
  schema: { parse: (x: unknown) => x },
  create(ctx) {
    const policy = ctx.project?.motionPolicy ?? {};
    if (policy.hover?.cardsMayMove === true) return {}; // policy explicitly allows it
    // …whole-file detection elided…
    ctx.report({ message: "Hover movement on card-like surface", scoreImpact: -8 });
    return {};
  },
};
// What was hard

Fast was easy. Right is the work.

One of these is solved, two are half done, and one is the next phase.

Pattern rules hit a precision wall. Most MVP rules decide from keywords anywhere in the file. That's quick to write and can misfire both ways: a check passes because of text beside a different element, or fails on text that has nothing to do with it. So the last commit changed the rules for writing rules. False positives are now correctness bugs, regex-only JSX rules are banned, and subjective checks stay advisory. The replacement is designed, not built: an adapter on Tailwind's official language service that follows the CSS import graph and has to pass fixtures with Windows paths and CRLF line endings.

Precision where it already works. TW_001 maps each Tailwind class to the CSS property it controls and lets the last one win. Its fixtures pin the classic traps: px-2 py-4 isn't a conflict, text-sm never competes with a text color, and md:p-4 is skipped instead of guessed. The shadcn title rules check each dialog and sheet on its own, so a titled dialog can't vouch for an untitled one in the same file. A VisuallyHidden title passes; an sr-only heading that isn't wired to DialogTitle doesn't.

The AI contributors needed a manager. The first day ran on an autonomous coding-agent loop with "co-owner" permissions. It was fast, with the engine, rules, and Action done in about a day, and it was noisy, leaving a trail of branches that never merged. The fix was structural: one owner, one backlog item per loop, and acceptance criteria for each. The rule registry is generated from source, a validator checks all 90 backlog items, and pnpm check fails if either drifts. Fixtures are the remaining gap: 3 of 16 rules meet the repo's own bar.

The contrast math is half-ported. The AlphaForge prototype's audit was real. It followed var() chains, converted OKLCH and hex to linear sRGB, computed WCAG ratios for light and dark themes, and flagged "below the minimum" separately from "outside this role's range." Core has the math now, but the product's theme rule is still a placeholder. The working version runs on this site as a much smaller, separate checker: 16 color-token pairs, each with a floor and most with a ceiling, so muted text that's too loud fails as surely as body text that's too faint. It runs in strict mode on every npm run check, along with a 12px font floor.

◐ Status

design-doctor is a private, pre-release MVP built in June 2026: 16 rules, the policy engine and init wizard, JSON, SARIF, and GitHub output, the agent skill, the Action, and 41 tests. Not built yet: the score, fix and explain commands, a skill installer, motion-token drift, contrast math in the product's rule, expiring suppressions, and line-level locations. The precision rework is planned but not started. Nothing is on npm and no license is chosen, so open source is the plan, not the state. Want to compare notes on design tooling? Reach out →