start-vibing 4.3.4 → 4.4.0

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 (44) hide show
  1. package/package.json +2 -2
  2. package/template/.claude/agents/sd-audit.md +32 -0
  3. package/template/.claude/skills/e2e-audit/DESIGN.md +294 -0
  4. package/template/.claude/skills/e2e-audit/SKILL.md +660 -0
  5. package/template/.claude/skills/e2e-audit/e2e/fixtures/auth.setup.ts +70 -0
  6. package/template/.claude/skills/e2e-audit/e2e/fixtures/auth.ts +21 -0
  7. package/template/.claude/skills/e2e-audit/e2e/fixtures/base.ts +90 -0
  8. package/template/.claude/skills/e2e-audit/e2e/fixtures/storage/.gitkeep +0 -0
  9. package/template/.claude/skills/e2e-audit/e2e/fixtures/storage/admin.json +50 -0
  10. package/template/.claude/skills/e2e-audit/e2e/fixtures/storage/manager.json +50 -0
  11. package/template/.claude/skills/e2e-audit/e2e/fixtures/storage/member.json +50 -0
  12. package/template/.claude/skills/e2e-audit/e2e/fixtures/storage/owner.json +50 -0
  13. package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-admin.page.ts +141 -0
  14. package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-billing.page.ts +47 -0
  15. package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-chat.page.ts +35 -0
  16. package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-home.page.ts +134 -0
  17. package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-integrations.page.ts +334 -0
  18. package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-knowledge.page.ts +30 -0
  19. package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-ontology.page.ts +71 -0
  20. package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-profile.page.ts +38 -0
  21. package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-teams.page.ts +123 -0
  22. package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-transcripts.page.ts +109 -0
  23. package/template/.claude/skills/e2e-audit/e2e/specs/auth/login.spec.ts +59 -0
  24. package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-admin.spec.ts +233 -0
  25. package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-billing.spec.ts +44 -0
  26. package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-chat.spec.ts +50 -0
  27. package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-home.spec.ts +243 -0
  28. package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-integrations.spec.ts +472 -0
  29. package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-knowledge.spec.ts +57 -0
  30. package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-ontology.spec.ts +72 -0
  31. package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-profile.spec.ts +48 -0
  32. package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-teams.spec.ts +247 -0
  33. package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-transcripts.spec.ts +122 -0
  34. package/template/.claude/skills/e2e-audit/e2e/specs/security/headers.spec.ts +39 -0
  35. package/template/.claude/skills/e2e-audit/e2e/specs/security/rbac.spec.ts +92 -0
  36. package/template/.claude/skills/e2e-audit/e2e/specs/security/xss.spec.ts +74 -0
  37. package/template/.claude/skills/e2e-audit/e2e/utils/console-collector.ts +89 -0
  38. package/template/.claude/skills/e2e-audit/e2e/utils/security-helpers.ts +114 -0
  39. package/template/.claude/skills/e2e-audit/e2e/utils/test-data.ts +64 -0
  40. package/template/.claude/skills/e2e-audit/runbook.md +115 -0
  41. package/template/.claude/skills/super-design/SKILL.md +42 -4
  42. package/template/.claude/skills/super-design/scripts/discover-surfaces.sh +197 -0
  43. package/template/.claude/skills/super-design/scripts/extract-project-rules.sh +240 -0
  44. package/template/.claude/skills/super-design/scripts/verify-audit.sh +34 -1
@@ -0,0 +1,114 @@
1
+ import { type Page, expect } from '@playwright/test'
2
+
3
+ export const XSS_PAYLOADS = [
4
+ '<script>alert("xss")</script>',
5
+ '<img src=x onerror=alert("xss")>',
6
+ '"><script>alert("xss")</script>',
7
+ "javascript:alert('xss')",
8
+ '<svg onload=alert("xss")>',
9
+ "'; DROP TABLE users; --",
10
+ '{{constructor.constructor("return this")()}}',
11
+ '${7*7}',
12
+ ]
13
+
14
+ export const REQUIRED_SECURITY_HEADERS = [
15
+ 'x-frame-options',
16
+ 'x-content-type-options',
17
+ 'referrer-policy',
18
+ ]
19
+
20
+ /**
21
+ * Test XSS payloads against a text input.
22
+ * Fills each payload and verifies no dialog (alert/confirm/prompt) fires.
23
+ */
24
+ export async function testXssOnInput(
25
+ page: Page,
26
+ inputLocator: ReturnType<Page['getByRole']>,
27
+ ) {
28
+ const dialogFired: string[] = []
29
+ const handler = (dialog: { message: () => string; dismiss: () => Promise<void> }) => {
30
+ dialogFired.push(dialog.message())
31
+ void dialog.dismiss()
32
+ }
33
+ page.on('dialog', handler)
34
+
35
+ for (const payload of XSS_PAYLOADS) {
36
+ await inputLocator.clear()
37
+ await inputLocator.fill(payload)
38
+ // Trigger potential execution (blur, submit)
39
+ await inputLocator.press('Tab')
40
+ }
41
+
42
+ page.off('dialog', handler)
43
+
44
+ if (dialogFired.length > 0) {
45
+ throw new Error(
46
+ `XSS detected! Dialog(s) fired with: ${dialogFired.join(', ')}`,
47
+ )
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Verify required security headers are present on a response.
53
+ */
54
+ export async function assertSecurityHeaders(page: Page, url: string) {
55
+ const response = await page.goto(url)
56
+ if (!response) throw new Error(`No response for ${url}`)
57
+
58
+ const headers = response.headers()
59
+
60
+ for (const header of REQUIRED_SECURITY_HEADERS) {
61
+ expect(
62
+ headers[header],
63
+ `Missing security header: ${header}`,
64
+ ).toBeDefined()
65
+ }
66
+
67
+ // X-Content-Type-Options should be nosniff
68
+ if (headers['x-content-type-options']) {
69
+ expect(headers['x-content-type-options']).toBe('nosniff')
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Verify that error responses don't contain stack traces.
75
+ */
76
+ export async function assertNoStackTraceInResponse(page: Page) {
77
+ const content = await page.content()
78
+ const stackTraceIndicators = [
79
+ 'at Object.',
80
+ 'at Module.',
81
+ 'at Function.',
82
+ 'node_modules/',
83
+ '.js:',
84
+ 'webpack-internal',
85
+ '__next',
86
+ ]
87
+
88
+ for (const indicator of stackTraceIndicators) {
89
+ expect(content).not.toContain(indicator)
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Test RBAC by navigating to a URL and checking if access is denied.
95
+ */
96
+ export async function assertAccessDenied(
97
+ page: Page,
98
+ url: string,
99
+ expectedRedirect?: string,
100
+ ) {
101
+ await page.goto(url)
102
+ const currentUrl = page.url()
103
+
104
+ if (expectedRedirect) {
105
+ expect(currentUrl).toContain(expectedRedirect)
106
+ } else {
107
+ // Should redirect to /unauthorized or /auth
108
+ const denied =
109
+ currentUrl.includes('/unauthorized') ||
110
+ currentUrl.includes('/auth') ||
111
+ currentUrl.includes('/login')
112
+ expect(denied).toBe(true)
113
+ }
114
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Shared test data constants for E2E tests.
3
+ * Keep this minimal — only data used across multiple test files.
4
+ */
5
+
6
+ export const ROUTES = {
7
+ // Dashboard (authenticated)
8
+ home: '/dashboard/home',
9
+ knowledge: '/dashboard/knowledge',
10
+ knowledgeDetail: (id: string) => `/dashboard/knowledge/${id}`,
11
+ chat: '/dashboard/chat',
12
+ integrations: '/dashboard/integrations',
13
+ integrationDetail: (id: string) => `/dashboard/integrations/${id}`,
14
+ integrationsNew: '/dashboard/integrations/new',
15
+ integrationsSuccess: '/dashboard/integrations/success',
16
+ teams: '/dashboard/teams',
17
+ profile: '/dashboard/profile',
18
+ admin: '/dashboard/admin',
19
+ billing: '/dashboard/billing',
20
+ ontology: '/dashboard/ontology',
21
+ transcripts: '/dashboard/transcripts',
22
+ transcriptDetail: (id: string) => `/dashboard/transcripts/${id}`,
23
+ transcriptDocument: (id: string) => `/dashboard/transcripts/documents/${id}`,
24
+
25
+ // Auth
26
+ root: '/',
27
+ authDesktop: '/auth/desktop',
28
+ authError: '/auth/error',
29
+ logout: '/auth/logout',
30
+ popupCallback: '/auth/popup-callback',
31
+ integrationCallbackSuccess: '/auth/integration/callback/success',
32
+ integrationCallbackError: '/auth/integration/callback/error',
33
+
34
+ // Other
35
+ unauthorized: '/unauthorized',
36
+ freeTrial: '/freetrial',
37
+ } as const
38
+
39
+ export const RBAC_ROUTES = {
40
+ /** Routes accessible only by ADMIN+ */
41
+ admin: [ROUTES.admin],
42
+ /** Routes accessible only by OWNER */
43
+ owner: [ROUTES.billing],
44
+ /** Routes accessible only by MANAGER+ */
45
+ manager: [ROUTES.integrationsNew],
46
+ /** Routes accessible by any authenticated user */
47
+ member: [
48
+ ROUTES.home,
49
+ ROUTES.knowledge,
50
+ ROUTES.chat,
51
+ ROUTES.integrations,
52
+ ROUTES.teams,
53
+ ROUTES.profile,
54
+ ROUTES.ontology,
55
+ ROUTES.transcripts,
56
+ ],
57
+ } as const
58
+
59
+ export const TIMEOUTS = {
60
+ navigation: 30_000,
61
+ action: 10_000,
62
+ toast: 5_000,
63
+ animation: 500,
64
+ } as const
@@ -0,0 +1,115 @@
1
+ # E2E Audit Runbook
2
+
3
+ ## Setup
4
+
5
+ ### 1. Install Playwright
6
+
7
+ ```bash
8
+ bun add -D @playwright/test
9
+ bunx playwright install chromium
10
+ ```
11
+
12
+ ### 2. Start the dev server
13
+
14
+ ```bash
15
+ bun run dev
16
+ ```
17
+
18
+ ### 3. Run tests
19
+
20
+ ```bash
21
+ # All E2E tests
22
+ bun run test:e2e
23
+
24
+ # With UI mode (visual debugger)
25
+ bunx playwright test --ui
26
+
27
+ # Specific test file
28
+ bunx playwright test tests/e2e/specs/dashboard-home.spec.ts
29
+
30
+ # By tag
31
+ bunx playwright test --grep @smoke
32
+ bunx playwright test --grep @security
33
+ bunx playwright test --grep @ux
34
+ ```
35
+
36
+ ## Test Organization
37
+
38
+ ```
39
+ tests/e2e/
40
+ ├── fixtures/ # Auth + base fixtures
41
+ │ ├── auth.ts # Per-role authentication setup
42
+ │ ├── base.ts # Extended test fixture (authenticatedPage)
43
+ │ └── storage/ # Generated auth state files (gitignored)
44
+ ├── pages/ # Page Object Models (1 per page)
45
+ ├── specs/ # Test specs (1 per page + security/)
46
+ │ └── security/ # Cross-cutting security tests
47
+ └── utils/ # Shared helpers
48
+ ├── console-collector.ts # Console message interceptor
49
+ ├── security-helpers.ts # XSS, header, RBAC helpers
50
+ └── test-data.ts # Routes, timeouts, test constants
51
+ ```
52
+
53
+ ## Adding Tests for a New Page
54
+
55
+ 1. **Create Page Object** in `tests/e2e/pages/{page-name}.page.ts`
56
+ 2. **Create Test Spec** in `tests/e2e/specs/{page-name}.spec.ts`
57
+ 3. **Create Page Spec** in `docs/e2e-audit/page-specs/{page-name}.md`
58
+ 4. **Run and fix**: `bunx playwright test tests/e2e/specs/{page-name}.spec.ts`
59
+ 5. **Update master audit** in `docs/e2e-audit/reports/master-audit.md`
60
+
61
+ ## Updating Tests After App Changes
62
+
63
+ 1. Run the e2e-audit skill to re-discover elements on changed pages
64
+ 2. Compare new page spec with existing
65
+ 3. Update POM, test spec, and page spec
66
+ 4. Run tests: `bun run test:e2e`
67
+ 5. Fix failures
68
+
69
+ ## Auth Setup
70
+
71
+ Tests use `storageState` for pre-authenticated sessions. The auth fixture at
72
+ `tests/e2e/fixtures/auth.ts` needs real login flows implemented per role.
73
+
74
+ For local development with OAuth, you may need to:
75
+ 1. Use the Cloudflare tunnel (`https://dev.hakutaku.ai`)
76
+ 2. Set `BASE_URL=https://dev.hakutaku.ai` when running tests
77
+ 3. Or mock auth via direct cookie/session injection
78
+
79
+ ## CI Integration
80
+
81
+ ```yaml
82
+ # Example GitHub Actions step
83
+ - name: E2E Tests
84
+ run: |
85
+ bunx playwright install chromium
86
+ bun run test:e2e
87
+ env:
88
+ BASE_URL: http://localhost:3000
89
+ ```
90
+
91
+ ## Debugging Failed Tests
92
+
93
+ ```bash
94
+ # View trace from failed test
95
+ bunx playwright show-trace test-results/{test-name}/trace.zip
96
+
97
+ # Run with headed browser
98
+ bunx playwright test --headed
99
+
100
+ # Run with slow motion
101
+ bunx playwright test --headed --slow-mo 500
102
+
103
+ # Debug specific test
104
+ bunx playwright test --debug tests/e2e/specs/dashboard-home.spec.ts
105
+ ```
106
+
107
+ ## Tags Reference
108
+
109
+ | Tag | Purpose | When to run |
110
+ |-----|---------|-------------|
111
+ | `@smoke` | Critical paths | Every PR |
112
+ | `@regression` | Full suite | Main merges |
113
+ | `@security` | Security tests | Weekly + before release |
114
+ | `@a11y` | Accessibility | Weekly |
115
+ | `@ux` | UX/UI validation | After UI changes |
@@ -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
+ }'