@kennethsolomon/shipkit 3.2.0 → 3.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/README.md +9 -3
- package/commands/sk/help.md +2 -2
- package/package.json +1 -1
- package/skills/sk:e2e/SKILL.md +161 -10
- package/skills/sk:mvp/SKILL.md +266 -0
- package/skills/sk:mvp/references/design-system.md +136 -0
- package/skills/sk:mvp/references/landing-page.md +236 -0
- package/skills/sk:mvp/references/stacks/laravel.md +321 -0
- package/skills/sk:mvp/references/stacks/nextjs.md +189 -0
- package/skills/sk:mvp/references/stacks/nuxt.md +250 -0
- package/skills/sk:mvp/references/stacks/react-vite.md +287 -0
- package/skills/sk:review/SKILL.md +118 -11
- package/skills/sk:write-tests/SKILL.md +42 -3
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# React + Vite + Tailwind — Stack Reference
|
|
2
|
+
|
|
3
|
+
## Scaffold
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm create vite@latest {project-name} -- --template react-ts
|
|
7
|
+
cd {project-name}
|
|
8
|
+
npm install
|
|
9
|
+
npm install -D tailwindcss @tailwindcss/vite
|
|
10
|
+
npm install react-router-dom
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Directory Structure
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
{project-name}/
|
|
17
|
+
├── src/
|
|
18
|
+
│ ├── main.tsx ← entry point (router setup)
|
|
19
|
+
│ ├── App.tsx ← route definitions
|
|
20
|
+
│ ├── index.css ← Tailwind directives + custom CSS
|
|
21
|
+
│ ├── pages/
|
|
22
|
+
│ │ ├── Landing.tsx ← landing page
|
|
23
|
+
│ │ ├── Dashboard.tsx ← dashboard
|
|
24
|
+
│ │ ├── {Feature1}.tsx
|
|
25
|
+
│ │ ├── {Feature2}.tsx
|
|
26
|
+
│ │ └── Settings.tsx
|
|
27
|
+
│ ├── components/
|
|
28
|
+
│ │ ├── landing/
|
|
29
|
+
│ │ │ ├── Navbar.tsx
|
|
30
|
+
│ │ │ ├── Hero.tsx
|
|
31
|
+
│ │ │ ├── Features.tsx
|
|
32
|
+
│ │ │ ├── HowItWorks.tsx
|
|
33
|
+
│ │ │ ├── Pricing.tsx
|
|
34
|
+
│ │ │ ├── Testimonials.tsx
|
|
35
|
+
│ │ │ ├── WaitlistForm.tsx
|
|
36
|
+
│ │ │ └── Footer.tsx
|
|
37
|
+
│ │ ├── app/
|
|
38
|
+
│ │ │ ├── Sidebar.tsx
|
|
39
|
+
│ │ │ ├── AppLayout.tsx
|
|
40
|
+
│ │ │ ├── DashboardCards.tsx
|
|
41
|
+
│ │ │ └── {feature components}
|
|
42
|
+
│ │ └── ui/
|
|
43
|
+
│ │ ├── Button.tsx
|
|
44
|
+
│ │ ├── Input.tsx
|
|
45
|
+
│ │ ├── Card.tsx
|
|
46
|
+
│ │ ├── Modal.tsx
|
|
47
|
+
│ │ └── Toast.tsx
|
|
48
|
+
│ ├── data/
|
|
49
|
+
│ │ └── mock.ts ← all fake data centralized
|
|
50
|
+
│ └── lib/
|
|
51
|
+
│ └── utils.ts ← shared helpers (cn, etc.)
|
|
52
|
+
├── public/
|
|
53
|
+
│ └── {static assets}
|
|
54
|
+
├── index.html
|
|
55
|
+
├── vite.config.ts
|
|
56
|
+
├── tailwind.config.ts
|
|
57
|
+
└── package.json
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Vite Config
|
|
61
|
+
|
|
62
|
+
`vite.config.ts`:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { defineConfig } from 'vite'
|
|
66
|
+
import react from '@vitejs/plugin-react'
|
|
67
|
+
import tailwindcss from '@tailwindcss/vite'
|
|
68
|
+
|
|
69
|
+
export default defineConfig({
|
|
70
|
+
plugins: [react(), tailwindcss()],
|
|
71
|
+
})
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Tailwind Config
|
|
75
|
+
|
|
76
|
+
`tailwind.config.ts`:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
export default {
|
|
80
|
+
content: ['./index.html', './src/**/*.{ts,tsx}'],
|
|
81
|
+
theme: {
|
|
82
|
+
extend: {
|
|
83
|
+
colors: {
|
|
84
|
+
bg: 'var(--color-bg)',
|
|
85
|
+
fg: 'var(--color-fg)',
|
|
86
|
+
accent: 'var(--color-accent)',
|
|
87
|
+
muted: 'var(--color-muted)',
|
|
88
|
+
},
|
|
89
|
+
fontFamily: {
|
|
90
|
+
display: ['var(--font-display)', 'serif'],
|
|
91
|
+
body: ['var(--font-body)', 'sans-serif'],
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
CSS variables and font imports in `src/index.css`:
|
|
99
|
+
|
|
100
|
+
```css
|
|
101
|
+
@import url('https://fonts.googleapis.com/css2?family={DisplayFont}:wght@400;600;700;800&family={BodyFont}:wght@400;500;600&display=swap');
|
|
102
|
+
|
|
103
|
+
@tailwind base;
|
|
104
|
+
@tailwind components;
|
|
105
|
+
@tailwind utilities;
|
|
106
|
+
|
|
107
|
+
:root {
|
|
108
|
+
--color-bg: #xxxxxx;
|
|
109
|
+
--color-fg: #xxxxxx;
|
|
110
|
+
--color-accent: #xxxxxx;
|
|
111
|
+
--color-muted: #xxxxxx;
|
|
112
|
+
--font-display: '{DisplayFont}', serif;
|
|
113
|
+
--font-body: '{BodyFont}', sans-serif;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
body {
|
|
117
|
+
font-family: var(--font-body);
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Router Setup
|
|
122
|
+
|
|
123
|
+
`src/App.tsx`:
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
|
|
127
|
+
import Landing from './pages/Landing'
|
|
128
|
+
import AppLayout from './components/app/AppLayout'
|
|
129
|
+
import Dashboard from './pages/Dashboard'
|
|
130
|
+
import Feature1 from './pages/{Feature1}'
|
|
131
|
+
import Feature2 from './pages/{Feature2}'
|
|
132
|
+
import Settings from './pages/Settings'
|
|
133
|
+
|
|
134
|
+
const router = createBrowserRouter([
|
|
135
|
+
{ path: '/', element: <Landing /> },
|
|
136
|
+
{
|
|
137
|
+
element: <AppLayout />,
|
|
138
|
+
children: [
|
|
139
|
+
{ path: '/dashboard', element: <Dashboard /> },
|
|
140
|
+
{ path: '/{feature-1}', element: <Feature1 /> },
|
|
141
|
+
{ path: '/{feature-2}', element: <Feature2 /> },
|
|
142
|
+
{ path: '/settings', element: <Settings /> },
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
])
|
|
146
|
+
|
|
147
|
+
export default function App() {
|
|
148
|
+
return <RouterProvider router={router} />
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## App Layout
|
|
153
|
+
|
|
154
|
+
`src/components/app/AppLayout.tsx`:
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
import { Outlet } from 'react-router-dom'
|
|
158
|
+
import Sidebar from './Sidebar'
|
|
159
|
+
|
|
160
|
+
export default function AppLayout() {
|
|
161
|
+
return (
|
|
162
|
+
<div className="flex min-h-screen bg-bg text-fg">
|
|
163
|
+
<Sidebar />
|
|
164
|
+
<main className="flex-1 p-6 lg:p-8">
|
|
165
|
+
<Outlet />
|
|
166
|
+
</main>
|
|
167
|
+
</div>
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Waitlist — Formspree Integration
|
|
173
|
+
|
|
174
|
+
Since React + Vite has no backend, use Formspree for email collection.
|
|
175
|
+
|
|
176
|
+
`src/components/landing/WaitlistForm.tsx`:
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
import { useState, FormEvent } from 'react'
|
|
180
|
+
|
|
181
|
+
// Replace YOUR_FORM_ID with your Formspree form ID
|
|
182
|
+
// Create one free at https://formspree.io
|
|
183
|
+
const FORMSPREE_URL = 'https://formspree.io/f/YOUR_FORM_ID'
|
|
184
|
+
|
|
185
|
+
export default function WaitlistForm() {
|
|
186
|
+
const [email, setEmail] = useState('')
|
|
187
|
+
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle')
|
|
188
|
+
const [message, setMessage] = useState('')
|
|
189
|
+
|
|
190
|
+
async function handleSubmit(e: FormEvent) {
|
|
191
|
+
e.preventDefault()
|
|
192
|
+
setStatus('loading')
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const res = await fetch(FORMSPREE_URL, {
|
|
196
|
+
method: 'POST',
|
|
197
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
198
|
+
body: JSON.stringify({ email }),
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
if (res.ok) {
|
|
202
|
+
setStatus('success')
|
|
203
|
+
setMessage("You're on the list! We'll notify you when we launch.")
|
|
204
|
+
} else {
|
|
205
|
+
throw new Error()
|
|
206
|
+
}
|
|
207
|
+
} catch {
|
|
208
|
+
setStatus('error')
|
|
209
|
+
setMessage('Something went wrong. Please try again.')
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (status === 'success') {
|
|
214
|
+
return <p className="text-green-600 font-medium text-lg">{message}</p>
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<form onSubmit={handleSubmit} className="flex flex-col sm:flex-row gap-3 max-w-md">
|
|
219
|
+
<input
|
|
220
|
+
type="email"
|
|
221
|
+
value={email}
|
|
222
|
+
onChange={e => setEmail(e.target.value)}
|
|
223
|
+
placeholder="you@example.com"
|
|
224
|
+
required
|
|
225
|
+
className="flex-1 px-4 py-3 rounded-lg border border-muted/30 bg-bg focus:ring-2 focus:ring-accent focus:outline-none"
|
|
226
|
+
/>
|
|
227
|
+
<button
|
|
228
|
+
type="submit"
|
|
229
|
+
disabled={status === 'loading'}
|
|
230
|
+
className="px-6 py-3 bg-accent text-white rounded-xl font-medium hover:opacity-90 transition-all disabled:opacity-50"
|
|
231
|
+
>
|
|
232
|
+
{status === 'loading' ? 'Joining...' : 'Join Waitlist'}
|
|
233
|
+
</button>
|
|
234
|
+
{status === 'error' && <p className="text-red-500 text-sm">{message}</p>}
|
|
235
|
+
</form>
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Mock Data
|
|
241
|
+
|
|
242
|
+
Centralize all fake data in `src/data/mock.ts`:
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
export const features = [
|
|
246
|
+
{ icon: '🎯', title: 'Feature One', description: 'Short benefit-driven description.' },
|
|
247
|
+
{ icon: '⚡', title: 'Feature Two', description: 'Short benefit-driven description.' },
|
|
248
|
+
{ icon: '🔒', title: 'Feature Three', description: 'Short benefit-driven description.' },
|
|
249
|
+
]
|
|
250
|
+
|
|
251
|
+
export const testimonials = [
|
|
252
|
+
{ quote: 'Realistic testimonial here.', name: 'Jane Smith', role: 'CTO, TechCo' },
|
|
253
|
+
// ...
|
|
254
|
+
]
|
|
255
|
+
|
|
256
|
+
export const pricingPlans = [
|
|
257
|
+
{ name: 'Free', price: '$0', features: ['Feature A', 'Feature B'], cta: 'Get Started' },
|
|
258
|
+
{ name: 'Pro', price: '$29/mo', features: ['Everything in Free', 'Feature C'], cta: 'Get Pro', popular: true },
|
|
259
|
+
{ name: 'Enterprise', price: 'Custom', features: ['Everything in Pro', 'Priority support'], cta: 'Contact Us' },
|
|
260
|
+
]
|
|
261
|
+
|
|
262
|
+
// Dashboard mock data
|
|
263
|
+
export const dashboardStats = [
|
|
264
|
+
{ label: 'Total Users', value: '2,847', change: '+12%' },
|
|
265
|
+
// ...
|
|
266
|
+
]
|
|
267
|
+
|
|
268
|
+
export const recentActivity = [
|
|
269
|
+
{ user: 'Jane Smith', action: 'created a new project', time: '2 hours ago' },
|
|
270
|
+
// ...
|
|
271
|
+
]
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Component Patterns
|
|
275
|
+
|
|
276
|
+
- Use functional components with TypeScript.
|
|
277
|
+
- Use `useState` for local state, no state management library needed.
|
|
278
|
+
- Navigation: `<Link to="/dashboard">` from react-router-dom.
|
|
279
|
+
- Keep components focused — one file per component.
|
|
280
|
+
- Props typed with interfaces or inline.
|
|
281
|
+
|
|
282
|
+
## Dev Server
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
npm run dev
|
|
286
|
+
# Runs on http://localhost:5173
|
|
287
|
+
```
|
|
@@ -58,16 +58,23 @@ If `tasks/security-findings.md` exists, read the most recent audit. Use any unre
|
|
|
58
58
|
Critical/High findings as additional targeted checks — verify the current diff doesn't
|
|
59
59
|
reintroduce previously flagged vulnerabilities.
|
|
60
60
|
|
|
61
|
-
### 2. Collect
|
|
61
|
+
### 2. Collect Changes + Blast Radius
|
|
62
|
+
|
|
63
|
+
Instead of reading the entire codebase or only the diff, build a **blast radius** — the minimal set of files that could be affected by the changes. This produces focused, high-signal context that leads to better review quality.
|
|
64
|
+
|
|
65
|
+
**2a — Baseline git info:**
|
|
62
66
|
|
|
63
67
|
```bash
|
|
64
68
|
# Determine base branch
|
|
65
|
-
git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@' || echo "main"
|
|
69
|
+
BASE=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@' || echo "main")
|
|
70
|
+
|
|
71
|
+
# Changed files and stats
|
|
72
|
+
CHANGED_FILES=$(git diff $BASE..HEAD --name-only)
|
|
73
|
+
git diff $BASE..HEAD --stat
|
|
74
|
+
git log $BASE..HEAD --oneline
|
|
66
75
|
|
|
67
|
-
#
|
|
68
|
-
git diff
|
|
69
|
-
git diff main..HEAD --stat
|
|
70
|
-
git log main..HEAD --oneline
|
|
76
|
+
# Full diff for reference
|
|
77
|
+
git diff $BASE..HEAD
|
|
71
78
|
|
|
72
79
|
# Check for uncommitted changes
|
|
73
80
|
git status --short
|
|
@@ -76,12 +83,103 @@ git status --short
|
|
|
76
83
|
If there are uncommitted changes, warn:
|
|
77
84
|
> **Warning:** You have uncommitted changes. These will NOT be included in the review. Commit or stash them first.
|
|
78
85
|
|
|
79
|
-
|
|
86
|
+
**2b — Extract changed symbols:**
|
|
87
|
+
|
|
88
|
+
Use **git hunk headers** as the primary extraction method. Git already parses the enclosing function/class name into every `@@` header — this is more reliable than regex or AST tools:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# Phase 1: Enclosing scope names from hunk headers (free from git, no parsing needed)
|
|
92
|
+
git diff $BASE..HEAD -U0 | grep '^@@' | sed 's/.*@@\s*//' | \
|
|
93
|
+
grep -oE '[A-Za-z_][A-Za-z0-9_]*\s*\(' | sed 's/\s*(//' | sort -u
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Then supplement with **new/modified definitions** from added lines using language-specific patterns. Only match definition keywords — not `const`, `export`, `type`, or other high-noise terms:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# Phase 2: Definitions from added lines (supplement, not replace)
|
|
100
|
+
# JS/TS: function foo(, class Foo, interface Foo
|
|
101
|
+
# Python: def foo(, class Foo
|
|
102
|
+
# Go: func foo(, func (r *T) foo(
|
|
103
|
+
# PHP: function foo(, class Foo
|
|
104
|
+
# Rust: fn foo(, struct Foo, impl Foo, trait Foo
|
|
105
|
+
git diff $BASE..HEAD | grep '^+' | grep -v '^+++' | \
|
|
106
|
+
grep -oE '(function|class|interface|def|fn|func|struct|trait|impl)\s+[A-Za-z_][A-Za-z0-9_]+' | \
|
|
107
|
+
awk '{print $2}' | sort -u
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Combine both phases. Filter out symbols shorter than 3 characters (too generic for blast-radius search).
|
|
111
|
+
|
|
112
|
+
Classify each symbol:
|
|
113
|
+
- **Modified/removed** — existed before the branch, changed or deleted now. These can break callers. **Run blast radius on these.**
|
|
114
|
+
- **New** — added in this branch, no prior callers exist. **Skip blast radius** (nothing to break).
|
|
115
|
+
|
|
116
|
+
To classify, check if the symbol appears in the base branch:
|
|
117
|
+
```bash
|
|
118
|
+
# If symbol exists in base branch files, it's modified/removed → needs blast radius
|
|
119
|
+
git show $BASE:$FILE 2>/dev/null | grep -q "\b$SYMBOL\b"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**2c — Find blast radius (modified/removed symbols only):**
|
|
123
|
+
|
|
124
|
+
For each modified/removed symbol, use **import-chain narrowing** to find dependents with minimal false positives:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
# Step 1: Find files that import the module containing the changed symbol
|
|
128
|
+
CHANGED_MODULE_PATHS=$(echo "$CHANGED_FILES" | sed 's/\.[^.]*$//' | sed 's/\/index$//')
|
|
129
|
+
for module_path in $CHANGED_MODULE_PATHS; do
|
|
130
|
+
rg -l "(import|require|from|use)\s.*$(basename $module_path)" \
|
|
131
|
+
--glob '!node_modules/**' --glob '!vendor/**' --glob '!dist/**' \
|
|
132
|
+
--glob '!build/**' --glob '!*.lock' --glob '!*.md' \
|
|
133
|
+
2>/dev/null
|
|
134
|
+
done | sort -u > /tmp/importers.txt
|
|
135
|
+
|
|
136
|
+
# Step 2: Within importers, find which ones reference the specific changed symbols
|
|
137
|
+
for symbol in $MODIFIED_SYMBOLS; do
|
|
138
|
+
rg -wl "$symbol" $(cat /tmp/importers.txt) 2>/dev/null
|
|
139
|
+
done | sort -u > /tmp/dependents.txt
|
|
140
|
+
|
|
141
|
+
# Remove files already in the changed set
|
|
142
|
+
comm -23 /tmp/dependents.txt <(echo "$CHANGED_FILES" | sort) > /tmp/blast_radius.txt
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Noise guard:** If a symbol produces >100 matches, it's too generic for grep-based analysis. Note it in the review as "unable to determine blast radius for `symbol` — manual verification recommended."
|
|
146
|
+
|
|
147
|
+
Log the blast radius before reading:
|
|
148
|
+
```
|
|
149
|
+
Blast Radius Summary
|
|
150
|
+
──────────────────────────────────
|
|
151
|
+
Changed files: X
|
|
152
|
+
Blast-radius dependents: Y (files importing changed symbols)
|
|
153
|
+
Total review scope: X+Y files
|
|
154
|
+
Symbols analyzed: N modified, M new (skipped)
|
|
155
|
+
|
|
156
|
+
Symbol → Dependents:
|
|
157
|
+
processOrder → src/checkout/cart.ts, src/api/orders.ts
|
|
158
|
+
validateInput → src/middleware/auth.ts
|
|
159
|
+
──────────────────────────────────
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**2d — Read context (focused, not exhaustive):**
|
|
163
|
+
|
|
164
|
+
Read in this priority order:
|
|
165
|
+
1. **Changed files in full** — not just the diff. The full file provides surrounding context (imports, related functions, class-level state) needed to judge whether the change is correct. For files >500 lines, read the changed function + 30 lines of surrounding context instead.
|
|
166
|
+
2. **The diff** — for precise change tracking (already collected above).
|
|
167
|
+
3. **Blast-radius dependent files** — read only the call sites that reference changed symbols. Use `rg -B5 -A10 "\bsymbol\b" dependent_file` to get the call site with surrounding context, not the entire file.
|
|
168
|
+
4. **Test files** for changed symbols — verify existing tests still cover the changed behavior.
|
|
169
|
+
|
|
170
|
+
Do **not** read unchanged files outside the blast radius.
|
|
171
|
+
|
|
172
|
+
Carry the blast-radius mapping (symbol → dependents) forward into Steps 3-9. When analyzing a changed function, always cross-reference its dependents.
|
|
80
173
|
|
|
81
174
|
### 3. Analyze — Correctness & Bugs
|
|
82
175
|
|
|
83
176
|
The most important dimension. A bug that ships is worse than ugly code that works.
|
|
84
177
|
|
|
178
|
+
**Blast-radius check (mandatory):** For every modified/removed symbol, verify its dependents (from Step 2c) are still compatible:
|
|
179
|
+
- Do callers pass arguments the changed function still accepts?
|
|
180
|
+
- Do callers depend on return values whose shape/type changed?
|
|
181
|
+
- Do callers rely on side effects the changed code no longer produces?
|
|
182
|
+
|
|
85
183
|
**Logic errors:**
|
|
86
184
|
- Wrong operator (`&&` vs `||`, `==` vs `===`, `<` vs `<=`)
|
|
87
185
|
- Inverted conditions, missing negation
|
|
@@ -114,7 +212,11 @@ The most important dimension. A bug that ships is worse than ugly code that work
|
|
|
114
212
|
|
|
115
213
|
### 4. Analyze — Security
|
|
116
214
|
|
|
117
|
-
Load `references/security-checklist.md` and apply its grep patterns
|
|
215
|
+
Load `references/security-checklist.md` and apply its grep patterns against the **diff and blast-radius files** (not the entire codebase). Only flag patterns **newly introduced** in the diff — pre-existing issues are out of scope unless they interact with the changed code.
|
|
216
|
+
|
|
217
|
+
**Blast-radius check:** If a validation or auth function was modified, check all its callers (from Step 2c) — a weakened check affects every endpoint that depends on it.
|
|
218
|
+
|
|
219
|
+
Check for:
|
|
118
220
|
|
|
119
221
|
**Injection (OWASP A03):**
|
|
120
222
|
- SQL, NoSQL, OS command, LDAP, template injection
|
|
@@ -184,6 +286,8 @@ Think about what happens at 10x, 100x current scale. Performance bugs are often
|
|
|
184
286
|
|
|
185
287
|
Production code must handle failure gracefully. The question isn't "does it work?" but "what happens when things go wrong?"
|
|
186
288
|
|
|
289
|
+
**Blast-radius check:** If error handling changed (e.g., function now throws instead of returning null, or error type changed), check all callers from Step 2c — they may not have matching try/catch or null checks.
|
|
290
|
+
|
|
187
291
|
**Error handling quality:**
|
|
188
292
|
- Swallowed errors (empty catch blocks, `.catch(() => {})`)
|
|
189
293
|
- Generic catch blocks that hide the actual error type
|
|
@@ -217,8 +321,9 @@ Think about the next engineer who reads this code. Is the intent clear? Does the
|
|
|
217
321
|
- Components doing too many things (should be split)
|
|
218
322
|
- Side effects in pure functions or constructors
|
|
219
323
|
|
|
220
|
-
**API design (if endpoints changed):**
|
|
324
|
+
**API design (if endpoints or function signatures changed):**
|
|
221
325
|
- Breaking changes to existing API contracts without versioning
|
|
326
|
+
- **Blast-radius check:** If a function signature changed, the blast radius from Step 2c is the definitive answer to whether it's a breaking change — every dependent file that calls the old signature will break
|
|
222
327
|
- Inconsistent response format across endpoints
|
|
223
328
|
- Missing or inconsistent HTTP status codes
|
|
224
329
|
- Unclear or missing error response schema
|
|
@@ -294,7 +399,8 @@ Format findings with severity levels and review dimensions:
|
|
|
294
399
|
|
|
295
400
|
**Changes:** X files changed, +Y/-Z lines
|
|
296
401
|
**Commits:** N commits
|
|
297
|
-
**
|
|
402
|
+
**Blast radius:** X changed files + Y dependents = Z total review scope
|
|
403
|
+
**Review dimensions:** Correctness, Security, Performance, Reliability, Design, Best Practices, Testing, Blast Radius
|
|
298
404
|
|
|
299
405
|
### Critical (must fix before merge)
|
|
300
406
|
- **[Correctness]** [FILE:LINE] Description of critical issue
|
|
@@ -323,7 +429,8 @@ Format findings with severity levels and review dimensions:
|
|
|
323
429
|
|
|
324
430
|
**Rules:**
|
|
325
431
|
- Maximum 20 items total (prioritize by severity, then by category)
|
|
326
|
-
- Every item must tag its review dimension: `[Correctness]`, `[Security]`, `[Performance]`, `[Reliability]`, `[Design]`, `[Best Practices]`, `[Testing]`
|
|
432
|
+
- Every item must tag its review dimension: `[Correctness]`, `[Security]`, `[Performance]`, `[Reliability]`, `[Design]`, `[Best Practices]`, `[Testing]`, `[Blast Radius]`
|
|
433
|
+
- Use `[Blast Radius]` for issues found in dependent files — callers broken by changed signatures, importers affected by removed exports, tests that no longer cover the changed behavior
|
|
327
434
|
- Every item must reference a specific file and line
|
|
328
435
|
- Every item must explain **why** it matters — the impact, not just the symptom
|
|
329
436
|
- Include a brief "What Looks Good" section (2-3 items) — acknowledge strong patterns so they're reinforced. This isn't cheerleading — it's calibrating signal.
|
|
@@ -145,16 +145,54 @@ If a frontend stack was detected, generate FE test files:
|
|
|
145
145
|
|
|
146
146
|
Skip this step if no FE stack was detected.
|
|
147
147
|
|
|
148
|
-
### 8b.
|
|
148
|
+
### 8b. Write E2E Spec Files (conditional)
|
|
149
|
+
|
|
150
|
+
**Only if `playwright.config.ts` or `playwright.config.js` is detected in the project root:**
|
|
151
|
+
|
|
152
|
+
Write `e2e/<feature>.spec.ts` files covering the acceptance criteria from `tasks/todo.md`. Follow these rules:
|
|
153
|
+
|
|
154
|
+
- Use `test.describe` / `test` blocks — not `describe`/`it`
|
|
155
|
+
- Use role-based locators: `getByRole`, `getByLabel`, `getByText`, `getByPlaceholder` — never CSS selectors
|
|
156
|
+
- Use `test.beforeEach` for shared setup (auth, navigation)
|
|
157
|
+
- Use `test.skip(!email, 'ENV_VAR not set — skipping')` guards for credential-dependent tests
|
|
158
|
+
- Auth credentials from env vars via `e2e/helpers/auth.ts` — never hardcode credentials
|
|
159
|
+
- Soft assertions (`expect.soft`) for non-critical checks; hard `expect` for gate conditions
|
|
160
|
+
|
|
161
|
+
E2E spec example structure:
|
|
162
|
+
```ts
|
|
163
|
+
import { test, expect } from '@playwright/test'
|
|
164
|
+
import { signIn, TEST_USERS } from './helpers/auth'
|
|
165
|
+
|
|
166
|
+
test.describe('[Feature] — [scenario]', () => {
|
|
167
|
+
test.beforeEach(async ({ page }) => {
|
|
168
|
+
const { email, password } = TEST_USERS.regular
|
|
169
|
+
test.skip(!email, 'E2E_USER_EMAIL not set — skipping')
|
|
170
|
+
await signIn(page, email, password)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
test('[behavior description]', async ({ page }) => {
|
|
174
|
+
await page.goto('/dashboard/feature')
|
|
175
|
+
await expect(page.getByRole('heading', { name: /title/i })).toBeVisible()
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Create `e2e/helpers/auth.ts` if it doesn't exist (see `/sk:e2e` Playwright Setup Reference).
|
|
181
|
+
|
|
182
|
+
**Run the E2E spec to confirm tests fail or skip** (they should fail until implementation, or skip if env vars aren't set — both are acceptable for the RED phase):
|
|
183
|
+
```bash
|
|
184
|
+
npx playwright test e2e/<feature>.spec.ts --reporter=list
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### 8c. Playwright MCP Inspection (optional)
|
|
149
188
|
|
|
150
|
-
**Only if
|
|
189
|
+
**Only if the Playwright MCP plugin is active in the session AND live selectors are needed:**
|
|
151
190
|
|
|
152
191
|
Use the Playwright MCP plugin to inspect live page state for more accurate selectors:
|
|
153
192
|
|
|
154
193
|
1. Navigate to target URL
|
|
155
194
|
2. Capture accessibility snapshot for role-based selectors
|
|
156
195
|
3. Screenshot for visual reference
|
|
157
|
-
4. Optionally run inline assertions for complex interactions
|
|
158
196
|
|
|
159
197
|
### 9. Verify Tests Fail (Red Phase)
|
|
160
198
|
|
|
@@ -170,6 +208,7 @@ Output:
|
|
|
170
208
|
```
|
|
171
209
|
BE tests written: X tests in Y files ([framework])
|
|
172
210
|
FE tests written: X tests in Y files ([framework]) ← omit if no FE stack
|
|
211
|
+
E2E specs written: X tests in Y files (Playwright) ← omit if no playwright.config.ts
|
|
173
212
|
Existing tests updated: X files
|
|
174
213
|
Status: RED (tests fail as expected — ready for implementation)
|
|
175
214
|
```
|