Detect Design Drift
Detect design-system drift — hardcoded values where tokens exist, and raw HTML primitives where registered components exist. The rule-based floor of design-pipeline (detect half). Reports findings; never modifies source. Pairs with a separate align-design-system fixer skill (deferred to a sibling sub-project).
When to Use
- Reviewing a PR that touches UI code, when you want to catch divergence from the design system before it ships
- After adding a new design token, to surface places in the codebase that still use the hardcoded equivalent
- After registering a new primitive (e.g. Button) in DESIGN.md
## Component Registry, to find existing raw-HTML usages that should adopt it
- As part of
harness validate so drift surfaces continuously, not only on demand
- As one of three verifiers composed by
harness check-design (alongside audit-component-anatomy and harness-design-craft)
- NOT for component-anatomy gaps (use audit-component-anatomy)
- NOT for aesthetic critique (use harness-design-craft)
- NOT for fixing drift — this skill detects only; the matching fixer (align-design-system) is a separate sub-project
Capability Roles
- Defines (Service Definition): the shared
Verifier<F, Cat, Meta> interface (packages/cli/src/shared/verifier.ts) it conforms to, alongside audit-component-anatomy and audit-brand-compliance.
- Provides (Provider): this skill — emits
DetectDriftOutput = Verifier<DriftFinding> (packages/cli/src/drift/index.ts).
- Consumes (Consumer):
harness-design-pipeline / harness check-design, which compose it generically via VerifierRegistry.
Process
Phase 1: SCAN — Load resolvers + walk files
Read project configuration. Check harness.config.json for:
design.strictness — strict / standard / permissive (default standard)
design.audit.driftDetection.enabled — gate for the verifier (default true)
design.audit.driftDetection.rules.{tokenBypass,primitiveAdoption} — per-rule toggles
design.audit.driftDetection.fastMode.maxFiles — validate-time scope cap
Load resolvers (soft-dependency — silent skip when absent):
design-system/tokens.json (W3C DTCG format) — parsed by loadTokenSet. Extracts colors, font families, spacing scale, and deprecated token paths. Returns null when the file doesn't exist; token-bypass rules then skip silently.
design-system/DESIGN.md ## Component Registry — parsed by loadComponentRegistry. Maps registered component types (Button/Input/Textarea/Link/Anchor) to their HTML primitive tag. Returns null when DESIGN.md or the section is absent; primitive-adoption rules then skip silently.
Collect candidate files. Walk the project root (or honor an explicit files arg from the caller). Skip node_modules, dist, build, coverage, and any dotfile directory. Honor only these extensions: .ts, .tsx, .js, .jsx, .css, .scss.
Phase 2: APPLY RULES — Two rule families
Token bypass rules (DRIFT-T*) — regex-based detection. For each file when tokens were loaded:
- DRIFT-T001 — Hex color literal (
#abc, #aabbcc, etc.) NOT present in the loaded palette. Case-insensitive palette match. Deduplicated per line:value.
- DRIFT-T002 — Font-family string literal NOT present in the typography palette. System fallbacks (
sans-serif, serif, monospace, system-ui, inherit) are always allowed.
- DRIFT-T003 — Pixel margin/padding/gap/positioning value NOT present in the spacing scale. Skipped entirely when no spacing tokens exist.
- DRIFT-T004 — Reference to a token flagged
$deprecated: true (or $extensions.harness.deprecated: true) in tokens.json. Matched in both 'token.path.form' and CSS-var kebab form (--token-path-form).
Primitive adoption rules (DRIFT-P*) — TS Compiler API JSX parsing. For each .jsx/.tsx file when the registry was loaded:
- DRIFT-P001 —
<button> JSX where Button is registered
- DRIFT-P002 —
<input> JSX where Input is registered
- DRIFT-P003 —
<a> JSX where Link or Anchor is registered
- DRIFT-P004 —
<textarea> JSX where Textarea is registered
- Lowercase JSX = HTML primitive (JSX semantics). Uppercase identifiers are skipped — they're already components. Member expressions (
<Foo.Bar>) are skipped.
Phase 3: REPORT — Severity, aggregate, persist
Severity from design.strictness:
strict — every finding error (CI blocks)
standard — DRIFT-T001/T002/P001 → error; everything else → warn
permissive — everything → info
Aggregate the run summary. bySeverity, byCode, totalFiles, durationMs. Also report catalog.rulesApplied (which rule families fired) and meta.{tokensLoaded, registryLoaded, mode} so callers can see what was active.
Persist findings to the graph (when composed by check-design). The check-design orchestrator passes drift findings to DesignConstraintAdapter.recordFindings() alongside anatomy and craft findings. Each finding becomes an idempotent VIOLATES_design edge keyed by (file, code, line) — re-runs do not double-write.
Harness Integration
harness validate — Fast-mode hook runs the detect-drift verifier. Findings respect design.strictness. Failures degrade gracefully: if the verifier throws, validate logs a warning and continues with other checks.
harness check-design — One of three composed verifiers. The orchestrator aggregates findings across audit-anatomy, design-craft, and detect-drift; persists all three to the graph; and surfaces a unified report.
mcp__harness__detect_drift — Programmatic API. Input: { path, mode, files?, designStrictness?, rules? }. Output: { findings, summary, catalog, meta }. Consumed by the design-pipeline orchestrator.
DesignConstraintAdapter.recordFindings() — Generic graph persistence entry point. Drift findings reuse the adapter; no extra graph plumbing.
- Future align-design-system skill — Separate sub-project. Reads the same finding codes (DRIFT-T*, DRIFT-P*) and applies fixes. Decoupling detect from align keeps each skill testable in isolation and lets the detect side ship first.
Success Criteria
See docs/changes/design-pipeline/detect-design-drift/proposal.md for the full 34 success criteria. Highlights:
- Zero false positives when tokens.json/DESIGN.md are absent (rules skip silently — no speculative findings)
- Token-bypass detection runs in < 1s on a 500-file repo (fast-mode budget)
- Primitive-adoption parses JSX correctly across multi-line / fragmented opening tags (TS Compiler API, not regex)
- Idempotent graph persistence — re-running produces zero duplicate edges
harness validate runtime budget < 3s on a 500-file repo with both rule families enabled
- Both rule families gated independently via config (a project can disable primitive-adoption without disabling token-bypass)
Rationalizations to Reject
These are common rationalizations that sound reasonable but lead to incorrect results. When you catch yourself thinking any of these, stop and follow the documented process instead.
| Rationalization |
Why It Is Wrong |
"I found the hardcoded #ff0000 and the token swap is obvious, so I'll just replace it in the source." |
This skill detects only and NEVER modifies source. Findings carry codemod-todo fix hints; applying them is the separate align-design-system fixer's job. Emit the DRIFT-T001 finding and stop. |
"tokens.json is absent, but I know the brand palette, so I'll flag hardcoded colors anyway." |
No findings without resolved inputs. When tokens.json doesn't load, all T* rules silently skip — a missing resolver means the project hasn't opted in, not that drift exists. Speculative findings are false positives. |
"The project registered a component for <select>, so I'll add a primitive-adoption finding for raw <select>." |
Only the four registered tags are in scope for v1 — <button>, <input>, <a>, <textarea>. Other tags are out of scope even if a component exists; subsumption ships in v1.x. |
"harness.config.json doesn't set strictness, so I'll treat every finding as error to be safe." |
Strictness is read from config, not assumed — default to standard, not strict. Under standard only DRIFT-T001/T002/P001 are errors; forcing everything to error misrepresents severity and can wrongly block CI. |
| "Graph persistence threw, so I'll abort the run." |
The graph is a consumer, not a gate. If recordFindings() fails, skip graph integration for that run — the findings still appear in the report. Never fail the verifier on a persistence error. |
Examples
Example: Hardcoded brand color outside the palette
Input: src/Card.tsx contains const styles = { color: "#ff0000" };, and design-system/tokens.json defines color.brand.primary as #0066cc (but no #ff0000).
Output:
DRIFT-T001 [error] src/Card.tsx:14 — Hardcoded color "#ff0000" is not in the design token palette
Fix: Replace "#ff0000" with a token reference (e.g. var(--color-...) or a token-system lookup).
If the color is intentionally one-off, add it to tokens.json first.
Severity is error under standard because brand-color drift is high-impact.
Example: Raw <button> where Button is registered
Input: src/SaveButton.tsx contains <button>, and design-system/DESIGN.md ## Component Registry lists Button mapped to packages/ui/src/Button.tsx.
Output:
DRIFT-P001 [error] src/SaveButton.tsx:8 — Raw <button> element where the registered component "Button" should be used
Fix: Import Button from your component library and replace <button> with <Button>.
If this raw primitive is intentional (e.g. inside the Button component's own implementation),
add a JSDoc `@allow-raw-primitive` annotation on the file.
Example: Reference to a deprecated token
Input: src/legacy.css contains .x { color: var(--color-brand-500); }, and tokens.json flags color.brand.500 as $deprecated: true with $description: "Use color.brand.primary instead".
Output:
DRIFT-T004 [warn] src/legacy.css:3 — Token "color.brand.500" is deprecated and should be migrated
Fix: Migrate references to "color.brand.500" to the replacement token noted in tokens.json $description,
or remove the deprecation if the token is still load-bearing.
Gates
- No findings without resolved inputs. If
tokens.json is absent, all T* rules silently skip. If DESIGN.md ## Component Registry is absent, all P* rules silently skip. Either resolver failing is not a verifier failure — the project simply hasn't opted in.
- No autofix. Findings include codemod-todo descriptions. The matching fixer (align-design-system) is a separate skill.
- No usage-side primitive findings beyond the four registered tags. Other tags (
<select>, <details>, etc.) are not in scope for v1 — even if the project registers a component for them. Subsumption ships in v1.x.
- Strictness from config, not assumed. Read
design.strictness from harness.config.json; default to standard if absent.
Escalation
- When token-bypass false positives appear in a third-party-shaped value. E.g. a vendor color brought in via a CSS variable that legitimately doesn't match the palette. Either add the value to
tokens.json (preferred — makes the palette authoritative) or scope the file via design.audit.driftDetection.rules.tokenBypass: false for a narrow path-overrides block (v1.x).
- When primitive-adoption fires inside the Button component's own implementation. That's a true positive that the rule can't tell apart from the usage form. Two paths: (1) add
@allow-raw-primitive JSDoc on the file (v1.x — annotation honored by the rule), or (2) extract the raw <button> into a private internal component until the annotation lands.
- When
harness validate runtime exceeds 3 seconds. Set design.audit.driftDetection.fastMode.maxFiles to cap the scope. The MCP tool ignores the cap (fast/full are equivalent in v1 — the cap is validate-side only).
- When the graph persistence fails. Skip graph integration for that run; findings still appear in the report. The graph is a consumer, not a gate.
- When a project ships its own design-system convention (not tokens.json, not DESIGN.md registry). v1 reads only the two declared input formats. Either (1) generate a tokens.json adapter that mirrors the existing convention (one-shot script), or (2) wait for v1.x's pluggable resolver interface.
Status
v1 — in implementation. See:
- Spec:
docs/changes/design-pipeline/detect-design-drift/proposal.md
- Roadmap entry: part of the
design-pipeline initiative (detect half) in docs/roadmap.md
- Sibling:
align-design-system (fix half — deferred to a separate sub-project)
1---2name: detect-design-drift3description: Detect Design Drift4---5# Detect Design Drift67> Detect design-system drift — hardcoded values where tokens exist, and raw HTML primitives where registered components exist. The rule-based floor of design-pipeline (detect half). Reports findings; never modifies source. Pairs with a separate align-design-system fixer skill (deferred to a sibling sub-project).89## When to Use1011- Reviewing a PR that touches UI code, when you want to catch divergence from the design system before it ships12- After adding a new design token, to surface places in the codebase that still use the hardcoded equivalent13- After registering a new primitive (e.g. Button) in DESIGN.md `## Component Registry`, to find existing raw-HTML usages that should adopt it14- As part of `harness validate` so drift surfaces continuously, not only on demand15- As one of three verifiers composed by `harness check-design` (alongside audit-component-anatomy and harness-design-craft)16- NOT for component-anatomy gaps (use audit-component-anatomy)17- NOT for aesthetic critique (use harness-design-craft)18- NOT for fixing drift — this skill detects only; the matching fixer (align-design-system) is a separate sub-project1920## Capability Roles2122<!-- Capability seam: this skill participates in a real extension point whose three roles are named and concrete. A seam with only one role filled is accidental single-implementation lock-in. See harness-skill-authoring Phase 1C. -->2324- **Defines (Service Definition):** the shared `Verifier<F, Cat, Meta>` interface (`packages/cli/src/shared/verifier.ts`) it conforms to, alongside `audit-component-anatomy` and `audit-brand-compliance`.25- **Provides (Provider):** **this skill** — emits `DetectDriftOutput = Verifier<DriftFinding>` (`packages/cli/src/drift/index.ts`).26- **Consumes (Consumer):** `harness-design-pipeline` / `harness check-design`, which compose it generically via `VerifierRegistry`.2728## Process2930### Phase 1: SCAN — Load resolvers + walk files31321. **Read project configuration.** Check `harness.config.json` for:33 - `design.strictness` — `strict` / `standard` / `permissive` (default `standard`)34 - `design.audit.driftDetection.enabled` — gate for the verifier (default `true`)35 - `design.audit.driftDetection.rules.{tokenBypass,primitiveAdoption}` — per-rule toggles36 - `design.audit.driftDetection.fastMode.maxFiles` — validate-time scope cap37382. **Load resolvers (soft-dependency — silent skip when absent):**39 - `design-system/tokens.json` (W3C DTCG format) — parsed by `loadTokenSet`. Extracts colors, font families, spacing scale, and deprecated token paths. Returns `null` when the file doesn't exist; token-bypass rules then skip silently.40 - `design-system/DESIGN.md` `## Component Registry` — parsed by `loadComponentRegistry`. Maps registered component types (Button/Input/Textarea/Link/Anchor) to their HTML primitive tag. Returns `null` when DESIGN.md or the section is absent; primitive-adoption rules then skip silently.41423. **Collect candidate files.** Walk the project root (or honor an explicit `files` arg from the caller). Skip `node_modules`, `dist`, `build`, `coverage`, and any dotfile directory. Honor only these extensions: `.ts`, `.tsx`, `.js`, `.jsx`, `.css`, `.scss`.4344### Phase 2: APPLY RULES — Two rule families45461. **Token bypass rules (DRIFT-T\*) — regex-based detection.** For each file when tokens were loaded:47 - **DRIFT-T001** — Hex color literal (`#abc`, `#aabbcc`, etc.) NOT present in the loaded palette. Case-insensitive palette match. Deduplicated per `line:value`.48 - **DRIFT-T002** — Font-family string literal NOT present in the typography palette. System fallbacks (`sans-serif`, `serif`, `monospace`, `system-ui`, `inherit`) are always allowed.49 - **DRIFT-T003** — Pixel margin/padding/gap/positioning value NOT present in the spacing scale. Skipped entirely when no spacing tokens exist.50 - **DRIFT-T004** — Reference to a token flagged `$deprecated: true` (or `$extensions.harness.deprecated: true`) in tokens.json. Matched in both `'token.path.form'` and CSS-var kebab form (`--token-path-form`).51522. **Primitive adoption rules (DRIFT-P\*) — TS Compiler API JSX parsing.** For each `.jsx`/`.tsx` file when the registry was loaded:53 - **DRIFT-P001** — `<button>` JSX where `Button` is registered54 - **DRIFT-P002** — `<input>` JSX where `Input` is registered55 - **DRIFT-P003** — `<a>` JSX where `Link` or `Anchor` is registered56 - **DRIFT-P004** — `<textarea>` JSX where `Textarea` is registered57 - Lowercase JSX = HTML primitive (JSX semantics). Uppercase identifiers are skipped — they're already components. Member expressions (`<Foo.Bar>`) are skipped.5859### Phase 3: REPORT — Severity, aggregate, persist60611. **Severity from `design.strictness`:**62 - `strict` — every finding `error` (CI blocks)63 - `standard` — DRIFT-T001/T002/P001 → `error`; everything else → `warn`64 - `permissive` — everything → `info`65662. **Aggregate the run summary.** `bySeverity`, `byCode`, `totalFiles`, `durationMs`. Also report `catalog.rulesApplied` (which rule families fired) and `meta.{tokensLoaded, registryLoaded, mode}` so callers can see what was active.67683. **Persist findings to the graph (when composed by check-design).** The check-design orchestrator passes drift findings to `DesignConstraintAdapter.recordFindings()` alongside anatomy and craft findings. Each finding becomes an idempotent `VIOLATES_design` edge keyed by `(file, code, line)` — re-runs do not double-write.6970## Harness Integration7172- **`harness validate`** — Fast-mode hook runs the detect-drift verifier. Findings respect `design.strictness`. Failures degrade gracefully: if the verifier throws, validate logs a warning and continues with other checks.73- **`harness check-design`** — One of three composed verifiers. The orchestrator aggregates findings across audit-anatomy, design-craft, and detect-drift; persists all three to the graph; and surfaces a unified report.74- **`mcp__harness__detect_drift`** — Programmatic API. Input: `{ path, mode, files?, designStrictness?, rules? }`. Output: `{ findings, summary, catalog, meta }`. Consumed by the design-pipeline orchestrator.75- **`DesignConstraintAdapter.recordFindings()`** — Generic graph persistence entry point. Drift findings reuse the adapter; no extra graph plumbing.76- **Future align-design-system skill** — Separate sub-project. Reads the same finding codes (DRIFT-T\*, DRIFT-P\*) and applies fixes. Decoupling detect from align keeps each skill testable in isolation and lets the detect side ship first.7778## Success Criteria7980See `docs/changes/design-pipeline/detect-design-drift/proposal.md` for the full 34 success criteria. Highlights:8182- Zero false positives when tokens.json/DESIGN.md are absent (rules skip silently — no speculative findings)83- Token-bypass detection runs in < 1s on a 500-file repo (fast-mode budget)84- Primitive-adoption parses JSX correctly across multi-line / fragmented opening tags (TS Compiler API, not regex)85- Idempotent graph persistence — re-running produces zero duplicate edges86- `harness validate` runtime budget < 3s on a 500-file repo with both rule families enabled87- Both rule families gated independently via config (a project can disable primitive-adoption without disabling token-bypass)8889## Rationalizations to Reject9091These are common rationalizations that sound reasonable but lead to incorrect results. When you catch yourself thinking any of these, stop and follow the documented process instead.9293| Rationalization | Why It Is Wrong |94| ----------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |95| "I found the hardcoded `#ff0000` and the token swap is obvious, so I'll just replace it in the source." | This skill detects only and NEVER modifies source. Findings carry codemod-todo fix hints; applying them is the separate align-design-system fixer's job. Emit the DRIFT-T001 finding and stop. |96| "`tokens.json` is absent, but I know the brand palette, so I'll flag hardcoded colors anyway." | No findings without resolved inputs. When `tokens.json` doesn't load, all T\* rules silently skip — a missing resolver means the project hasn't opted in, not that drift exists. Speculative findings are false positives. |97| "The project registered a component for `<select>`, so I'll add a primitive-adoption finding for raw `<select>`." | Only the four registered tags are in scope for v1 — `<button>`, `<input>`, `<a>`, `<textarea>`. Other tags are out of scope even if a component exists; subsumption ships in v1.x. |98| "`harness.config.json` doesn't set strictness, so I'll treat every finding as `error` to be safe." | Strictness is read from config, not assumed — default to `standard`, not `strict`. Under `standard` only DRIFT-T001/T002/P001 are errors; forcing everything to `error` misrepresents severity and can wrongly block CI. |99| "Graph persistence threw, so I'll abort the run." | The graph is a consumer, not a gate. If `recordFindings()` fails, skip graph integration for that run — the findings still appear in the report. Never fail the verifier on a persistence error. |100101## Examples102103### Example: Hardcoded brand color outside the palette104105**Input:** `src/Card.tsx` contains `const styles = { color: "#ff0000" };`, and `design-system/tokens.json` defines `color.brand.primary` as `#0066cc` (but no `#ff0000`).106107**Output:**108109```110DRIFT-T001 [error] src/Card.tsx:14 — Hardcoded color "#ff0000" is not in the design token palette111 Fix: Replace "#ff0000" with a token reference (e.g. var(--color-...) or a token-system lookup).112 If the color is intentionally one-off, add it to tokens.json first.113```114115Severity is `error` under `standard` because brand-color drift is high-impact.116117### Example: Raw `<button>` where Button is registered118119**Input:** `src/SaveButton.tsx` contains `<button onClick={...}>Save</button>`, and `design-system/DESIGN.md` `## Component Registry` lists `Button` mapped to `packages/ui/src/Button.tsx`.120121**Output:**122123```124DRIFT-P001 [error] src/SaveButton.tsx:8 — Raw <button> element where the registered component "Button" should be used125 Fix: Import Button from your component library and replace <button> with <Button>.126 If this raw primitive is intentional (e.g. inside the Button component's own implementation),127 add a JSDoc `@allow-raw-primitive` annotation on the file.128```129130### Example: Reference to a deprecated token131132**Input:** `src/legacy.css` contains `.x { color: var(--color-brand-500); }`, and `tokens.json` flags `color.brand.500` as `$deprecated: true` with `$description: "Use color.brand.primary instead"`.133134**Output:**135136```137DRIFT-T004 [warn] src/legacy.css:3 — Token "color.brand.500" is deprecated and should be migrated138 Fix: Migrate references to "color.brand.500" to the replacement token noted in tokens.json $description,139 or remove the deprecation if the token is still load-bearing.140```141142## Gates143144- **No findings without resolved inputs.** If `tokens.json` is absent, all T\* rules silently skip. If `DESIGN.md ## Component Registry` is absent, all P\* rules silently skip. Either resolver failing is not a verifier failure — the project simply hasn't opted in.145- **No autofix.** Findings include codemod-todo descriptions. The matching fixer (align-design-system) is a separate skill.146- **No usage-side primitive findings beyond the four registered tags.** Other tags (`<select>`, `<details>`, etc.) are not in scope for v1 — even if the project registers a component for them. Subsumption ships in v1.x.147- **Strictness from config, not assumed.** Read `design.strictness` from `harness.config.json`; default to `standard` if absent.148149## Escalation150151- **When token-bypass false positives appear in a third-party-shaped value.** E.g. a vendor color brought in via a CSS variable that legitimately doesn't match the palette. Either add the value to `tokens.json` (preferred — makes the palette authoritative) or scope the file via `design.audit.driftDetection.rules.tokenBypass: false` for a narrow path-overrides block (v1.x).152- **When primitive-adoption fires inside the Button component's own implementation.** That's a true positive that the rule can't tell apart from the usage form. Two paths: (1) add `@allow-raw-primitive` JSDoc on the file (v1.x — annotation honored by the rule), or (2) extract the raw `<button>` into a private internal component until the annotation lands.153- **When `harness validate` runtime exceeds 3 seconds.** Set `design.audit.driftDetection.fastMode.maxFiles` to cap the scope. The MCP tool ignores the cap (`fast`/`full` are equivalent in v1 — the cap is validate-side only).154- **When the graph persistence fails.** Skip graph integration for that run; findings still appear in the report. The graph is a consumer, not a gate.155- **When a project ships its own design-system convention (not tokens.json, not DESIGN.md registry).** v1 reads only the two declared input formats. Either (1) generate a tokens.json adapter that mirrors the existing convention (one-shot script), or (2) wait for v1.x's pluggable resolver interface.156157## Status158159**v1 — in implementation.** See:160161- Spec: `docs/changes/design-pipeline/detect-design-drift/proposal.md`162- Roadmap entry: part of the `design-pipeline` initiative (detect half) in `docs/roadmap.md`163- Sibling: `align-design-system` (fix half — deferred to a separate sub-project)