David Bowman

LoupeNo. 028

⏺ Loupe audit — 56 findings, zero blocking

  Config present (loupe.config.json, extends wpds), baseline configured at
  .loupe/baseline.json but never recorded — so every finding here is live, none
  accepted.

  No stylesheet/unparsed. Every SCSS file parsed. Report is complete.

  Shape

  Three rules, and one dominates hard:

  ┌───────────────────────────────┬───────┬──────────┐
  │             rule              │ count │ severity │
  ├───────────────────────────────┼───────┼──────────┤
  │ system/superseded-component   │ 45    │ advisory │
  ├───────────────────────────────┼───────┼──────────┤
  │ system/raw-element            │ 10    │ advisory │
  ├───────────────────────────────┼───────┼──────────┤
  │ patterns/superseded-composite │ 1     │ advisory │
  └───────────────────────────────┴───────┴──────────┘

  Nothing blocks. The eight blocking rules (system/override, layout/in-css,
  typography/in-css, layout/margin-on-child, system/inline-style,
  system/locked-component, stylesheet/unparsed, suppression/malformed) are all
  clean — that's the ~130 suppressions from the prior WPDS pass doing their job.

  Clustering

  40 of 45 superseded-component findings sit in src/admin-ui/global-styles/ — 10
  files, all the brand-editor work from the recent commits. Same handful of
  imports repeated: HStack/VStack/FlexItem/Spacer → Stack (@wordpress/ui),
  View/ZStack/Grid → plain element + CSS, InputControl/TextControl/Spinner →
  @wordpress/ui equivalents, DropdownMenu+MenuGroup/MenuItem → Menu.

  Remaining 5: screens/blueprint-edit.js (TabPanel → Tabs), screens/brands.js
  (menu trio + Grid), components/site-details-form.tsx (TextControl).

  system/raw-element is the opposite — thinly spread, 1–3 per file across
  app.tsx, admin-page.tsx, brand-picker.tsx, build-summary.tsx, chat-mount.tsx,
  step-row.tsx. Mostly <p> → <Text>.

  One composite: site-detail 1 new message (click) ↓ tack(Text, TextControl,

────────────────────────────────────────────────────────────────────────────────
❯ 
────────────────────────────────────────────────────────────────────────────────
  davidbowman@MacBookPro | site-foundry | wpds-audit
  CTX 9% ████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
  ⏵⏵ auto mode on (shift+tab to cycle)
In-harness audit output. This checks the repo for patterns that indicate incorrect design system usage.
• patterns/superseded-composite (1) — a sanctioned composition already covers this — see the finding for which pattern replaces the shape that was built
    src/components/site-details-form.tsx:252  Stack(Text,TextControl,Text) — `form-row` covers this; use `Field`

• system/raw-element (10) — raw element — use its design-system analog
    src/app.tsx:555  <p> → <Text>
    src/components/admin-page.tsx:163  <span> → <Text>
    src/components/admin-page.tsx:301  <div> → <Stack>
    src/components/brand-picker.tsx:101  <span> → <Text>
    src/components/build-summary.tsx:131  <h3> → <Heading>
    src/components/build-summary.tsx:142  <p> → <Text>
    src/components/build-summary.tsx:168  <p> → <Text>
    src/components/chat-mount.tsx:240  <p> → <Text>
    src/components/chat-mount.tsx:252  <p> → <Text>
    src/components/step-row.tsx:155  <div> → <Stack>

• system/superseded-component (45) — a current, recommended component now covers this job — see the finding for what replaces it
    src/admin-ui/global-styles/brand-name.js:7  TextControl (@wordpress/components) → InputControl (@wordpress/ui)
    src/admin-ui/global-styles/color-ramps.js:15  FlexItem (@wordpress/components) → Stack (@wordpress/ui)
    src/admin-ui/global-styles/color-ramps.js:15  HStack (@wordpress/components) → Stack (@wordpress/ui, direction="row")
    src/admin-ui/global-styles/color-ramps.js:15  ZStack (@wordpress/components) → CSS grid or absolute positioning (Planned for deprecation per its own Storybook status.)
    src/admin-ui/global-styles/custom-token-groups.js:11  InputControl (@wordpress/components) → InputControl (@wordpress/ui)
    src/admin-ui/global-styles/custom-tokens.js:32  FlexItem (@wordpress/components) → Stack (@wordpress/ui)
    src/admin-ui/global-styles/custom-tokens.js:32  HStack (@wordpress/components) → Stack (@wordpress/ui, direction="row")
    src/admin-ui/global-styles/custom-tokens.js:32  Spacer (@wordpress/components) → Stack's gap prop, or a --wpds-dimension-gap-* token directly
    src/admin-ui/global-styles/custom-tokens.js:32  View (@wordpress/components) → a plain element (Planned for deprecation per its own Storybook status.)
    src/admin-ui/global-styles/custom-tokens.js:32  VStack (@wordpress/components) → Stack (@wordpress/ui, direction="column")
    src/admin-ui/global-styles/detail-header.js:8  DropdownMenu (@wordpress/components) → Menu (@wordpress/components) -- core only; Menu (@wordpress/ui) outside it
    ...

To suppress a specific finding, add `wpds-allow <ruleId> -- <reason>` on the offending line or the line above.
CLI audit output
⏺ Registry vs. this repo

  Read profiles/wpds/patterns/index.json (26 patterns). No project patterns/ dir
  — repo runs on the shipped registry only.

  Already on the canonical

  confirm-modal→ConfirmDialog (4),
  navigator-drilldown/drilldown-row/value-preview-row→Navigator+useNavigator
  (5), card→Card (3 from @wordpress/ui), color-dropdown-item→Dropdown (2),
  segmented-choice→ToggleGroupControl (1), entity-dataform→DataForm (1). The
  drilldown reflex is being followed.

  The one real gap: form-row

  Field is exported by the installed @wordpress/ui@0.21.0 and is recommended.
  Zero imports in src.

  The audit reported one finding (site-details-form.tsx:252). It's actually
  three — the engine only saw the first because the other two fall under the
  four-node floor:

  248  Stack(column,gap=xs,.sfp-form__field) + <label .sfp-form__label
  htmlFor="sfp-slug">   + affixed TextControl
  297  Stack(column,gap=xs,.sfp-form__field) + <label .sfp-form__label
  htmlFor="sfp-language"> + injected <select>
  314  Stack(column,gap=xs,.sfp-form__field) + <label .sfp-form__label
  htmlFor="sfp-email">  + TextControl

  Same hand-rolled row three times, with manual htmlFor/id pairing that
  Field.Root/.Label/.Control does for you. Field takes className, so the
  .sfp-form* selectors the file header says e2e depends on survive the swap.

  Cost to watch: site-details-form.scss carries ~20 suppressions pinning the URL
  row's segmented seams to the current DOM. Field.Root replacing the outer
  Stack is cheap; the affix group stays inside .Control untouched. Field.Label
  replacing the raw <label> is where .sfp-form__label rules need re-checking.

  Patterns with no landing spot

  - page-header — canonical is Page from @wordpress/admin-ui, which is not a
    dependency. AdminPage stays hand-rolled unless you add the package.
  - overflow-actions/filter- 1 new message (click) ↓ downMenu/MenuGroup/MenuItem

────────────────────────────────────────────────────────────────────────────────
❯ 
────────────────────────────────────────────────────────────────────────────────
  davidbowman@MacBookPro | site-foundry | wpds-audit
  CTX 9% ████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
  ⏵⏵ auto mode on (shift+tab to cycle)
In-harness pattern audit output. This searches the repo for UI that should be using system patterns or code duplication that could become new ones.
src/admin-ui/global-styles/brand-specimen.js:91  system/raw-element  specimen content: a DS component would paint over the brand typography this pane exists to show
src/admin-ui/global-styles/brand-specimen.js:160  system/inline-style  paints one of the brand's own palette entries; no token can stand in for user data
src/admin-ui/global-styles/brand-specimen.js:169  system/raw-element  specimen content: a DS component would paint over the brand typography this pane exists to show
src/admin-ui/global-styles/brand-specimen.js:175  system/raw-element  specimen content: a DS component would paint over the brand typography this pane exists to show
src/admin-ui/global-styles/brand-specimen.js:184  system/raw-element  specimen content: a DS component would paint over the brand typography this pane exists to show
src/admin-ui/global-styles/brand-styles-ui.js:24  system/superseded-component  keeps markup parity with core's screen-root, which core's own CSS targets
src/admin-ui/global-styles/import-tokens.js:66  system/raw-element  hidden file input; FormFileUpload cannot mark its own input webkitdirectory
src/admin-ui/global-styles/logo-control.js:104  system/raw-element  interactive picker box: hosts a DropZone and a thumbnail, and carries the chrome a Stack has no props for
src/admin-ui/global-styles/logo-control.scss:45  tokens/hardcoded-value  carries core's MediaEdit number verbatim; its rules live in the wp-editor stylesheet this screen doesn't load
src/admin-ui/global-styles/navigation-row.js:32  system/raw-element  layout cell that must inherit the parent button's typography
src/admin-ui/global-styles/navigation-row.scss:12  layout/margin-on-child  auto margin is the flex idiom for pushing one child to the end; the parent has no per-child prop for it
src/admin-ui/global-styles/semantic-palette.js:145  system/raw-element  core's circular-option-picker markup, which its package CSS targets
src/admin-ui/global-styles/semantic-palette.js:155  system/inline-style  the swatch color is data; core's ColorPalette paints its options the same way
src/admin-ui/screens.scss:43  tokens/hardcoded-value  viewport inset, no on-role WPDS token
src/admin-ui/screens.scss:45  tokens/hardcoded-value  viewport inset, no on-role WPDS token
src/admin-ui/screens/brands.js:323  system/raw-element  two-pane CSS grid (fixed sidebar + fluid preview); Stack is flex-only
src/admin-ui/screens/brands.js:341  system/raw-element  <Text> render prop placeholder, not a raw heading
src/admin-ui/screens/brands.scss:35  system/override  Card.Header takes the card's own gutter; the page header runs a wider inline one
src/admin-ui/screens/brands.scss:38  system/override  Card.Header has no divider prop; core's page header draws the same rule
src/admin-ui/screens/brands.scss:66  system/override  Card.Content takes the card gutter; this body stands in for a screen and carries that gutter too
...
Output suppressions along with the reasons given for suppressing.
Early prototype of a visualizer to aid designers as they audit code.
loupe <command> [options]

Commands:
  check                 Analyze the project (default)
  fix                   Apply the fixes the engine can make without judgement
  baseline              Record current findings as the accepted backlog
  suppressions          List every active suppression directive
  explain <rule>        Print a rule's guidance
  presets               List bundled presets
  detect                Show the config that would be inferred for this project
  init                  Write the inferred config to loupe.config.json
  status                Show the effective config: rules, severities, what blocks
  hook                  Claude Code hook mode (reads payload on stdin)

Options:
  --changed             Only report findings in files changed vs. HEAD (default for hook)
  --all                 Report across the whole repo (default for check)
  --files a,b           Only report findings in these files
  --format <f>          agent | json | sarif | html  (default: agent)
                        html writes a self-contained page — redirect it to a file
  --max-findings <n>    Cap findings shown; 0 = no cap (default: 0)
  --severity <s>        error | warn — minimum severity to report
  --baseline <path>     Read/write the baseline here instead of the configured
                        path (default: .loupe-baseline.json in the project)
  --no-baseline         Ignore the baseline file for this run
  --dry-run             Report what 'fix' would change without writing
  --force               Overwrite an existing config (init)
  --no-color            Never colour the output (colour is on only when stdout
                        is a terminal; NO_COLOR and FORCE_COLOR are honoured)
  --preset <name>       Use a bundled preset instead of discovering config
  --patterns <path>     Composite pattern registry (JSON)
  --components <path>   Component currency registry (JSON)
  --profiles <dir>      Directory of design-system profiles; the ones matching
                        this project's imports supply their registries
  --profile <id>        Use this profile regardless of what the project imports
  --config <path>       Explicit config file
  --root <path>         Project root (default: the directory holding the config
                        that governs the cwd, or CLAUDE_PROJECT_DIR under hook)

Environment:
  LOUPE_OFF=1           Silence the hook for this session; 'loupe' typed by hand
                        still reports. Start Claude Code as 'LOUPE_OFF=1 claude'.
                        1/true/yes/on turn it on; anything else — 0 and false
                        included — leaves the hook running.

With no config file present, the project is inspected and a config inferred.
Run 'loupe detect' to see what that produces, 'init' to write it down.
Currently available commands
config:     /Users/davidbowman/Github/site-foundry/loupe.config.json
namespaces: sfp-, sfn-, sf-, site-, spec-
suppress:   wpds-allow <ruleId> -- <reason>
baseline:   /Users/davidbowman/Github/site-foundry/.loupe/baseline.json  (none recorded yet)
state:      /Users/davidbowman/Github/site-foundry/.loupe
options:    from ~/.claude settings; blocking on
plugin:     not running as a plugin
profile:    wpds (WordPress Design System)

rules:
  suppression/malformed            BLOCKS
  stylesheet/unparsed              BLOCKS
  css/dead-class                   advisory
  system/locked-component          BLOCKS
  system/superseded-component      advisory
  patterns/duplicate-composite     advisory
  patterns/superseded-composite    advisory
  patterns/unregistered-composite  advisory
  tokens/unknown-token             advisory
  tokens/hardcoded-value           advisory
  layout/margin-on-child           BLOCKS
  layout/negative-margin           advisory
  layout/in-css                    BLOCKS
  typography/untokenized           advisory
  system/override                  BLOCKS
  system/inline-style              BLOCKS
  system/raw-element               advisory
  typography/in-css                BLOCKS

Turns are blocked by: suppression/malformed, stylesheet/unparsed, system/locked-component, layout/margin-on-child, layout/in-css, system/override, system/inline-style, typography/in-css
Loupe per-project config
▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
   Plugins  Discover   Installed   Marketplaces   Errors

   Loupe @ loupe
   Scope: user
   Version: 0.4.0
   Keeps agent-written wp-admin interfaces on the WordPress Design System. An
   authoring skill that sets the rules before the first draft, a conformance
   engine that correlates JSX component bindings with the CSS styling them —
   across stylesheets, inline styles and CSS-in-JS — names the exact token
   that replaces a hardcoded value, and catches composites rebuilt across
   files, plus hooks that report drift at write time rather than at review
   time.

   Author: David Bowman
   Status: Enabled · Last used: today

   Installed components:
   ● Agents: loupe-fixer
   ● Skills: loupe-remediation, loupe-authoring, loupe-patterns
   ● Hooks: PostToolUse, Stop
   ● MCP Servers: design-system


   ❯ Disable plugin
     Add to favorites
     Mark for update
     Configure options
     Update now
     Uninstall
     Back to plugin list

   ctrl+p to navigate · Enter to select · Esc to go back
Harness plugin options

I created Loupe to help agents and their humans use design systems more effectively. I found that agents would constantly write code that rendered correctly, but was full of latent debt that would break and decay over time. The errors were almost always issues that required a knowledge of the design system while comparing more than one file — tasks like using custom CSS to style a component instead of a built-in prop, using a raw HTML element where a system component was available, or creating a new pattern where an existing one should be used instead. These are all errors that can pass visual tests and basic linting, and potentially ship to production if designers aren’t in the loop inspecting the code.

Once enough of these errors were in the codebase, I found that the inertia of the status quo would severely hamper future agents from doing things correctly, even when directly instructed to do so. Errors were frequently assumed to be intentional and agents would repeatedly defend bad implementation decisions.

Loupe provides a way to programmatically check for bad usage patterns and reliably enforce fixes as they are being written. It catches basic things (like hard-coded style values) as well as more abstract ones (like creating new interaction patterns where existing ones should be used instead). It supports these checks with a combination of scripted pattern matching and inference-based auditing, depending on the use case. Together with hard enforcement and tools for effective mitigation, it helps keep everyone’s agents on the rails and serves as a practical and effective method of documenting and enforcing a design system in production.

Date
2026
Designed at
Automattic
Work
Agent Experience Design, Design Engineering, CLI Design