start-vibing 4.3.4 → 4.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/package.json +2 -2
  2. package/template/.claude/agents/sd-audit.md +32 -0
  3. package/template/.claude/commands/e2e-audit.md +16 -0
  4. package/template/.claude/hooks/e2e-audit-session-start.sh +4 -0
  5. package/template/.claude/settings.json +4 -0
  6. package/template/.claude/skills/e2e-audit/SKILL.md +216 -0
  7. package/template/.claude/skills/e2e-audit/findings.schema.json +98 -0
  8. package/template/.claude/skills/e2e-audit/references/api-contract-playbook.md +66 -0
  9. package/template/.claude/skills/e2e-audit/references/auth-setup-playbook.md +78 -0
  10. package/template/.claude/skills/e2e-audit/references/coverage-gap-playbook.md +95 -0
  11. package/template/.claude/skills/e2e-audit/references/post-run-feedback-playbook.md +80 -0
  12. package/template/.claude/skills/e2e-audit/scripts/detect-stack.sh +205 -0
  13. package/template/.claude/skills/e2e-audit/scripts/detect-uncovered.sh +137 -0
  14. package/template/.claude/skills/e2e-audit/scripts/discover-api-surface.sh +242 -0
  15. package/template/.claude/skills/e2e-audit/scripts/discover-routes.sh +163 -0
  16. package/template/.claude/skills/e2e-audit/scripts/inventory-existing-tests.sh +161 -0
  17. package/template/.claude/skills/e2e-audit/scripts/verify-audit.sh +88 -0
  18. package/template/.claude/skills/e2e-audit/templates/auth-setup.ts.tpl +24 -0
  19. package/template/.claude/skills/e2e-audit/templates/base-fixture.ts.tpl +75 -0
  20. package/template/.claude/skills/e2e-audit/templates/findings-report.md.tpl +54 -0
  21. package/template/.claude/skills/e2e-audit/templates/post-run-feedback.md.tpl +36 -0
  22. package/template/.claude/skills/super-design/SKILL.md +42 -4
  23. package/template/.claude/skills/super-design/scripts/discover-surfaces.sh +197 -0
  24. package/template/.claude/skills/super-design/scripts/extract-project-rules.sh +240 -0
  25. package/template/.claude/skills/super-design/scripts/verify-audit.sh +34 -1
@@ -8,7 +8,7 @@ description: >
8
8
  UX audit (WCAG 2.2 AA, Nielsen heuristics, Baymard, CWV), and synthesized
9
9
  overview. Re-audits only what changed since last run. On explicit user request,
10
10
  applies surgical fixes with full rollback.
11
- version: 0.6.4
11
+ version: 0.7.0
12
12
  ---
13
13
 
14
14
  # super-design
@@ -22,17 +22,36 @@ Four-phase pipeline with 6 specialist agents:
22
22
  nav, cards, modals, forms, tokens — per competitor × mobile+desktop),
23
23
  produces market-analysis.md + component-comparison.md.
24
24
  2. **UI/UX audit** (sd-audit) — drives browser via Playwright MCP directly.
25
- Five layers:
25
+ Six layers:
26
26
  - Route discovery + static snap (Nielsen + WCAG 2.2 AA + Baymard + CWV)
27
+ - **Step 1.5 source-first discovery** (0.7.0+) —
28
+ `discover-surfaces.sh` reads the repo FIRST and emits an authoritative
29
+ inventory of modals, forms, triggers, internal nav, and Next.js
30
+ layout/error/loading/not-found/parallel/intercepting routes BEFORE
31
+ Playwright runs. `extract-project-rules.sh` parses FORBIDDEN tables
32
+ from CLAUDE.md / AGENTS.md / .cursorrules into audit-applicable
33
+ rules. Runtime cross-checks surface these as `modal-coverage-gap`,
34
+ `form-coverage-gap`, and `project-forbidden-<slug>` findings.
27
35
  - **Step 2.5 component/modal/flow discovery** (Phase A inventory, B modal
28
36
  enumeration, C flow exercising, D state matrix, E form coverage) — this
29
37
  is where modal contents, empty/loading/error states, and flow errors
30
- get real evidence instead of "checklist hypothetical".
38
+ get real evidence instead of "checklist hypothetical". Phase B now
39
+ cross-references `surfaces.json` and files a `modal-coverage-gap`
40
+ finding for anything declared in source but never opened.
31
41
  - **Step 3g design-intelligence scoring** (17-category rubric → DIS 0–100)
32
42
  catches implicit best practices checklists miss (cards-in-flex-col,
33
- low density, weak CTA hierarchy, vibecode smell).
43
+ low density, weak CTA hierarchy, vibecode smell). Emits MANDATORY
44
+ `design-intelligence-craft-summary` finding per page × viewport so
45
+ overview.md has one holistic verdict row ("admin mobile is 38/100
46
+ WEAK — holistic redesign scope") per combination, not just discrete
47
+ per-category findings.
34
48
  - **Step 3h mobile-native audit** (21-item Duolingo/Linear/Arc/Cash-App
35
49
  checklist) — replaces "responsive-web-on-a-phone" thinking.
50
+ - **Step 3i project-rule enforcement** (0.7.0+) — consumes
51
+ `project-rules.json` and fires primary findings keyed to the
52
+ project's own FORBIDDEN wording (e.g. `project-forbidden-use-cards-on-mobile`)
53
+ when the rule is violated at runtime. Not a tag, not a severity
54
+ bump — the project owner's rule IS the rule source.
36
55
  - C16 ≤ 4 → **DSC-choice** proposal: sd-synthesis runs
37
56
  `scripts/score-typeui.mjs --from-audit <dir>` to derive a 7-axis site
38
57
  fingerprint (density/contrast/geometry/color/typography/motion/audience)
@@ -189,6 +208,25 @@ Windows git-bash + Linux.
189
208
  `*.fixture.json`, `fixtures/<name>.json`, or `$SUPER_DESIGN_FIXTURES`
190
209
  env JSON; falls back to `@fixture-default` with a warning. Consumers
191
210
  (hash-pages, sd-audit) MUST strip the suffix before navigating.
211
+ - `discover-surfaces.sh` (0.7.0+) — source-first static scan for
212
+ modals (`<Dialog|Sheet|Drawer|Modal|Popover|AlertDialog|DropdownMenu|CommandDialog|...>`),
213
+ forms (`<form>` / `useForm(` / `<Form>`), triggers (`<*Trigger>`),
214
+ internal navigation (`<Link href>` / `router.push`), and Next.js
215
+ `layout.tsx` / `error.tsx` / `loading.tsx` / `not-found.tsx` / parallel
216
+ routes (`@foo/`) / intercepting routes (`(.)foo/`). Emits
217
+ `$SESSION_DIR/surfaces.json`. sd-audit Step 2.5 Phase B cross-checks
218
+ runtime observations against this inventory and files
219
+ `modal-coverage-gap` / `form-coverage-gap` findings for declared
220
+ components never exercised.
221
+ - `extract-project-rules.sh` (0.7.0+) — parses FORBIDDEN tables from
222
+ `CLAUDE.md` / `AGENTS.md` / `GEMINI.md` / `.claude/CLAUDE.md` /
223
+ `.cursorrules` into an authoritative rule source. Classifies each
224
+ rule as audit-applicable (mobile / design / ux / perf / a11y) or
225
+ code-level (skip — belongs to typecheck/lint). Emits
226
+ `$SESSION_DIR/project-rules.json`. sd-audit Step 3i fires primary
227
+ findings keyed to the project's own wording (e.g.
228
+ `project-forbidden-use-cards-on-mobile`) — the project owner's rule
229
+ IS the rule source, not a tag or severity bump.
192
230
  - `build-import-graph.sh` — builds `.super-design/import-graph.json`
193
231
  (`{nodes, edges, hash, backend}`) and persists `import_graph_sha` to
194
232
  state. Prefers `npx madge --json <roots>`; falls back to a regex
@@ -0,0 +1,197 @@
1
+ #!/usr/bin/env bash
2
+ # discover-surfaces.sh — source-first discovery of modals, forms, triggers,
3
+ # internal navigation, and Next.js layout/error/loading/parallel surfaces.
4
+ #
5
+ # Purpose: sd-audit cannot trust runtime-only modal discovery (click every
6
+ # trigger in Playwright). A modal hidden behind a flow step, a feature flag,
7
+ # or an auth gate will be missed. This script reads the SOURCE CODE first
8
+ # and emits an authoritative inventory; Step 2.5 Phase B cross-checks at
9
+ # runtime and emits `modal-coverage-gap` findings for anything declared in
10
+ # source but never opened during the audit.
11
+ #
12
+ # Output: JSON object on stdout with shape:
13
+ # {
14
+ # "modals": [{ "component": "CreateUserDialog", "file": "...", "used_in": [...] }],
15
+ # "forms": [{ "id": "LoginForm", "file": "...", "fields": [...] }],
16
+ # "triggers": [{ "kind": "DialogTrigger", "file": "..." }],
17
+ # "navigation": [{ "from": "src/...", "to": "/users", "kind": "Link" }],
18
+ # "layouts": ["src/app/layout.tsx", "src/app/(app)/layout.tsx"],
19
+ # "errors": ["src/app/error.tsx"],
20
+ # "loading": ["src/app/loading.tsx"],
21
+ # "not_found": ["src/app/not-found.tsx"],
22
+ # "parallel": ["src/app/@modal"],
23
+ # "intercepting": ["src/app/(.)photos"],
24
+ # "framework": "next" | "remix" | "sveltekit" | "astro" | "nuxt" | "unknown"
25
+ # }
26
+ #
27
+ # Best-effort grep-based scanner. No AST. False positives are OK —
28
+ # sd-audit treats this as an inventory, not a contract. Missed items
29
+ # are the real failure mode.
30
+ set -euo pipefail
31
+
32
+ log() { printf '[discover-surfaces] %s\n' "$*" >&2; }
33
+
34
+ detect_framework() {
35
+ if [[ -f next.config.js || -f next.config.ts || -f next.config.mjs ]]; then echo "next"
36
+ elif [[ -f remix.config.js || -d app/routes ]]; then echo "remix"
37
+ elif [[ -f svelte.config.js && -d src/routes ]]; then echo "sveltekit"
38
+ elif [[ -f astro.config.mjs || -f astro.config.ts ]]; then echo "astro"
39
+ elif [[ -f nuxt.config.ts || -f nuxt.config.js ]]; then echo "nuxt"
40
+ else echo "unknown"; fi
41
+ }
42
+
43
+ # Source roots to scan. Frameworks differ; union keeps the scanner simple.
44
+ SCAN_ROOTS=()
45
+ for d in src app src/app src/components components src/pages pages src/features features src/routes app/routes; do
46
+ [[ -d "$d" ]] && SCAN_ROOTS+=("$d")
47
+ done
48
+ if [[ ${#SCAN_ROOTS[@]} -eq 0 ]]; then
49
+ log "no known source roots found; emitting empty inventory"
50
+ jq -n '{modals:[],forms:[],triggers:[],navigation:[],layouts:[],errors:[],loading:[],not_found:[],parallel:[],intercepting:[],framework:"unknown"}'
51
+ exit 0
52
+ fi
53
+
54
+ # File extensions we care about.
55
+ EXT_GLOB='\.(tsx|jsx|ts|js|svelte|vue|astro)$'
56
+
57
+ # --- 1. MODALS --------------------------------------------------------------
58
+ # Match common modal/overlay component usages. We key on JSX opening tags so
59
+ # we catch both <Dialog> and <Dialog.Root>. The component NAME is what
60
+ # the audit logs as the `component` field.
61
+ MODAL_PATTERN='<(Dialog|AlertDialog|Sheet|Drawer|Modal|Popover|HoverCard|Tooltip|CommandDialog|DropdownMenu|ContextMenu|Menubar|NavigationMenu|Select|Combobox|DatePicker|ColorPicker)\b'
62
+
63
+ modals_json="$(
64
+ {
65
+ for root in "${SCAN_ROOTS[@]}"; do
66
+ grep -rEHn --include='*.tsx' --include='*.jsx' --include='*.ts' --include='*.js' \
67
+ --include='*.svelte' --include='*.vue' --include='*.astro' \
68
+ "$MODAL_PATTERN" "$root" 2>/dev/null || true
69
+ done
70
+ } | awk -F: '{
71
+ file=$1; line=$2;
72
+ # capture component name between < and the next non-word char
73
+ match($0, /<([A-Z][A-Za-z0-9_]*)/, m);
74
+ if (m[1] != "") printf "{\"component\":\"%s\",\"file\":\"%s\",\"line\":%s}\n", m[1], file, line;
75
+ }' | jq -s 'unique_by([.component,.file])'
76
+ )"
77
+
78
+ # --- 2. FORMS ---------------------------------------------------------------
79
+ # Match react-hook-form useForm() calls, <Form> / <form> elements, and
80
+ # zod schema imports co-located with forms.
81
+ FORM_PATTERN='(useForm\s*\(|<Form[[:space:]>]|<form[[:space:]>])'
82
+
83
+ forms_json="$(
84
+ {
85
+ for root in "${SCAN_ROOTS[@]}"; do
86
+ grep -rEHln --include='*.tsx' --include='*.jsx' --include='*.ts' --include='*.js' \
87
+ --include='*.svelte' --include='*.vue' --include='*.astro' \
88
+ "$FORM_PATTERN" "$root" 2>/dev/null || true
89
+ done
90
+ } | sort -u | awk '{ printf "{\"file\":\"%s\"}\n", $0 }' | jq -s '.'
91
+ )"
92
+
93
+ # --- 3. TRIGGERS ------------------------------------------------------------
94
+ # Explicit *Trigger components (Radix / shadcn convention) tell us the
95
+ # connection between a button and its overlay.
96
+ TRIGGER_PATTERN='<(DialogTrigger|SheetTrigger|DrawerTrigger|PopoverTrigger|DropdownMenuTrigger|AlertDialogTrigger|HoverCardTrigger|TooltipTrigger|SelectTrigger|ComboboxTrigger|ContextMenuTrigger|MenubarTrigger|NavigationMenuTrigger)\b'
97
+
98
+ triggers_json="$(
99
+ {
100
+ for root in "${SCAN_ROOTS[@]}"; do
101
+ grep -rEHn --include='*.tsx' --include='*.jsx' \
102
+ "$TRIGGER_PATTERN" "$root" 2>/dev/null || true
103
+ done
104
+ } | awk -F: '{
105
+ file=$1; line=$2;
106
+ match($0, /<([A-Z][A-Za-z0-9_]*Trigger)/, m);
107
+ if (m[1] != "") printf "{\"kind\":\"%s\",\"file\":\"%s\",\"line\":%s}\n", m[1], file, line;
108
+ }' | jq -s '.'
109
+ )"
110
+
111
+ # --- 4. NAVIGATION ----------------------------------------------------------
112
+ # Internal nav: <Link href=...>, <Link to=...>, router.push("/..."),
113
+ # redirect("/..."), navigate("/..."). We only keep destinations that start
114
+ # with "/" (internal) — external URLs are not audit targets here.
115
+ nav_json="$(
116
+ {
117
+ for root in "${SCAN_ROOTS[@]}"; do
118
+ grep -rEHno --include='*.tsx' --include='*.jsx' --include='*.ts' --include='*.js' \
119
+ "(href|to)=[\"']/[^\"']*[\"']|(router\.push|redirect|navigate)\(\s*[\"']/[^\"']*[\"']" \
120
+ "$root" 2>/dev/null || true
121
+ done
122
+ } | awk -F: '{
123
+ file=$1; line=$2; rest=""; for (i=3;i<=NF;i++) rest = rest (i==3?"":":") $i;
124
+ # extract first "/..." path literal from the match
125
+ match(rest, /[\"'"'"']\/[^\"'"'"']*[\"'"'"']/, m);
126
+ if (m[0] != "") {
127
+ dest=m[0]; gsub(/[\"'"'"']/, "", dest);
128
+ kind = (rest ~ /push|redirect|navigate/) ? "imperative" : "link";
129
+ printf "{\"from\":\"%s\",\"to\":\"%s\",\"kind\":\"%s\",\"line\":%s}\n", file, dest, kind, line;
130
+ }
131
+ }' | jq -s 'unique_by([.from,.to])'
132
+ )"
133
+
134
+ # --- 5. NEXT.JS LAYOUT / ERROR / LOADING / NOT-FOUND / PARALLEL -------------
135
+ # Empty arrays for non-Next frameworks.
136
+ FW="$(detect_framework)"
137
+ layouts_json='[]'
138
+ errors_json='[]'
139
+ loading_json='[]'
140
+ notfound_json='[]'
141
+ parallel_json='[]'
142
+ intercepting_json='[]'
143
+
144
+ if [[ "$FW" == "next" ]]; then
145
+ # layout.tsx / template.tsx nesting defines wrapper chrome (headers,
146
+ # sidebars) — sd-audit must audit these at EVERY viewport because a
147
+ # layout that misbehaves on mobile breaks every child page.
148
+ layouts_json="$(
149
+ find app src/app -type f \( -name 'layout.tsx' -o -name 'layout.ts' -o -name 'layout.jsx' -o -name 'layout.js' -o -name 'template.tsx' -o -name 'template.ts' \) 2>/dev/null \
150
+ | jq -Rn '[inputs]'
151
+ )"
152
+ errors_json="$(
153
+ find app src/app -type f \( -name 'error.tsx' -o -name 'global-error.tsx' \) 2>/dev/null \
154
+ | jq -Rn '[inputs]'
155
+ )"
156
+ loading_json="$(
157
+ find app src/app -type f -name 'loading.tsx' 2>/dev/null | jq -Rn '[inputs]'
158
+ )"
159
+ notfound_json="$(
160
+ find app src/app -type f -name 'not-found.tsx' 2>/dev/null | jq -Rn '[inputs]'
161
+ )"
162
+ # Parallel route slots: directories starting with @
163
+ parallel_json="$(
164
+ find app src/app -type d -name '@*' 2>/dev/null | jq -Rn '[inputs]'
165
+ )"
166
+ # Intercepting routes: directories starting with (.) / (..) / (...)
167
+ intercepting_json="$(
168
+ find app src/app -type d \( -name '(.)*' -o -name '(..)*' -o -name '(...)*' \) 2>/dev/null | jq -Rn '[inputs]'
169
+ )"
170
+ fi
171
+
172
+ # --- ASSEMBLE ---------------------------------------------------------------
173
+ jq -n \
174
+ --argjson modals "$modals_json" \
175
+ --argjson forms "$forms_json" \
176
+ --argjson triggers "$triggers_json" \
177
+ --argjson navigation "$nav_json" \
178
+ --argjson layouts "$layouts_json" \
179
+ --argjson errors "$errors_json" \
180
+ --argjson loading "$loading_json" \
181
+ --argjson not_found "$notfound_json" \
182
+ --argjson parallel "$parallel_json" \
183
+ --argjson intercepting "$intercepting_json" \
184
+ --arg framework "$FW" \
185
+ '{
186
+ framework: $framework,
187
+ modals: $modals,
188
+ forms: $forms,
189
+ triggers: $triggers,
190
+ navigation: $navigation,
191
+ layouts: $layouts,
192
+ errors: $errors,
193
+ loading: $loading,
194
+ not_found: $not_found,
195
+ parallel: $parallel,
196
+ intercepting: $intercepting
197
+ }'
@@ -0,0 +1,240 @@
1
+ #!/usr/bin/env bash
2
+ # extract-project-rules.sh — parse project FORBIDDEN rules from
3
+ # CLAUDE.md / AGENTS.md / .cursorrules into an authoritative rule
4
+ # source for sd-audit Step 3h.
5
+ #
6
+ # User-owned FORBIDDEN tables ("Use Cards on mobile", "Waterfall data
7
+ # fetching", "Make responsive UI only") are HIGHER-PRIORITY than generic
8
+ # Nielsen/WCAG heuristics because the project owner has already
9
+ # codified them as the right answer for this codebase. sd-audit treats
10
+ # violations as primary findings — NOT as a severity bump or a tag on
11
+ # another finding. If CLAUDE.md says "no cards on mobile" and we see a
12
+ # Card in a mobile flex-col, the rule fires as
13
+ # `project-forbidden-use-cards-on-mobile` with the project's own wording.
14
+ #
15
+ # Output: JSON on stdout
16
+ # {
17
+ # "source_files": ["CLAUDE.md", ".claude/CLAUDE.md"],
18
+ # "rules": [
19
+ # {
20
+ # "raw": "Use Cards on mobile",
21
+ # "reason": "Waste space in flex-col",
22
+ # "slug": "use-cards-on-mobile",
23
+ # "audit_applicable": true,
24
+ # "detector_family": "mobile",
25
+ # "template_id": "M2",
26
+ # "detector_hint": "flag <Card> inside flex-col at viewport ≤ 768"
27
+ # }
28
+ # ]
29
+ # }
30
+ #
31
+ # `audit_applicable: false` means the rule is code-level (e.g. "Files >
32
+ # 400 lines", "Use `any` type", "'use client' at top level") and does
33
+ # NOT produce an audit finding. sd-audit only iterates applicable rules.
34
+ set -euo pipefail
35
+
36
+ log() { printf '[extract-project-rules] %s\n' "$*" >&2; }
37
+
38
+ # Files to scan in order of authority.
39
+ CANDIDATE_FILES=(
40
+ "CLAUDE.md"
41
+ "AGENTS.md"
42
+ "GEMINI.md"
43
+ ".claude/CLAUDE.md"
44
+ ".cursorrules"
45
+ )
46
+
47
+ SOURCE_FILES=()
48
+ for f in "${CANDIDATE_FILES[@]}"; do
49
+ [[ -f "$f" ]] && SOURCE_FILES+=("$f")
50
+ done
51
+
52
+ if [[ ${#SOURCE_FILES[@]} -eq 0 ]]; then
53
+ log "no project rule files found; emitting empty rule set"
54
+ jq -n '{source_files:[],rules:[]}'
55
+ exit 0
56
+ fi
57
+
58
+ # Extract FORBIDDEN table rows. We look for Markdown tables under any
59
+ # heading that contains the word "FORBIDDEN" (case-insensitive). Rows
60
+ # have shape: `| <action> | <reason> |`. We skip header, separator, and
61
+ # empty rows.
62
+ #
63
+ # Implementation: awk state machine — enter "in table" when we see a
64
+ # FORBIDDEN-ish heading followed by a table header; exit when we see a
65
+ # blank line followed by a new heading or the next "---" separator
66
+ # OUTSIDE the table.
67
+ extract_rows() {
68
+ local file="$1"
69
+ awk '
70
+ BEGIN { mode=0; seen_header=0 }
71
+ /^#+[[:space:]]+.*[Ff][Oo][Rr][Bb][Ii][Dd][Dd][Ee][Nn]/ { mode=1; seen_header=0; next }
72
+ # Close on next top-level or section heading when we were inside
73
+ mode == 1 && /^#+[[:space:]]/ && !/[Ff][Oo][Rr][Bb][Ii][Dd][Dd][Ee][Nn]/ { mode=0; seen_header=0; next }
74
+ mode == 1 && /^\|.*\|.*\|/ {
75
+ if (seen_header == 0) { seen_header=1; next }
76
+ # skip separator row
77
+ if ($0 ~ /^\|[[:space:]]*[-]+/) { next }
78
+ # split on | and take cells 1 and 2 (cells 0 and last are empty)
79
+ n = split($0, cells, "|");
80
+ # cells[2] = action, cells[3] = reason (after leading |)
81
+ action = cells[2]; reason = cells[3];
82
+ gsub(/^[[:space:]]+|[[:space:]]+$/, "", action);
83
+ gsub(/^[[:space:]]+|[[:space:]]+$/, "", reason);
84
+ if (action != "" && action !~ /^-+$/ && action !~ /^Action$/i) {
85
+ printf "%s\t%s\n", action, reason;
86
+ }
87
+ }
88
+ ' "$file"
89
+ }
90
+
91
+ # Slugify: lowercase, non-word → dash, collapse, trim.
92
+ slugify() {
93
+ printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+|-+$//g'
94
+ }
95
+
96
+ # Classify a rule into {audit_applicable, detector_family, template_id,
97
+ # detector_hint}. Keyword-based — additive over time.
98
+ #
99
+ # Audit-applicable families:
100
+ # mobile → M1-M15 (cards on mobile, responsive-only, card grid on mobile, etc.)
101
+ # design → V1-V8 (Cards used everywhere, masonry on mobile, etc.)
102
+ # ux → U1-U10 (no loading states, waterfall fetching hits UX)
103
+ # perf → P1-P10 (bundle size, waterfall as perf issue)
104
+ # a11y → A1-A15 (contrast, focus, etc.)
105
+ #
106
+ # Non-audit (code-level, skip): "Files >", "'use client'", "Use any
107
+ # type", "wildcard imports", "skip typecheck", etc.
108
+ classify() {
109
+ local raw="$1"
110
+ local low
111
+ low="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]')"
112
+
113
+ # -- code-level (NOT audit-applicable) --
114
+ case "$low" in
115
+ *"files >"*|*"> 400 lines"*|*"> 300 lines"*|*"wildcard icon"*|\
116
+ *"'use client'"*|*"use \`any\`"*|*"use any type"*|\
117
+ *"relative imports"*|*"skip typecheck"*|*"skip lint"*|\
118
+ *"define types in"*|*"@types"*|*"skip docker"*|*"write in non-english"*|\
119
+ *"commit directly to main"*|*"skip code-simplifier"*|\
120
+ *"auto-document"*|*"mix doc types"*|*"leave docs unlinked"*|\
121
+ *"skip claude.md"*|*"use mui"*|*"skip todo"*|*"stack last change"*|\
122
+ *"skip research"*|*"skip superpowers"*|*"skip documenter"*|\
123
+ *"skip planning"*|*"skip domain"*|*"skip cn utility"*|\
124
+ *"skip flow documentation"*|*"waterfall data fetching"*)
125
+ # note: waterfall IS audit-adjacent via perf, but the primary
126
+ # signal is in package code not in rendered UI. Keep as code-level.
127
+ echo "false code-level - -"
128
+ return
129
+ ;;
130
+ esac
131
+
132
+ # -- mobile (M family) --
133
+ case "$low" in
134
+ *"card"*"mobile"*|*"mobile"*"card"*|*"cards on mobile"*|\
135
+ *"masonry grid on mobile"*|*"3d elements on mobile"*)
136
+ echo "true mobile M2 flag <Card>/card components inside vertical stack at viewport ≤ 768"
137
+ return
138
+ ;;
139
+ *"responsive ui only"*|*"responsive only"*|*"make responsive"*)
140
+ echo "true mobile M-distinct check mobile viewport is a distinct layout not a scaled-down desktop"
141
+ return
142
+ ;;
143
+ *"hamburger-only"*|*"no bottom tabs"*|*"bottom nav"*)
144
+ echo "true mobile M1 require bottom tabs on mobile for primary nav"
145
+ return
146
+ ;;
147
+ *"centered modal on mobile"*|*"centered dialog on mobile"*)
148
+ echo "true mobile M6 require bottom-sheet on mobile, not centered dialog"
149
+ return
150
+ ;;
151
+ esac
152
+
153
+ # -- design (V family) --
154
+ case "$low" in
155
+ *"neumorphism"*)
156
+ echo "true design V-neumorphism flag neumorphic styles on form surfaces (accessibility risk)"
157
+ return
158
+ ;;
159
+ *"scroll animations on dashboard"*)
160
+ echo "true design V-scroll flag scroll-triggered animations on dashboard/app routes"
161
+ return
162
+ ;;
163
+ *"mix animation libraries"*)
164
+ echo "true design V-animmix flag presence of multiple animation libs (framer + gsap + lottie)"
165
+ return
166
+ ;;
167
+ esac
168
+
169
+ # -- ux (U family) --
170
+ case "$low" in
171
+ *"skip loading.tsx"*|*"no loading state"*|*"skip skeleton"*)
172
+ echo "true ux U3 flag absence of loading state on data-fetching page"
173
+ return
174
+ ;;
175
+ *"skip empty state"*|*"no empty state"*)
176
+ echo "true ux U4 flag absence of empty state on zero-data views"
177
+ return
178
+ ;;
179
+ esac
180
+
181
+ # -- default: unknown but still emit, detector_family=generic --
182
+ echo "true generic - match rule text against page copy / component names"
183
+ }
184
+
185
+ # Build the rules array.
186
+ rules_tsv=""
187
+ for f in "${SOURCE_FILES[@]}"; do
188
+ rows="$(extract_rows "$f" || true)"
189
+ [[ -z "$rows" ]] && continue
190
+ while IFS=$'\t' read -r action reason; do
191
+ [[ -z "$action" ]] && continue
192
+ cls="$(classify "$action")"
193
+ slug="$(slugify "$action")"
194
+ # pack: raw \t reason \t slug \t applicable \t family \t template \t hint \t source
195
+ IFS=$'\t' read -r applicable family tpl hint <<<"$cls"
196
+ printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
197
+ "$action" "$reason" "$slug" "$applicable" "$family" "$tpl" "$hint" "$f"
198
+ rules_tsv+="1"
199
+ done <<<"$rows"
200
+ done
201
+
202
+ # Emit JSON. jq slurps TSV lines from stdin.
203
+ rules_json="$(
204
+ {
205
+ for f in "${SOURCE_FILES[@]}"; do
206
+ rows="$(extract_rows "$f" || true)"
207
+ [[ -z "$rows" ]] && continue
208
+ while IFS=$'\t' read -r action reason; do
209
+ [[ -z "$action" ]] && continue
210
+ cls="$(classify "$action")"
211
+ slug="$(slugify "$action")"
212
+ IFS=$'\t' read -r applicable family tpl hint <<<"$cls"
213
+ jq -n \
214
+ --arg raw "$action" \
215
+ --arg reason "$reason" \
216
+ --arg slug "$slug" \
217
+ --arg applicable "$applicable" \
218
+ --arg family "$family" \
219
+ --arg tpl "$tpl" \
220
+ --arg hint "$hint" \
221
+ --arg source "$f" \
222
+ '{
223
+ raw: $raw,
224
+ reason: $reason,
225
+ slug: $slug,
226
+ source_file: $source,
227
+ audit_applicable: ($applicable == "true"),
228
+ detector_family: $family,
229
+ template_id: $tpl,
230
+ detector_hint: $hint
231
+ }'
232
+ done <<<"$rows"
233
+ done
234
+ } | jq -s 'unique_by(.slug)'
235
+ )"
236
+
237
+ jq -n \
238
+ --argjson sources "$(printf '%s\n' "${SOURCE_FILES[@]}" | jq -Rn '[inputs]')" \
239
+ --argjson rules "${rules_json:-[]}" \
240
+ '{ source_files: $sources, rules: $rules }'
@@ -54,7 +54,9 @@ while IFS=$'\t' read -r id shot snap; do
54
54
  done
55
55
 
56
56
  # 4. snapshot_quote must appear verbatim in the snapshot.
57
- jq -c '.[]' "$FINDINGS" | while read -r f; do
57
+ # Skipped for meta findings (coverage, craft-summary, project-rule)
58
+ # where evidence is aggregate, not a single DOM quote.
59
+ jq -c '.[] | select(.rule | test("^(audit-coverage-|design-intelligence-craft-summary|modal-coverage-gap|form-coverage-gap|project-forbidden-)") | not)' "$FINDINGS" | while read -r f; do
58
60
  id=$(echo "$f" | jq -r .id)
59
61
  q=$(echo "$f" | jq -r .snapshot_quote)
60
62
  s=$(echo "$f" | jq -r .snapshot_path)
@@ -63,6 +65,37 @@ jq -c '.[]' "$FINDINGS" | while read -r f; do
63
65
  fi
64
66
  done
65
67
 
68
+ # 5. design-intelligence/<slug>_<vp>.json MUST exist for every
69
+ # page × viewport combination in MATRIX. This enforces the
70
+ # 0.7.0 mandatory per-combo craft scoring.
71
+ DIS_DIR="$SESSION_DIR/design-intelligence"
72
+ if [ -d "$DIS_DIR" ]; then
73
+ DIS_COUNT=$(find "$DIS_DIR" -maxdepth 1 -type f -name '*.json' | wc -l)
74
+ if [ "$DIS_COUNT" -lt 1 ]; then
75
+ warn "design-intelligence/ exists but is empty — Step 3g skipped"
76
+ fi
77
+ else
78
+ warn "no design-intelligence/ directory — Step 3g (craft scoring) did not run"
79
+ fi
80
+
81
+ # 6. Viewport quota check: if >= 10 total findings, mobile must be
82
+ # >= 30% unless an explicit audit-coverage-skewed meta finding
83
+ # was emitted (which acknowledges the gap).
84
+ TOTAL=$(jq '[.[] | select(.viewport)] | length' "$FINDINGS")
85
+ if [ "$TOTAL" -ge 10 ]; then
86
+ MOBILE=$(jq '[.[] | select(.viewport == "mobile")] | length' "$FINDINGS")
87
+ COVERAGE_ACK=$(jq '[.[] | select(.rule == "audit-coverage-skewed")] | length' "$FINDINGS")
88
+ # integer math: mobile * 100 / total < 30 means < 30%
89
+ if [ "$COVERAGE_ACK" -eq 0 ] && [ $(( MOBILE * 100 / TOTAL )) -lt 30 ]; then
90
+ warn "mobile viewport is $MOBILE / $TOTAL (< 30%) and no audit-coverage-skewed meta finding was emitted"
91
+ fi
92
+ fi
93
+
94
+ # 7. surfaces.json + project-rules.json should exist (0.7.0 contract).
95
+ # Absence is a warning, not a fatal — allows stale sessions to still verify.
96
+ [ -f "$SESSION_DIR/surfaces.json" ] || warn "missing surfaces.json (Step 1.5 skipped)"
97
+ [ -f "$SESSION_DIR/project-rules.json" ] || warn "missing project-rules.json (Step 1.5 skipped)"
98
+
66
99
  COUNT=$(jq 'length' "$FINDINGS")
67
100
  if [ "$STRICT" -eq 1 ] && [ "$WARNINGS" -gt 0 ]; then
68
101
  echo "STRICT: $COUNT findings verified, $WARNINGS warning(s)" >&2