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.
- package/package.json +2 -2
- package/template/.claude/agents/sd-audit.md +32 -0
- package/template/.claude/skills/e2e-audit/DESIGN.md +294 -0
- package/template/.claude/skills/e2e-audit/SKILL.md +660 -0
- package/template/.claude/skills/e2e-audit/e2e/fixtures/auth.setup.ts +70 -0
- package/template/.claude/skills/e2e-audit/e2e/fixtures/auth.ts +21 -0
- package/template/.claude/skills/e2e-audit/e2e/fixtures/base.ts +90 -0
- package/template/.claude/skills/e2e-audit/e2e/fixtures/storage/.gitkeep +0 -0
- package/template/.claude/skills/e2e-audit/e2e/fixtures/storage/admin.json +50 -0
- package/template/.claude/skills/e2e-audit/e2e/fixtures/storage/manager.json +50 -0
- package/template/.claude/skills/e2e-audit/e2e/fixtures/storage/member.json +50 -0
- package/template/.claude/skills/e2e-audit/e2e/fixtures/storage/owner.json +50 -0
- package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-admin.page.ts +141 -0
- package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-billing.page.ts +47 -0
- package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-chat.page.ts +35 -0
- package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-home.page.ts +134 -0
- package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-integrations.page.ts +334 -0
- package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-knowledge.page.ts +30 -0
- package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-ontology.page.ts +71 -0
- package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-profile.page.ts +38 -0
- package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-teams.page.ts +123 -0
- package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-transcripts.page.ts +109 -0
- package/template/.claude/skills/e2e-audit/e2e/specs/auth/login.spec.ts +59 -0
- package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-admin.spec.ts +233 -0
- package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-billing.spec.ts +44 -0
- package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-chat.spec.ts +50 -0
- package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-home.spec.ts +243 -0
- package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-integrations.spec.ts +472 -0
- package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-knowledge.spec.ts +57 -0
- package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-ontology.spec.ts +72 -0
- package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-profile.spec.ts +48 -0
- package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-teams.spec.ts +247 -0
- package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-transcripts.spec.ts +122 -0
- package/template/.claude/skills/e2e-audit/e2e/specs/security/headers.spec.ts +39 -0
- package/template/.claude/skills/e2e-audit/e2e/specs/security/rbac.spec.ts +92 -0
- package/template/.claude/skills/e2e-audit/e2e/specs/security/xss.spec.ts +74 -0
- package/template/.claude/skills/e2e-audit/e2e/utils/console-collector.ts +89 -0
- package/template/.claude/skills/e2e-audit/e2e/utils/security-helpers.ts +114 -0
- package/template/.claude/skills/e2e-audit/e2e/utils/test-data.ts +64 -0
- package/template/.claude/skills/e2e-audit/runbook.md +115 -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
|
@@ -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.
|
|
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
|
+
}'
|