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)
• 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.
⏺ 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)
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 ...
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.
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
▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ 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
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