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.
- package/package.json +2 -2
- package/template/.claude/agents/sd-audit.md +32 -0
- package/template/.claude/commands/e2e-audit.md +16 -0
- package/template/.claude/hooks/e2e-audit-session-start.sh +4 -0
- package/template/.claude/settings.json +4 -0
- package/template/.claude/skills/e2e-audit/SKILL.md +216 -0
- package/template/.claude/skills/e2e-audit/findings.schema.json +98 -0
- package/template/.claude/skills/e2e-audit/references/api-contract-playbook.md +66 -0
- package/template/.claude/skills/e2e-audit/references/auth-setup-playbook.md +78 -0
- package/template/.claude/skills/e2e-audit/references/coverage-gap-playbook.md +95 -0
- package/template/.claude/skills/e2e-audit/references/post-run-feedback-playbook.md +80 -0
- package/template/.claude/skills/e2e-audit/scripts/detect-stack.sh +205 -0
- package/template/.claude/skills/e2e-audit/scripts/detect-uncovered.sh +137 -0
- package/template/.claude/skills/e2e-audit/scripts/discover-api-surface.sh +242 -0
- package/template/.claude/skills/e2e-audit/scripts/discover-routes.sh +163 -0
- package/template/.claude/skills/e2e-audit/scripts/inventory-existing-tests.sh +161 -0
- package/template/.claude/skills/e2e-audit/scripts/verify-audit.sh +88 -0
- package/template/.claude/skills/e2e-audit/templates/auth-setup.ts.tpl +24 -0
- package/template/.claude/skills/e2e-audit/templates/base-fixture.ts.tpl +75 -0
- package/template/.claude/skills/e2e-audit/templates/findings-report.md.tpl +54 -0
- package/template/.claude/skills/e2e-audit/templates/post-run-feedback.md.tpl +36 -0
- package/template/.claude/skills/super-design/SKILL.md +42 -4
- package/template/.claude/skills/super-design/scripts/discover-surfaces.sh +197 -0
- package/template/.claude/skills/super-design/scripts/extract-project-rules.sh +240 -0
- 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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|