@lvlup-sw/axiom 0.2.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/.claude-plugin/plugin.json +16 -0
- package/CLAUDE.md +26 -0
- package/LICENSE +21 -0
- package/package.json +37 -0
- package/skills/audit/SKILL.md +126 -0
- package/skills/audit/references/composition-guide.md +105 -0
- package/skills/backend-quality/SKILL.md +40 -0
- package/skills/backend-quality/references/deterministic-checks.md +151 -0
- package/skills/backend-quality/references/dimensions.md +206 -0
- package/skills/backend-quality/references/findings-format.md +61 -0
- package/skills/backend-quality/references/scoring-model.md +86 -0
- package/skills/critique/SKILL.md +132 -0
- package/skills/critique/references/dependency-patterns.md +319 -0
- package/skills/critique/references/solid-principles.md +359 -0
- package/skills/distill/SKILL.md +83 -0
- package/skills/distill/references/dead-code-patterns.md +152 -0
- package/skills/distill/references/simplification-guide.md +128 -0
- package/skills/harden/SKILL.md +161 -0
- package/skills/harden/references/error-patterns.md +180 -0
- package/skills/harden/references/resilience-checklist.md +82 -0
- package/skills/scan/SKILL.md +102 -0
- package/skills/scan/references/check-catalog.md +68 -0
- package/skills/verify/SKILL.md +102 -0
- package/skills/verify/references/contract-testing.md +185 -0
- package/skills/verify/references/test-antipatterns.md +161 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# Dependency Patterns Reference
|
|
2
|
+
|
|
3
|
+
Coupling metrics, dependency direction analysis, and circular dependency detection for architecture review.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Healthy vs Unhealthy Dependency Patterns
|
|
8
|
+
|
|
9
|
+
### Healthy: Inward-Pointing Dependencies
|
|
10
|
+
|
|
11
|
+
Dependencies flow from outer layers (infrastructure, I/O, frameworks) toward inner layers (domain, core business logic). The core has no knowledge of the outer layers.
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
+-------------------------------------------------------+
|
|
15
|
+
| Infrastructure |
|
|
16
|
+
| (HTTP, Database, Filesystem, External APIs) |
|
|
17
|
+
| |
|
|
18
|
+
| +-----------------------------------------------+ |
|
|
19
|
+
| | Application Layer | |
|
|
20
|
+
| | (Use Cases, Orchestration, Commands) | |
|
|
21
|
+
| | | |
|
|
22
|
+
| | +---------------------------------------+ | |
|
|
23
|
+
| | | Domain Core | | |
|
|
24
|
+
| | | (Entities, Value Objects, Rules) | | |
|
|
25
|
+
| | | | | |
|
|
26
|
+
| | | * No imports from outer layers | | |
|
|
27
|
+
| | | * Defines interfaces others impl | | |
|
|
28
|
+
| | +---------------------------------------+ | |
|
|
29
|
+
| | | |
|
|
30
|
+
| +-----------------------------------------------+ |
|
|
31
|
+
| |
|
|
32
|
+
+-------------------------------------------------------+
|
|
33
|
+
|
|
34
|
+
Dependency direction: Infrastructure --> Application --> Domain
|
|
35
|
+
(arrows point INWARD)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Characteristics of healthy patterns:**
|
|
39
|
+
- Domain core has zero imports from infrastructure or framework code
|
|
40
|
+
- Interfaces are defined in the domain, implemented in infrastructure
|
|
41
|
+
- The composition root (entry point) is the only place that wires concrete implementations
|
|
42
|
+
- Modules at the same layer communicate through well-defined interfaces
|
|
43
|
+
|
|
44
|
+
### Unhealthy: Outward-Pointing Dependencies
|
|
45
|
+
|
|
46
|
+
Domain or core modules import directly from infrastructure, creating tight coupling.
|
|
47
|
+
|
|
48
|
+
```text
|
|
49
|
+
+-------------------------------------------------------+
|
|
50
|
+
| Infrastructure |
|
|
51
|
+
| (HTTP, Database, Filesystem, External APIs) |
|
|
52
|
+
| |
|
|
53
|
+
| +-----------------------------------------------+ |
|
|
54
|
+
| | Application Layer | |
|
|
55
|
+
| | | |
|
|
56
|
+
| | +---------------------------------------+ | |
|
|
57
|
+
| | | Domain Core | | |
|
|
58
|
+
| | | | | |
|
|
59
|
+
| | | import { Pool } from 'pg' <---+---+---+----- VIOLATION
|
|
60
|
+
| | | import { S3 } from 'aws-sdk' <---+---+---+----- VIOLATION
|
|
61
|
+
| | | import { readFile } from 'fs' <---+---+---+----- VIOLATION
|
|
62
|
+
| | | | | |
|
|
63
|
+
| | +---------------------------------------+ | |
|
|
64
|
+
| | | |
|
|
65
|
+
| +-----------------------------------------------+ |
|
|
66
|
+
| |
|
|
67
|
+
+-------------------------------------------------------+
|
|
68
|
+
|
|
69
|
+
Dependency direction: Domain --> Infrastructure
|
|
70
|
+
(arrows point OUTWARD = unhealthy)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Symptoms of unhealthy patterns:**
|
|
74
|
+
- Business logic modules import database drivers, HTTP clients, or framework packages
|
|
75
|
+
- Changing an infrastructure library forces changes in domain code
|
|
76
|
+
- Unit-testing domain logic requires mocking infrastructure dependencies
|
|
77
|
+
- Module-level globals hold infrastructure state (lazy-init singletons)
|
|
78
|
+
|
|
79
|
+
### Unhealthy: Peer-to-Peer Coupling
|
|
80
|
+
|
|
81
|
+
Modules at the same layer bypass interfaces and depend on each other's internals.
|
|
82
|
+
|
|
83
|
+
```text
|
|
84
|
+
Module A <---------> Module B
|
|
85
|
+
| |
|
|
86
|
+
+-------> Module C <-+
|
|
87
|
+
|
|
|
88
|
+
+-------> Module A (circular!)
|
|
89
|
+
|
|
90
|
+
Every module knows about every other module's internals.
|
|
91
|
+
Changes propagate unpredictably.
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Coupling Metrics
|
|
97
|
+
|
|
98
|
+
### Afferent Coupling (Ca)
|
|
99
|
+
|
|
100
|
+
**Definition:** The number of external modules that depend on (import from) a given module.
|
|
101
|
+
|
|
102
|
+
**Interpretation:**
|
|
103
|
+
- High Ca = many dependents = this module is heavily relied upon
|
|
104
|
+
- Modules with high Ca should be very stable (changes break many consumers)
|
|
105
|
+
- If a high-Ca module is also frequently changing, it is a fragility risk
|
|
106
|
+
|
|
107
|
+
**How to measure:**
|
|
108
|
+
```text
|
|
109
|
+
For module M:
|
|
110
|
+
Ca(M) = count of unique modules that import from M
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Efferent Coupling (Ce)
|
|
114
|
+
|
|
115
|
+
**Definition:** The number of external modules that a given module depends on (imports).
|
|
116
|
+
|
|
117
|
+
**Interpretation:**
|
|
118
|
+
- High Ce = many dependencies = this module is vulnerable to upstream changes
|
|
119
|
+
- Modules with high Ce are hard to test in isolation
|
|
120
|
+
- High Ce in a core module signals possible DIP violation
|
|
121
|
+
|
|
122
|
+
**How to measure:**
|
|
123
|
+
```text
|
|
124
|
+
For module M:
|
|
125
|
+
Ce(M) = count of unique modules that M imports from
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Instability (I)
|
|
129
|
+
|
|
130
|
+
**Definition:** `I = Ce / (Ca + Ce)` where 0 means maximally stable and 1 means maximally unstable.
|
|
131
|
+
|
|
132
|
+
**Interpretation:**
|
|
133
|
+
|
|
134
|
+
| I value | Meaning | Expectation |
|
|
135
|
+
|---------|---------|-------------|
|
|
136
|
+
| I = 0 | Maximally stable | Many dependents, no dependencies. Hard to change. Should be abstract. |
|
|
137
|
+
| I = 1 | Maximally unstable | No dependents, many dependencies. Easy to change. Should be concrete. |
|
|
138
|
+
| 0 < I < 1 | Mixed | Assess whether stability matches the module's role. |
|
|
139
|
+
|
|
140
|
+
**The Stable Dependencies Principle:** Modules should depend only on modules that are more stable than themselves. An unstable module (I near 1) depending on another unstable module creates fragility chains.
|
|
141
|
+
|
|
142
|
+
```text
|
|
143
|
+
STABLE (I=0.1) <---- UNSTABLE (I=0.8) OK: unstable depends on stable
|
|
144
|
+
STABLE (I=0.1) ----> UNSTABLE (I=0.8) BAD: stable depends on unstable
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Abstractness (A)
|
|
148
|
+
|
|
149
|
+
**Definition:** `A = abstract_types / total_types` where 0 means fully concrete and 1 means fully abstract.
|
|
150
|
+
|
|
151
|
+
**Interpretation:**
|
|
152
|
+
- High abstractness = mostly interfaces, abstract classes, type definitions
|
|
153
|
+
- Low abstractness = mostly concrete implementations
|
|
154
|
+
|
|
155
|
+
### The Main Sequence
|
|
156
|
+
|
|
157
|
+
The ideal relationship between abstractness (A) and instability (I) follows the "main sequence" diagonal:
|
|
158
|
+
|
|
159
|
+
```text
|
|
160
|
+
A (Abstractness)
|
|
161
|
+
1 | Zone of .
|
|
162
|
+
| Uselessness .
|
|
163
|
+
| . Main Sequence
|
|
164
|
+
| . (A + I = 1)
|
|
165
|
+
| .
|
|
166
|
+
| .
|
|
167
|
+
| .
|
|
168
|
+
| . Zone of
|
|
169
|
+
| . Pain
|
|
170
|
+
| .
|
|
171
|
+
0 +---+---+---+---+----> I (Instability)
|
|
172
|
+
0 1
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
- **Zone of Pain (low A, low I):** Concrete and stable. Hard to change but heavily depended on. Database schemas, core utilities.
|
|
176
|
+
- **Zone of Uselessness (high A, high I):** Abstract and unstable. Interfaces nobody implements. Dead abstractions.
|
|
177
|
+
- **Main Sequence (A + I ~ 1):** Balanced. Stable modules are abstract; unstable modules are concrete.
|
|
178
|
+
|
|
179
|
+
**Distance from Main Sequence:** `D = |A + I - 1|` — closer to 0 is better. Modules with D > 0.5 deserve investigation.
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Circular Dependency Detection
|
|
184
|
+
|
|
185
|
+
### What Are Circular Dependencies?
|
|
186
|
+
|
|
187
|
+
A circular dependency exists when module A depends on module B, and module B (directly or transitively) depends back on module A.
|
|
188
|
+
|
|
189
|
+
### Types of Circular Dependencies
|
|
190
|
+
|
|
191
|
+
**Direct cycles:**
|
|
192
|
+
```text
|
|
193
|
+
A ----imports----> B
|
|
194
|
+
B ----imports----> A
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**Transitive cycles:**
|
|
198
|
+
```text
|
|
199
|
+
A ----imports----> B
|
|
200
|
+
B ----imports----> C
|
|
201
|
+
C ----imports----> A
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**Barrel-file-mediated cycles:**
|
|
205
|
+
```text
|
|
206
|
+
feature/index.ts re-exports from:
|
|
207
|
+
- feature/handler.ts
|
|
208
|
+
- feature/types.ts
|
|
209
|
+
|
|
210
|
+
feature/handler.ts imports from feature/index.ts
|
|
211
|
+
(to get types — but barrel re-exports handler too!)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Detection Approach
|
|
215
|
+
|
|
216
|
+
1. **Build the import graph:** Parse all source files, extract import/require statements, resolve to file paths
|
|
217
|
+
2. **Run cycle detection:** Apply depth-first search (DFS) with back-edge detection on the import graph
|
|
218
|
+
3. **Classify cycles:** Direct (2 modules), short transitive (3-4 modules), long transitive (5+ modules)
|
|
219
|
+
4. **Assess severity:**
|
|
220
|
+
- Direct cycles involving core/domain modules: **HIGH**
|
|
221
|
+
- Barrel-file-mediated cycles: **MEDIUM** (often accidental, easy to fix)
|
|
222
|
+
- Transitive cycles in leaf modules: **LOW**
|
|
223
|
+
|
|
224
|
+
### Remediation Strategies
|
|
225
|
+
|
|
226
|
+
| Pattern | Fix |
|
|
227
|
+
|---------|-----|
|
|
228
|
+
| Direct A <-> B cycle | Extract shared types into a third module C, both A and B depend on C |
|
|
229
|
+
| Barrel-file cycle | Use direct imports instead of barrel re-exports |
|
|
230
|
+
| Transitive cycle through shared state | Introduce an event bus or mediator pattern |
|
|
231
|
+
| Cycle caused by type imports only | Use `import type` (TypeScript) to break the runtime cycle |
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Layered Architecture Violations
|
|
236
|
+
|
|
237
|
+
### What Constitutes a Layer
|
|
238
|
+
|
|
239
|
+
A layered architecture organizes code into horizontal layers with strict dependency rules:
|
|
240
|
+
|
|
241
|
+
```text
|
|
242
|
+
+-------------------------------------------------------+
|
|
243
|
+
| Infrastructure | | Presentation |
|
|
244
|
+
| (DB, FS, APIs) | | (routes, controllers) |
|
|
245
|
+
+----------|-----+--------------+-----|------------------+
|
|
246
|
+
| |
|
|
247
|
+
v v
|
|
248
|
+
+-----------------------------------------------+
|
|
249
|
+
| Application |
|
|
250
|
+
| (use cases, orchestrators, commands) |
|
|
251
|
+
+----------------------|------------------------+
|
|
252
|
+
|
|
|
253
|
+
v
|
|
254
|
+
+-----------------------------------------------+
|
|
255
|
+
| Domain (core) |
|
|
256
|
+
| (entities, value objects, domain services) |
|
|
257
|
+
+-----------------------------------------------+
|
|
258
|
+
|
|
259
|
+
Arrows = dependency direction (pointing INWARD toward the core).
|
|
260
|
+
Infrastructure and Presentation both depend inward on Application/Domain.
|
|
261
|
+
Domain (core) NEVER imports from any outer layer.
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Common Layer Violations
|
|
265
|
+
|
|
266
|
+
| Violation | Signal | Severity |
|
|
267
|
+
|-----------|--------|----------|
|
|
268
|
+
| Domain imports infrastructure | `import { query } from '../db'` in a domain file | **HIGH** |
|
|
269
|
+
| Application imports presentation | Use case module imports an HTTP request type | **MEDIUM** |
|
|
270
|
+
| Skip-layer dependency | Presentation directly calls infrastructure, bypassing application | **MEDIUM** |
|
|
271
|
+
| Bidirectional layer dependency | Application imports domain AND domain imports application | **HIGH** |
|
|
272
|
+
|
|
273
|
+
### How to Detect Layer Violations
|
|
274
|
+
|
|
275
|
+
1. **Establish layer boundaries:** Map directories to layers (e.g., `src/domain/`, `src/infra/`, `src/app/`)
|
|
276
|
+
2. **Build import graph with layer annotations:** Tag each module with its layer
|
|
277
|
+
3. **Check direction:** For each import, verify the importing module's layer is the same or closer to the periphery than the imported module's layer
|
|
278
|
+
4. **Flag violations:** Any import pointing outward (from a core layer to a peripheral layer) is a violation
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Dependency Inversion in Practice
|
|
283
|
+
|
|
284
|
+
### When to Use Interfaces vs Direct Dependencies
|
|
285
|
+
|
|
286
|
+
**Use an interface (abstraction boundary) when:**
|
|
287
|
+
- The dependency crosses an architectural layer boundary (e.g., application -> infrastructure)
|
|
288
|
+
- You need to swap implementations (testing, multi-environment, migration)
|
|
289
|
+
- The dependency is volatile (external API, third-party library likely to change)
|
|
290
|
+
- Multiple implementations exist or are planned (e.g., FileStorage: local, S3, GCS)
|
|
291
|
+
|
|
292
|
+
**Use a direct dependency when:**
|
|
293
|
+
- Both modules are in the same layer and same bounded context
|
|
294
|
+
- The dependency is a stable, well-tested utility (e.g., a date library, a hash function)
|
|
295
|
+
- The abstraction would be a 1:1 mirror of the concrete API (pointless indirection)
|
|
296
|
+
- The module is a pure function or value object with no side effects
|
|
297
|
+
|
|
298
|
+
### Practical Decision Flowchart
|
|
299
|
+
|
|
300
|
+
```text
|
|
301
|
+
Does the dependency cross a layer boundary?
|
|
302
|
+
| |
|
|
303
|
+
YES NO
|
|
304
|
+
| |
|
|
305
|
+
Use interface Is the dependency volatile?
|
|
306
|
+
| |
|
|
307
|
+
YES NO
|
|
308
|
+
| |
|
|
309
|
+
Use interface Direct dependency is fine
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Common Anti-Patterns
|
|
313
|
+
|
|
314
|
+
| Anti-Pattern | Description | Fix |
|
|
315
|
+
|-------------|-------------|-----|
|
|
316
|
+
| Interface mirroring | Interface is an exact copy of one concrete class | Remove interface, use direct dependency |
|
|
317
|
+
| God interface | One interface with 15+ methods for all possible operations | Split into role-specific interfaces (ISP) |
|
|
318
|
+
| Premature abstraction | Interface created "just in case" with only one implementation | Remove until a second implementation materializes |
|
|
319
|
+
| Leaky abstraction | Interface exposes implementation details (SQL in method names, HTTP status codes in domain interface) | Redesign interface using domain language |
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
# SOLID Principles Reference
|
|
2
|
+
|
|
3
|
+
Detection heuristics and severity guidance for agent-driven architectural assessment.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## S — Single Responsibility Principle (SRP)
|
|
8
|
+
|
|
9
|
+
### Definition
|
|
10
|
+
|
|
11
|
+
A module should have one, and only one, reason to change. Each class or module should encapsulate a single concern so that a change in one business requirement affects only one module.
|
|
12
|
+
|
|
13
|
+
### Violation Signals
|
|
14
|
+
|
|
15
|
+
1. A class or module handles both data persistence AND business logic
|
|
16
|
+
2. A single file contains multiple unrelated public interfaces or exported functions
|
|
17
|
+
3. The module name contains "And" or "Manager" or "Helper" (symptom of mixed concerns)
|
|
18
|
+
4. More than 3 constructor/initialization parameters from different domains
|
|
19
|
+
5. The module is modified in nearly every feature branch (high churn across unrelated features)
|
|
20
|
+
|
|
21
|
+
### Severity Guide
|
|
22
|
+
|
|
23
|
+
| Severity | When to assign |
|
|
24
|
+
|----------|---------------|
|
|
25
|
+
| **HIGH** | Module mixes I/O (network, filesystem, database) with core business rules. Changes to infrastructure force changes to domain logic. |
|
|
26
|
+
| **MEDIUM** | Module handles 2-3 related but distinct concerns (e.g., validation + transformation). Concerns could be separated but aren't yet causing bugs. |
|
|
27
|
+
| **LOW** | Module has a slightly broad scope but concerns are closely related. Separation would be premature. |
|
|
28
|
+
|
|
29
|
+
### Code Examples
|
|
30
|
+
|
|
31
|
+
**Violation:**
|
|
32
|
+
```
|
|
33
|
+
class OrderService {
|
|
34
|
+
validateOrder(order) { ... } // business rule
|
|
35
|
+
calculateTotal(order) { ... } // business rule
|
|
36
|
+
saveToDatabase(order) { ... } // persistence
|
|
37
|
+
sendConfirmationEmail(order) { ... } // notification
|
|
38
|
+
generatePdfInvoice(order) { ... } // rendering
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Healthy alternative:**
|
|
43
|
+
```
|
|
44
|
+
class OrderValidator {
|
|
45
|
+
validate(order) { ... }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
class OrderCalculator {
|
|
49
|
+
calculateTotal(order) { ... }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class OrderRepository {
|
|
53
|
+
save(order) { ... }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
class OrderNotifier {
|
|
57
|
+
sendConfirmation(order) { ... }
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Detection Heuristics
|
|
62
|
+
|
|
63
|
+
- Count the number of distinct "domains" imported by a module (e.g., database, HTTP, email, file system). More than 2 suggests SRP violation.
|
|
64
|
+
- Check if removing one public method would make half the imports unnecessary. If so, the method belongs elsewhere.
|
|
65
|
+
- Look for methods that could be tested independently with completely different mock setups — they likely belong in different modules.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## O — Open/Closed Principle (OCP)
|
|
70
|
+
|
|
71
|
+
### Definition
|
|
72
|
+
|
|
73
|
+
Software entities should be open for extension but closed for modification. You should be able to add new behavior without changing existing, tested code.
|
|
74
|
+
|
|
75
|
+
### Violation Signals
|
|
76
|
+
|
|
77
|
+
1. Adding a new variant requires modifying an existing `switch` or `if/else` chain
|
|
78
|
+
2. A function has a growing list of type-check branches (`if (type === "A") ... else if (type === "B") ...`)
|
|
79
|
+
3. Feature additions consistently require editing the same core file
|
|
80
|
+
4. Configuration objects grow new boolean flags for each new feature
|
|
81
|
+
5. Functions accept a `type` or `kind` string and branch on its value
|
|
82
|
+
|
|
83
|
+
### Severity Guide
|
|
84
|
+
|
|
85
|
+
| Severity | When to assign |
|
|
86
|
+
|----------|---------------|
|
|
87
|
+
| **HIGH** | Every new feature requires modifying a critical-path module (e.g., event dispatcher, router). Risk of regression in existing behavior with each change. |
|
|
88
|
+
| **MEDIUM** | A switch/case or if/else chain has 5+ branches and is growing. Extension is possible but requires touching stable code. |
|
|
89
|
+
| **LOW** | A conditional has 2-3 branches in a non-critical path. The branching is manageable and unlikely to grow further. |
|
|
90
|
+
|
|
91
|
+
### Code Examples
|
|
92
|
+
|
|
93
|
+
**Violation:**
|
|
94
|
+
```
|
|
95
|
+
function calculateDiscount(customer) {
|
|
96
|
+
if (customer.type === "regular") {
|
|
97
|
+
return 0.05
|
|
98
|
+
} else if (customer.type === "premium") {
|
|
99
|
+
return 0.10
|
|
100
|
+
} else if (customer.type === "vip") {
|
|
101
|
+
return 0.20
|
|
102
|
+
}
|
|
103
|
+
// Adding a new customer type means editing this function
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Healthy alternative:**
|
|
108
|
+
```
|
|
109
|
+
// Strategy pattern — new types are added without modifying existing code
|
|
110
|
+
const discountStrategies = {
|
|
111
|
+
regular: () => 0.05,
|
|
112
|
+
premium: () => 0.10,
|
|
113
|
+
vip: () => 0.20,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function calculateDiscount(customer) {
|
|
117
|
+
const strategy = discountStrategies[customer.type]
|
|
118
|
+
return strategy ? strategy() : 0
|
|
119
|
+
}
|
|
120
|
+
// New types: just add an entry to the map
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Detection Heuristics
|
|
124
|
+
|
|
125
|
+
- Search for `switch` statements on a `type` or `kind` field — count the branches. Five or more suggests OCP violation.
|
|
126
|
+
- Check git history: if the same file is modified in >50% of feature branches, it may be closed to extension.
|
|
127
|
+
- Look for "registry" or "map" patterns that could replace branching logic.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## L — Liskov Substitution Principle (LSP)
|
|
132
|
+
|
|
133
|
+
### Definition
|
|
134
|
+
|
|
135
|
+
Subtypes must be substitutable for their base types without altering the correctness of the program. If a function works with a base type, it must work identically with any derived type.
|
|
136
|
+
|
|
137
|
+
### Violation Signals
|
|
138
|
+
|
|
139
|
+
1. A subclass throws an exception for a method the base class supports
|
|
140
|
+
2. A subclass overrides a method to do nothing (empty override or no-op)
|
|
141
|
+
3. A function checks `instanceof` or the concrete type before calling a method
|
|
142
|
+
4. A subclass narrows the accepted input range or widens the output range beyond the base contract
|
|
143
|
+
5. Documentation says "do not call X on this subtype" — a direct substitutability violation
|
|
144
|
+
|
|
145
|
+
### Severity Guide
|
|
146
|
+
|
|
147
|
+
| Severity | When to assign |
|
|
148
|
+
|----------|---------------|
|
|
149
|
+
| **HIGH** | Substituting the subtype causes runtime errors, data corruption, or silently wrong results. Code contains `instanceof` guards to work around the violation. |
|
|
150
|
+
| **MEDIUM** | Subtype behaves differently in edge cases that callers may not handle. No runtime error, but correctness depends on knowing the concrete type. |
|
|
151
|
+
| **LOW** | Subtype overrides behavior in a benign way (e.g., logging or metrics) that does not affect correctness. |
|
|
152
|
+
|
|
153
|
+
### Code Examples
|
|
154
|
+
|
|
155
|
+
**Violation:**
|
|
156
|
+
```
|
|
157
|
+
class Bird {
|
|
158
|
+
fly() { return "flying" }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
class Penguin extends Bird {
|
|
162
|
+
fly() { throw new Error("Penguins cannot fly") }
|
|
163
|
+
// Violates LSP: callers expecting Bird.fly() to work will crash
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function makeBirdFly(bird) {
|
|
167
|
+
// Forced to add a type check — LSP violation symptom
|
|
168
|
+
if (bird instanceof Penguin) {
|
|
169
|
+
return bird.swim()
|
|
170
|
+
}
|
|
171
|
+
return bird.fly()
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**Healthy alternative:**
|
|
176
|
+
```
|
|
177
|
+
interface Movable {
|
|
178
|
+
move(): string
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
class Sparrow implements Movable {
|
|
182
|
+
move() { return "flying" }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
class Penguin implements Movable {
|
|
186
|
+
move() { return "swimming" }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function makeAnimalMove(animal: Movable) {
|
|
190
|
+
return animal.move() // Works for all implementations
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Detection Heuristics
|
|
195
|
+
|
|
196
|
+
- Search for `instanceof` checks in functions that accept a base type — this is the classic LSP smell.
|
|
197
|
+
- Look for empty method overrides or methods that throw `NotImplementedError` / `UnsupportedOperationError`.
|
|
198
|
+
- Check if any subclass method has a comment like "not applicable" or "unused".
|
|
199
|
+
- Look for type narrowing (`as ConcreteType`) immediately after receiving a base-typed parameter.
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## I — Interface Segregation Principle (ISP)
|
|
204
|
+
|
|
205
|
+
### Definition
|
|
206
|
+
|
|
207
|
+
No client should be forced to depend on methods it does not use. Interfaces should be small and focused so that implementing classes are not burdened with irrelevant obligations.
|
|
208
|
+
|
|
209
|
+
### Violation Signals
|
|
210
|
+
|
|
211
|
+
1. An interface has more than 7-8 methods (likely too broad)
|
|
212
|
+
2. Implementing classes leave methods as no-ops or throw "not supported"
|
|
213
|
+
3. A single interface is imported by clients that only use a subset of its methods
|
|
214
|
+
4. Interface changes force updates in modules that don't use the changed method
|
|
215
|
+
5. Parameters typed as a broad interface when only 1-2 properties are accessed
|
|
216
|
+
|
|
217
|
+
### Severity Guide
|
|
218
|
+
|
|
219
|
+
| Severity | When to assign |
|
|
220
|
+
|----------|---------------|
|
|
221
|
+
| **HIGH** | A broad interface forces implementors to provide dangerous no-op stubs for critical operations (e.g., `delete()` that silently does nothing). Clients may call methods that appear supported but aren't. |
|
|
222
|
+
| **MEDIUM** | Interface is large (8+ methods) and implementors stub out 2-3 methods. No safety risk but increases maintenance burden. |
|
|
223
|
+
| **LOW** | Interface is slightly broad but all implementors genuinely use most methods. Splitting would add complexity without clear benefit. |
|
|
224
|
+
|
|
225
|
+
### Code Examples
|
|
226
|
+
|
|
227
|
+
**Violation:**
|
|
228
|
+
```
|
|
229
|
+
interface Worker {
|
|
230
|
+
work(): void
|
|
231
|
+
eat(): void
|
|
232
|
+
sleep(): void
|
|
233
|
+
attendMeeting(): void
|
|
234
|
+
writeReport(): void
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
class Robot implements Worker {
|
|
238
|
+
work() { /* ... */ }
|
|
239
|
+
eat() { /* no-op — robots don't eat */ }
|
|
240
|
+
sleep() { /* no-op */ }
|
|
241
|
+
attendMeeting() { /* no-op */ }
|
|
242
|
+
writeReport() { /* ... */ }
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**Healthy alternative:**
|
|
247
|
+
```
|
|
248
|
+
interface Workable {
|
|
249
|
+
work(): void
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
interface Reportable {
|
|
253
|
+
writeReport(): void
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
interface HumanNeeds {
|
|
257
|
+
eat(): void
|
|
258
|
+
sleep(): void
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
class Robot implements Workable, Reportable {
|
|
262
|
+
work() { /* ... */ }
|
|
263
|
+
writeReport() { /* ... */ }
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
class HumanWorker implements Workable, Reportable, HumanNeeds {
|
|
267
|
+
work() { /* ... */ }
|
|
268
|
+
writeReport() { /* ... */ }
|
|
269
|
+
eat() { /* ... */ }
|
|
270
|
+
sleep() { /* ... */ }
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Detection Heuristics
|
|
275
|
+
|
|
276
|
+
- Count methods per interface. More than 7 is a signal worth investigating.
|
|
277
|
+
- Search for empty method bodies or `throw new Error("Not implemented")` in classes implementing an interface.
|
|
278
|
+
- Check if any implementation only uses <50% of the interface methods meaningfully.
|
|
279
|
+
- Look for function parameters typed with a broad interface where only 1-2 fields/methods are accessed in the body.
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## D — Dependency Inversion Principle (DIP)
|
|
284
|
+
|
|
285
|
+
### Definition
|
|
286
|
+
|
|
287
|
+
High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details — details should depend on abstractions.
|
|
288
|
+
|
|
289
|
+
### Violation Signals
|
|
290
|
+
|
|
291
|
+
1. A domain/business-logic module directly imports a database driver, HTTP client, or file system module
|
|
292
|
+
2. Constructor creates its own dependencies instead of receiving them (no dependency injection)
|
|
293
|
+
3. Module-level `import` of a concrete implementation where an interface/type would suffice
|
|
294
|
+
4. A core module imports from an `infrastructure/`, `adapters/`, or `io/` directory
|
|
295
|
+
5. Changing a database library requires editing business logic files
|
|
296
|
+
|
|
297
|
+
### Severity Guide
|
|
298
|
+
|
|
299
|
+
| Severity | When to assign |
|
|
300
|
+
|----------|---------------|
|
|
301
|
+
| **HIGH** | Core business logic directly instantiates or imports infrastructure (database, network, filesystem). Impossible to test without real infrastructure or heavy mocking. |
|
|
302
|
+
| **MEDIUM** | A module depends on a concrete implementation but the dependency is injected (not self-created). The direction is wrong but testability is preserved. |
|
|
303
|
+
| **LOW** | A utility module depends on a specific library for convenience. The dependency is isolated and easily swappable. |
|
|
304
|
+
|
|
305
|
+
### Code Examples
|
|
306
|
+
|
|
307
|
+
**Violation:**
|
|
308
|
+
```
|
|
309
|
+
// Domain module directly importing infrastructure
|
|
310
|
+
import { Pool } from 'pg'
|
|
311
|
+
import { S3Client } from '@aws-sdk/client-s3'
|
|
312
|
+
|
|
313
|
+
class OrderService {
|
|
314
|
+
private db = new Pool({ connectionString: process.env.DB_URL })
|
|
315
|
+
private s3 = new S3Client({ region: 'us-east-1' })
|
|
316
|
+
|
|
317
|
+
async createOrder(data) {
|
|
318
|
+
// Business logic tightly coupled to Postgres and S3
|
|
319
|
+
await this.db.query('INSERT INTO orders ...', [data])
|
|
320
|
+
await this.s3.send(new PutObjectCommand({ ... }))
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
**Healthy alternative:**
|
|
326
|
+
```
|
|
327
|
+
// Domain depends on abstractions
|
|
328
|
+
interface OrderRepository {
|
|
329
|
+
save(order: Order): Promise<void>
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
interface FileStorage {
|
|
333
|
+
upload(key: string, data: Buffer): Promise<void>
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
class OrderService {
|
|
337
|
+
constructor(
|
|
338
|
+
private repo: OrderRepository,
|
|
339
|
+
private storage: FileStorage,
|
|
340
|
+
) {}
|
|
341
|
+
|
|
342
|
+
async createOrder(data) {
|
|
343
|
+
const order = Order.create(data)
|
|
344
|
+
await this.repo.save(order)
|
|
345
|
+
await this.storage.upload(order.invoiceKey, order.toPdf())
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Infrastructure implements the abstractions
|
|
350
|
+
class PostgresOrderRepository implements OrderRepository { ... }
|
|
351
|
+
class S3FileStorage implements FileStorage { ... }
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### Detection Heuristics
|
|
355
|
+
|
|
356
|
+
- Check import paths in domain/core modules: do they import from `infrastructure/`, `adapters/`, `db/`, `io/`, or driver-specific packages?
|
|
357
|
+
- Search for `new` keyword in domain modules creating infrastructure objects (database connections, HTTP clients, cache clients).
|
|
358
|
+
- Look for `process.env` access in domain modules — environment configuration is an infrastructure concern.
|
|
359
|
+
- Check if the composition root (entry point / DI container) is the only place where concrete implementations are wired to abstractions.
|