jdi-cli 0.1.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/AGENTS.md +209 -0
- package/ARCHITECTURE.md +210 -0
- package/COMMANDS.md +241 -0
- package/CREATE-EXAMPLE.md +385 -0
- package/CREATE.md +315 -0
- package/EXTENSION.md +141 -0
- package/LICENSE +21 -0
- package/MEMORY.md +471 -0
- package/PORTABILITY.md +438 -0
- package/README.md +789 -0
- package/bin/git-hooks/post-commit +16 -0
- package/bin/git-hooks/pre-commit +21 -0
- package/bin/jdi-build.ps1 +381 -0
- package/bin/jdi-build.sh +332 -0
- package/bin/jdi-doctor.ps1 +403 -0
- package/bin/jdi-doctor.sh +400 -0
- package/bin/jdi-install-caveman.ps1 +97 -0
- package/bin/jdi-install-caveman.sh +99 -0
- package/bin/jdi-install-playwright.ps1 +319 -0
- package/bin/jdi-install-playwright.sh +284 -0
- package/bin/jdi-install.ps1 +154 -0
- package/bin/jdi-install.sh +132 -0
- package/bin/jdi-uninstall.ps1 +309 -0
- package/bin/jdi-uninstall.sh +264 -0
- package/bin/jdi-update.ps1 +215 -0
- package/bin/jdi-update.sh +209 -0
- package/bin/jdi.js +460 -0
- package/bin/lib/jdi-monitor.ps1 +66 -0
- package/bin/lib/jdi-monitor.sh +74 -0
- package/bin/lib/jdi-truncate.ps1 +96 -0
- package/bin/lib/jdi-truncate.sh +99 -0
- package/bin/lib/ui.js +197 -0
- package/core/agents/jdi-adopter.md +465 -0
- package/core/agents/jdi-architect.md +894 -0
- package/core/agents/jdi-asker.md +153 -0
- package/core/agents/jdi-bootstrap.md +247 -0
- package/core/agents/jdi-planner.md +254 -0
- package/core/agents/jdi-researcher.md +303 -0
- package/core/commands/jdi-adopt.md +155 -0
- package/core/commands/jdi-bootstrap.md +81 -0
- package/core/commands/jdi-create.md +80 -0
- package/core/commands/jdi-discuss.md +80 -0
- package/core/commands/jdi-do.md +200 -0
- package/core/commands/jdi-loop.md +315 -0
- package/core/commands/jdi-new.md +131 -0
- package/core/commands/jdi-plan.md +73 -0
- package/core/commands/jdi-ship.md +146 -0
- package/core/commands/jdi-verify.md +159 -0
- package/core/skills/clean-code/SKILL.md +261 -0
- package/core/skills/dry/SKILL.md +150 -0
- package/core/skills/frontend-rules/SKILL.md +386 -0
- package/core/skills/frontend-validator/SKILL.md +567 -0
- package/core/skills/kiss/SKILL.md +178 -0
- package/core/skills/solid/SKILL.md +281 -0
- package/core/skills/yagni/SKILL.md +207 -0
- package/core/templates/agent.md +72 -0
- package/core/templates/doer-specialist.md +216 -0
- package/core/templates/reviewer-specialist.md +405 -0
- package/core/templates/skill.md +66 -0
- package/package.json +70 -0
- package/runtimes/antigravity/agents.md +74 -0
- package/runtimes/antigravity/skills/clean-code/SKILL.md +252 -0
- package/runtimes/antigravity/skills/dry/SKILL.md +141 -0
- package/runtimes/antigravity/skills/frontend-rules/SKILL.md +376 -0
- package/runtimes/antigravity/skills/frontend-validator/SKILL.md +559 -0
- package/runtimes/antigravity/skills/jdi-adopt/SKILL.md +155 -0
- package/runtimes/antigravity/skills/jdi-adopter/SKILL.md +436 -0
- package/runtimes/antigravity/skills/jdi-architect/SKILL.md +872 -0
- package/runtimes/antigravity/skills/jdi-asker/SKILL.md +125 -0
- package/runtimes/antigravity/skills/jdi-asker/references/context-template.md +34 -0
- package/runtimes/antigravity/skills/jdi-asker/references/decision-format.md +19 -0
- package/runtimes/antigravity/skills/jdi-asker/scripts/find_phase_dir.sh +25 -0
- package/runtimes/antigravity/skills/jdi-bootstrap/SKILL.md +81 -0
- package/runtimes/antigravity/skills/jdi-create/SKILL.md +80 -0
- package/runtimes/antigravity/skills/jdi-discuss/SKILL.md +80 -0
- package/runtimes/antigravity/skills/jdi-discuss/scripts/run_command.sh +62 -0
- package/runtimes/antigravity/skills/jdi-do/SKILL.md +200 -0
- package/runtimes/antigravity/skills/jdi-loop/SKILL.md +315 -0
- package/runtimes/antigravity/skills/jdi-new/SKILL.md +131 -0
- package/runtimes/antigravity/skills/jdi-plan/SKILL.md +73 -0
- package/runtimes/antigravity/skills/jdi-planner/SKILL.md +225 -0
- package/runtimes/antigravity/skills/jdi-researcher/SKILL.md +274 -0
- package/runtimes/antigravity/skills/jdi-ship/SKILL.md +146 -0
- package/runtimes/antigravity/skills/jdi-verify/SKILL.md +159 -0
- package/runtimes/antigravity/skills/kiss/SKILL.md +169 -0
- package/runtimes/antigravity/skills/solid/SKILL.md +272 -0
- package/runtimes/antigravity/skills/yagni/SKILL.md +198 -0
- package/runtimes/claude/CLAUDE.md +91 -0
- package/runtimes/claude/agents/jdi-adopter.md +430 -0
- package/runtimes/claude/agents/jdi-architect.md +864 -0
- package/runtimes/claude/agents/jdi-asker.md +119 -0
- package/runtimes/claude/agents/jdi-bootstrap.md +213 -0
- package/runtimes/claude/agents/jdi-planner.md +221 -0
- package/runtimes/claude/agents/jdi-researcher.md +269 -0
- package/runtimes/claude/commands/jdi-adopt.md +155 -0
- package/runtimes/claude/commands/jdi-bootstrap.md +81 -0
- package/runtimes/claude/commands/jdi-create.md +80 -0
- package/runtimes/claude/commands/jdi-discuss.md +80 -0
- package/runtimes/claude/commands/jdi-do.md +200 -0
- package/runtimes/claude/commands/jdi-loop.md +315 -0
- package/runtimes/claude/commands/jdi-new.md +131 -0
- package/runtimes/claude/commands/jdi-plan.md +73 -0
- package/runtimes/claude/commands/jdi-ship.md +146 -0
- package/runtimes/claude/commands/jdi-verify.md +159 -0
- package/runtimes/claude/settings.example.json +132 -0
- package/runtimes/claude/skills/clean-code/SKILL.md +247 -0
- package/runtimes/claude/skills/dry/SKILL.md +136 -0
- package/runtimes/claude/skills/frontend-rules/SKILL.md +369 -0
- package/runtimes/claude/skills/frontend-validator/SKILL.md +553 -0
- package/runtimes/claude/skills/kiss/SKILL.md +164 -0
- package/runtimes/claude/skills/solid/SKILL.md +267 -0
- package/runtimes/claude/skills/yagni/SKILL.md +193 -0
- package/runtimes/copilot/agents/jdi-adopter.agent.md +430 -0
- package/runtimes/copilot/agents/jdi-architect.agent.md +864 -0
- package/runtimes/copilot/agents/jdi-asker.agent.md +119 -0
- package/runtimes/copilot/agents/jdi-bootstrap.agent.md +213 -0
- package/runtimes/copilot/agents/jdi-planner.agent.md +221 -0
- package/runtimes/copilot/agents/jdi-researcher.agent.md +269 -0
- package/runtimes/copilot/copilot-instructions.md +80 -0
- package/runtimes/copilot/prompts/jdi-adopt.prompt.md +155 -0
- package/runtimes/copilot/prompts/jdi-bootstrap.prompt.md +81 -0
- package/runtimes/copilot/prompts/jdi-create.prompt.md +80 -0
- package/runtimes/copilot/prompts/jdi-discuss.prompt.md +80 -0
- package/runtimes/copilot/prompts/jdi-do.prompt.md +200 -0
- package/runtimes/copilot/prompts/jdi-loop.prompt.md +315 -0
- package/runtimes/copilot/prompts/jdi-new.prompt.md +131 -0
- package/runtimes/copilot/prompts/jdi-plan.prompt.md +73 -0
- package/runtimes/copilot/prompts/jdi-ship.prompt.md +146 -0
- package/runtimes/copilot/prompts/jdi-verify.prompt.md +159 -0
- package/runtimes/opencode/AGENTS.md +87 -0
- package/runtimes/opencode/agents/jdi-adopter.md +434 -0
- package/runtimes/opencode/agents/jdi-architect.md +861 -0
- package/runtimes/opencode/agents/jdi-asker.md +123 -0
- package/runtimes/opencode/agents/jdi-bootstrap.md +217 -0
- package/runtimes/opencode/agents/jdi-planner.md +225 -0
- package/runtimes/opencode/agents/jdi-researcher.md +273 -0
- package/runtimes/opencode/commands/jdi-adopt.md +155 -0
- package/runtimes/opencode/commands/jdi-bootstrap.md +81 -0
- package/runtimes/opencode/commands/jdi-create.md +80 -0
- package/runtimes/opencode/commands/jdi-discuss.md +80 -0
- package/runtimes/opencode/commands/jdi-do.md +200 -0
- package/runtimes/opencode/commands/jdi-loop.md +315 -0
- package/runtimes/opencode/commands/jdi-new.md +131 -0
- package/runtimes/opencode/commands/jdi-plan.md +73 -0
- package/runtimes/opencode/commands/jdi-ship.md +146 -0
- package/runtimes/opencode/commands/jdi-verify.md +159 -0
- package/runtimes/opencode/opencode.example.jsonc +169 -0
- package/runtimes/opencode/skills/clean-code/SKILL.md +247 -0
- package/runtimes/opencode/skills/dry/SKILL.md +136 -0
- package/runtimes/opencode/skills/frontend-rules/SKILL.md +369 -0
- package/runtimes/opencode/skills/frontend-validator/SKILL.md +553 -0
- package/runtimes/opencode/skills/kiss/SKILL.md +164 -0
- package/runtimes/opencode/skills/solid/SKILL.md +267 -0
- package/runtimes/opencode/skills/yagni/SKILL.md +193 -0
- package/templates-jdi-folder/config.json +18 -0
- package/templates-jdi-folder/registry.md +31 -0
- package/templates-jdi-folder/reviewers.md +33 -0
- package/templates-jdi-folder/skills-registry.md +32 -0
- package/templates-jdi-folder/specialists.md +39 -0
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: frontend-validator
|
|
3
|
+
description: Validates live UI via Playwright + axe-core. Detects Playwright; installs if missing (with user consent). Spawns dev server, navigates critical routes on mobile+desktop, captures console errors, network failures, a11y violations, screenshots, layout shifts. Structured JSON output for the reviewer to parse.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Skill: jdi-frontend-validator
|
|
7
|
+
|
|
8
|
+
Runs UI validation in a real browser. Without Playwright installed, installs with consent. Output always JSON in `.jdi/cache/ui-findings.json` for the parent reviewer to consume.
|
|
9
|
+
|
|
10
|
+
## When to apply
|
|
11
|
+
|
|
12
|
+
Reviewer calls at gate 7. Preconditions in PROJECT.md:
|
|
13
|
+
|
|
14
|
+
```yaml
|
|
15
|
+
frontend:
|
|
16
|
+
has_frontend: true
|
|
17
|
+
frontend_url: http://localhost:5173
|
|
18
|
+
dev_command: pnpm dev
|
|
19
|
+
critical_paths:
|
|
20
|
+
- /
|
|
21
|
+
- /login
|
|
22
|
+
- /dashboard
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Missing any key -> abort with descriptive error.
|
|
26
|
+
|
|
27
|
+
## Procedure
|
|
28
|
+
|
|
29
|
+
### Step 1: Pre-flight
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# bash
|
|
33
|
+
test -d .jdi/ || { echo "No .jdi/. Run /jdi-new"; exit 1; }
|
|
34
|
+
test -f .jdi/PROJECT.md || { echo "PROJECT.md missing"; exit 1; }
|
|
35
|
+
|
|
36
|
+
# Read frontend.has_frontend, frontend_url, dev_command, critical_paths
|
|
37
|
+
# (simple YAML parse - assume well-defined format)
|
|
38
|
+
|
|
39
|
+
# Create cache
|
|
40
|
+
mkdir -p .jdi/cache/screenshots
|
|
41
|
+
|
|
42
|
+
# .gitignore guarantee
|
|
43
|
+
grep -q '^\.jdi/cache/' .gitignore 2>/dev/null || echo '.jdi/cache/' >> .gitignore
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```powershell
|
|
47
|
+
# PowerShell
|
|
48
|
+
if (-not (Test-Path .jdi)) { Write-Error "No .jdi/. Run /jdi-new"; exit 1 }
|
|
49
|
+
if (-not (Test-Path .jdi/PROJECT.md)) { Write-Error "PROJECT.md missing"; exit 1 }
|
|
50
|
+
|
|
51
|
+
New-Item -ItemType Directory -Force -Path .jdi/cache/screenshots | Out-Null
|
|
52
|
+
|
|
53
|
+
if (-not (Test-Path .gitignore) -or -not (Select-String -Path .gitignore -Pattern '^\.jdi/cache/' -Quiet)) {
|
|
54
|
+
Add-Content .gitignore '.jdi/cache/'
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Step 2: Detect package manager
|
|
59
|
+
|
|
60
|
+
Lockfile detection (more reliable than `which`):
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# bash
|
|
64
|
+
if [ -f pnpm-lock.yaml ]; then PKG_MGR=pnpm
|
|
65
|
+
elif [ -f yarn.lock ]; then PKG_MGR=yarn
|
|
66
|
+
elif [ -f bun.lockb ] || [ -f bun.lock ]; then PKG_MGR=bun
|
|
67
|
+
elif [ -f package-lock.json ]; then PKG_MGR=npm
|
|
68
|
+
else PKG_MGR=npm # fallback
|
|
69
|
+
fi
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```powershell
|
|
73
|
+
# PowerShell
|
|
74
|
+
if (Test-Path pnpm-lock.yaml) { $PKG_MGR = "pnpm" }
|
|
75
|
+
elseif (Test-Path yarn.lock) { $PKG_MGR = "yarn" }
|
|
76
|
+
elseif ((Test-Path bun.lockb) -or (Test-Path bun.lock)) { $PKG_MGR = "bun" }
|
|
77
|
+
elseif (Test-Path package-lock.json) { $PKG_MGR = "npm" }
|
|
78
|
+
else { $PKG_MGR = "npm" }
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Corresponding install command:
|
|
82
|
+
|
|
83
|
+
| Pkg mgr | Install dev dep | Run binary |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| npm | `npm install --save-dev <pkg>` | `npx <bin>` |
|
|
86
|
+
| pnpm | `pnpm add -D <pkg>` | `pnpm exec <bin>` or `pnpm dlx <bin>` |
|
|
87
|
+
| yarn | `yarn add -D <pkg>` | `yarn <bin>` |
|
|
88
|
+
| bun | `bun add -d <pkg>` | `bunx <bin>` |
|
|
89
|
+
|
|
90
|
+
### Step 3: Detect Playwright
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# bash
|
|
94
|
+
PW_BIN="npx --no-install playwright"
|
|
95
|
+
[ "$PKG_MGR" = "pnpm" ] && PW_BIN="pnpm exec playwright"
|
|
96
|
+
[ "$PKG_MGR" = "yarn" ] && PW_BIN="yarn playwright"
|
|
97
|
+
[ "$PKG_MGR" = "bun" ] && PW_BIN="bunx playwright"
|
|
98
|
+
|
|
99
|
+
if ! $PW_BIN --version >/dev/null 2>&1; then
|
|
100
|
+
PLAYWRIGHT_MISSING=1
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
# Check axe-core/playwright separately
|
|
104
|
+
if [ -f package.json ] && ! grep -q '@axe-core/playwright' package.json; then
|
|
105
|
+
AXE_MISSING=1
|
|
106
|
+
fi
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
```powershell
|
|
110
|
+
# PowerShell - simplified
|
|
111
|
+
$pwExists = $false
|
|
112
|
+
try {
|
|
113
|
+
$null = & npx --no-install playwright --version 2>$null
|
|
114
|
+
if ($LASTEXITCODE -eq 0) { $pwExists = $true }
|
|
115
|
+
} catch {}
|
|
116
|
+
if (-not $pwExists) { $env:PLAYWRIGHT_MISSING = "1" }
|
|
117
|
+
|
|
118
|
+
$axeExists = $false
|
|
119
|
+
if (Test-Path package.json) {
|
|
120
|
+
if (Select-String -Path package.json -Pattern '@axe-core/playwright' -Quiet) { $axeExists = $true }
|
|
121
|
+
}
|
|
122
|
+
if (-not $axeExists) { $env:AXE_MISSING = "1" }
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Step 4: If missing, ask consent + install
|
|
126
|
+
|
|
127
|
+
Use AskUserQuestion (or prompt fallback if runtime doesn't support it):
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
Playwright not installed in this project.
|
|
131
|
+
|
|
132
|
+
Install now?
|
|
133
|
+
- [Yes, install with Chromium] (~150MB, 2-5min, recommended)
|
|
134
|
+
- [Yes, install with all browsers] (~500MB, 5-10min)
|
|
135
|
+
- [No, skip gate 7 this time] (gate returns SKIPPED)
|
|
136
|
+
- [Cancel entire review]
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Choice mapping:
|
|
140
|
+
|
|
141
|
+
**Yes, Chromium:**
|
|
142
|
+
```bash
|
|
143
|
+
$INSTALL_CMD @playwright/test @axe-core/playwright
|
|
144
|
+
$PLAYWRIGHT_INSTALL chromium
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Where:
|
|
148
|
+
- `$INSTALL_CMD` = `pnpm add -D` / `npm install --save-dev` / etc based on PKG_MGR
|
|
149
|
+
- `$PLAYWRIGHT_INSTALL` = `$PW_BIN install`
|
|
150
|
+
|
|
151
|
+
**Yes, all browsers:**
|
|
152
|
+
```bash
|
|
153
|
+
$INSTALL_CMD @playwright/test @axe-core/playwright
|
|
154
|
+
$PLAYWRIGHT_INSTALL
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**No, skip:**
|
|
158
|
+
Returns `{ "status": "SKIPPED", "reason": "user declined Playwright install" }` - reviewer marks gate 7 as SKIPPED (not BLOCK).
|
|
159
|
+
|
|
160
|
+
**Cancel review:**
|
|
161
|
+
Returns error code, reviewer aborts.
|
|
162
|
+
|
|
163
|
+
### Step 5: Generate temporary Playwright spec
|
|
164
|
+
|
|
165
|
+
Creates `.jdi/cache/playwright-check.spec.js` (gitignored). Content:
|
|
166
|
+
|
|
167
|
+
```javascript
|
|
168
|
+
// Auto-generated by jdi-frontend-validator. DO NOT edit manually.
|
|
169
|
+
// @ts-check
|
|
170
|
+
const { test, expect } = require('@playwright/test');
|
|
171
|
+
const AxeBuilder = require('@axe-core/playwright').default;
|
|
172
|
+
const fs = require('fs');
|
|
173
|
+
const path = require('path');
|
|
174
|
+
|
|
175
|
+
const URL = process.env.JDI_FRONTEND_URL;
|
|
176
|
+
const ROUTES = (process.env.JDI_ROUTES || '/').split(',').map(r => r.trim()).filter(Boolean);
|
|
177
|
+
const OUT = process.env.JDI_OUT || '.jdi/cache/ui-findings.json';
|
|
178
|
+
const SCREENSHOT_DIR = process.env.JDI_SCREENSHOT_DIR || '.jdi/cache/screenshots';
|
|
179
|
+
|
|
180
|
+
const VIEWPORTS = [
|
|
181
|
+
{ name: 'mobile', width: 375, height: 667 },
|
|
182
|
+
{ name: 'desktop', width: 1280, height: 720 }
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
const findings = {
|
|
186
|
+
metadata: { url: URL, routes: ROUTES, timestamp: new Date().toISOString() },
|
|
187
|
+
console: [],
|
|
188
|
+
network: [],
|
|
189
|
+
a11y: [],
|
|
190
|
+
layout: [],
|
|
191
|
+
screenshots: [],
|
|
192
|
+
navigationFailures: []
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
for (const route of ROUTES) {
|
|
196
|
+
for (const viewport of VIEWPORTS) {
|
|
197
|
+
test(`${route} @ ${viewport.name}`, async ({ page }) => {
|
|
198
|
+
await page.setViewportSize({ width: viewport.width, height: viewport.height });
|
|
199
|
+
|
|
200
|
+
page.on('console', msg => {
|
|
201
|
+
if (msg.type() === 'error') {
|
|
202
|
+
findings.console.push({
|
|
203
|
+
route,
|
|
204
|
+
viewport: viewport.name,
|
|
205
|
+
text: msg.text(),
|
|
206
|
+
location: msg.location()
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
page.on('requestfailed', req => {
|
|
212
|
+
findings.network.push({
|
|
213
|
+
route,
|
|
214
|
+
viewport: viewport.name,
|
|
215
|
+
url: req.url(),
|
|
216
|
+
method: req.method(),
|
|
217
|
+
failure: req.failure()?.errorText,
|
|
218
|
+
severity: 'requestfailed'
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
page.on('response', res => {
|
|
223
|
+
if (res.status() >= 500) {
|
|
224
|
+
findings.network.push({
|
|
225
|
+
route,
|
|
226
|
+
viewport: viewport.name,
|
|
227
|
+
url: res.url(),
|
|
228
|
+
status: res.status(),
|
|
229
|
+
severity: '5xx'
|
|
230
|
+
});
|
|
231
|
+
} else if (res.status() >= 400 && res.status() !== 404) {
|
|
232
|
+
findings.network.push({
|
|
233
|
+
route,
|
|
234
|
+
viewport: viewport.name,
|
|
235
|
+
url: res.url(),
|
|
236
|
+
status: res.status(),
|
|
237
|
+
severity: '4xx'
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const targetUrl = `${URL}${route}`;
|
|
243
|
+
let response;
|
|
244
|
+
try {
|
|
245
|
+
response = await page.goto(targetUrl, { waitUntil: 'networkidle', timeout: 30000 });
|
|
246
|
+
} catch (err) {
|
|
247
|
+
findings.navigationFailures.push({
|
|
248
|
+
route,
|
|
249
|
+
viewport: viewport.name,
|
|
250
|
+
error: err.message
|
|
251
|
+
});
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (!response || !response.ok()) {
|
|
256
|
+
findings.navigationFailures.push({
|
|
257
|
+
route,
|
|
258
|
+
viewport: viewport.name,
|
|
259
|
+
status: response?.status() ?? 'no-response',
|
|
260
|
+
url: targetUrl
|
|
261
|
+
});
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Detect horizontal scroll
|
|
266
|
+
const hasHScroll = await page.evaluate(() => {
|
|
267
|
+
return document.documentElement.scrollWidth > document.documentElement.clientWidth + 1;
|
|
268
|
+
});
|
|
269
|
+
if (hasHScroll) {
|
|
270
|
+
findings.layout.push({
|
|
271
|
+
route,
|
|
272
|
+
viewport: viewport.name,
|
|
273
|
+
issue: 'horizontal_scroll'
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// axe-core a11y scan
|
|
278
|
+
try {
|
|
279
|
+
const axeResults = await new AxeBuilder({ page })
|
|
280
|
+
.withTags(['wcag2a', 'wcag2aa', 'wcag22aa', 'best-practice'])
|
|
281
|
+
.analyze();
|
|
282
|
+
|
|
283
|
+
for (const v of axeResults.violations) {
|
|
284
|
+
findings.a11y.push({
|
|
285
|
+
route,
|
|
286
|
+
viewport: viewport.name,
|
|
287
|
+
id: v.id,
|
|
288
|
+
impact: v.impact,
|
|
289
|
+
help: v.help,
|
|
290
|
+
helpUrl: v.helpUrl,
|
|
291
|
+
nodes: v.nodes.length,
|
|
292
|
+
sample: v.nodes.slice(0, 3).map(n => ({
|
|
293
|
+
target: n.target,
|
|
294
|
+
html: n.html.slice(0, 200)
|
|
295
|
+
}))
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
} catch (err) {
|
|
299
|
+
// axe-core failure doesn't block run
|
|
300
|
+
findings.a11y.push({
|
|
301
|
+
route,
|
|
302
|
+
viewport: viewport.name,
|
|
303
|
+
error: `axe-core failed: ${err.message}`
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Screenshot
|
|
308
|
+
const safeName = (route === '/' ? 'root' : route.replace(/^\//, '').replace(/\//g, '_'));
|
|
309
|
+
const screenshotPath = path.join(SCREENSHOT_DIR, `${safeName}_${viewport.name}.png`);
|
|
310
|
+
await page.screenshot({ path: screenshotPath, fullPage: true });
|
|
311
|
+
findings.screenshots.push({ route, viewport: viewport.name, path: screenshotPath });
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
test.afterAll(() => {
|
|
317
|
+
fs.writeFileSync(OUT, JSON.stringify(findings, null, 2));
|
|
318
|
+
});
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
And inline config `.jdi/cache/playwright.config.js`:
|
|
322
|
+
|
|
323
|
+
```javascript
|
|
324
|
+
module.exports = {
|
|
325
|
+
testDir: '.jdi/cache',
|
|
326
|
+
testMatch: 'playwright-check.spec.js',
|
|
327
|
+
timeout: 60000,
|
|
328
|
+
retries: 0,
|
|
329
|
+
workers: 1,
|
|
330
|
+
reporter: [['line']],
|
|
331
|
+
use: {
|
|
332
|
+
headless: true,
|
|
333
|
+
ignoreHTTPSErrors: true
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Step 6: Spawn dev server
|
|
339
|
+
|
|
340
|
+
```bash
|
|
341
|
+
# bash
|
|
342
|
+
DEV_LOG=.jdi/cache/dev-server.log
|
|
343
|
+
DEV_PID_FILE=.jdi/cache/dev-server.pid
|
|
344
|
+
|
|
345
|
+
# Spawn in background, redirecting log
|
|
346
|
+
nohup $DEV_COMMAND > $DEV_LOG 2>&1 &
|
|
347
|
+
echo $! > $DEV_PID_FILE
|
|
348
|
+
|
|
349
|
+
# Wait ready (poll URL, timeout 60s)
|
|
350
|
+
READY=0
|
|
351
|
+
for i in $(seq 1 60); do
|
|
352
|
+
if curl -sSf -o /dev/null --max-time 2 "$FRONTEND_URL"; then
|
|
353
|
+
READY=1
|
|
354
|
+
break
|
|
355
|
+
fi
|
|
356
|
+
sleep 1
|
|
357
|
+
done
|
|
358
|
+
|
|
359
|
+
if [ $READY -eq 0 ]; then
|
|
360
|
+
# Cleanup
|
|
361
|
+
kill $(cat $DEV_PID_FILE) 2>/dev/null
|
|
362
|
+
echo '{"status":"INCONCLUSIVE","reason":"dev server failed to start in 60s","logs":"'$DEV_LOG'"}' > .jdi/cache/ui-findings.json
|
|
363
|
+
exit 0 # doesn't fail the reviewer - INCONCLUSIVE is WARN
|
|
364
|
+
fi
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
```powershell
|
|
368
|
+
# PowerShell
|
|
369
|
+
$DEV_LOG = ".jdi/cache/dev-server.log"
|
|
370
|
+
$DEV_PID_FILE = ".jdi/cache/dev-server.pid"
|
|
371
|
+
|
|
372
|
+
$proc = Start-Process -FilePath pwsh -ArgumentList "-NoProfile", "-Command", $DEV_COMMAND `
|
|
373
|
+
-RedirectStandardOutput $DEV_LOG -RedirectStandardError $DEV_LOG `
|
|
374
|
+
-PassThru -WindowStyle Hidden
|
|
375
|
+
$proc.Id | Out-File -FilePath $DEV_PID_FILE
|
|
376
|
+
|
|
377
|
+
$ready = $false
|
|
378
|
+
for ($i = 0; $i -lt 60; $i++) {
|
|
379
|
+
try {
|
|
380
|
+
$r = Invoke-WebRequest -Uri $FRONTEND_URL -UseBasicParsing -TimeoutSec 2 -ErrorAction Stop
|
|
381
|
+
if ($r.StatusCode -eq 200) { $ready = $true; break }
|
|
382
|
+
} catch {}
|
|
383
|
+
Start-Sleep -Seconds 1
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (-not $ready) {
|
|
387
|
+
Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue
|
|
388
|
+
$err = @{ status="INCONCLUSIVE"; reason="dev server failed to start in 60s"; logs=$DEV_LOG } | ConvertTo-Json
|
|
389
|
+
$err | Out-File .jdi/cache/ui-findings.json
|
|
390
|
+
exit 0
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Step 7: Run Playwright
|
|
395
|
+
|
|
396
|
+
```bash
|
|
397
|
+
# bash
|
|
398
|
+
JDI_FRONTEND_URL="$FRONTEND_URL" \
|
|
399
|
+
JDI_ROUTES="$(echo $CRITICAL_PATHS | tr '\n' ',')" \
|
|
400
|
+
JDI_OUT=".jdi/cache/ui-findings.json" \
|
|
401
|
+
JDI_SCREENSHOT_DIR=".jdi/cache/screenshots" \
|
|
402
|
+
$PW_BIN test --config=.jdi/cache/playwright.config.js 2>&1 | tee .jdi/cache/playwright.log
|
|
403
|
+
|
|
404
|
+
# Playwright exit code doesn't matter - findings already written by afterAll
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
```powershell
|
|
408
|
+
# PowerShell
|
|
409
|
+
$env:JDI_FRONTEND_URL = $FRONTEND_URL
|
|
410
|
+
$env:JDI_ROUTES = ($CRITICAL_PATHS -join ',')
|
|
411
|
+
$env:JDI_OUT = ".jdi/cache/ui-findings.json"
|
|
412
|
+
$env:JDI_SCREENSHOT_DIR = ".jdi/cache/screenshots"
|
|
413
|
+
|
|
414
|
+
& npx playwright test --config=.jdi/cache/playwright.config.js 2>&1 | Tee-Object -FilePath .jdi/cache/playwright.log
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### Step 8: Kill dev server (always, even on failure)
|
|
418
|
+
|
|
419
|
+
```bash
|
|
420
|
+
# bash
|
|
421
|
+
if [ -f $DEV_PID_FILE ]; then
|
|
422
|
+
PID=$(cat $DEV_PID_FILE)
|
|
423
|
+
# Kill process + children (dev server usually has children: node, esbuild, vite, etc)
|
|
424
|
+
pkill -P $PID 2>/dev/null
|
|
425
|
+
kill $PID 2>/dev/null
|
|
426
|
+
# Safeguard on common ports (in case wrong PID)
|
|
427
|
+
# Vite: 5173, Next: 3000, etc - skip aggressive cleanup
|
|
428
|
+
rm $DEV_PID_FILE
|
|
429
|
+
fi
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
```powershell
|
|
433
|
+
# PowerShell
|
|
434
|
+
if (Test-Path $DEV_PID_FILE) {
|
|
435
|
+
$pid = Get-Content $DEV_PID_FILE
|
|
436
|
+
# Kill children first
|
|
437
|
+
Get-CimInstance Win32_Process -Filter "ParentProcessId=$pid" -ErrorAction SilentlyContinue |
|
|
438
|
+
ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
|
|
439
|
+
# Kill the parent
|
|
440
|
+
Stop-Process -Id $pid -Force -ErrorAction SilentlyContinue
|
|
441
|
+
Remove-Item $DEV_PID_FILE
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### Step 9: Return to parent reviewer
|
|
446
|
+
|
|
447
|
+
Skill doesn't write REVIEW.md. Only writes `.jdi/cache/ui-findings.json` + screenshots.
|
|
448
|
+
|
|
449
|
+
Reviewer reads the JSON, classifies severities, and writes "UI Validation" section in REVIEW.md.
|
|
450
|
+
|
|
451
|
+
## Finding classification (reference for the reviewer)
|
|
452
|
+
|
|
453
|
+
| Finding | Severity |
|
|
454
|
+
|---|---|
|
|
455
|
+
| `console.error` on any route | BLOCK |
|
|
456
|
+
| `network.5xx` on critical_path | BLOCK |
|
|
457
|
+
| `network.4xx` on critical_path | WARN |
|
|
458
|
+
| `network.requestfailed` (CORS/abort/etc) | WARN |
|
|
459
|
+
| `navigationFailures` (404/timeout/etc on critical_path) | BLOCK |
|
|
460
|
+
| `a11y.impact=critical` | BLOCK |
|
|
461
|
+
| `a11y.impact=serious` | BLOCK |
|
|
462
|
+
| `a11y.impact=moderate` | WARN |
|
|
463
|
+
| `a11y.impact=minor` | INFO |
|
|
464
|
+
| `layout.horizontal_scroll` on mobile | BLOCK |
|
|
465
|
+
| `layout.horizontal_scroll` on desktop | INFO |
|
|
466
|
+
| `axe-core failed` (technical error) | WARN |
|
|
467
|
+
| `INCONCLUSIVE` (dev server timeout) | WARN |
|
|
468
|
+
| `SKIPPED` (user declined install) | WARN |
|
|
469
|
+
|
|
470
|
+
## Expected inputs
|
|
471
|
+
|
|
472
|
+
From PROJECT.md (passed as environment variables by the reviewer):
|
|
473
|
+
- `frontend.frontend_url` -> `FRONTEND_URL`
|
|
474
|
+
- `frontend.dev_command` -> `DEV_COMMAND`
|
|
475
|
+
- `frontend.critical_paths` -> `CRITICAL_PATHS` (list)
|
|
476
|
+
|
|
477
|
+
## Outputs
|
|
478
|
+
|
|
479
|
+
Files created in `.jdi/cache/` (gitignored):
|
|
480
|
+
- `ui-findings.json` - structured findings
|
|
481
|
+
- `screenshots/*.png` - 1 per route x viewport
|
|
482
|
+
- `dev-server.log` - dev server log
|
|
483
|
+
- `playwright.log` - Playwright run log
|
|
484
|
+
- `playwright-check.spec.js` - generated spec
|
|
485
|
+
- `playwright.config.js` - generated config
|
|
486
|
+
|
|
487
|
+
NEVER commit `.jdi/cache/`.
|
|
488
|
+
|
|
489
|
+
## Anti-patterns
|
|
490
|
+
|
|
491
|
+
- Running against prod URL - dev local only. Prod is out of scope for this gate
|
|
492
|
+
- Testing flows that require login - MVP doesn't support auth setup. Critical paths must be public OR pre-authenticated manually (cookie/session passed via PROJECT.md in follow-up)
|
|
493
|
+
- Blocking review if Playwright install fails - degrade to SKIPPED
|
|
494
|
+
- Leaving dev server alive after gate - always kill, even on error
|
|
495
|
+
- Committing screenshots - .gitignore guaranteed in pre-flight
|
|
496
|
+
- Running parallel (workers > 1) - local dev server doesn't scale, and race conditions confuse findings
|
|
497
|
+
- Using `--headed` in CI - always headless
|
|
498
|
+
- Trusting Playwright exit code - findings come from afterAll, even with test failure
|
|
499
|
+
|
|
500
|
+
## References
|
|
501
|
+
|
|
502
|
+
- `references/playwright-setup.md` - Detailed install per package manager + troubleshoot
|
|
503
|
+
- `references/dev-server-detection.md` - Heuristics for detecting ready (curl, wait-on, polling)
|
|
504
|
+
- `references/axe-rules.md` - Mapping of axe rule IDs -> WCAG -> severity
|
|
505
|
+
- `references/auth-flows.md` - Roadmap for authenticated flows (future)
|
|
506
|
+
|
|
507
|
+
## Examples
|
|
508
|
+
|
|
509
|
+
### Example 1: Vite + React, Playwright missing, user accepts install
|
|
510
|
+
|
|
511
|
+
```
|
|
512
|
+
1. Reviewer triggers gate 7
|
|
513
|
+
2. Skill detects `npx playwright --version` -> exit 1
|
|
514
|
+
3. Lockfile = pnpm-lock.yaml -> PKG_MGR=pnpm
|
|
515
|
+
4. AskUserQuestion -> user picks "Yes, Chromium"
|
|
516
|
+
5. pnpm add -D @playwright/test @axe-core/playwright
|
|
517
|
+
6. pnpm exec playwright install chromium
|
|
518
|
+
7. Spawns `pnpm dev` in bg, PID 12345
|
|
519
|
+
8. Waits http://localhost:5173 -> ready in 4s
|
|
520
|
+
9. Runs Playwright on /, /login, /dashboard x mobile + desktop = 6 navigations
|
|
521
|
+
10. Findings:
|
|
522
|
+
- 1 console error (uncaught promise) on /dashboard mobile + desktop
|
|
523
|
+
- 0 network errors
|
|
524
|
+
- 2 a11y serious on /login (missing label + contrast)
|
|
525
|
+
- 1 horizontal scroll on /dashboard mobile
|
|
526
|
+
11. Kill PID 12345 + children
|
|
527
|
+
12. Writes .jdi/cache/ui-findings.json
|
|
528
|
+
13. Reviewer reads JSON, marks gate 7 = BLOCK (3 issues), writes REVIEW.md
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
### Example 2: API-only, has_frontend=false
|
|
532
|
+
|
|
533
|
+
Skill is not even loaded. Reviewer skips gate 7 with SKIPPED.
|
|
534
|
+
|
|
535
|
+
### Example 3: Dev server fails to start
|
|
536
|
+
|
|
537
|
+
```
|
|
538
|
+
1. Spawns `pnpm dev` -> process dies after 2s (port 5173 occupied)
|
|
539
|
+
2. Poll of 60s expires without 200 OK
|
|
540
|
+
3. Cleanup of PID
|
|
541
|
+
4. Writes {"status":"INCONCLUSIVE","reason":"dev server failed to start in 60s"}
|
|
542
|
+
5. Reviewer marks gate 7 = WARN with link to dev-server.log
|
|
543
|
+
6. Review not blocked, but user alerted
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
### Example 4: User declines to install Playwright
|
|
547
|
+
|
|
548
|
+
```
|
|
549
|
+
1. AskUserQuestion -> "No, skip gate 7"
|
|
550
|
+
2. Writes {"status":"SKIPPED","reason":"user declined Playwright install"}
|
|
551
|
+
3. Reviewer marks gate 7 = SKIPPED (warn not block)
|
|
552
|
+
4. REVIEW.md notes "UI Validation: SKIPPED - run /jdi-verify again when you accept installing"
|
|
553
|
+
```
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: kiss
|
|
3
|
+
description: KISS (Keep It Simple, Stupid). The simplest solution that solves the problem wins. Complexity only justified by real measured pain. Each layer/abstraction must pay its own cost. Applies in any language.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Skill: KISS
|
|
7
|
+
|
|
8
|
+
> Simplicity is the best design. Every complexity must pay its own cost.
|
|
9
|
+
|
|
10
|
+
KISS is not "dumb code". It's **rejecting unjustified complexity**. Every interface, every layer, every abstraction has maintenance cost — only worth it if it solves real pain.
|
|
11
|
+
|
|
12
|
+
## Rules
|
|
13
|
+
|
|
14
|
+
### 1. Default is the simplest
|
|
15
|
+
|
|
16
|
+
Ask before adding:
|
|
17
|
+
- **Function** vs class vs framework?
|
|
18
|
+
- **Variable** vs config vs feature flag?
|
|
19
|
+
- **If/else** vs strategy pattern vs plugin system?
|
|
20
|
+
- **Sync** vs async vs queue vs event bus?
|
|
21
|
+
- **Inline** vs helper vs lib?
|
|
22
|
+
|
|
23
|
+
Start with the leftmost. Only step up if there is a real requirement.
|
|
24
|
+
|
|
25
|
+
### 2. Complexity must justify pain
|
|
26
|
+
|
|
27
|
+
**Allowed:**
|
|
28
|
+
- New pattern if it has 3+ real cases using it
|
|
29
|
+
- Abstraction layer if it has 2+ implementations that exist today
|
|
30
|
+
- Cache if measurement shows hot path
|
|
31
|
+
- Async if there's unacceptable latency synchronous
|
|
32
|
+
- Plugin system if there are confirmed external extenders
|
|
33
|
+
|
|
34
|
+
**Forbidden:**
|
|
35
|
+
- "Will scale later" without current requirement
|
|
36
|
+
- "Other people might need it" without other people
|
|
37
|
+
- "To make it generic" without 2nd use case
|
|
38
|
+
- "Will look cleaner" trading 5 clear lines for 50 elegant ones
|
|
39
|
+
- Enterprise pattern in small codebase (Repository + UoW + Mediator + CQRS for 10-controller app)
|
|
40
|
+
|
|
41
|
+
### 3. Cognitive load is a real metric
|
|
42
|
+
|
|
43
|
+
Code you read 10x and write 1x. Optimize for reading:
|
|
44
|
+
- **Named variables** > composite expression
|
|
45
|
+
- **Early return** > nested if/else
|
|
46
|
+
- **Linear function** > jumps between callbacks
|
|
47
|
+
- **Explicit types** > magical inference in large codebase
|
|
48
|
+
- **Simple procedural code** > fancy OOP for 50 lines
|
|
49
|
+
|
|
50
|
+
Rule: code that needs a comment explaining "why so complex" is too complex.
|
|
51
|
+
|
|
52
|
+
### 4. Indicators of over-engineering
|
|
53
|
+
|
|
54
|
+
Signs the code went over the line:
|
|
55
|
+
|
|
56
|
+
- Interface with 1 implementation
|
|
57
|
+
- Factory/Builder for something instantiated 1x
|
|
58
|
+
- Generic <T> only used with 1 type
|
|
59
|
+
- Config with a key that never changed
|
|
60
|
+
- Abstraction layer that only encapsulates a call to another layer (pass-through)
|
|
61
|
+
- Inheritance hierarchy > 2 levels
|
|
62
|
+
- File with more setup than logic
|
|
63
|
+
- Test that needs 30 lines of mock to run 5 lines of logic
|
|
64
|
+
|
|
65
|
+
### 5. Refactor is the opposite direction
|
|
66
|
+
|
|
67
|
+
Natural tendency: code grows in complexity. Refactoring = REMOVE complexity that no longer pays.
|
|
68
|
+
|
|
69
|
+
Ask:
|
|
70
|
+
- Does this layer still exist to solve a problem, or did it become tradition?
|
|
71
|
+
- Does this abstraction have 2+ implementations today?
|
|
72
|
+
- If I delete this, what breaks?
|
|
73
|
+
- Can I solve it with 5 lines instead of 50?
|
|
74
|
+
|
|
75
|
+
## Anti-patterns
|
|
76
|
+
|
|
77
|
+
| Anti-pattern | Symptom |
|
|
78
|
+
|---|---|
|
|
79
|
+
| Interface + 1 implementation | `IUserService` + `UserService` (only 1) — delete the interface, use the class |
|
|
80
|
+
| Generic `<T>` used with 1 type | `Repository<User>` but never `Repository<Order>` — concretize |
|
|
81
|
+
| Factory for new() | `UserFactory.create()` that only does `return new User()` |
|
|
82
|
+
| Config string that never changed | `MAX_RETRIES: 3` in config + nobody ever changed it — hardcode |
|
|
83
|
+
| Inheritance > 2 levels | `BaseEntity -> AuditableEntity -> SoftDeletableEntity -> User` — flatten via composition |
|
|
84
|
+
| Pass-through layer | `Controller -> Service -> Repository -> DbContext` where Service only calls Repository without logic — delete Service |
|
|
85
|
+
| Enterprise pattern without demand | Mediator/CQRS in small app — replace with direct call |
|
|
86
|
+
| Comment explaining "why so complex" | Code lost the war — refactor |
|
|
87
|
+
| Mock setup > logic test | Test gets fragile; code under test is over-coupled |
|
|
88
|
+
| Unused future-proof params | `(opts?: { future?: boolean })` without caller passing — remove |
|
|
89
|
+
|
|
90
|
+
## Procedure
|
|
91
|
+
|
|
92
|
+
### Doer (before writing)
|
|
93
|
+
|
|
94
|
+
1. Ask: "What is the **simplest** version that meets the current requirement?"
|
|
95
|
+
2. Write that version.
|
|
96
|
+
3. Only step up complexity if you hit real pain.
|
|
97
|
+
4. After writing, ask: "Can I delete any layer/parameter/abstraction without losing functionality?"
|
|
98
|
+
|
|
99
|
+
### Reviewer (gate 5)
|
|
100
|
+
|
|
101
|
+
Over-engineering heuristics:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
# Interfaces with 1 implementation
|
|
105
|
+
grep -RnE '^(public |export )?interface I?[A-Z]\w+' src/ | while read iface; do
|
|
106
|
+
name=$(echo "$iface" | grep -oE '[A-Z]\w+\b' | head -1)
|
|
107
|
+
count=$(grep -RnE "class \w+\s*:\s*$name|implements $name" src/ | wc -l)
|
|
108
|
+
[[ $count -eq 1 ]] && echo "WARN: $iface has only 1 implementation"
|
|
109
|
+
done
|
|
110
|
+
|
|
111
|
+
# Deep inheritance (> 2 levels)
|
|
112
|
+
# (depends on stack — specific heuristic)
|
|
113
|
+
|
|
114
|
+
# Very large or nested functions
|
|
115
|
+
grep -cE '^\s{20,}\S' src/**/* # lines with 20+ spaces = deep nesting
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Match -> WARN with suggestion to simplify.
|
|
119
|
+
|
|
120
|
+
## Inputs
|
|
121
|
+
|
|
122
|
+
- Diff/content of the file
|
|
123
|
+
- Context: codebase size (over-engineering is relative)
|
|
124
|
+
|
|
125
|
+
## Outputs
|
|
126
|
+
|
|
127
|
+
Does NOT produce a file. Modifies judgement.
|
|
128
|
+
|
|
129
|
+
## Examples
|
|
130
|
+
|
|
131
|
+
### Example 1: Interface with 1 impl
|
|
132
|
+
|
|
133
|
+
Wrong:
|
|
134
|
+
```typescript
|
|
135
|
+
interface ILogger { log(msg: string): void }
|
|
136
|
+
class ConsoleLogger implements ILogger { log(msg) { console.log(msg) } }
|
|
137
|
+
const logger: ILogger = new ConsoleLogger()
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Right (KISS):
|
|
141
|
+
```typescript
|
|
142
|
+
function log(msg: string) { console.log(msg) }
|
|
143
|
+
// or
|
|
144
|
+
class Logger { static log(msg: string) { console.log(msg) } }
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Add interface when 2nd impl arrives, not before.
|
|
148
|
+
|
|
149
|
+
### Example 2: Pass-through service
|
|
150
|
+
|
|
151
|
+
Wrong:
|
|
152
|
+
```csharp
|
|
153
|
+
public class UserService {
|
|
154
|
+
public User GetById(int id) => _repo.GetById(id); // only calls repo
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Right: use `_repo` directly in the controller. Add Service when there is real logic (validation, multi-step, transaction, event).
|
|
159
|
+
|
|
160
|
+
### Example 3: Hardcodable config
|
|
161
|
+
|
|
162
|
+
Wrong: `appsettings.json -> "MaxItemsPerPage": 50` that nobody ever changed in 2 years.
|
|
163
|
+
|
|
164
|
+
Right: `const MAX_ITEMS_PER_PAGE = 50` in the code. Move back to config if some client actually needs to customize.
|