start-vibing 4.3.3 → 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/references/audit-methodology.md +63 -7
- 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
|
|
@@ -311,13 +311,69 @@ source section cited.
|
|
|
311
311
|
|
|
312
312
|
> Source: §3.4.
|
|
313
313
|
|
|
314
|
-
**`baymard-search-*` — Search (12 rules, §3.6):**
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
314
|
+
**`baymard-search-*` — Search (12 rules, §3.6):**
|
|
315
|
+
1. `baymard-search-01` persist search query in the field after submission
|
|
316
|
+
2. `baymard-search-02` autocomplete list capped at 10 items desktop, 4–8 mobile
|
|
317
|
+
3. `baymard-search-03` style category-scope suggestions distinctly within autocomplete
|
|
318
|
+
4. `baymard-search-04` highlight the predictive (un-typed) portion of autocomplete suggestions
|
|
319
|
+
5. `baymard-search-05` copy active autocomplete suggestion into the field on keyboard navigation
|
|
320
|
+
6. `baymard-search-06` support misspellings in autocomplete (spell-tolerant suggestions)
|
|
321
|
+
7. `baymard-search-07` autodirect exact category-match queries to the category page
|
|
322
|
+
8. `baymard-search-08` support synonym and alternate-term searches
|
|
323
|
+
9. `baymard-search-09` support abbreviation and symbol searches
|
|
324
|
+
10. `baymard-search-10` allow "search within" current category on mobile
|
|
325
|
+
11. `baymard-search-11` support non-product queries (return policy, order status, FAQ)
|
|
326
|
+
12. `baymard-search-12` provide 5 proven no-results recovery paths (spelling fix, broaden, contact, etc.)
|
|
327
|
+
|
|
328
|
+
> Source: §3.6 bullets + https://baymard.com/ecommerce-design-examples/34-autocomplete-suggestions + per-rule URLs in `/docs/research/baymard-public-rules.md` (B-S-01…B-S-12).
|
|
329
|
+
|
|
330
|
+
**`baymard-filter-*` — Filter (10 rules, §3.6):**
|
|
331
|
+
1. `baymard-filter-01` provide the 5 essential filter types (category, price, rating, brand, user-relevant attribute)
|
|
332
|
+
2. `baymard-filter-02` have filters for every attribute displayed in list items
|
|
333
|
+
3. `baymard-filter-03` display applied filters in a visible overview area, individually removable
|
|
334
|
+
4. `baymard-filter-04` use OR logic within a facet, AND logic across facets
|
|
335
|
+
5. `baymard-filter-05` show product counts per filter value and update them dynamically
|
|
336
|
+
6. `baymard-filter-06` collapse long facet lists to top 4–6 values with a "Show more" link
|
|
337
|
+
7. `baymard-filter-07` explain industry-specific or jargon filter labels
|
|
338
|
+
8. `baymard-filter-08` promote important filters above the product grid / above the fold
|
|
339
|
+
9. `baymard-filter-09` provide 4 essential sort options (featured, price asc/desc, rating, newest)
|
|
340
|
+
10. `baymard-filter-10` persist filter state when user returns from a product detail page
|
|
341
|
+
|
|
342
|
+
> Source: §3.6 bullets + https://baymard.com/blog/promoting-product-filters + https://baymard.com/blog/have-filters-for-list-item-info + per-rule URLs in `/docs/research/baymard-public-rules.md` (B-F-01…B-F-10).
|
|
343
|
+
|
|
344
|
+
**`baymard-bread-*` — Breadcrumbs (6 rules, §3.7):**
|
|
345
|
+
1. `baymard-bread-01` provide both hierarchy-based AND history-based breadcrumbs
|
|
346
|
+
2. `baymard-bread-02` include the full category hierarchy in the breadcrumb trail
|
|
347
|
+
3. `baymard-bread-03` suppress homepage and current product page layers from the trail
|
|
348
|
+
4. `baymard-bread-04` make breadcrumbs swipeable on mobile for long paths
|
|
349
|
+
5. `baymard-bread-05` use clear tappability cues on mobile breadcrumbs (size, underline, chevron)
|
|
350
|
+
6. `baymard-bread-06` expose `BreadcrumbList` structured-data markup for SEO and assistive tech
|
|
351
|
+
|
|
352
|
+
> Rules 01–05 from verified public research (B-B-01…B-B-05). Rule 06 enumerated from §3.7 narrative; supersede when Baymard Premium IDs become available.
|
|
353
|
+
> Source: §3.7 + https://baymard.com/blog/ecommerce-breadcrumbs + per-rule URLs in `/docs/research/baymard-public-rules.md`.
|
|
354
|
+
|
|
355
|
+
**`baymard-pdp-*` — PDP / Product Detail (18 rules, §3.8):**
|
|
356
|
+
1. `baymard-pdp-01` use button-style size selectors (swatches), not dropdowns
|
|
357
|
+
2. `baymard-pdp-02` provide at least one in-scale product image (size/context reference)
|
|
358
|
+
3. `baymard-pdp-03` show product on a human model for worn or carried items
|
|
359
|
+
4. `baymard-pdp-04` provide sufficient image resolution and zoom on desktop and mobile
|
|
360
|
+
5. `baymard-pdp-05` support both pinch and double-tap zoom gestures on mobile
|
|
361
|
+
6. `baymard-pdp-06` allow guest users to save / wishlist items without account creation
|
|
362
|
+
7. `baymard-pdp-07` display a return-policy link or summary directly on the product page
|
|
363
|
+
8. `baymard-pdp-08` show the lowest shipping-cost estimate (and ETA) on the product page
|
|
364
|
+
9. `baymard-pdp-09` allow carousel navigation across reviewer-submitted images (UGC)
|
|
365
|
+
10. `baymard-pdp-10` respond publicly to negative reviews to demonstrate accountability
|
|
366
|
+
11. `baymard-pdp-11` display price-per-unit for multi-quantity or weight-priced products
|
|
367
|
+
12. `baymard-pdp-12` synchronize product data (price, stock, images) across variation selections
|
|
368
|
+
13. `baymard-pdp-13` include descriptive text or graphics on key product images (annotations)
|
|
369
|
+
14. `baymard-pdp-14` single dominant Add-to-Cart CTA — not 3–6 competing colorful buttons
|
|
370
|
+
15. `baymard-pdp-15` prefer accordion over horizontal tabs for PDP detail sections
|
|
371
|
+
16. `baymard-pdp-16` show out-of-stock variants as visible-but-disabled (never remove from UI)
|
|
372
|
+
17. `baymard-pdp-17` offer a "notify me when back in stock" flow for OOS variants
|
|
373
|
+
18. `baymard-pdp-18` surface size-guide inline on the PDP — not hidden behind a separate page
|
|
374
|
+
|
|
375
|
+
> Rules 01–13 from verified public research (B-P-01…B-P-13). Rules 14–18 enumerated from §3.8 narrative; supersede when Baymard Premium IDs become available.
|
|
376
|
+
> Source: §3.8 + https://baymard.com/research/product-page + per-rule URLs in `/docs/research/baymard-public-rules.md`.
|
|
321
377
|
|
|
322
378
|
### 3.1 Cart abandonment
|
|
323
379
|
**70.19% average abandonment** across 49 studies 2006–2023, range 55–84.27% (https://baymard.com/lists/cart-abandonment-rate). By device: mobile 77.06%, tablet 66.39%, desktop 70.01%. **Reasons** (excluding 43% "just browsing"): extra costs 48%; forced account creation 24%; slow delivery 19%; distrust with CC 18–19%; too long/complicated 17–18%; couldn't see total up front 16%; errors/crashes 13%; returns policy 12%; declined CC 9%; limited payment methods 7%.
|
|
@@ -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
|
+
}'
|