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,159 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: jdi-verify
|
|
3
|
+
description: Runs phase quality gates via reviewer specialist. Build, tests, coverage, lint, security checks. Verdict APPROVED / APPROVED_WITH_WARNINGS / BLOCKED.
|
|
4
|
+
argument_hint: "<phase_number>"
|
|
5
|
+
runtime_intent:
|
|
6
|
+
invokes_agent: dynamic
|
|
7
|
+
runtime_overrides:
|
|
8
|
+
claude:
|
|
9
|
+
allowed-tools: [Read, Bash, Grep, Glob, Agent]
|
|
10
|
+
copilot:
|
|
11
|
+
tools: [read, grep, glob, terminal]
|
|
12
|
+
opencode:
|
|
13
|
+
subtask: true
|
|
14
|
+
model: anthropic/claude-sonnet-4-20250514
|
|
15
|
+
antigravity:
|
|
16
|
+
triggers:
|
|
17
|
+
- "/jdi-verify"
|
|
18
|
+
- "verify phase {N}"
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
<objective>
|
|
22
|
+
Verifies the phase was delivered correctly. Runs gates defined in the project's reviewer specialist. Verdict blocks or releases the ship.
|
|
23
|
+
</objective>
|
|
24
|
+
|
|
25
|
+
<arguments>
|
|
26
|
+
- `phase_number` (required)
|
|
27
|
+
</arguments>
|
|
28
|
+
|
|
29
|
+
<process>
|
|
30
|
+
|
|
31
|
+
### Step 1: Validation
|
|
32
|
+
```bash
|
|
33
|
+
test -d .jdi/ || { echo "Not a JDI project."; exit 1; }
|
|
34
|
+
|
|
35
|
+
# Verify reviewer exists
|
|
36
|
+
ls .jdi/agents/jdi-reviewer-*.md 2>/dev/null | head -1 || {
|
|
37
|
+
echo "Reviewer missing. /jdi-bootstrap."
|
|
38
|
+
exit 1
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Verify phase was executed
|
|
42
|
+
ls .jdi/phases/{NN}*/SUMMARY.md 2>/dev/null || {
|
|
43
|
+
echo "Phase {N} not executed. /jdi-do {N}."
|
|
44
|
+
exit 1
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Context budget warm-up (does not block)
|
|
48
|
+
JDI_LIB="$(dirname "$(command -v jdi 2>/dev/null || echo /usr/local/bin/jdi)")/../lib"
|
|
49
|
+
if [ -f "$JDI_LIB/jdi-monitor.sh" ]; then
|
|
50
|
+
bash "$JDI_LIB/jdi-monitor.sh" .jdi/PROJECT.md .jdi/DECISIONS.md .jdi/phases/{NN}*/PLAN.md .jdi/phases/{NN}*/SUMMARY.md || true
|
|
51
|
+
fi
|
|
52
|
+
# Windows: pwsh -File "$JDI_LIB/jdi-monitor.ps1" -Paths @(...)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Step 2: Resolve reviewer specialist(s)
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
REVIEWERS=$(grep -oE 'jdi-reviewer-[a-z0-9-]+' .jdi/reviewers.md | sort -u)
|
|
59
|
+
REVIEWER_COUNT=$(echo "$REVIEWERS" | wc -l)
|
|
60
|
+
echo "Reviewers registered: $REVIEWER_COUNT"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Single-stack** (`REVIEWER_COUNT == 1`): one reviewer, normal flow.
|
|
64
|
+
**Multi-stack** (`REVIEWER_COUNT > 1`): chain reviewers in registry order. Each writes its own REVIEW segment; aggregate verdict = worst-case (1 BLOCK = overall BLOCK).
|
|
65
|
+
|
|
66
|
+
### Step 3: Spawn reviewer(s)
|
|
67
|
+
|
|
68
|
+
**Single-stack:**
|
|
69
|
+
```
|
|
70
|
+
Agent(
|
|
71
|
+
subagent_type="${REVIEWERS}",
|
|
72
|
+
description="Verify phase {N}",
|
|
73
|
+
prompt="phase={N}, mode=verify"
|
|
74
|
+
)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Multi-stack:** spawn each reviewer in sequence (NOT parallel — build/test commands may conflict on ports, locks, output dirs):
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
for REVIEWER in $REVIEWERS:
|
|
81
|
+
Agent(
|
|
82
|
+
subagent_type="$REVIEWER",
|
|
83
|
+
description="Verify phase {N} ({REVIEWER})",
|
|
84
|
+
prompt="phase={N}, mode=verify, reviewer_segment=${REVIEWER}"
|
|
85
|
+
)
|
|
86
|
+
# Each reviewer appends to .jdi/phases/{NN-slug}/REVIEW.md under section
|
|
87
|
+
# "## Reviewer: {REVIEWER}" with its own gate results and verdict
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Each reviewer scopes its gates to its `file_glob` (from frontmatter `scope.file_glob`). Coverage threshold enforced only on files matching the glob.
|
|
91
|
+
|
|
92
|
+
Reviewers are read-only. Wait for completion before next.
|
|
93
|
+
|
|
94
|
+
### Step 4: Read aggregate verdict
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
test -f .jdi/phases/{NN}*/REVIEW.md || { echo "REVIEW.md not created"; exit 1; }
|
|
98
|
+
|
|
99
|
+
# Collect all per-reviewer verdicts
|
|
100
|
+
VERDICTS=$(grep -oE 'Verdict:\*\* (APPROVED|APPROVED_WITH_WARNINGS|BLOCKED)' .jdi/phases/{NN}*/REVIEW.md | awk '{print $2}')
|
|
101
|
+
|
|
102
|
+
# Worst-case wins: BLOCK > WARNINGS > APPROVED
|
|
103
|
+
if echo "$VERDICTS" | grep -q BLOCKED; then
|
|
104
|
+
VERDICT=BLOCKED
|
|
105
|
+
elif echo "$VERDICTS" | grep -q APPROVED_WITH_WARNINGS; then
|
|
106
|
+
VERDICT=APPROVED_WITH_WARNINGS
|
|
107
|
+
else
|
|
108
|
+
VERDICT=APPROVED
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
# For single-stack, this collapses to the single reviewer's verdict — backward compatible.
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Step 5: Update STATE
|
|
115
|
+
|
|
116
|
+
```markdown
|
|
117
|
+
current_phase: {N}
|
|
118
|
+
phase_status: {verified|blocked}
|
|
119
|
+
phase_verdict: {APPROVED|APPROVED_WITH_WARNINGS|BLOCKED}
|
|
120
|
+
next_step: {if APPROVED or WITH_WARNINGS: /jdi-ship {N}; if BLOCKED: fix and /jdi-do {N} again}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
git add .jdi/phases/{NN-slug}/REVIEW.md .jdi/STATE.md
|
|
125
|
+
git commit -m "docs({NN-slug}): verify phase ({VERDICT})"
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Step 6: Confirm
|
|
129
|
+
|
|
130
|
+
**APPROVED:**
|
|
131
|
+
```
|
|
132
|
+
Phase {N}: APPROVED. Next: /jdi-ship {N}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**APPROVED_WITH_WARNINGS:**
|
|
136
|
+
```
|
|
137
|
+
Phase {N}: APPROVED_WITH_WARNINGS ({count} warnings).
|
|
138
|
+
REVIEW.md: .jdi/phases/{NN-slug}/REVIEW.md
|
|
139
|
+
Next: /jdi-ship {N} (or fix first)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**BLOCKED:**
|
|
143
|
+
```
|
|
144
|
+
Phase {N}: BLOCKED ({count} blockers). REVIEW.md: .jdi/phases/{NN-slug}/REVIEW.md
|
|
145
|
+
Fix → /jdi-do {N} → /jdi-verify {N}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
</process>
|
|
149
|
+
|
|
150
|
+
<gates>
|
|
151
|
+
- pre: SUMMARY.md exists + reviewer registered in .jdi/reviewers.md
|
|
152
|
+
- post: REVIEW.md created + STATE updated
|
|
153
|
+
</gates>
|
|
154
|
+
|
|
155
|
+
<errors>
|
|
156
|
+
- Reviewer missing -> /jdi-bootstrap
|
|
157
|
+
- SUMMARY missing -> /jdi-do
|
|
158
|
+
- Reviewer fails -> show error, keep state, suggest retry
|
|
159
|
+
</errors>
|
|
@@ -0,0 +1,169 @@
|
|
|
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
|
+
triggers:
|
|
5
|
+
- "KISS"
|
|
6
|
+
- "keep simple"
|
|
7
|
+
- "over-engineering"
|
|
8
|
+
- "simplicity"
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Skill: KISS
|
|
12
|
+
|
|
13
|
+
> Simplicity is the best design. Every complexity must pay its own cost.
|
|
14
|
+
|
|
15
|
+
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.
|
|
16
|
+
|
|
17
|
+
## Rules
|
|
18
|
+
|
|
19
|
+
### 1. Default is the simplest
|
|
20
|
+
|
|
21
|
+
Ask before adding:
|
|
22
|
+
- **Function** vs class vs framework?
|
|
23
|
+
- **Variable** vs config vs feature flag?
|
|
24
|
+
- **If/else** vs strategy pattern vs plugin system?
|
|
25
|
+
- **Sync** vs async vs queue vs event bus?
|
|
26
|
+
- **Inline** vs helper vs lib?
|
|
27
|
+
|
|
28
|
+
Start with the leftmost. Only step up if there is a real requirement.
|
|
29
|
+
|
|
30
|
+
### 2. Complexity must justify pain
|
|
31
|
+
|
|
32
|
+
**Allowed:**
|
|
33
|
+
- New pattern if it has 3+ real cases using it
|
|
34
|
+
- Abstraction layer if it has 2+ implementations that exist today
|
|
35
|
+
- Cache if measurement shows hot path
|
|
36
|
+
- Async if there's unacceptable latency synchronous
|
|
37
|
+
- Plugin system if there are confirmed external extenders
|
|
38
|
+
|
|
39
|
+
**Forbidden:**
|
|
40
|
+
- "Will scale later" without current requirement
|
|
41
|
+
- "Other people might need it" without other people
|
|
42
|
+
- "To make it generic" without 2nd use case
|
|
43
|
+
- "Will look cleaner" trading 5 clear lines for 50 elegant ones
|
|
44
|
+
- Enterprise pattern in small codebase (Repository + UoW + Mediator + CQRS for 10-controller app)
|
|
45
|
+
|
|
46
|
+
### 3. Cognitive load is a real metric
|
|
47
|
+
|
|
48
|
+
Code you read 10x and write 1x. Optimize for reading:
|
|
49
|
+
- **Named variables** > composite expression
|
|
50
|
+
- **Early return** > nested if/else
|
|
51
|
+
- **Linear function** > jumps between callbacks
|
|
52
|
+
- **Explicit types** > magical inference in large codebase
|
|
53
|
+
- **Simple procedural code** > fancy OOP for 50 lines
|
|
54
|
+
|
|
55
|
+
Rule: code that needs a comment explaining "why so complex" is too complex.
|
|
56
|
+
|
|
57
|
+
### 4. Indicators of over-engineering
|
|
58
|
+
|
|
59
|
+
Signs the code went over the line:
|
|
60
|
+
|
|
61
|
+
- Interface with 1 implementation
|
|
62
|
+
- Factory/Builder for something instantiated 1x
|
|
63
|
+
- Generic <T> only used with 1 type
|
|
64
|
+
- Config with a key that never changed
|
|
65
|
+
- Abstraction layer that only encapsulates a call to another layer (pass-through)
|
|
66
|
+
- Inheritance hierarchy > 2 levels
|
|
67
|
+
- File with more setup than logic
|
|
68
|
+
- Test that needs 30 lines of mock to run 5 lines of logic
|
|
69
|
+
|
|
70
|
+
### 5. Refactor is the opposite direction
|
|
71
|
+
|
|
72
|
+
Natural tendency: code grows in complexity. Refactoring = REMOVE complexity that no longer pays.
|
|
73
|
+
|
|
74
|
+
Ask:
|
|
75
|
+
- Does this layer still exist to solve a problem, or did it become tradition?
|
|
76
|
+
- Does this abstraction have 2+ implementations today?
|
|
77
|
+
- If I delete this, what breaks?
|
|
78
|
+
- Can I solve it with 5 lines instead of 50?
|
|
79
|
+
|
|
80
|
+
## Anti-patterns
|
|
81
|
+
|
|
82
|
+
| Anti-pattern | Symptom |
|
|
83
|
+
|---|---|
|
|
84
|
+
| Interface + 1 implementation | `IUserService` + `UserService` (only 1) — delete the interface, use the class |
|
|
85
|
+
| Generic `<T>` used with 1 type | `Repository<User>` but never `Repository<Order>` — concretize |
|
|
86
|
+
| Factory for new() | `UserFactory.create()` that only does `return new User()` |
|
|
87
|
+
| Config string that never changed | `MAX_RETRIES: 3` in config + nobody ever changed it — hardcode |
|
|
88
|
+
| Inheritance > 2 levels | `BaseEntity -> AuditableEntity -> SoftDeletableEntity -> User` — flatten via composition |
|
|
89
|
+
| Pass-through layer | `Controller -> Service -> Repository -> DbContext` where Service only calls Repository without logic — delete Service |
|
|
90
|
+
| Enterprise pattern without demand | Mediator/CQRS in small app — replace with direct call |
|
|
91
|
+
| Comment explaining "why so complex" | Code lost the war — refactor |
|
|
92
|
+
| Mock setup > logic test | Test gets fragile; code under test is over-coupled |
|
|
93
|
+
| Unused future-proof params | `(opts?: { future?: boolean })` without caller passing — remove |
|
|
94
|
+
|
|
95
|
+
## Procedure
|
|
96
|
+
|
|
97
|
+
### Doer (before writing)
|
|
98
|
+
|
|
99
|
+
1. Ask: "What is the **simplest** version that meets the current requirement?"
|
|
100
|
+
2. Write that version.
|
|
101
|
+
3. Only step up complexity if you hit real pain.
|
|
102
|
+
4. After writing, ask: "Can I delete any layer/parameter/abstraction without losing functionality?"
|
|
103
|
+
|
|
104
|
+
### Reviewer (gate 5)
|
|
105
|
+
|
|
106
|
+
Over-engineering heuristics:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# Interfaces with 1 implementation
|
|
110
|
+
grep -RnE '^(public |export )?interface I?[A-Z]\w+' src/ | while read iface; do
|
|
111
|
+
name=$(echo "$iface" | grep -oE '[A-Z]\w+\b' | head -1)
|
|
112
|
+
count=$(grep -RnE "class \w+\s*:\s*$name|implements $name" src/ | wc -l)
|
|
113
|
+
[[ $count -eq 1 ]] && echo "WARN: $iface has only 1 implementation"
|
|
114
|
+
done
|
|
115
|
+
|
|
116
|
+
# Deep inheritance (> 2 levels)
|
|
117
|
+
# (depends on stack — specific heuristic)
|
|
118
|
+
|
|
119
|
+
# Very large or nested functions
|
|
120
|
+
grep -cE '^\s{20,}\S' src/**/* # lines with 20+ spaces = deep nesting
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Match -> WARN with suggestion to simplify.
|
|
124
|
+
|
|
125
|
+
## Inputs
|
|
126
|
+
|
|
127
|
+
- Diff/content of the file
|
|
128
|
+
- Context: codebase size (over-engineering is relative)
|
|
129
|
+
|
|
130
|
+
## Outputs
|
|
131
|
+
|
|
132
|
+
Does NOT produce a file. Modifies judgement.
|
|
133
|
+
|
|
134
|
+
## Examples
|
|
135
|
+
|
|
136
|
+
### Example 1: Interface with 1 impl
|
|
137
|
+
|
|
138
|
+
Wrong:
|
|
139
|
+
```typescript
|
|
140
|
+
interface ILogger { log(msg: string): void }
|
|
141
|
+
class ConsoleLogger implements ILogger { log(msg) { console.log(msg) } }
|
|
142
|
+
const logger: ILogger = new ConsoleLogger()
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Right (KISS):
|
|
146
|
+
```typescript
|
|
147
|
+
function log(msg: string) { console.log(msg) }
|
|
148
|
+
// or
|
|
149
|
+
class Logger { static log(msg: string) { console.log(msg) } }
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Add interface when 2nd impl arrives, not before.
|
|
153
|
+
|
|
154
|
+
### Example 2: Pass-through service
|
|
155
|
+
|
|
156
|
+
Wrong:
|
|
157
|
+
```csharp
|
|
158
|
+
public class UserService {
|
|
159
|
+
public User GetById(int id) => _repo.GetById(id); // only calls repo
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Right: use `_repo` directly in the controller. Add Service when there is real logic (validation, multi-step, transaction, event).
|
|
164
|
+
|
|
165
|
+
### Example 3: Hardcodable config
|
|
166
|
+
|
|
167
|
+
Wrong: `appsettings.json -> "MaxItemsPerPage": 50` that nobody ever changed in 2 years.
|
|
168
|
+
|
|
169
|
+
Right: `const MAX_ITEMS_PER_PAGE = 50` in the code. Move back to config if some client actually needs to customize.
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: solid
|
|
3
|
+
description: SOLID. Robert C. Martin's 5 OO design principles - SRP, OCP, LSP, ISP, DIP. Applicable in any language with types/classes/interfaces (C#, Java, TS, Python, Go, Rust, Kotlin, Swift, etc). Direct summary + anti-patterns + detection heuristics.
|
|
4
|
+
triggers:
|
|
5
|
+
- "SOLID"
|
|
6
|
+
- "SOLID principles"
|
|
7
|
+
- "SRP OCP LSP ISP DIP"
|
|
8
|
+
- "OO design"
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Skill: SOLID
|
|
12
|
+
|
|
13
|
+
5 design principles — **S**ingle Responsibility, **O**pen/Closed, **L**iskov Substitution, **I**nterface Segregation, **D**ependency Inversion.
|
|
14
|
+
|
|
15
|
+
Applicable in any language with classes or interfaces. In FP/procedural, principles map to modules and functions (SRP: 1 function 1 responsibility; ISP: minimal parameters; DIP: dependency injection via parameter).
|
|
16
|
+
|
|
17
|
+
## S - Single Responsibility Principle (SRP)
|
|
18
|
+
|
|
19
|
+
> A class/module must have **1 reason to change**.
|
|
20
|
+
|
|
21
|
+
**1 reason = 1 stakeholder or 1 axis of change**, not "1 action".
|
|
22
|
+
|
|
23
|
+
`UserService` that does auth + persistence + sends email violates — there are 3 reasons to change (security team changes auth, DBA changes persistence, marketing changes email).
|
|
24
|
+
|
|
25
|
+
### Violation symptoms
|
|
26
|
+
- Class with generic name (`Manager`, `Helper`, `Service`, `Util`)
|
|
27
|
+
- Methods without thematic coherence
|
|
28
|
+
- Multiple imports of unrelated libraries (DB + email + crypto in the same class)
|
|
29
|
+
- Diff of one class affects separate domains in different sprints
|
|
30
|
+
|
|
31
|
+
### Fix
|
|
32
|
+
Extract responsibilities into separate classes:
|
|
33
|
+
- `UserAuthenticator` (auth)
|
|
34
|
+
- `UserRepository` (persistence)
|
|
35
|
+
- `UserNotifier` (email)
|
|
36
|
+
|
|
37
|
+
Compose in a coordinator if needed, but each piece has 1 reason.
|
|
38
|
+
|
|
39
|
+
## O - Open/Closed Principle (OCP)
|
|
40
|
+
|
|
41
|
+
> Open for **extension**, closed for **modification**.
|
|
42
|
+
|
|
43
|
+
Adding new behavior shouldn't require editing tested code. Use polymorphism, strategy, plugins, or config — not infinite if/else.
|
|
44
|
+
|
|
45
|
+
### Violation symptoms
|
|
46
|
+
- `switch (type)` that grows with every new feature
|
|
47
|
+
- `if (provider === "x") ... else if ... else if ...`
|
|
48
|
+
- Each new feature edits N existing classes instead of adding 1 new one
|
|
49
|
+
|
|
50
|
+
### Fix
|
|
51
|
+
- **Strategy pattern**: each case becomes an impl of an interface
|
|
52
|
+
- **Polymorphism**: subclass override instead of external switch
|
|
53
|
+
- **Registry**: `registry.register("x", handler)` — new feature just adds
|
|
54
|
+
- **Visitor**: for closed type hierarchies
|
|
55
|
+
|
|
56
|
+
### When to ignore
|
|
57
|
+
OCP is aspirational, not absolute. Apply at points of **known** variation (payment strategies are multiple), don't speculate (KISS + YAGNI).
|
|
58
|
+
|
|
59
|
+
## L - Liskov Substitution Principle (LSP)
|
|
60
|
+
|
|
61
|
+
> Subtype must be **substitutable** for the supertype without breaking behavior.
|
|
62
|
+
|
|
63
|
+
If `Bird` has method `fly()`, `Penguin extends Bird` violates LSP — penguin doesn't fly. Caller receiving `Bird` breaks when it receives `Penguin`.
|
|
64
|
+
|
|
65
|
+
### Violation symptoms
|
|
66
|
+
- Subclass that **throws exception** on inherited method ("not supported")
|
|
67
|
+
- Subclass that **strengthens precondition** (`base accepts >= 0`, sub accepts `> 0`)
|
|
68
|
+
- Subclass that **weakens postcondition** (base guarantees "sorted", sub doesn't guarantee)
|
|
69
|
+
- Caller needs `if (instanceof Subtype)` to handle a special case
|
|
70
|
+
|
|
71
|
+
### Fix
|
|
72
|
+
- Rethink hierarchy: `Penguin` isn't a flying `Bird`, it's a `Bird` that walks. Create `FlyingBird : Bird` and `Penguin : Bird` without fly.
|
|
73
|
+
- Composition over inheritance: prefer composed interfaces over deep hierarchies.
|
|
74
|
+
- Refactor to capabilities: `Flyable`, `Swimmable`, `Walkable` (overlap with ISP).
|
|
75
|
+
|
|
76
|
+
## I - Interface Segregation Principle (ISP)
|
|
77
|
+
|
|
78
|
+
> Clients should not be forced to depend on interfaces they **do not use**.
|
|
79
|
+
|
|
80
|
+
Big interfaces ("fat interfaces") force impls to stub meaningless methods (throwing NotImplemented), and callers to import broad dependencies.
|
|
81
|
+
|
|
82
|
+
### Violation symptoms
|
|
83
|
+
- Interface with 15 methods, each caller uses 2-3
|
|
84
|
+
- Impl with several methods throwing `throw new NotImplementedException()`
|
|
85
|
+
- Huge test mock to use 1 method of the interface
|
|
86
|
+
|
|
87
|
+
### Fix
|
|
88
|
+
Split into smaller, cohesive interfaces:
|
|
89
|
+
```
|
|
90
|
+
IFileReader { read() }
|
|
91
|
+
IFileWriter { write() }
|
|
92
|
+
// callers pick the subset they need
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
In FP/Go, same principle: minimal parameters, structural typing doesn't force implementing everything.
|
|
96
|
+
|
|
97
|
+
## D - Dependency Inversion Principle (DIP)
|
|
98
|
+
|
|
99
|
+
> High-level modules **do not** depend on low-level. Both depend on **abstractions**.
|
|
100
|
+
|
|
101
|
+
Business logic doesn't care about "PostgreSQL", "AWS S3", "SendGrid". It cares about abstractions (`UserRepository`, `BlobStorage`, `EmailSender`). Concrete implementations live in outer layers.
|
|
102
|
+
|
|
103
|
+
### Violation symptoms
|
|
104
|
+
- Domain class imports `pg`, `aws-sdk`, `sendgrid`, `axios`
|
|
105
|
+
- Business logic testable only with real infra (DB up, S3 mocked, etc)
|
|
106
|
+
- Swapping provider requires rewriting business logic
|
|
107
|
+
|
|
108
|
+
### Fix
|
|
109
|
+
- **Dependency injection** (constructor / property / parameter)
|
|
110
|
+
- Define interfaces in the domain module; impls live in infra/adapter layer (overlap with Hexagonal/Clean Architecture)
|
|
111
|
+
- Composition root injects the right impl
|
|
112
|
+
|
|
113
|
+
### DIP != IoC container
|
|
114
|
+
DIP is a principle. IoC container is **one** tool. You can apply DIP with manual constructor, simple factory, or parameter injection — no framework.
|
|
115
|
+
|
|
116
|
+
## Summary of the 5
|
|
117
|
+
|
|
118
|
+
| Letter | Focus | Key question |
|
|
119
|
+
|---|---|---|
|
|
120
|
+
| **S** | Unit cohesion | How many reasons to change this class/module? (>1 = violates) |
|
|
121
|
+
| **O** | Stability against extension | Does adding a new feature edit tested code or add new code? |
|
|
122
|
+
| **L** | Inheritance contract | Does replacing parent with child break any caller? |
|
|
123
|
+
| **I** | Interface size | Does every impl/caller use all methods? |
|
|
124
|
+
| **D** | Direction of dependency | Does domain import infra or does infra import domain? |
|
|
125
|
+
|
|
126
|
+
## General anti-patterns
|
|
127
|
+
|
|
128
|
+
| Anti-pattern | Principle violated |
|
|
129
|
+
|---|---|
|
|
130
|
+
| `God class` with 30+ heterogeneous methods | SRP |
|
|
131
|
+
| `switch (kind)` in N callers, grows with each feature | OCP |
|
|
132
|
+
| Subclass with `throw NotSupportedException()` | LSP |
|
|
133
|
+
| `IRepository` with 20 generic methods | ISP |
|
|
134
|
+
| Domain service importing concrete ORM | DIP |
|
|
135
|
+
| Inheritance > 3 levels | LSP + SRP (usually) |
|
|
136
|
+
| Constructor with 8+ parameters | SRP (too many responsibilities) |
|
|
137
|
+
| Util class with 50 unrelated functions | SRP |
|
|
138
|
+
|
|
139
|
+
## Procedure
|
|
140
|
+
|
|
141
|
+
### Doer
|
|
142
|
+
|
|
143
|
+
Before creating class/module/interface:
|
|
144
|
+
- **SRP**: describe in 1 sentence. If you need "and", split.
|
|
145
|
+
- **ISP**: list expected callers. Does each one need **all** methods? If not, split.
|
|
146
|
+
- **DIP**: what does this depend on? Concretions (DB, HTTP, FS) -> inject as abstraction.
|
|
147
|
+
|
|
148
|
+
Before subclassing:
|
|
149
|
+
- **LSP**: does substitution by parent work in all callers? If not, wrong hierarchy.
|
|
150
|
+
|
|
151
|
+
Before adding `if/switch` at a growing point:
|
|
152
|
+
- **OCP**: is strategy/registry worth it?
|
|
153
|
+
|
|
154
|
+
### Reviewer (gate 5)
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
# SRP — very large classes
|
|
158
|
+
find src/ -name '*.cs' -o -name '*.ts' -o -name '*.py' | while read f; do
|
|
159
|
+
loc=$(wc -l < "$f")
|
|
160
|
+
[[ $loc -gt 400 ]] && echo "WARN SRP: $f has $loc lines, possible god class"
|
|
161
|
+
done
|
|
162
|
+
|
|
163
|
+
# OCP — big switches on hot paths
|
|
164
|
+
grep -RnE 'switch\s*\(' src/ | head
|
|
165
|
+
# (manually: evaluate if it grows with each feature)
|
|
166
|
+
|
|
167
|
+
# LSP — NotImplemented in subclass
|
|
168
|
+
grep -RnE 'NotImplemented|UnsupportedOperation|throw new.*not (implemented|supported)' src/
|
|
169
|
+
|
|
170
|
+
# ISP — giant interfaces
|
|
171
|
+
grep -RnA50 '^(public |export )?interface' src/ | grep -cE '^\s+\w+\s*\(' | sort -rn
|
|
172
|
+
# count > 10 methods -> WARN
|
|
173
|
+
|
|
174
|
+
# DIP — domain module importing concretions
|
|
175
|
+
grep -RnE 'from.*pg|from.*aws-sdk|using Npgsql|using AWSSDK' src/domain/ src/core/
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Relevant match -> WARN with principle cited.
|
|
179
|
+
|
|
180
|
+
## Inputs
|
|
181
|
+
|
|
182
|
+
- Diff/content of the file
|
|
183
|
+
- Project structure (to detect domain vs infra)
|
|
184
|
+
|
|
185
|
+
## Outputs
|
|
186
|
+
|
|
187
|
+
Does NOT produce a file. Modifies judgement.
|
|
188
|
+
|
|
189
|
+
## Examples
|
|
190
|
+
|
|
191
|
+
### Example 1: SRP violated
|
|
192
|
+
|
|
193
|
+
Wrong:
|
|
194
|
+
```csharp
|
|
195
|
+
public class OrderService {
|
|
196
|
+
public Order Create(...) { /* validate + save + email + log + audit */ }
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
5 reasons to change.
|
|
201
|
+
|
|
202
|
+
Right:
|
|
203
|
+
```csharp
|
|
204
|
+
public class OrderValidator { }
|
|
205
|
+
public class OrderRepository { }
|
|
206
|
+
public class OrderNotifier { }
|
|
207
|
+
public class OrderService {
|
|
208
|
+
public Order Create(...) {
|
|
209
|
+
_validator.Validate(...)
|
|
210
|
+
var order = _repo.Save(...)
|
|
211
|
+
_notifier.Notify(order)
|
|
212
|
+
return order;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Example 2: OCP violated
|
|
218
|
+
|
|
219
|
+
Wrong:
|
|
220
|
+
```typescript
|
|
221
|
+
function calcDiscount(type: string, amount: number) {
|
|
222
|
+
if (type === "vip") return amount * 0.2
|
|
223
|
+
else if (type === "newcomer") return amount * 0.1
|
|
224
|
+
else if (type === "blackfriday") return amount * 0.5
|
|
225
|
+
return 0
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Each new type edits this function.
|
|
230
|
+
|
|
231
|
+
Right:
|
|
232
|
+
```typescript
|
|
233
|
+
const strategies: Record<string, (amount: number) => number> = {
|
|
234
|
+
vip: a => a * 0.2,
|
|
235
|
+
newcomer: a => a * 0.1,
|
|
236
|
+
blackfriday: a => a * 0.5,
|
|
237
|
+
}
|
|
238
|
+
function calcDiscount(type: string, amount: number) {
|
|
239
|
+
return strategies[type]?.(amount) ?? 0
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
New type: register in `strategies`, don't touch `calcDiscount`.
|
|
244
|
+
|
|
245
|
+
### Example 3: DIP violated
|
|
246
|
+
|
|
247
|
+
Wrong:
|
|
248
|
+
```python
|
|
249
|
+
# domain/order.py
|
|
250
|
+
import psycopg2
|
|
251
|
+
class OrderService:
|
|
252
|
+
def get(self, id):
|
|
253
|
+
conn = psycopg2.connect(...)
|
|
254
|
+
...
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Domain coupled to Postgres.
|
|
258
|
+
|
|
259
|
+
Right:
|
|
260
|
+
```python
|
|
261
|
+
# domain/order.py
|
|
262
|
+
class OrderService:
|
|
263
|
+
def __init__(self, repo: OrderRepository): # abstraction
|
|
264
|
+
self._repo = repo
|
|
265
|
+
def get(self, id): return self._repo.get(id)
|
|
266
|
+
|
|
267
|
+
# infra/postgres_order_repo.py
|
|
268
|
+
class PostgresOrderRepository(OrderRepository):
|
|
269
|
+
def get(self, id): ... # concrete impl
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Domain doesn't know Postgres exists.
|