@polymorphism-tech/morph-spec 4.8.1 → 4.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +2 -2
  2. package/claude-plugin.json +1 -1
  3. package/docs/CHEATSHEET.md +1 -1
  4. package/docs/QUICKSTART.md +1 -1
  5. package/framework/hooks/dev/guard-version-numbers.js +1 -1
  6. package/framework/skills/level-1-workflows/phase-clarify/SKILL.md +1 -1
  7. package/framework/skills/level-1-workflows/phase-codebase-analysis/SKILL.md +1 -1
  8. package/framework/skills/level-1-workflows/phase-design/SKILL.md +1 -1
  9. package/framework/skills/level-1-workflows/phase-implement/SKILL.md +1 -1
  10. package/framework/skills/level-1-workflows/phase-setup/SKILL.md +1 -1
  11. package/framework/skills/level-1-workflows/phase-tasks/SKILL.md +1 -1
  12. package/framework/skills/level-1-workflows/phase-uiux/SKILL.md +1 -1
  13. package/package.json +4 -4
  14. package/.morph/analytics/threads-log.jsonl +0 -54
  15. package/.morph/state.json +0 -198
  16. package/docs/ARCHITECTURE.md +0 -328
  17. package/docs/COMMAND-FLOWS.md +0 -398
  18. package/docs/plans/2026-02-22-claude-docs-morph-alignment-analysis.md +0 -514
  19. package/docs/plans/2026-02-22-claude-settings.md +0 -517
  20. package/docs/plans/2026-02-22-morph-cc-alignment-impl.md +0 -730
  21. package/docs/plans/2026-02-22-morph-spec-next.md +0 -480
  22. package/docs/plans/2026-02-22-native-alignment-design.md +0 -201
  23. package/docs/plans/2026-02-22-native-alignment-impl.md +0 -927
  24. package/docs/plans/2026-02-22-native-enrichment-design.md +0 -246
  25. package/docs/plans/2026-02-22-native-enrichment.md +0 -737
  26. package/docs/plans/2026-02-23-ddd-architecture-refactor.md +0 -1155
  27. package/docs/plans/2026-02-23-ddd-nextsteps.md +0 -684
  28. package/docs/plans/2026-02-23-infra-architect-refactor.md +0 -439
  29. package/docs/plans/2026-02-23-nextjs-code-review-design.md +0 -157
  30. package/docs/plans/2026-02-23-nextjs-code-review-impl.md +0 -1256
  31. package/docs/plans/2026-02-23-nextjs-standards-design.md +0 -150
  32. package/docs/plans/2026-02-23-nextjs-standards-impl.md +0 -1848
  33. package/docs/plans/2026-02-24-cli-radical-simplification.md +0 -592
  34. package/docs/plans/2026-02-24-framework-failure-points.md +0 -125
  35. package/docs/plans/2026-02-24-morph-init-design.md +0 -337
  36. package/docs/plans/2026-02-24-morph-init-impl.md +0 -1269
  37. package/docs/plans/2026-02-24-tutorial-command-design.md +0 -71
  38. package/docs/plans/2026-02-24-tutorial-command.md +0 -298
  39. package/scripts/bump-version.js +0 -248
  40. package/scripts/generate-refs.js +0 -336
  41. package/scripts/generate-standards-registry.js +0 -44
  42. package/scripts/install-dev-hooks.js +0 -138
  43. package/scripts/scan-nextjs.mjs +0 -169
  44. package/scripts/validate-real.mjs +0 -255
@@ -1,1256 +0,0 @@
1
- # Next.js Code Review & Scan Implementation Plan
2
-
3
- **Status:** COMPLETE
4
-
5
- > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
6
-
7
- **Goal:** Add full Next.js code review coverage to morph-spec — wire the existing validator into the auto-validation pipeline, build a CLI scan script, and create the `code-review-nextjs` skill with a reference example.
8
-
9
- **Architecture:** Three-layer system. Layer 1 (auto): `nextjs-component` validator wired into `validation-runner.js` via `agents.json`, runs automatically on `morph-spec validate`. Layer 2 (CLI): `scripts/scan-nextjs.mjs` wraps `validateNextComponent` + adds extra checks, usable in CI. Layer 3 (LLM): `code-review-nextjs` skill references the scan script first, then manual checklist covering all 8 Next.js standards.
10
-
11
- **Tech Stack:** Node.js/ESM, `glob` package, `node:test` + `node:assert/strict`, `node:fs/promises`, `node:os`, chalk
12
-
13
- ---
14
-
15
- ## Key Context
16
-
17
- - `validateNextComponent(content, filePath)` is in `src/lib/validators/nextjs/next-component-validator.js`
18
- - Returns `{ type: 'error'|'warning', message, suggestion, file, line? }[]`
19
- - Currently exports only `validateNextComponent` — no project-level scanner
20
- - `validation-runner.js:runSingleValidator()` uses a `switch` on validatorId and expects `{ errors, warnings, issues: [{level, message, file, line?, solution?}] }`
21
- - `agents.json` `nextjs-expert.validators` currently = `["packages"]` — bug, missing `nextjs-component`
22
- - `glob` package is available in package.json
23
- - Skills installer copies entire directory trees from `framework/skills/level-0-meta/{name}/` → `.claude/skills/{name}/`
24
-
25
- ---
26
-
27
- ### Task 1: Wire nextjs-component validator (bug fix + TDD)
28
-
29
- **Files:**
30
- - Modify: `src/lib/validators/nextjs/next-component-validator.js` (add project-level scanner)
31
- - Modify: `src/lib/validators/nextjs/index.js` (export new function)
32
- - Modify: `src/lib/validators/validation-runner.js` (add case)
33
- - Modify: `framework/agents.json` (fix validators array)
34
- - Create: `test/validators/nextjs/next-component-files-validator.test.js`
35
-
36
- **Step 1: Write failing tests first**
37
-
38
- Create `test/validators/nextjs/next-component-files-validator.test.js`:
39
-
40
- ```js
41
- import { test, describe, beforeEach, afterEach } from 'node:test';
42
- import assert from 'node:assert/strict';
43
- import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
44
- import { join } from 'node:path';
45
- import { tmpdir } from 'node:os';
46
- import { validateNextComponentFiles } from '../../../src/lib/validators/nextjs/next-component-validator.js';
47
-
48
- describe('validateNextComponentFiles', () => {
49
- let tempDir;
50
-
51
- beforeEach(() => {
52
- tempDir = join(tmpdir(), `morph-nextjs-files-test-${Date.now()}`);
53
- mkdirSync(join(tempDir, 'src', 'components'), { recursive: true });
54
- });
55
-
56
- afterEach(() => {
57
- rmSync(tempDir, { recursive: true, force: true });
58
- });
59
-
60
- test('returns { errors: 0, warnings: 0, issues: [] } for a clean project', async () => {
61
- writeFileSync(
62
- join(tempDir, 'src', 'components', 'user-card.tsx'),
63
- `'use client';\nimport { useState } from 'react';\nexport function UserCard() { const [x] = useState(0); return <div>{x}</div>; }`
64
- );
65
- const result = await validateNextComponentFiles(tempDir, {});
66
- assert.equal(result.errors, 0);
67
- assert.equal(result.warnings, 0);
68
- assert.equal(result.issues.length, 0);
69
- });
70
-
71
- test('returns error when useState used without use client', async () => {
72
- writeFileSync(
73
- join(tempDir, 'src', 'components', 'broken.tsx'),
74
- `import { useState } from 'react';\nexport function Broken() { const [x] = useState(0); return <div>{x}</div>; }`
75
- );
76
- const result = await validateNextComponentFiles(tempDir, {});
77
- assert.ok(result.errors > 0, 'should have errors');
78
- assert.ok(result.issues.length > 0, 'should have issues');
79
- assert.equal(result.issues[0].level, 'error');
80
- });
81
-
82
- test('returns warning when use client has no interactivity', async () => {
83
- writeFileSync(
84
- join(tempDir, 'src', 'components', 'static.tsx'),
85
- `'use client';\nexport function Static() { return <div>Hello</div>; }`
86
- );
87
- const result = await validateNextComponentFiles(tempDir, {});
88
- assert.ok(result.warnings > 0, 'should have warnings');
89
- assert.equal(result.issues[0].level, 'warning');
90
- });
91
-
92
- test('maps ValidationIssue.suggestion to issue.solution', async () => {
93
- writeFileSync(
94
- join(tempDir, 'src', 'components', 'static.tsx'),
95
- `'use client';\nexport function Static() { return <div>Hello</div>; }`
96
- );
97
- const result = await validateNextComponentFiles(tempDir, {});
98
- assert.ok(result.issues[0].solution, 'solution should be populated from suggestion');
99
- });
100
-
101
- test('returns { errors: 0, warnings: 0 } for empty src/ directory', async () => {
102
- const result = await validateNextComponentFiles(tempDir, {});
103
- assert.equal(result.errors, 0);
104
- assert.equal(result.warnings, 0);
105
- });
106
-
107
- test('scans multiple files and accumulates issues', async () => {
108
- // file 1: useState without use client (error)
109
- writeFileSync(
110
- join(tempDir, 'src', 'components', 'bad-a.tsx'),
111
- `import { useState } from 'react';\nexport function BadA() { const [x] = useState(0); return <div>{x}</div>; }`
112
- );
113
- // file 2: unnecessary use client (warning)
114
- writeFileSync(
115
- join(tempDir, 'src', 'components', 'bad-b.tsx'),
116
- `'use client';\nexport function BadB() { return <div>Static</div>; }`
117
- );
118
- const result = await validateNextComponentFiles(tempDir, {});
119
- assert.ok(result.errors >= 1, 'should have at least 1 error from bad-a');
120
- assert.ok(result.warnings >= 1, 'should have at least 1 warning from bad-b');
121
- assert.ok(result.issues.length >= 2, 'should have issues from both files');
122
- });
123
- });
124
- ```
125
-
126
- **Step 2: Run tests to confirm they FAIL (module not found)**
127
-
128
- ```bash
129
- cd "R:/Polymorphism Tech/repos/morph-spec-framework" && node --test test/validators/nextjs/next-component-files-validator.test.js 2>&1 | head -10
130
- ```
131
-
132
- Expected: Error — `validateNextComponentFiles is not exported`
133
-
134
- **Step 3: Add `validateNextComponentFiles` to `next-component-validator.js`**
135
-
136
- Add after the existing `validateNextComponent` function (after the `toKebabCase` helper):
137
-
138
- ```js
139
- // ============================================
140
- // PROJECT-LEVEL SCANNER
141
- // ============================================
142
-
143
- /**
144
- * Validates all Next.js component files in a project directory.
145
- * Wraps validateNextComponent for use in validation-runner.js.
146
- *
147
- * @param {string} projectPath - Root path of the project
148
- * @param {Object} options - Validator options
149
- * @returns {{ errors: number, warnings: number, issues: Array }}
150
- */
151
- export async function validateNextComponentFiles(projectPath, options = {}) {
152
- const { glob } = await import('glob');
153
- const { readFileSync } = await import('node:fs');
154
- const { join } = await import('node:path');
155
-
156
- const result = { errors: 0, warnings: 0, issues: [] };
157
-
158
- // Find all .tsx and .ts files under src/
159
- const srcDir = join(projectPath, 'src');
160
- const pattern = `${srcDir.replace(/\\/g, '/')}/**/*.{tsx,ts}`;
161
- const files = await glob(pattern, { ignore: ['**/node_modules/**', '**/.next/**'] });
162
-
163
- for (const filePath of files) {
164
- let content;
165
- try {
166
- content = readFileSync(filePath, 'utf8');
167
- } catch {
168
- continue;
169
- }
170
-
171
- // Make path relative for cleaner output
172
- const relPath = filePath.replace(projectPath.replace(/\\/g, '/') + '/', '').replace(/\\/g, '/');
173
- const fileIssues = validateNextComponent(content, relPath);
174
-
175
- for (const issue of fileIssues) {
176
- result.issues.push({
177
- level: issue.type, // 'error' | 'warning' → validation-runner format
178
- message: issue.message,
179
- file: issue.file,
180
- line: issue.line,
181
- solution: issue.suggestion,
182
- });
183
- if (issue.type === 'error') result.errors++;
184
- else result.warnings++;
185
- }
186
- }
187
-
188
- return result;
189
- }
190
- ```
191
-
192
- **Step 4: Export from index.js**
193
-
194
- Edit `src/lib/validators/nextjs/index.js` — add:
195
- ```js
196
- export { validateNextComponent, validateNextComponentFiles } from './next-component-validator.js';
197
- ```
198
-
199
- **Step 5: Add case to validation-runner.js**
200
-
201
- In `src/lib/validators/validation-runner.js`, inside `runSingleValidator()` switch, add before the `default:` case:
202
-
203
- ```js
204
- case 'nextjs-component': {
205
- const { validateNextComponentFiles } = await import('./nextjs/next-component-validator.js');
206
- return await validateNextComponentFiles(projectPath, options);
207
- }
208
- ```
209
-
210
- **Step 6: Fix agents.json**
211
-
212
- Find `"nextjs-expert"` entry, change:
213
- ```json
214
- "validators": ["packages"]
215
- ```
216
- to:
217
- ```json
218
- "validators": ["packages", "nextjs-component"]
219
- ```
220
-
221
- **Step 7: Validate JSON**
222
-
223
- ```bash
224
- cd "R:/Polymorphism Tech/repos/morph-spec-framework" && node -e "JSON.parse(require('fs').readFileSync('framework/agents.json','utf8')); console.log('OK')"
225
- ```
226
-
227
- **Step 8: Run the new tests — must all pass**
228
-
229
- ```bash
230
- cd "R:/Polymorphism Tech/repos/morph-spec-framework" && node --test test/validators/nextjs/next-component-files-validator.test.js 2>&1
231
- ```
232
-
233
- Expected: 6/6 pass
234
-
235
- **Step 9: Run full test suite**
236
-
237
- ```bash
238
- cd "R:/Polymorphism Tech/repos/morph-spec-framework" && npm test 2>&1 | tail -8
239
- ```
240
-
241
- Expected: 0 failures (652+ pass)
242
-
243
- **Step 10: Commit**
244
-
245
- ```bash
246
- cd "R:/Polymorphism Tech/repos/morph-spec-framework" && git add src/lib/validators/nextjs/ test/validators/nextjs/next-component-files-validator.test.js framework/agents.json src/lib/validators/validation-runner.js && git commit -m "fix(validators): wire nextjs-component validator — add validateNextComponentFiles, register in validation-runner, fix agents.json"
247
- ```
248
-
249
- ---
250
-
251
- ### Task 2: Create scan-nextjs.mjs (E2E TDD with real temp dirs)
252
-
253
- **Files:**
254
- - Create: `scripts/scan-nextjs.mjs`
255
- - Create: `test/scripts/scan-nextjs.test.mjs`
256
-
257
- **Step 1: Write failing tests FIRST — real temp dirs, real user cases**
258
-
259
- Create `test/scripts/scan-nextjs.test.mjs`:
260
-
261
- ```js
262
- import { test, describe, beforeEach, afterEach } from 'node:test';
263
- import assert from 'node:assert/strict';
264
- import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
265
- import { join } from 'node:path';
266
- import { tmpdir } from 'node:os';
267
- import { execSync } from 'node:child_process';
268
-
269
- const SCAN_SCRIPT = join(process.cwd(), 'scripts', 'scan-nextjs.mjs');
270
-
271
- /**
272
- * Run scan-nextjs.mjs against a temp dir.
273
- * Returns { stdout, stderr, exitCode }
274
- */
275
- function runScan(targetDir, extraArgs = '') {
276
- try {
277
- const stdout = execSync(
278
- `node "${SCAN_SCRIPT}" "${targetDir}" ${extraArgs}`,
279
- { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
280
- );
281
- return { stdout, stderr: '', exitCode: 0 };
282
- } catch (err) {
283
- return {
284
- stdout: err.stdout || '',
285
- stderr: err.stderr || '',
286
- exitCode: err.status ?? 1,
287
- };
288
- }
289
- }
290
-
291
- describe('scan-nextjs.mjs — E2E with real temp directories', () => {
292
- let tempDir;
293
-
294
- beforeEach(() => {
295
- tempDir = join(tmpdir(), `morph-scan-nextjs-${Date.now()}`);
296
- mkdirSync(join(tempDir, 'src', 'components'), { recursive: true });
297
- mkdirSync(join(tempDir, 'src', 'features', 'users', 'components'), { recursive: true });
298
- mkdirSync(join(tempDir, 'src', 'app'), { recursive: true });
299
- });
300
-
301
- afterEach(() => {
302
- rmSync(tempDir, { recursive: true, force: true });
303
- });
304
-
305
- // =============================================
306
- // User case 1: Clean project — developer did everything right
307
- // =============================================
308
- test('user case: clean project with correctly written components → 0 issues, exit 0', () => {
309
- writeFileSync(
310
- join(tempDir, 'src', 'features', 'users', 'components', 'user-card.tsx'),
311
- `'use client';\nimport { useState } from 'react';\nexport function UserCard({ user }) {\n const [open, setOpen] = useState(false);\n return <div onClick={() => setOpen(!open)}>{user.name}</div>;\n}`
312
- );
313
- writeFileSync(
314
- join(tempDir, 'src', 'app', 'page.tsx'),
315
- `export default async function Page() {\n return <div>Home</div>;\n}`
316
- );
317
- const { stdout, exitCode } = runScan(tempDir);
318
- assert.equal(exitCode, 0, `Expected exit 0, got ${exitCode}. stdout: ${stdout}`);
319
- assert.ok(stdout.includes('0 issues') || stdout.includes('No issues'), `Expected 0 issues in output: ${stdout}`);
320
- });
321
-
322
- // =============================================
323
- // User case 2: Developer forgot 'use client' on interactive component
324
- // =============================================
325
- test('user case: useState without use client → CRITICAL finding, exit 1', () => {
326
- writeFileSync(
327
- join(tempDir, 'src', 'components', 'counter.tsx'),
328
- `import { useState } from 'react';\nexport function Counter() {\n const [count, setCount] = useState(0);\n return <button onClick={() => setCount(c => c+1)}>{count}</button>;\n}`
329
- );
330
- const { stdout, exitCode } = runScan(tempDir);
331
- assert.equal(exitCode, 1, 'Should exit 1 when errors found');
332
- assert.ok(stdout.toLowerCase().includes('critical') || stdout.toLowerCase().includes('error'), `Expected CRITICAL in output: ${stdout}`);
333
- assert.ok(stdout.includes('use client'), `Expected 'use client' mention: ${stdout}`);
334
- });
335
-
336
- // =============================================
337
- // User case 3: Developer added 'use client' defensively to all components
338
- // =============================================
339
- test('user case: unnecessary use client on static component → HIGH warning, exit 0', () => {
340
- writeFileSync(
341
- join(tempDir, 'src', 'components', 'static-badge.tsx'),
342
- `'use client';\nexport function StaticBadge({ label }) {\n return <span className="badge">{label}</span>;\n}`
343
- );
344
- const { stdout, exitCode } = runScan(tempDir);
345
- // Warnings alone should NOT fail (exit 0)
346
- assert.equal(exitCode, 0, 'Warnings should not cause exit 1');
347
- assert.ok(stdout.toLowerCase().includes('warning') || stdout.toLowerCase().includes('high'), `Expected warning in output: ${stdout}`);
348
- });
349
-
350
- // =============================================
351
- // User case 4: Developer created component file with PascalCase name
352
- // =============================================
353
- test('user case: PascalCase filename → HIGH warning in output', () => {
354
- writeFileSync(
355
- join(tempDir, 'src', 'components', 'UserList.tsx'),
356
- `export function UserList() { return <ul/>; }`
357
- );
358
- const { stdout } = runScan(tempDir);
359
- assert.ok(stdout.includes('UserList') || stdout.toLowerCase().includes('kebab'), `Expected file name mention: ${stdout}`);
360
- });
361
-
362
- // =============================================
363
- // User case 5: Developer used useEffect to fetch data (anti-pattern)
364
- // =============================================
365
- test('user case: useEffect for data fetching → CRITICAL finding', () => {
366
- writeFileSync(
367
- join(tempDir, 'src', 'components', 'user-list.tsx'),
368
- `'use client';\nimport { useEffect, useState } from 'react';\nexport function UserList() {\n const [users, setUsers] = useState([]);\n useEffect(() => {\n fetch('/api/users').then(r => r.json()).then(setUsers);\n }, []);\n return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;\n}`
369
- );
370
- const { stdout } = runScan(tempDir);
371
- assert.ok(
372
- stdout.toLowerCase().includes('useeffect') || stdout.toLowerCase().includes('fetch') || stdout.toLowerCase().includes('tanstack'),
373
- `Expected useEffect/fetch warning in output: ${stdout}`
374
- );
375
- });
376
-
377
- // =============================================
378
- // User case 6: Mixed project — some clean, some violations
379
- // =============================================
380
- test('user case: mixed project — correct count in summary', () => {
381
- // Clean file
382
- writeFileSync(
383
- join(tempDir, 'src', 'app', 'layout.tsx'),
384
- `export default function RootLayout({ children }) { return <html><body>{children}</body></html>; }`
385
- );
386
- // Error: useState without use client
387
- writeFileSync(
388
- join(tempDir, 'src', 'components', 'bad-component.tsx'),
389
- `import { useState } from 'react';\nexport function Bad() { const [x] = useState(0); return <div>{x}</div>; }`
390
- );
391
- const { stdout, exitCode } = runScan(tempDir);
392
- assert.equal(exitCode, 1);
393
- // Should mention file count
394
- assert.ok(/\d+ file/.test(stdout), `Expected file count in output: ${stdout}`);
395
- });
396
-
397
- // =============================================
398
- // User case 7: --json flag for CI integration
399
- // =============================================
400
- test('--json flag produces valid JSON output', () => {
401
- writeFileSync(
402
- join(tempDir, 'src', 'components', 'good.tsx'),
403
- `'use client';\nimport { useState } from 'react';\nexport function Good() { const [x] = useState(0); return <div onClick={() => {}}>{x}</div>; }`
404
- );
405
- const { stdout, exitCode } = runScan(tempDir, '--json');
406
- assert.equal(exitCode, 0);
407
- let parsed;
408
- assert.doesNotThrow(() => { parsed = JSON.parse(stdout); }, 'Output must be valid JSON');
409
- assert.ok(typeof parsed.files === 'number', 'JSON must have files count');
410
- assert.ok(Array.isArray(parsed.issues), 'JSON must have issues array');
411
- assert.ok(typeof parsed.errors === 'number', 'JSON must have errors count');
412
- assert.ok(typeof parsed.warnings === 'number', 'JSON must have warnings count');
413
- assert.ok(typeof parsed.exitCode === 'number', 'JSON must have exitCode');
414
- });
415
-
416
- // =============================================
417
- // User case 8: Empty src/ directory
418
- // =============================================
419
- test('empty src/ directory → 0 files scanned, exit 0', () => {
420
- const { stdout, exitCode } = runScan(tempDir);
421
- assert.equal(exitCode, 0);
422
- assert.ok(stdout.includes('0 file') || stdout.includes('No files') || stdout.includes('0 issues'), `Expected 0 files or 0 issues: ${stdout}`);
423
- });
424
-
425
- // =============================================
426
- // User case 9: Non-existent path → graceful error
427
- // =============================================
428
- test('non-existent path → exit 2, error message', () => {
429
- const { stdout, stderr, exitCode } = runScan(join(tempDir, 'does-not-exist'));
430
- assert.equal(exitCode, 2, `Expected exit 2, got ${exitCode}`);
431
- const combined = stdout + stderr;
432
- assert.ok(combined.toLowerCase().includes('error') || combined.toLowerCase().includes('not found') || combined.toLowerCase().includes('does not exist'), `Expected error message: ${combined}`);
433
- });
434
- });
435
- ```
436
-
437
- **Step 2: Run tests to confirm they all FAIL**
438
-
439
- ```bash
440
- cd "R:/Polymorphism Tech/repos/morph-spec-framework" && node --test test/scripts/scan-nextjs.test.mjs 2>&1 | head -15
441
- ```
442
-
443
- Expected: `Cannot find module` or all fail with script not found.
444
-
445
- **Step 3: Implement `scripts/scan-nextjs.mjs`**
446
-
447
- Create `scripts/scan-nextjs.mjs`:
448
-
449
- ```js
450
- #!/usr/bin/env node
451
- /**
452
- * scan-nextjs.mjs
453
- *
454
- * CLI scan for Next.js CRITICAL/HIGH violations.
455
- * Runs automatically before code-review-nextjs skill for mechanical checks.
456
- *
457
- * Usage:
458
- * node scripts/scan-nextjs.mjs [path] # defaults to src/
459
- * node scripts/scan-nextjs.mjs --json # JSON output for CI
460
- *
461
- * Exit codes: 0 = clean, 1 = errors found, 2 = scan failed
462
- */
463
-
464
- import { readFileSync, existsSync } from 'node:fs';
465
- import { join, resolve } from 'node:path';
466
- import { validateNextComponent } from '../src/lib/validators/nextjs/next-component-validator.js';
467
-
468
- // ============================================
469
- // CLI ARGUMENT PARSING
470
- // ============================================
471
-
472
- const args = process.argv.slice(2);
473
- const jsonMode = args.includes('--json');
474
- const pathArg = args.find(a => !a.startsWith('--'));
475
- const targetPath = pathArg ? resolve(pathArg) : resolve('src');
476
-
477
- // ============================================
478
- // ADDITIONAL CHECKS (beyond validateNextComponent)
479
- // ============================================
480
-
481
- /**
482
- * Check for useEffect used for data fetching anti-pattern.
483
- * @param {string} content
484
- * @param {string} filePath
485
- * @returns {import('../src/lib/validators/nextjs/next-component-validator.js').ValidationIssue[]}
486
- */
487
- function checkUseEffectFetch(content, filePath) {
488
- const issues = [];
489
- // Pattern: useEffect containing fetch() or axios() call
490
- if (/useEffect\s*\(\s*\(\s*\)\s*=>/.test(content) && /\bfetch\s*\(/.test(content)) {
491
- issues.push({
492
- type: 'error',
493
- message: "useEffect used for data fetching — use Server Components or TanStack Query (useQuery) instead",
494
- suggestion: "Replace with a Server Component fetch or TanStack Query useQuery hook",
495
- file: filePath,
496
- });
497
- }
498
- return issues;
499
- }
500
-
501
- /**
502
- * Check for default exports on non-special files (should use named exports).
503
- * @param {string} content
504
- * @param {string} filePath
505
- * @returns {import('../src/lib/validators/nextjs/next-component-validator.js').ValidationIssue[]}
506
- */
507
- function checkDefaultExport(content, filePath) {
508
- const issues = [];
509
- const fileName = filePath.split('/').pop() ?? '';
510
- const SPECIAL = ['page.tsx', 'layout.tsx', 'loading.tsx', 'error.tsx', 'not-found.tsx'];
511
- if (!SPECIAL.includes(fileName) && /^export\s+default\s+/m.test(content) && fileName.endsWith('.tsx')) {
512
- issues.push({
513
- type: 'warning',
514
- message: `Default export in '${fileName}' — prefer named exports for easier refactoring`,
515
- suggestion: "Change to: export function ComponentName() {}",
516
- file: filePath,
517
- });
518
- }
519
- return issues;
520
- }
521
-
522
- // ============================================
523
- // FILE WALKER
524
- // ============================================
525
-
526
- /**
527
- * Recursively find all .tsx and .ts files in a directory.
528
- * @param {string} dir
529
- * @returns {string[]}
530
- */
531
- function findTsxFiles(dir) {
532
- const { readdirSync, statSync } = await import('node:fs');
533
- // Use sync version since we imported at top
534
- const results = [];
535
- const entries = readdirSync(dir, { withFileTypes: true });
536
- for (const entry of entries) {
537
- const fullPath = join(dir, entry.name);
538
- if (entry.name === 'node_modules' || entry.name === '.next' || entry.name === 'dist') continue;
539
- if (entry.isDirectory()) {
540
- results.push(...findTsxFiles(fullPath));
541
- } else if (entry.name.endsWith('.tsx') || entry.name.endsWith('.ts')) {
542
- results.push(fullPath);
543
- }
544
- }
545
- return results;
546
- }
547
-
548
- // ============================================
549
- // MAIN
550
- // ============================================
551
-
552
- function main() {
553
- // Validate path exists
554
- if (!existsSync(targetPath)) {
555
- const msg = `Error: Path does not exist: ${targetPath}`;
556
- if (jsonMode) {
557
- process.stdout.write(JSON.stringify({ error: msg, exitCode: 2 }) + '\n');
558
- } else {
559
- process.stderr.write(msg + '\n');
560
- }
561
- process.exit(2);
562
- }
563
-
564
- // Find all files
565
- let files;
566
- try {
567
- files = findTsxFiles(targetPath);
568
- } catch (err) {
569
- const msg = `Error scanning directory: ${err.message}`;
570
- if (jsonMode) {
571
- process.stdout.write(JSON.stringify({ error: msg, exitCode: 2 }) + '\n');
572
- } else {
573
- process.stderr.write(msg + '\n');
574
- }
575
- process.exit(2);
576
- }
577
-
578
- // Run validators
579
- const allIssues = [];
580
- for (const filePath of files) {
581
- let content;
582
- try {
583
- content = readFileSync(filePath, 'utf8');
584
- } catch {
585
- continue;
586
- }
587
- const relPath = filePath.replace(targetPath, '').replace(/\\/g, '/').replace(/^\//, '');
588
- const issues = [
589
- ...validateNextComponent(content, relPath),
590
- ...checkUseEffectFetch(content, relPath),
591
- ...checkDefaultExport(content, relPath),
592
- ];
593
- allIssues.push(...issues);
594
- }
595
-
596
- const errors = allIssues.filter(i => i.type === 'error');
597
- const warnings = allIssues.filter(i => i.type === 'warning');
598
-
599
- // ---- JSON output mode ----
600
- if (jsonMode) {
601
- process.stdout.write(JSON.stringify({
602
- files: files.length,
603
- issues: allIssues.map(i => ({ severity: i.type, message: i.message, file: i.file, line: i.line ?? null, suggestion: i.suggestion ?? null })),
604
- errors: errors.length,
605
- warnings: warnings.length,
606
- exitCode: errors.length > 0 ? 1 : 0,
607
- }, null, 2) + '\n');
608
- process.exit(errors.length > 0 ? 1 : 0);
609
- }
610
-
611
- // ---- Human-readable output ----
612
- console.log(`\n🔍 Scanning ${targetPath.replace(process.cwd(), '.')} (${files.length} file${files.length !== 1 ? 's' : ''})...\n`);
613
-
614
- if (allIssues.length === 0) {
615
- console.log(`✅ No issues found — ${files.length} file${files.length !== 1 ? 's' : ''} scanned\n`);
616
- console.log(`Summary: ${files.length} files scanned | 0 issues (0 critical, 0 high, 0 medium)\n`);
617
- process.exit(0);
618
- }
619
-
620
- if (errors.length > 0) {
621
- console.log(`CRITICAL (${errors.length})`);
622
- for (const issue of errors) {
623
- const loc = issue.line ? `:${issue.line}` : '';
624
- console.log(` ${issue.file}${loc} — ${issue.message}`);
625
- if (issue.suggestion) console.log(` → ${issue.suggestion}`);
626
- }
627
- console.log('');
628
- }
629
-
630
- if (warnings.length > 0) {
631
- console.log(`HIGH/MEDIUM (${warnings.length})`);
632
- for (const issue of warnings) {
633
- console.log(` ${issue.file} — ${issue.message}`);
634
- }
635
- console.log('');
636
- }
637
-
638
- const clean = files.length - new Set(allIssues.map(i => i.file)).size;
639
- if (clean > 0) console.log(`✅ No issues found in ${clean} file${clean !== 1 ? 's' : ''}\n`);
640
-
641
- console.log(`Summary: ${files.length} files scanned | ${allIssues.length} issue${allIssues.length !== 1 ? 's' : ''} (${errors.length} critical, ${warnings.length} high/medium)\n`);
642
- process.exit(errors.length > 0 ? 1 : 0);
643
- }
644
-
645
- main();
646
- ```
647
-
648
- **NOTE FOR IMPLEMENTER:** The `findTsxFiles` function above uses `import` inside a regular function which won't work. Fix it to use the top-level `readdirSync` and `statSync` that are already imported at the top of the file. Replace the body of `findTsxFiles` with a synchronous version using `readdirSync`/`statSync` directly (they were imported at the top with `readFileSync`):
649
-
650
- ```js
651
- import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
652
- ```
653
-
654
- And `findTsxFiles`:
655
- ```js
656
- function findTsxFiles(dir) {
657
- const results = [];
658
- const entries = readdirSync(dir, { withFileTypes: true });
659
- for (const entry of entries) {
660
- const fullPath = join(dir, entry.name);
661
- if (['node_modules', '.next', 'dist'].includes(entry.name)) continue;
662
- if (entry.isDirectory()) {
663
- results.push(...findTsxFiles(fullPath));
664
- } else if (entry.name.endsWith('.tsx') || entry.name.endsWith('.ts')) {
665
- results.push(fullPath);
666
- }
667
- }
668
- return results;
669
- }
670
- ```
671
-
672
- **Step 4: Run tests — all 9 should pass**
673
-
674
- ```bash
675
- cd "R:/Polymorphism Tech/repos/morph-spec-framework" && node --test test/scripts/scan-nextjs.test.mjs 2>&1
676
- ```
677
-
678
- Expected: 9/9 pass
679
-
680
- **Step 5: Smoke-test the scan script manually**
681
-
682
- ```bash
683
- cd "R:/Polymorphism Tech/repos/morph-spec-framework" && node scripts/scan-nextjs.mjs scripts/ 2>&1
684
- ```
685
-
686
- Expected: clean output (scripts/ has no .tsx files so 0 files scanned)
687
-
688
- ```bash
689
- node scripts/scan-nextjs.mjs scripts/ --json 2>&1
690
- ```
691
-
692
- Expected: valid JSON with `{ "files": 0, "issues": [], "errors": 0, ... }`
693
-
694
- **Step 6: Run full test suite**
695
-
696
- ```bash
697
- cd "R:/Polymorphism Tech/repos/morph-spec-framework" && npm test 2>&1 | tail -8
698
- ```
699
-
700
- Expected: 0 failures
701
-
702
- **Step 7: Commit**
703
-
704
- ```bash
705
- cd "R:/Polymorphism Tech/repos/morph-spec-framework" && git add scripts/scan-nextjs.mjs test/scripts/scan-nextjs.test.mjs && git commit -m "feat(scripts): add scan-nextjs.mjs — CLI scan for use-client, hooks, kebab-case, useEffect-fetch violations"
706
- ```
707
-
708
- ---
709
-
710
- ### Task 3: Create code-review-nextjs skill + reference example
711
-
712
- **Files:**
713
- - Create: `framework/skills/level-0-meta/code-review-nextjs/SKILL.md`
714
- - Create: `framework/skills/level-0-meta/code-review-nextjs/references/review-example-nextjs.md`
715
-
716
- **Note:** No unit tests needed for content files. The skills-installer test suite already validates skill directory structure. We verify installation in Task 4.
717
-
718
- **Step 1: Create the directory structure**
719
-
720
- ```bash
721
- mkdir -p "R:/Polymorphism Tech/repos/morph-spec-framework/framework/skills/level-0-meta/code-review-nextjs/references"
722
- ```
723
-
724
- **Step 2: Create `SKILL.md`**
725
-
726
- Create `framework/skills/level-0-meta/code-review-nextjs/SKILL.md`:
727
-
728
- ```markdown
729
- ---
730
- name: code-review-nextjs
731
- description: Next.js code review checklist covering naming conventions, component architecture (Server vs Client), data fetching patterns, form implementation, state management, TypeScript/Zod discipline, feature boundaries, and testing. Use after implementing Next.js code, before creating PRs, or when reviewing TSX/TS code for compliance with MORPH-SPEC Next.js standards.
732
- user-invocable: true
733
- allowed-tools: Read, Write, Edit, Bash, Glob, Grep
734
- ---
735
-
736
- # Next.js Code Review Checklist
737
-
738
- > Comprehensive checklist for Next.js code review: naming, component architecture, data fetching, forms, state, TypeScript, structure, and testing.
739
- > **Ref:** `framework/standards/frontend/nextjs/naming-conventions.md`
740
- > **Ref:** `framework/standards/frontend/nextjs/app-router.md`
741
- > **Ref:** `framework/standards/frontend/nextjs/components.md`
742
- > **Ref:** `framework/standards/frontend/nextjs/data-fetching.md`
743
- > **Ref:** `framework/standards/frontend/nextjs/forms.md`
744
- > **Ref:** `framework/standards/frontend/nextjs/state-management.md`
745
- > **Ref:** `framework/standards/frontend/nextjs/testing.md`
746
- > **Example:** `references/review-example-nextjs.md` — filled-in review showing expected finding format.
747
- > **Script:** `scripts/scan-nextjs.mjs` — automated scan for CRITICAL/HIGH violations before manual review.
748
-
749
- ---
750
-
751
- ## Step 1 — Run automated scan first
752
-
753
- ```bash
754
- node scripts/scan-nextjs.mjs src/
755
- ```
756
-
757
- Review and address CRITICAL findings before proceeding with the manual checklist below. Warnings can be addressed during review.
758
-
759
- ---
760
-
761
- ## Naming & Style (ref: naming-conventions.md)
762
-
763
- - [ ] `[CRITICAL]` File names use kebab-case (`user-card.tsx`, NOT `UserCard.tsx`)
764
- - [ ] `[HIGH]` Components exported as named exports (`export function UserCard`, NOT `export default function UserCard`)
765
- - [ ] `[HIGH]` Hook files named `use-{action}.ts` and export `use{Action}()`
766
- - [ ] `[HIGH]` Schema files named `{feature}.schemas.ts` with Zod exports
767
- - [ ] `[HIGH]` Type files named `{feature}.types.ts` with `z.infer<>` derived types
768
- - [ ] `[MEDIUM]` No abbreviations in public API names (`repository` not `repo`)
769
- - [ ] `[MEDIUM]` Feature folder uses kebab-case (`features/user-management/`, NOT `features/userManagement/`)
770
-
771
- ---
772
-
773
- ## Component Architecture (ref: app-router.md, components.md)
774
-
775
- ### Server vs Client Discipline
776
- - [ ] `[CRITICAL]` `'use client'` only on components that use hooks or event handlers
777
- - [ ] `[CRITICAL]` No `useEffect(() => { fetch(...) }, [])` for data fetching — use Server Components or TanStack Query
778
- - [ ] `[HIGH]` Server Components used for initial page data (no `'use client'` on page files)
779
- - [ ] `[HIGH]` `loading.tsx`, `error.tsx` co-located with every `page.tsx`
780
- - [ ] `[HIGH]` No business logic in `app/` route files — import from `features/`
781
-
782
- ### Three-Tier Hierarchy
783
- - [ ] `[CRITICAL]` No edits to `components/ui/` files — shadcn primitives are never modified
784
- - [ ] `[HIGH]` `components/` (Tier 2) has no imports from `features/` — no domain knowledge
785
- - [ ] `[HIGH]` Feature components (`features/*/components/`) do not import from other features
786
- - [ ] `[MEDIUM]` Feature cross-dependencies extracted to `components/` or `lib/`
787
-
788
- ---
789
-
790
- ## Data Fetching (ref: data-fetching.md)
791
-
792
- - [ ] `[CRITICAL]` No `useEffect` + `useState` pattern for API data — use `useQuery`
793
- - [ ] `[HIGH]` TanStack Query v5 syntax: `useQuery({ queryKey: [...], queryFn: async () => {} })`
794
- - [ ] `[HIGH]` Query key factory pattern used (`userKeys.lists()`, NOT hardcoded `['users']`)
795
- - [ ] `[HIGH]` `useMutation` used for POST/PUT/DELETE — not inline `fetch` in handlers
796
- - [ ] `[HIGH]` API responses validated with Zod before use
797
- - [ ] `[MEDIUM]` `onSuccess` in `useMutation` invalidates relevant query keys
798
- - [ ] `[MEDIUM]` `QueryClientProvider` wraps app in `app/layout.tsx`, not per-component
799
-
800
- ---
801
-
802
- ## Forms (ref: forms.md)
803
-
804
- - [ ] `[CRITICAL]` Zod schema defined BEFORE TypeScript type — type derived with `z.infer<>`
805
- - [ ] `[HIGH]` `useForm` uses `zodResolver(schema)` — NOT manual validation
806
- - [ ] `[HIGH]` shadcn `<Form>`, `<FormField>`, `<FormItem>`, `<FormLabel>`, `<FormControl>`, `<FormMessage>` used
807
- - [ ] `[HIGH]` Form submission uses `useMutation` — NOT direct `fetch` in `onSubmit`
808
- - [ ] `[MEDIUM]` `isPending` used for loading state — NOT `isLoading` (v5 renamed)
809
- - [ ] `[MEDIUM]` Root-level errors displayed: `form.formState.errors.root`
810
- - [ ] `[MEDIUM]` No `useState` for individual form fields — react-hook-form handles this
811
-
812
- ---
813
-
814
- ## State Management (ref: state-management.md)
815
-
816
- - [ ] `[HIGH]` No Zustand/Redux installed without documented justification
817
- - [ ] `[HIGH]` No React Context used for server state (API data) — use TanStack Query
818
- - [ ] `[MEDIUM]` Local UI state (open/closed, selected) in `useState` — not global store
819
- - [ ] `[MEDIUM]` Context only for truly global UI: theme, auth user, locale
820
- - [ ] `[LOW]` No prop drilling beyond 3 levels — consider Context or co-location
821
-
822
- ---
823
-
824
- ## TypeScript Discipline
825
-
826
- - [ ] `[CRITICAL]` No `any` type — use `unknown` and narrow, or fix the actual type
827
- - [ ] `[HIGH]` Types derived from Zod schemas (`type User = z.infer<typeof userSchema>`)
828
- - [ ] `[HIGH]` No duplicate type definitions — if the server returns it, define it once with Zod
829
- - [ ] `[MEDIUM]` API response types come from the shared schema, not manually written
830
- - [ ] `[MEDIUM]` `noUncheckedIndexedAccess` respected — array accesses use optional chaining
831
- - [ ] `[LOW]` No type assertions (`as User`) without validation
832
-
833
- ---
834
-
835
- ## Feature Structure (ref: project-structure.md)
836
-
837
- - [ ] `[HIGH]` Feature exposes public API via `features/{name}/index.ts` — no deep path imports
838
- - [ ] `[HIGH]` Consumers import from index: `from '@/features/users'` NOT `from '@/features/users/components/user-list'`
839
- - [ ] `[MEDIUM]` New features create: `components/`, `hooks/`, `types/` subdirectories
840
- - [ ] `[MEDIUM]` TanStack Query hooks in `features/{name}/hooks/` — NOT co-located with components
841
- - [ ] `[MEDIUM]` Zod schemas in `features/{name}/types/{name}.schemas.ts`
842
- - [ ] `[LOW]` Feature index only exports what external consumers actually need (no over-exposure)
843
-
844
- ---
845
-
846
- ## Testing (ref: testing.md)
847
-
848
- - [ ] `[HIGH]` Test files co-located with source (`user-card.test.tsx` next to `user-card.tsx`)
849
- - [ ] `[HIGH]` `@testing-library/user-event` used — NOT `fireEvent`
850
- - [ ] `[HIGH]` API calls mocked with MSW — NOT `jest.mock('fetch')` or `global.fetch = jest.fn()`
851
- - [ ] `[MEDIUM]` Hook tests use `QueryClientWrapper` helper with `retry: false`
852
- - [ ] `[MEDIUM]` Tests cover happy path + one error path per component
853
- - [ ] `[LOW]` No snapshot tests — use `expect(screen.getByText(...))`
854
-
855
- ---
856
-
857
- ## Quick Pre-Merge Checklist
858
-
859
- ```
860
- [ ] node scripts/scan-nextjs.mjs src/ — 0 CRITICAL findings
861
- [ ] File names are kebab-case (UserCard.tsx → user-card.tsx)
862
- [ ] 'use client' only on interactive components (hooks or event handlers)
863
- [ ] No useEffect for data fetching — Server Component or useQuery
864
- [ ] Zod schema defined first, type derived with z.infer<>
865
- [ ] zodResolver connected to useForm, useMutation for submit
866
- [ ] Query key factory used (userKeys.lists() not ['users'])
867
- [ ] Feature public API via features/{name}/index.ts
868
- [ ] components/ui/ files untouched (shadcn CLI only)
869
- [ ] Tests: userEvent not fireEvent, MSW not mock fetch
870
- ```
871
-
872
- ---
873
-
874
- *Covers: naming-conventions.md + app-router.md + components.md + data-fetching.md + forms.md + state-management.md + testing.md + project-structure.md*
875
- *MORPH-SPEC by Polymorphism Tech*
876
- ```
877
-
878
- **Step 3: Create `references/review-example-nextjs.md`**
879
-
880
- Create `framework/skills/level-0-meta/code-review-nextjs/references/review-example-nextjs.md`:
881
-
882
- ```markdown
883
- # Code Review Report — User Management Feature
884
-
885
- > Example of a well-structured Next.js code review output. Filled-in reference — not a template.
886
-
887
- **Scope:** `src/features/users/` + `src/app/(dashboard)/users/page.tsx`
888
- **Date:** 2026-02-23
889
- **Reviewer:** code-review-nextjs skill
890
-
891
- ---
892
-
893
- ## Automated Scan Results
894
-
895
- ```bash
896
- $ node scripts/scan-nextjs.mjs src/features/users/
897
-
898
- CRITICAL (1)
899
- features/users/components/UserList.tsx:1 — React hooks used (useState) without 'use client'
900
-
901
- HIGH/MEDIUM (2)
902
- features/users/components/UserList.tsx — PascalCase filename (should be user-list.tsx)
903
- features/users/components/user-form.tsx — 'use client' directive present but no interactivity detected
904
-
905
- Summary: 8 files scanned | 3 issues (1 critical, 2 high/medium)
906
- ```
907
-
908
- ---
909
-
910
- ## Findings by Severity
911
-
912
- | Severity | Count |
913
- |----------|-------|
914
- | CRITICAL | 2 |
915
- | HIGH | 3 |
916
- | MEDIUM | 2 |
917
- | LOW | 1 |
918
-
919
- ---
920
-
921
- ### [CRITICAL] Missing 'use client' on interactive component
922
-
923
- **File:** `src/features/users/components/UserList.tsx:1`
924
-
925
- **Current code:**
926
- ```tsx
927
- import { useState } from 'react';
928
-
929
- export function UserList() {
930
- const [selected, setSelected] = useState<string | null>(null);
931
- return <div onClick={() => setSelected('test')}>...</div>;
932
- }
933
- ```
934
-
935
- **Suggested fix:**
936
- ```tsx
937
- 'use client';
938
-
939
- import { useState } from 'react';
940
- // ... rest unchanged
941
- ```
942
-
943
- **Why:** React hooks (`useState`) require `'use client'`. Without it, Next.js treats this as a Server Component and the `useState` call fails at runtime.
944
-
945
- ---
946
-
947
- ### [CRITICAL] useEffect for data fetching
948
-
949
- **File:** `src/features/users/components/user-list.tsx:8`
950
-
951
- **Current code:**
952
- ```tsx
953
- 'use client';
954
- import { useEffect, useState } from 'react';
955
-
956
- export function UserList() {
957
- const [users, setUsers] = useState([]);
958
- useEffect(() => {
959
- fetch('/api/users').then(r => r.json()).then(setUsers);
960
- }, []);
961
- return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
962
- }
963
- ```
964
-
965
- **Suggested fix:**
966
- ```tsx
967
- // Option A — Server Component (no 'use client' needed)
968
- // app/(dashboard)/users/page.tsx
969
- async function getUsers() {
970
- const res = await fetch(`${process.env.API_URL}/api/users`, { next: { revalidate: 30 } });
971
- return res.json();
972
- }
973
- export default async function UsersPage() {
974
- const users = await getUsers();
975
- return <UserList initialData={users} />;
976
- }
977
-
978
- // Option B — TanStack Query (when client interactivity needed)
979
- 'use client';
980
- import { useUsers } from '@/features/users/hooks/use-users';
981
- export function UserList() {
982
- const { data: users = [], isLoading } = useUsers();
983
- if (isLoading) return <div>Loading...</div>;
984
- return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
985
- }
986
- ```
987
-
988
- **Why:** `useEffect` + `useState` for fetching: (1) doubles renders, (2) no caching, (3) no SSR, (4) no error/loading states, (5) can't be cancelled. TanStack Query or Server Components solve all five.
989
-
990
- ---
991
-
992
- ### [HIGH] PascalCase file name
993
-
994
- **File:** `src/features/users/components/UserList.tsx`
995
-
996
- **Suggested fix:** Rename to `user-list.tsx`
997
-
998
- **Why:** Linux servers are case-sensitive. `UserList.tsx` and `user-list.tsx` are different files on the server but the same on macOS/Windows, causing import failures in production.
999
-
1000
- ---
1001
-
1002
- ### [HIGH] Default export on non-page component
1003
-
1004
- **File:** `src/features/users/components/user-card.tsx:3`
1005
-
1006
- **Current code:**
1007
- ```tsx
1008
- export default function UserCard({ user }: { user: User }) {
1009
- ```
1010
-
1011
- **Suggested fix:**
1012
- ```tsx
1013
- export function UserCard({ user }: { user: User }) {
1014
- ```
1015
-
1016
- **Why:** Named exports are refactor-safe — IDEs track renames automatically. Default exports are anonymous in imports (`import X from './user-card'` allows `X` to be anything).
1017
-
1018
- ---
1019
-
1020
- ### [HIGH] Type defined manually instead of deriving from Zod schema
1021
-
1022
- **File:** `src/features/users/types/user.types.ts:1`
1023
-
1024
- **Current code:**
1025
- ```ts
1026
- export type User = {
1027
- id: string;
1028
- name: string;
1029
- email: string;
1030
- role: 'admin' | 'user';
1031
- };
1032
- ```
1033
-
1034
- **Suggested fix:**
1035
- ```ts
1036
- // user.schemas.ts — single source of truth
1037
- export const userSchema = z.object({
1038
- id: z.string(),
1039
- name: z.string(),
1040
- email: z.string().email(),
1041
- role: z.enum(['admin', 'user']),
1042
- });
1043
-
1044
- // user.types.ts — derived, never written manually
1045
- export type User = z.infer<typeof userSchema>;
1046
- ```
1047
-
1048
- **Why:** Manually written types drift from API responses. `z.infer<>` ensures the TypeScript type is always in sync with the runtime validation.
1049
-
1050
- ---
1051
-
1052
- ### [MEDIUM] Deep path import instead of feature index
1053
-
1054
- **File:** `src/app/(dashboard)/users/page.tsx:2`
1055
-
1056
- **Current code:**
1057
- ```tsx
1058
- import { UserList } from '@/features/users/components/user-list';
1059
- ```
1060
-
1061
- **Suggested fix:**
1062
- ```tsx
1063
- import { UserList } from '@/features/users';
1064
- ```
1065
-
1066
- **Why:** Deep path imports expose internal structure. When `user-list.tsx` moves or is renamed, every consumer breaks. The feature index is the stable public API.
1067
-
1068
- ---
1069
-
1070
- ### [MEDIUM] Query key not using factory pattern
1071
-
1072
- **File:** `src/features/users/hooks/use-users.ts:8`
1073
-
1074
- **Current code:**
1075
- ```ts
1076
- return useQuery({
1077
- queryKey: ['users'],
1078
- ```
1079
-
1080
- **Suggested fix:**
1081
- ```ts
1082
- // query-keys.ts
1083
- export const userKeys = {
1084
- all: ['users'] as const,
1085
- lists: () => [...userKeys.all, 'list'] as const,
1086
- };
1087
-
1088
- // use-users.ts
1089
- return useQuery({
1090
- queryKey: userKeys.lists(),
1091
- ```
1092
-
1093
- **Why:** Hardcoded `['users']` cannot be selectively invalidated. `queryClient.invalidateQueries({ queryKey: userKeys.lists() })` correctly invalidates only list queries, not detail queries with the same prefix.
1094
-
1095
- ---
1096
-
1097
- ### [LOW] Test uses fireEvent instead of userEvent
1098
-
1099
- **File:** `src/features/users/components/user-card.test.tsx:14`
1100
-
1101
- **Current code:**
1102
- ```tsx
1103
- fireEvent.click(screen.getByRole('button', { name: /edit/i }));
1104
- ```
1105
-
1106
- **Suggested fix:**
1107
- ```tsx
1108
- await userEvent.click(screen.getByRole('button', { name: /edit/i }));
1109
- ```
1110
-
1111
- **Why:** `fireEvent.click` dispatches a synthetic event. `userEvent.click` simulates the full browser interaction sequence (pointerdown, mousedown, focus, click, etc.) — closer to real user behaviour.
1112
-
1113
- ---
1114
-
1115
- ## Top Priorities
1116
-
1117
- 1. `UserList.tsx:1` — CRITICAL missing `'use client'` (2 min fix)
1118
- 2. `user-list.tsx:8` — CRITICAL useEffect fetch → useQuery (15 min)
1119
- 3. `UserList.tsx` — HIGH rename to `user-list.tsx` (2 min)
1120
- 4. `user-card.tsx:3` — HIGH named export (2 min)
1121
- 5. `user.types.ts:1` — HIGH derive from Zod (10 min)
1122
-
1123
- ---
1124
-
1125
- ## Passed Checks ✅
1126
-
1127
- - `components/ui/` files untouched
1128
- - `zodResolver` correctly wired in `user-form.tsx`
1129
- - `isPending` used (v5 correct — not `isLoading`)
1130
- - `loading.tsx` and `error.tsx` present in `app/(dashboard)/users/`
1131
- - Feature has `index.ts` re-exporting public API
1132
- - Test file co-located with component
1133
-
1134
- ---
1135
-
1136
- *MORPH-SPEC by Polymorphism Tech*
1137
- ```
1138
-
1139
- **Step 4: Verify files exist**
1140
-
1141
- ```bash
1142
- ls "R:/Polymorphism Tech/repos/morph-spec-framework/framework/skills/level-0-meta/code-review-nextjs/"
1143
- ```
1144
-
1145
- Expected: `SKILL.md` + `references/` directory
1146
-
1147
- **Step 5: Commit**
1148
-
1149
- ```bash
1150
- cd "R:/Polymorphism Tech/repos/morph-spec-framework" && git add framework/skills/level-0-meta/code-review-nextjs/ && git commit -m "feat(skills): add code-review-nextjs skill — 9-category checklist + filled review example"
1151
- ```
1152
-
1153
- ---
1154
-
1155
- ### Task 4: Install skill + final end-to-end verification
1156
-
1157
- **Files:**
1158
- - Modified: `.claude/skills/code-review-nextjs/` (installed by running installSkills)
1159
- - No test changes needed — existing skills-installer tests will cover the new skill
1160
-
1161
- **Step 1: Check if skills-installer test count needs updating**
1162
-
1163
- The existing `test/utils/skills-installer.test.js` likely has a count assertion for level-0-meta skills. Check:
1164
-
1165
- ```bash
1166
- grep -n "level-0-meta\|skill.*count\|\.length" "R:/Polymorphism Tech/repos/morph-spec-framework/test/utils/skills-installer.test.js" | head -20
1167
- ```
1168
-
1169
- If there's an assertion like `skills.length === N` for level-0-meta, update it to `N+1` since we added `code-review-nextjs`.
1170
-
1171
- **Step 2: Manually install the skill to .claude/skills/**
1172
-
1173
- ```bash
1174
- cd "R:/Polymorphism Tech/repos/morph-spec-framework" && node -e "
1175
- import('./src/utils/skills-installer.js').then(m => m.installSkills('.', 'framework')).then(r => console.log('Installed:', r.installed, 'skills'))
1176
- " --input-type=module
1177
- ```
1178
-
1179
- **Step 3: Verify skill is installed**
1180
-
1181
- ```bash
1182
- ls ".claude/skills/code-review-nextjs/" 2>/dev/null && echo "INSTALLED" || echo "NOT FOUND"
1183
- ```
1184
-
1185
- Expected: `INSTALLED`, with `SKILL.md` and `references/` present.
1186
-
1187
- **Step 4: Run full test suite — 0 failures**
1188
-
1189
- ```bash
1190
- cd "R:/Polymorphism Tech/repos/morph-spec-framework" && npm test 2>&1 | tail -10
1191
- ```
1192
-
1193
- Expected: 0 failures (659+ tests pass)
1194
-
1195
- **Step 5: E2E smoke test — run the full pipeline manually**
1196
-
1197
- ```bash
1198
- # Create a realistic temp project with real violations
1199
- node -e "
1200
- import { mkdirSync, writeFileSync } from 'node:fs';
1201
- import { join } from 'node:path';
1202
- const dir = '/tmp/morph-review-e2e-demo';
1203
- mkdirSync(join(dir, 'src/features/users/components'), { recursive: true });
1204
- mkdirSync(join(dir, 'src/app/(dashboard)/users'), { recursive: true });
1205
-
1206
- // Bad: useState without use client
1207
- writeFileSync(join(dir, 'src/features/users/components/UserList.tsx'),
1208
- \`import { useState } from 'react';
1209
- export function UserList() {
1210
- const [users, setUsers] = useState([]);
1211
- return <ul/>;
1212
- }\`);
1213
-
1214
- // Bad: useEffect fetch
1215
- writeFileSync(join(dir, 'src/features/users/components/user-form.tsx'),
1216
- \`'use client';
1217
- import { useEffect, useState } from 'react';
1218
- export function UserForm() {
1219
- const [data, setData] = useState(null);
1220
- useEffect(() => { fetch('/api/users').then(r => r.json()).then(setData); }, []);
1221
- return <form/>;
1222
- }\`);
1223
-
1224
- // Good: clean server component
1225
- writeFileSync(join(dir, 'src/app/(dashboard)/users/page.tsx'),
1226
- \`export default async function UsersPage() { return <div>Users</div>; }\`);
1227
-
1228
- console.log('Demo project created at', dir);
1229
- " --input-type=module
1230
-
1231
- node scripts/scan-nextjs.mjs /tmp/morph-review-e2e-demo/src/
1232
- ```
1233
-
1234
- Expected output: Shows CRITICAL findings for `UserList.tsx` (useState without use client) and `user-form.tsx` (useEffect fetch), clean pass for `page.tsx`.
1235
-
1236
- **Step 6: Test --json flag**
1237
-
1238
- ```bash
1239
- node scripts/scan-nextjs.mjs /tmp/morph-review-e2e-demo/src/ --json | node -e "const d=require('fs').readFileSync('/dev/stdin','utf8'); const j=JSON.parse(d); console.log('files:', j.files, '| errors:', j.errors, '| warnings:', j.warnings, '| exitCode:', j.exitCode)"
1240
- ```
1241
-
1242
- Expected: valid JSON with correct counts.
1243
-
1244
- **Step 7: Commit final state**
1245
-
1246
- ```bash
1247
- cd "R:/Polymorphism Tech/repos/morph-spec-framework" && git add .claude/skills/code-review-nextjs/ && git commit -m "feat(skills): install code-review-nextjs to .claude/skills/ — available as /code-review-nextjs"
1248
- ```
1249
-
1250
- **Step 8: Final git log**
1251
-
1252
- ```bash
1253
- cd "R:/Polymorphism Tech/repos/morph-spec-framework" && git log --oneline -6
1254
- ```
1255
-
1256
- Expected: 4 clean commits covering all 4 tasks.