paqad-ai
Version:
Spec-driven development framework — AI agents that think before they type
286 lines (204 loc) • 19.4 kB
Markdown
---
name: design-test
description: 'Audits the running UI against the project design-system contract by running the design review helpers in order and writing one report with a JSON sidecar, and retests an earlier report. Use when the workflow is design-test or design-retest.'
metadata:
model_tier: reasoning
cacheable: 'false'
output_format: markdown
paqad-meta: skill.meta.yaml
---
## What It Does
Audit the running UI against the project's own design-system contract. Mirror of the pentest workflow: same shape, same resumability, but graded against `docs/instructions/design-system/*` instead of OWASP WSTG. The contract is the standard; the framework holds no hard-coded color/spacing rules.
It runs the design review helpers in the order below and gathers their output into one report; each helper keeps its own procedure. A retest of an earlier report is the Retest mode of this skill.
## Use This When
Run this workflow whenever the user says anything equivalent to:
- "run design test", "design check", "design audit", "ui audit", "design-system audit", "check design system", "validate the ui", "validate design"
Do **not** improvise a design audit. Always follow the steps below in order. For a retest of an existing report, use the `design-retest` workflow instead.
Use the Retest mode below when the workflow is `design-retest`.
## Inputs
### Source-of-Truth Model
| Source | Role |
| ----------------------------------- | --------------------------------------------------------------------------------- |
| Playwright walk of the running app | **primary evidence** |
| UI source AST + CSS scan | **primary evidence** |
| `docs/instructions/design-system/*` | **contract**: what reality is graded against |
| `docs/modules/*/ui/**` | **subject under audit**: never trusted as evidence; cross-checked against reality |
| Playwright test files | **coverage evidence**: what's tested ≠ what exists; gap = finding |
Docs are help, not truth. Any disagreement between docs and reality produces a `documentation-drift` finding with bidirectional resolution ("either implement the state or remove the claim").
## Procedure
Progress is tracked in `.paqad/design-test/runs/<run_id>/progress.json` (same shape as pentest). The workflow is resumable: if a step is already marked `completed` with the same input hash, skip it.
### Step 0: readiness-gate
Grade `docs/instructions/design-system/` before any other step runs. The `design-system-coverage` skill produces the tier; it is always the first skill and gates everything else (analog of `stride-threat-model`).
| Tier | Criteria | Behavior |
| ------------ | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Missing** | Directory absent, or all six contract files empty | **Stop.** Decision-pause packet via `AskUserQuestion`: "Run `create documentation` for the design system first?" Offers inline invocation of `documentation-update` workflow, then resume. |
| **Bare** | Tokens partial; no component inventory; no a11y matrix | Warn + prompt. Run in exploratory mode, findings flagged `confidence: low`. Also emit `DT-DS-XXXX` findings listing gaps in the contract itself. |
| **Adequate** | Tokens + components + a11y + at least one of {patterns, motion, responsive} | Proceed with normal scoring. |
| **Strong** | All six contract files populated | Proceed; if `design_test.strict: true` is set in `project-profile.yaml`, DT-blocker findings gate the dev workflow. Strict mode never auto-engages: explicit opt-in only. |
### Step 1: collect-context
- Load `docs/instructions/design-system/**` → the contract.
- Derive component inventory from **AST scan of `src/components/**`** + Storybook stories if present. **Do not derive from `docs/modules/\*/ui/**`**: instead audit those docs against the derived inventory.
- Load UI source: theme/style files, Tailwind config (`tailwind.config.*`), design-token sources (`src/design-tokens/`), CSS modules / CSS-in-JS.
- Load Playwright tests as coverage evidence.
- Emit `contract-summary.json`, `surface-summary.json`, `test-summary.json` to `.paqad/design-test/runs/<run_id>/artifacts/`.
Skills run **in this order** (`design-system-coverage` gates everything else, like STRIDE):
1. `design-system-coverage`: already ran in Step 0; readiness inventory reused as input to every downstream skill
2. `token-conformance-review`: scan UI source for hard-coded values that should reference tokens. **High severity by default**, hex literals, raw `px`/`rem`/`em`, font stacks not in `tokens.md`, color names not registered as tokens. The presence of a `primary` token while a component inlines `#1a73e8` is a finding, not a stylistic preference.
3. `component-conformance-review`: every used component must be in `components.md`; `!important`, inline `style=`, shadcn primitives wrapped without spec, undocumented Tailwind utility combinations → findings
4. `state-coverage-review`: every component in the AST-derived inventory must implement its declared states (default/hover/focus/disabled/loading/error/empty) AND be exercised by Playwright
5. `accessibility-review`: WCAG 2.2 A/AA: contrast, focus visibility, keyboard order, ARIA, target size, prefers-reduced-motion. Findings tagged `WCAG-2.2.x.x.x` (analog of WSTG)
6. `responsive-review`: declared breakpoints exercised; no horizontal scroll; touch targets ≥ spec
7. `motion-review`: animations match `motion.md`; `prefers-reduced-motion` respected
8. `copy-and-ia-review`: voice/tone/labeling consistency vs `patterns.md`
### Step 2: run-project-scripts
Five scripts, run in parallel, **shipped inside paqad-ai** (not seeded into the project: see "Footprint policy" below):
1. `runtime/scripts/design/scan-tokens.sh`: AST/grep for hard-coded design values
2. `runtime/scripts/design/scan-overrides.sh`: `!important`, inline `style=`, undocumented Tailwind class combos
3. `runtime/scripts/design/enumerate-surface.sh`: screen/route inventory (Next `app/` / `pages/` / framework router)
4. `runtime/scripts/design/axe-static.sh`: `axe-core` against component snapshots without booting the app
5. `runtime/scripts/design/coverage.sh`: Playwright tests cross-referenced against the AST-derived component inventory
Artefacts → `.paqad/design-test/runs/<run_id>/artifacts/`.
### Step 2.5: generate-local-design-playbook
For each suspected finding category, emit `docs/design-test/<timestamp>-local-playbook.md` with a Playwright snippet per category: same "break locally first" pattern pentest uses with curl. **Templates only, never auto-executed.** Categories: contrast probe, focus-ring probe, motion respect, touch-target probe, token-leak probe.
Example playbook entries:
```ts
// contrast probe: assert text/bg pair meets WCAG-2.2-1.4.3 (4.5:1)
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('contrast on /pricing', async ({ page }) => {
await page.goto('/pricing');
const results = await new AxeBuilder({ page }).withTags(['wcag2aa']).analyze();
expect(results.violations.filter((v) => v.id === 'color-contrast')).toEqual([]);
});
// focus-ring probe: keyboard reach must produce a visible focus outline
test('focus ring visible on primary button', async ({ page }) => {
await page.goto('/');
await page.keyboard.press('Tab');
const outline = await page
.locator(':focus-visible')
.evaluate((el) => getComputedStyle(el).outlineWidth);
expect(outline).not.toBe('0px');
});
// token-leak probe: computed color must resolve to a declared token
test('button bg = color.primary.500', async ({ page }) => {
await page.goto('/');
const bg = await page
.locator('button.primary')
.evaluate((el) => getComputedStyle(el).backgroundColor);
expect(bg).toBe('rgb(26, 115, 232)'); // declared in tokens.md as color.primary.500
});
```
### Step 3: live-validation (Playwright phase)
Equivalent of pentest's live-validation step. The runner brings up the app via `design_test.dev_command` (from the `design_test` block in `project-profile.yaml`) and walks it. It **reuses the project's `playwright.config.ts`** (browsers, projects, baseURL): overridable through the `design_test` config block.
- **Surface walk**: visit every route from the inventory; screenshot at declared breakpoints
- **Token diff**: inspect computed styles (color, font-family, font-size, line-height, radius, spacing, shadow) at each route; diff against `tokens.md`. One finding per mismatched property. This is the padding/margin/font/color/border check, mechanized.
- **State sweep**: for every component in the inventory, drive its declared states via Playwright
- **A11y dynamic**: `@axe-core/playwright` against each route in default + dark-mode + reduced-motion + RTL (if declared)
- **Visual regression**: first run creates a baseline in `.paqad/design-test/baselines/` (git-ignored); subsequent runs diff; `paqad design accept-baseline` action promotes approved diffs to a tracked path
- **Asset budget**: fonts loaded match `tokens.md`; no extra families
- **Performance budget**: LCP / CLS / INP vs declared budgets (only if `patterns.md` has a perf budget)
If the app can't be reached → record `blocked_checks` and proceed (identical to pentest behavior).
Skills consulted in this step: `accessibility-review`, `responsive-review`, `motion-review`, `state-coverage-review` (all keyed to the live walk's evidence).
### Step 4: synthesize-findings
Reuse `finding-normalizer`; the DT category vocabulary lives in its `assets/vocabulary.txt`. Stable `DT-XXXX` IDs. Each finding:
- `severity`: blocker / high / medium / low / nit
- `category`: token / component / state / a11y / responsive / motion / copy / performance / **documentation-drift**
- `surface`: screen + component
- `contract_ref`: clause in `docs/instructions/design-system/*.md` violated
- `evidence`: `file:line` for static; screenshot + selector for live
- `resolution`: concrete fix (e.g. "replace `#1a73e8` at `Button.tsx:42` with `color.primary.500` from `tokens.md`") so an LLM can resolve without re-deriving context
- `status`: open / accepted / waived (with reason) / fixed
- `playbook_ref`: link to playbook snippet that reproduces it
`token` findings default to **high** severity (raises visibility of hard-coded design values from day one). `documentation-drift` defaults to medium; rises to high if drift is in the contract itself (token declared in `tokens.md` never used, or used in code but not declared).
### Step 5: write-report
- `docs/design-test/<timestamp>.md`: human report
- `docs/design-test/<timestamp>.json`: machine sidecar (so `design-retest` preserves IDs)
- **DS-COV Coverage Matrix**: every contract clause × every route → covered/blocked (analog of WSTG matrix)
- Ordered findings (blocker → nit), remediation priorities, link to playbook
#### DS-COV Coverage Matrix Template
| Contract clause | Route | Status |
| ------------------------------------ | ---------- | ----------------- |
| tokens.md → color.primary.500 | /pricing | covered / blocked |
| tokens.md → spacing.4 | / | covered / blocked |
| components.md → Button > hover state | /signup | covered / blocked |
| accessibility.md → WCAG-2.2-1.4.3 | (all) | covered / blocked |
| motion.md → reduced-motion respected | (all) | covered / blocked |
| responsive.md → breakpoint:sm | (all) | covered / blocked |
| patterns.md → override budget | (codebase) | covered / blocked |
### Retest mode
Use this mode for the `design-retest` workflow.
Re-evaluate prior design-test findings against the current state of the UI so a retest report can classify each finding as `fixed`, `still-open`, or `needs-manual-verification` without re-deriving context. Mirror of `pentest-retest`.
Run this workflow whenever the user says anything equivalent to:
- "design retest", "design-retest", "retest design", "retest design findings"
#### Inputs
- The source `docs/design-test/<timestamp>.json` sidecar (the machine-readable companion of a prior design-test report).
- `docs/instructions/design-system/**` (contract: may have changed since the original run).
- Fresh evidence: AST scan + Playwright walk against the current working tree.
If the source sidecar path is not supplied in the request, default to the most recent file matching `docs/design-test/*.json`.
Progress is tracked in `.paqad/design-test/runs/<run_id>/progress.json` with `mode: retest` and `source_report: <path>`.
#### Step 1: load-source-findings
- Read the source sidecar. Validate against the design-test finding schema (same one `finding-normalizer` enforces).
- Build an in-memory index keyed by `DT-XXXX` id.
- Preserve every finding's original `severity`, `category`, `contract_ref`, and `playbook_ref`: retest does **not** re-derive these.
#### Step 2: gather-fresh-evidence
For each finding, gather only the evidence needed to re-classify it:
- `category: token` / `component` → re-run `scan-tokens.sh` and `scan-overrides.sh` over the file referenced in `evidence`; check whether the offending literal still exists at `file:line` (or has moved, in which case search the file).
- `category: state` / `a11y` / `responsive` / `motion` → re-run the relevant Playwright snippet from the original playbook (`playbook_ref`).
- `category: documentation-drift` → re-read the referenced `docs/modules/*/ui/**` file and the contract clause; check whether they now agree.
- `category: performance` → re-run the perf budget probe.
#### Step 3: classify-status
For each finding, pick a status from the `assets/status-vocabulary.txt` of the `retest-verification` skill:
- `fixed`: the offending evidence is gone AND the contract clause is now satisfied.
- `still-open`: the offending evidence remains.
- `needs-manual-verification`: automated evidence is inconclusive (e.g., a visual-regression diff that requires a human eye).
Findings whose `contract_ref` no longer exists in the contract get a synthetic `still-open` with a `contract-removed` note: the user removed the rule rather than fixing the code; the maintainer should confirm intent.
#### Step 4: write-retest-report
- `docs/design-test/<original-timestamp>-retest-<retest-timestamp>.md`: human report
- `docs/design-test/<original-timestamp>-retest-<retest-timestamp>.json`: machine sidecar with the same `DT-XXXX` ids and the new `status` field
- Summary block at the top: `fixed: N | still-open: N | needs-manual-verification: N | contract-removed: N`
Retest mode rules:
- Never invent new `DT-XXXX` ids during a retest. New findings discovered during retest are out of scope: run a fresh `design-test` for those.
- Never lower a finding's severity during retest. Severity is a property of the source report; retest only updates `status`.
- If the source sidecar is missing or malformed, stop and ask the user to point at a valid sidecar.
- Always write both the `.md` and `.json` retest outputs.
## Output Contract
### Footprint Policy
**Zero project footprint.** Diverges from pentest's "scripts live in the project" model: pentest scripts are meant to be tweakable by the project's security team; design-test scripts are infrastructure the project shouldn't touch. This divergence is intentional.
- All runners (scan-tokens, scan-overrides, enumerate-surface, axe-static, coverage, runtime-checks, retest) live inside paqad-ai under `runtime/scripts/design/`.
- `runtime-checks.ts` is a `.ts` file (Playwright is a Node API), run via `tsx` shipped as a paqad-ai dep.
- Stack-specific behavior is **framework code** inside the runners, they read the project's `package.json`, `playwright.config.ts`, `tailwind.config.*`, CSS-in-JS imports, and branch internally. One runner, many stacks.
- Outputs land in the project (`docs/design-test/*`, `.paqad/design-test/runs/*`): work products, not infrastructure. Same pattern as pentest.
### What Is and Isn't Project-Tunable
**Framework-owned, not project-editable:**
- This skill, including its Retest mode
- All 9 skills (the 8 review skills + `design-system-sync`)
- Runners under `runtime/scripts/design/`
- Routing rules
- Finding schema and severity vocabulary
**Project-tunable, via declarative inputs only:**
- The contract itself: `docs/instructions/design-system/*.md`
- A bounded `design_test` config block (schema-validated) in `project-profile.yaml`:
- `design_test.strict: true|false` (default false: advisory unless opted in)
- `design_test.app_url`
- `design_test.dev_command`
- `design_test.routes_include` / `routes_exclude`
- `design_test.baselines_path`
- `design_test.budgets` (schema framework-defined; values project-supplied)
- `design_test.with: [visual-regression, dark-mode-parity, rtl-parity, i18n-width]` (optional checks)
**Explicitly off-limits:**
- Project-side plugins loaded by the runner
- Project-side scripts the workflow shells out to
- Project-overridable thresholds outside the declared schema
- Swapping a framework skill for a project-custom one
- Editing this skill in the project
## Escalate / Stop Conditions
- Never skip steps. If a script is unavailable, record it in `blocked_checks` and proceed.
- Do not run destructive operations (no writes to the live database, no mutations via the API). The Playwright walk is read-only.
- Always write both the `.md` report and the `.json` sidecar; the `design-retest` workflow depends on the sidecar to preserve `DT-XXXX` IDs.
- The local design playbook (Step 2.5) is **generated only**: never execute it automatically.
- `docs/modules/*/ui/**` is never evidence; any disagreement with the AST-derived inventory produces a `documentation-drift` finding.
- Hard-coded design values (hex, raw px/rem, ad-hoc font stacks) are never acceptable when a token exists. `token` findings default to high severity.
## Resources
- Helper skills, in the order the Procedure calls them: `design-system-coverage`, `token-conformance-review`, `component-conformance-review`, `state-coverage-review`, `accessibility-review`, `responsive-review`, `motion-review`, `copy-and-ia-review`, `finding-normalizer`.
- `references/helper-map.md`: read it when you need to see which helper a step calls.
- `agents/openai.yaml`