@rmrdeveloper/sideroom-pi 7.0.1 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +162 -125
- package/assets/artifacts/GUIDELINES_TEMPLATE.md +193 -413
- package/extensions/ask/model.ts +60 -15
- package/extensions/guidelines/guard.ts +136 -0
- package/extensions/guidelines/index.ts +9 -0
- package/extensions/guidelines/prompt.ts +5 -4
- package/package.json +1 -1
- package/skills/sideroom-domain-modeling/SKILL.md +81 -0
- package/skills/sideroom-domain-scaffold/SKILL.md +70 -0
- package/skills/sideroom-grill/SKILL.md +61 -0
- package/skills/sideroom-guidelines/SKILL.md +12 -8
- package/skills/sideroom-guidelines/references/languages/go.md +369 -19
- package/skills/sideroom-guidelines/references/languages/java.md +249 -16
- package/skills/sideroom-guidelines/references/languages/php-laravel.md +242 -20
- package/skills/sideroom-guidelines/references/languages/python.md +312 -12
- package/skills/sideroom-guidelines/references/languages/rust.md +244 -12
- package/skills/sideroom-guidelines/references/languages/typescript.md +248 -29
|
@@ -1,492 +1,272 @@
|
|
|
1
|
-
# Guidelines
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
|
10
|
-
|
|
11
|
-
|
|
|
12
|
-
|
|
|
13
|
-
|
|
|
14
|
-
|
|
|
15
|
-
|
|
|
16
|
-
|
|
|
17
|
-
|
|
|
18
|
-
|
|
|
19
|
-
|
|
|
20
|
-
|
|
|
21
|
-
|
|
|
22
|
-
|
|
|
23
|
-
|
|
|
24
|
-
|
|
|
25
|
-
|
|
|
26
|
-
|
|
|
27
|
-
|
|
|
28
|
-
|
|
|
29
|
-
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
1
|
+
# Coding Guidelines
|
|
2
|
+
|
|
3
|
+
Rules every change in this repo must follow. Ordered by how often they're violated and how cheap they are to check — mechanical rules first, judgment calls last.
|
|
4
|
+
|
|
5
|
+
**Adapting this template to a new language:** keep `RULE` and `WHY` text as-is unless the language genuinely doesn't support the concept (e.g. Python has no braces — see rule 1). Swap the code fence language tag and rewrite the bad/good snippet idiomatically. Do not add prose. Do not merge two rules into one section.
|
|
6
|
+
|
|
7
|
+
## Index
|
|
8
|
+
|
|
9
|
+
| # | Rule | Tier |
|
|
10
|
+
|---|------|------|
|
|
11
|
+
| 1 | [Braced conditionals](#1-braced-conditionals) | mechanical |
|
|
12
|
+
| 2 | [No magic strings](#2-no-magic-strings) | mechanical |
|
|
13
|
+
| 3 | [Guard clauses](#3-guard-clauses) | structural |
|
|
14
|
+
| 4 | [Fail fast](#4-fail-fast) | structural |
|
|
15
|
+
| 5 | [Explicit error handling](#5-explicit-error-handling) | structural |
|
|
16
|
+
| 6 | [Clear names](#6-clear-names) | structural |
|
|
17
|
+
| 7 | [Command Query Separation](#7-command-query-separation-cqs) | structural |
|
|
18
|
+
| 8 | [Null/undefined handling](#8-nullundefined-handling) | structural |
|
|
19
|
+
| 9 | [Immutability by default](#9-immutability-by-default) | structural |
|
|
20
|
+
| 10 | [Validate once](#10-validate-once) | structural |
|
|
21
|
+
| 11 | [Single Responsibility](#11-single-responsibility-srp) | architectural |
|
|
22
|
+
| 12 | [DRY (real duplication only)](#12-dry-real-duplication-only) | architectural |
|
|
23
|
+
| 13 | [KISS](#13-kiss) | architectural |
|
|
24
|
+
| 14 | [YAGNI](#14-yagni) | architectural |
|
|
25
|
+
| 15 | [Law of Demeter](#15-law-of-demeter) | architectural |
|
|
26
|
+
| 16 | [Composition over inheritance](#16-composition-over-inheritance) | architectural |
|
|
27
|
+
| 17 | [Dependency direction](#17-dependency-direction) | architectural |
|
|
28
|
+
| 18 | [Testability](#18-testability-as-a-design-constraint) | architectural |
|
|
29
|
+
| 19 | [Comments](#19-comments) | architectural |
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
### 1. Braced conditionals
|
|
34
|
+
RULE: Every `if`/`else` body uses braces, even a one-line guard. Never bind a statement to a conditional by indentation alone. (Python: no braces exist — use a full indented suite, never a one-liner.)
|
|
35
|
+
```js
|
|
36
|
+
// bad
|
|
37
|
+
if (!user) return 0;
|
|
38
|
+
// good
|
|
39
|
+
if (!user) {
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
```
|
|
36
43
|
|
|
37
|
-
|
|
44
|
+
### 2. No magic strings
|
|
45
|
+
RULE: Name domain/protocol literals once (const, enum, shared contract) and reuse the name. One-off strings with no reuse and no domain meaning (a single log label) may stay inline.
|
|
46
|
+
```js
|
|
47
|
+
// bad
|
|
48
|
+
if (order.status === 'pending_payment') { /* ... */ }
|
|
49
|
+
// good
|
|
50
|
+
const ORDER_STATUS = { PENDING_PAYMENT: 'pending_payment' };
|
|
51
|
+
if (order.status === ORDER_STATUS.PENDING_PAYMENT) { /* ... */ }
|
|
52
|
+
```
|
|
38
53
|
|
|
54
|
+
### 3. Guard clauses
|
|
55
|
+
RULE: Validate and exit early. Keep the happy path last, at the shallowest indent. No pyramid nesting.
|
|
39
56
|
```js
|
|
40
|
-
//
|
|
57
|
+
// bad
|
|
41
58
|
function getDiscount(user) {
|
|
42
59
|
if (user) {
|
|
43
|
-
if (user.isActive) {
|
|
44
|
-
if (user.hasSubscription) {
|
|
45
|
-
return 0.2;
|
|
46
|
-
} else {
|
|
47
|
-
return 0;
|
|
48
|
-
}
|
|
49
|
-
} else {
|
|
50
|
-
return 0;
|
|
51
|
-
}
|
|
52
|
-
} else {
|
|
53
|
-
return 0;
|
|
60
|
+
if (user.isActive) { return user.hasSubscription ? 0.2 : 0; }
|
|
54
61
|
}
|
|
62
|
+
return 0;
|
|
55
63
|
}
|
|
56
|
-
|
|
57
|
-
// Good — guard clauses, happy path at the end, shallow indent
|
|
64
|
+
// good
|
|
58
65
|
function getDiscount(user) {
|
|
59
|
-
if (!user) {
|
|
60
|
-
return 0;
|
|
61
|
-
}
|
|
62
|
-
if (!user.isActive) {
|
|
63
|
-
return 0;
|
|
64
|
-
}
|
|
65
|
-
if (!user.hasSubscription) {
|
|
66
|
+
if (!user || !user.isActive || !user.hasSubscription) {
|
|
66
67
|
return 0;
|
|
67
68
|
}
|
|
68
69
|
return 0.2;
|
|
69
70
|
}
|
|
70
71
|
```
|
|
71
72
|
|
|
72
|
-
###
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
including a one-line guard clause. Never rely on indentation alone to bind a
|
|
76
|
-
statement to a conditional. In Python, which has no braces, use a complete
|
|
77
|
-
indented suite and never a one-line conditional body.
|
|
78
|
-
|
|
79
|
-
### Fail fast
|
|
80
|
-
|
|
81
|
-
Invalid input, impossible state, or a broken dependency should fail immediately with a clear error. Prefer that over chains of fallbacks, silent defaults, or "keep going somehow."
|
|
82
|
-
|
|
83
|
-
**Relationship to "Validate once":** fail fast applies to type and state invariants that should never happen if upstream contracts hold (a required field is `null`, an enum has an impossible value). It is not license to re-check business rules the boundary already validated — that's re-validation, not fail-fast. If a downstream failure reveals that an upstream contract was violated, fix or harden the boundary; do not add a second check that quietly duplicates it.
|
|
84
|
-
|
|
73
|
+
### 4. Fail fast
|
|
74
|
+
RULE: Invalid input, impossible state, or a broken dependency throws immediately with a clear message. No fallback chains or silent defaults.
|
|
75
|
+
WHY: This is for invariants that should never happen if upstream contracts hold — not license to re-check business rules a boundary already validated (see rule 10).
|
|
85
76
|
```js
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
// Good — fails immediately with a clear error
|
|
93
|
-
function getShippingCost(order) {
|
|
94
|
-
const zone = ZONES[order.zoneId];
|
|
95
|
-
if (!zone) {
|
|
96
|
-
throw new Error(`Unknown zoneId: ${order.zoneId}`);
|
|
97
|
-
}
|
|
98
|
-
return zone.baseCost;
|
|
77
|
+
// bad
|
|
78
|
+
const zone = ZONES[order.zoneId] || ZONES.default; // hides a bad zoneId
|
|
79
|
+
// good
|
|
80
|
+
const zone = ZONES[order.zoneId];
|
|
81
|
+
if (!zone) {
|
|
82
|
+
throw new Error(`Unknown zoneId: ${order.zoneId}`);
|
|
99
83
|
}
|
|
100
84
|
```
|
|
101
85
|
|
|
102
|
-
###
|
|
103
|
-
|
|
104
|
-
A function, class, or module has one reason to change. If it does two jobs, split it.
|
|
105
|
-
|
|
86
|
+
### 5. Explicit error handling
|
|
87
|
+
RULE: Never swallow an error in an empty or generic `catch`. Re-throw with context, or let it propagate.
|
|
106
88
|
```js
|
|
107
|
-
//
|
|
108
|
-
async function
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
const normalized = { ...data, email: data.email.trim().toLowerCase() };
|
|
113
|
-
await db.users.insert(normalized);
|
|
114
|
-
await mailer.send(normalized.email, 'welcome');
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Good — each unit has a single reason to change
|
|
118
|
-
function validateUser(data) {
|
|
119
|
-
if (!data.email) {
|
|
120
|
-
throw new Error('email required');
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
function normalizeUser(data) {
|
|
124
|
-
return { ...data, email: data.email.trim().toLowerCase() };
|
|
89
|
+
// bad
|
|
90
|
+
async function loadUser(id) {
|
|
91
|
+
try { return await api.getUser(id); }
|
|
92
|
+
catch (e) { return null; } // caller has no idea it failed
|
|
125
93
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
await notifyWelcome(user.email);
|
|
131
|
-
return user;
|
|
94
|
+
// good
|
|
95
|
+
async function loadUser(id) {
|
|
96
|
+
try { return await api.getUser(id); }
|
|
97
|
+
catch (e) { throw new Error(`Failed to load user ${id}: ${e.message}`, { cause: e }); }
|
|
132
98
|
}
|
|
133
99
|
```
|
|
134
100
|
|
|
135
|
-
###
|
|
136
|
-
|
|
137
|
-
Do not copy logic that means the same thing. Extract only when duplication is real — not as premature abstraction.
|
|
138
|
-
|
|
139
|
-
**Practical rule:** extract when at least 2 of these 3 conditions hold:
|
|
140
|
-
|
|
141
|
-
- The same logic appears **3 or more times** (twice is tolerated; the third confirms the pattern).
|
|
142
|
-
- A future change to the business rule would need to be applied in **all** places at once (if not, it's not real duplication, just coincidence).
|
|
143
|
-
- The copied code represents the **same domain concept**, not just similar-looking code.
|
|
144
|
-
|
|
145
|
-
**Not real duplication (do not extract):**
|
|
146
|
-
|
|
147
|
-
- Two validations that look alike today but belong to different concepts (e.g. validating a user's email vs a vendor's email) — they'll likely diverge later.
|
|
148
|
-
- Boilerplate required by the surrounding structure (e.g. two handlers with the same shape because the calling convention requires it), where the shape is imposed from outside and not a domain decision.
|
|
149
|
-
|
|
150
|
-
**Example:**
|
|
151
|
-
|
|
152
|
-
```ts
|
|
153
|
-
// Bad: extracted prematurely, only one real use
|
|
154
|
-
function formatName(x: string) { return x.trim().toUpperCase(); }
|
|
155
|
-
|
|
156
|
-
// Good: appears 3 times with the same domain meaning → extract
|
|
157
|
-
function normalizeTicketStatus(status: string): TicketStatus { ... }
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
### KISS
|
|
161
|
-
|
|
162
|
-
Ship the simplest solution that solves the current problem. No extra layers, hooks, or configurability "just in case."
|
|
163
|
-
|
|
101
|
+
### 6. Clear names
|
|
102
|
+
RULE: Names reveal role or domain meaning. No `data`, `info`, `temp`, `result`, `obj`, `val`, `x`. A long honest name beats a short vague one.
|
|
164
103
|
```js
|
|
165
|
-
//
|
|
166
|
-
function
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
)
|
|
170
|
-
// ...unneeded logic for a single real use case
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Good — solves the current problem, nothing more
|
|
174
|
-
function formatPrice(value) {
|
|
175
|
-
return `$${value.toFixed(2)}`;
|
|
104
|
+
// bad
|
|
105
|
+
function process(data) { return data.filter((x) => x.val > 0); }
|
|
106
|
+
// good
|
|
107
|
+
function getActiveSubscriptions(subscriptions) {
|
|
108
|
+
return subscriptions.filter((subscription) => subscription.remainingDays > 0);
|
|
176
109
|
}
|
|
177
110
|
```
|
|
178
111
|
|
|
179
|
-
###
|
|
180
|
-
|
|
181
|
-
Build only what the current requirement needs. Don't add fields, params, branches, or abstractions for a future case that hasn't arrived. This differs from KISS: KISS is about keeping the _chosen_ solution simple; YAGNI is about not building things nobody asked for yet.
|
|
182
|
-
|
|
112
|
+
### 7. Command Query Separation (CQS)
|
|
113
|
+
RULE: A function either does something (side effect) or returns something (query) — never both. A "getter" must never mutate.
|
|
183
114
|
```js
|
|
184
|
-
//
|
|
185
|
-
function
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Good — build for the requirement that actually exists
|
|
190
|
-
function createInvoice(order) {
|
|
191
|
-
return { total: order.total, items: order.items };
|
|
192
|
-
}
|
|
115
|
+
// bad
|
|
116
|
+
function getNextId(counter) { counter.value++; return counter.value; }
|
|
117
|
+
// good
|
|
118
|
+
function peekNextId(counter) { return counter.value + 1; }
|
|
119
|
+
function incrementCounter(counter) { counter.value++; }
|
|
193
120
|
```
|
|
194
121
|
|
|
195
|
-
###
|
|
196
|
-
|
|
197
|
-
Prefer composing small, focused units (functions, objects, mixins) over deep inheritance chains. Inheritance couples subclasses to implementation details of their parent and tends to break when requirements diverge.
|
|
198
|
-
|
|
122
|
+
### 8. Null/undefined handling
|
|
123
|
+
RULE: Pick one convention (e.g. `undefined` = not set, `null` = explicitly empty) and never mix them for the same meaning. Never overload null to mean several business states — give each state its own explicit value.
|
|
199
124
|
```js
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
class Dog extends Animal {
|
|
210
|
-
makeSound() {
|
|
211
|
-
return 'Woof';
|
|
212
|
-
}
|
|
213
|
-
// forced to inherit `fly`, which makes no sense for a Dog
|
|
125
|
+
// bad — null means three different things
|
|
126
|
+
if (!user) return null;
|
|
127
|
+
if (!user.plan) return null;
|
|
128
|
+
if (user.plan.discount === 0) return null; // legit zero, but looks like "no discount"
|
|
129
|
+
// good — zero is a real value
|
|
130
|
+
function getDiscount(user) {
|
|
131
|
+
if (!user || !user.plan) return 0;
|
|
132
|
+
return user.plan.discount;
|
|
214
133
|
}
|
|
215
|
-
|
|
216
|
-
// Good — compose only the behaviors that apply
|
|
217
|
-
const canBark = { makeSound: () => 'Woof' };
|
|
218
|
-
const canFly = { fly: () => 'Flying' };
|
|
219
|
-
const dog = { ...canBark };
|
|
220
|
-
const bird = { ...canBark, ...canFly };
|
|
221
134
|
```
|
|
222
135
|
|
|
223
|
-
###
|
|
224
|
-
|
|
225
|
-
A unit should only interact with its immediate collaborators, not reach through them to grab something several levels deep. Deep chains couple you to structure that isn't yours to know.
|
|
226
|
-
|
|
136
|
+
### 9. Immutability by default
|
|
137
|
+
RULE: Return new values instead of mutating parameters. Never hide a side effect inside what looks like a pure transform.
|
|
227
138
|
```js
|
|
228
|
-
//
|
|
229
|
-
function
|
|
230
|
-
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Good — ask the object for what you need, let it own its structure
|
|
234
|
-
function getCityName(user) {
|
|
235
|
-
return user.getCityName();
|
|
236
|
-
}
|
|
139
|
+
// bad
|
|
140
|
+
function addItem(cart, item) { cart.items.push(item); return cart; }
|
|
141
|
+
// good
|
|
142
|
+
function addItem(cart, item) { return { ...cart, items: [...cart.items, item] }; }
|
|
237
143
|
```
|
|
238
144
|
|
|
239
|
-
###
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Good — separate the query from the command
|
|
251
|
-
function peekNextId(counter) {
|
|
252
|
-
return counter.value + 1;
|
|
145
|
+
### 10. Validate once
|
|
146
|
+
RULE: Validate at the boundary that owns the input (schema, FormRequest, DTO). Downstream code trusts that contract — no re-checking the same required/type/range rule in a helper, use case, or adapter further in.
|
|
147
|
+
```php
|
|
148
|
+
// bad — Service re-checks what the FormRequest already validated
|
|
149
|
+
class ProfileService {
|
|
150
|
+
public function update(array $data) {
|
|
151
|
+
if (empty($data['email'])) { throw new \InvalidArgumentException('email required'); }
|
|
152
|
+
$this->user->update($data);
|
|
153
|
+
}
|
|
253
154
|
}
|
|
254
|
-
|
|
255
|
-
|
|
155
|
+
// good — Service trusts the already-validated input
|
|
156
|
+
class ProfileService {
|
|
157
|
+
public function update(array $validated) { $this->user->update($validated); }
|
|
256
158
|
}
|
|
257
159
|
```
|
|
258
160
|
|
|
259
|
-
###
|
|
260
|
-
|
|
261
|
-
Let errors surface with context instead of swallowing them. An empty or generic `catch` hides the real failure and makes debugging production issues much harder.
|
|
262
|
-
|
|
161
|
+
### 11. Single Responsibility (SRP)
|
|
162
|
+
RULE: A function, class, or module has one reason to change. If it validates AND transforms AND persists AND notifies, split it.
|
|
263
163
|
```js
|
|
264
|
-
//
|
|
265
|
-
async function
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}
|
|
164
|
+
// bad
|
|
165
|
+
async function saveUser(data) {
|
|
166
|
+
if (!data.email) throw new Error('email required');
|
|
167
|
+
const normalized = { ...data, email: data.email.trim().toLowerCase() };
|
|
168
|
+
await db.users.insert(normalized);
|
|
169
|
+
await mailer.send(normalized.email, 'welcome');
|
|
271
170
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
171
|
+
// good
|
|
172
|
+
function validateUser(data) { if (!data.email) throw new Error('email required'); }
|
|
173
|
+
function normalizeUser(data) { return { ...data, email: data.email.trim().toLowerCase() }; }
|
|
174
|
+
async function createUser(data) {
|
|
175
|
+
validateUser(data);
|
|
176
|
+
const user = normalizeUser(data);
|
|
177
|
+
await db.users.insert(user);
|
|
178
|
+
await notifyWelcome(user.email);
|
|
179
|
+
return user;
|
|
280
180
|
}
|
|
281
181
|
```
|
|
282
182
|
|
|
283
|
-
###
|
|
284
|
-
|
|
285
|
-
|
|
183
|
+
### 12. DRY (real duplication only)
|
|
184
|
+
RULE: Extract when at least 2 of 3 hold: (a) the logic appears 3+ times, (b) a future rule change would need to touch all occurrences at once, (c) the copies represent the same domain concept. Do not extract on the first or second occurrence, and do not extract look-alike code that belongs to different concepts (a user's email validation vs a vendor's) — they will diverge.
|
|
185
|
+
```ts
|
|
186
|
+
// bad — extracted after one real use
|
|
187
|
+
function formatName(x: string) { return x.trim().toUpperCase(); }
|
|
188
|
+
// good — same domain concept, 3rd occurrence confirms the pattern
|
|
189
|
+
function normalizeTicketStatus(status: string): TicketStatus { /* ... */ }
|
|
190
|
+
```
|
|
286
191
|
|
|
192
|
+
### 13. KISS
|
|
193
|
+
RULE: Ship the simplest solution for the current, real requirement. No configurability, hooks, or layers added "just in case."
|
|
287
194
|
```js
|
|
288
|
-
//
|
|
289
|
-
function
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Good — returns a new value, caller's original data stays untouched
|
|
295
|
-
function addItem(cart, item) {
|
|
296
|
-
return { ...cart, items: [...cart.items, item] };
|
|
297
|
-
}
|
|
195
|
+
// bad
|
|
196
|
+
function formatPrice(value, { currency = 'USD', locale = 'en-US', showSymbol = true } = {}) { /* ... */ }
|
|
197
|
+
// good
|
|
198
|
+
function formatPrice(value) { return `$${value.toFixed(2)}`; }
|
|
298
199
|
```
|
|
299
200
|
|
|
300
|
-
###
|
|
301
|
-
|
|
302
|
-
Pick one convention and apply it consistently: e.g. `undefined` for "not yet set" and `null` for "explicitly empty," or vice versa — the specific choice matters less than not mixing both for the same meaning. Don't use `null`/`undefined` as a stand-in for a business state that deserves its own explicit value.
|
|
303
|
-
|
|
201
|
+
### 14. YAGNI
|
|
202
|
+
RULE: Don't add fields, params, or branches for a use case that hasn't arrived. (Differs from KISS: KISS simplifies the chosen solution; YAGNI stops you building the unrequested one.)
|
|
304
203
|
```js
|
|
305
|
-
//
|
|
306
|
-
function
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
310
|
-
if (!user.plan) {
|
|
311
|
-
return null; // no plan
|
|
312
|
-
}
|
|
313
|
-
if (user.plan.discount === 0) {
|
|
314
|
-
return null; // legitimately zero discount
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// Good — each case is explicit, zero is a real value
|
|
319
|
-
function getDiscount(user) {
|
|
320
|
-
if (!user || !user.plan) {
|
|
321
|
-
return 0;
|
|
322
|
-
}
|
|
323
|
-
return user.plan.discount;
|
|
324
|
-
}
|
|
204
|
+
// bad
|
|
205
|
+
function createInvoice(order, { supportsRecurring = false, supportsMultiCurrency = false } = {}) { /* ... */ }
|
|
206
|
+
// good
|
|
207
|
+
function createInvoice(order) { return { total: order.total, items: order.items }; }
|
|
325
208
|
```
|
|
326
209
|
|
|
327
|
-
###
|
|
328
|
-
|
|
329
|
-
Code that's easy to test is usually well-designed: side effects are isolated, dependencies are injected rather than hardcoded, and units do one thing. If a function is hard to test, that's often a signal the design needs to change, not a signal to skip the test.
|
|
330
|
-
|
|
210
|
+
### 15. Law of Demeter
|
|
211
|
+
RULE: Talk only to immediate collaborators. Never reach through an object to grab something several levels deep inside its internal structure.
|
|
331
212
|
```js
|
|
332
|
-
//
|
|
333
|
-
function
|
|
334
|
-
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// Good — dependency is injected, trivial to test with a fixed date
|
|
338
|
-
function isSubscriptionExpired(subscription, now = new Date()) {
|
|
339
|
-
return subscription.expiresAt < now;
|
|
340
|
-
}
|
|
213
|
+
// bad
|
|
214
|
+
function getCityName(user) { return user.address.city.name; }
|
|
215
|
+
// good
|
|
216
|
+
function getCityName(user) { return user.getCityName(); }
|
|
341
217
|
```
|
|
342
218
|
|
|
343
|
-
###
|
|
344
|
-
|
|
345
|
-
|
|
219
|
+
### 16. Composition over inheritance
|
|
220
|
+
RULE: Compose small, focused behaviors instead of building inheritance chains that force unrelated methods onto every subclass.
|
|
221
|
+
```js
|
|
222
|
+
// bad
|
|
223
|
+
class Animal { makeSound() {} fly() {} }
|
|
224
|
+
class Dog extends Animal { makeSound() { return 'Woof'; } /* stuck with fly() */ }
|
|
225
|
+
// good
|
|
226
|
+
const canBark = { makeSound: () => 'Woof' };
|
|
227
|
+
const canFly = { fly: () => 'Flying' };
|
|
228
|
+
const dog = { ...canBark };
|
|
229
|
+
const bird = { ...canBark, ...canFly };
|
|
230
|
+
```
|
|
346
231
|
|
|
232
|
+
### 17. Dependency direction
|
|
233
|
+
RULE: Inner layers (domain/business logic) never import outer layers (framework, DB, HTTP). Outer layers depend inward, never the reverse.
|
|
347
234
|
```js
|
|
348
|
-
//
|
|
235
|
+
// bad
|
|
349
236
|
import { MysqlConnection } from '../infra/mysql';
|
|
350
237
|
function calculateInvoiceTotal(invoiceId) {
|
|
351
238
|
const invoice = new MysqlConnection().query('SELECT * FROM invoices WHERE id = ?', [invoiceId]);
|
|
352
239
|
return invoice.items.reduce((sum, item) => sum + item.price, 0);
|
|
353
240
|
}
|
|
354
|
-
|
|
355
|
-
// Good — domain logic depends only on the shape of the data, not its source
|
|
241
|
+
// good
|
|
356
242
|
function calculateInvoiceTotal(invoice) {
|
|
357
243
|
return invoice.items.reduce((sum, item) => sum + item.price, 0);
|
|
358
244
|
}
|
|
359
245
|
```
|
|
360
246
|
|
|
361
|
-
###
|
|
362
|
-
|
|
363
|
-
Variables, parameters, functions, and types must say what they hold or do. Prefer domain words over vague fillers (`data`, `info`, `item`, `temp`, `result`, `obj`, `val`, `x`). If the honest name is long, that is fine — a short vague name is not.
|
|
364
|
-
|
|
247
|
+
### 18. Testability as a design constraint
|
|
248
|
+
RULE: Inject dependencies (clock, network, random) instead of hardcoding them. If a function is hard to test, redesign it — don't skip the test.
|
|
365
249
|
```js
|
|
366
|
-
//
|
|
367
|
-
function
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// Good — names reveal role and domain meaning
|
|
373
|
-
function getActiveSubscriptions(subscriptions) {
|
|
374
|
-
return subscriptions.filter((subscription) => subscription.remainingDays > 0);
|
|
375
|
-
}
|
|
250
|
+
// bad
|
|
251
|
+
function isSubscriptionExpired(subscription) { return subscription.expiresAt < new Date(); }
|
|
252
|
+
// good
|
|
253
|
+
function isSubscriptionExpired(subscription, now = new Date()) { return subscription.expiresAt < now; }
|
|
376
254
|
```
|
|
377
255
|
|
|
378
|
-
### Comments
|
|
379
|
-
|
|
380
|
-
Do not leave comments that add no value. Prefer clear names and structure so the code explains itself. Comment only what is genuinely important and non-obvious: why a trade-off was made, a constraint the reader would miss, or a hazard that names alone cannot carry. Delete narration, restatements of the next line, and leftover TODOs that no longer mean anything.
|
|
381
|
-
|
|
382
|
-
**Examples:**
|
|
383
|
-
|
|
256
|
+
### 19. Comments
|
|
257
|
+
RULE: Comment only non-obvious intent — a trade-off, a constraint, a hazard names can't carry. Delete narration, restated logic, and stale TODOs. Test: if deleting the comment leaves the code equally clear, delete it.
|
|
384
258
|
```ts
|
|
385
|
-
//
|
|
259
|
+
// bad
|
|
386
260
|
// increment the counter by 1
|
|
387
261
|
counter++;
|
|
388
|
-
|
|
389
|
-
//
|
|
390
|
-
//
|
|
391
|
-
const user = getUserById(id);
|
|
392
|
-
|
|
393
|
-
// Good — explains a non-obvious trade-off
|
|
394
|
-
// We poll instead of using a webhook because the provider doesn't
|
|
395
|
-
// guarantee single delivery; downstream dedupe would cost more than polling.
|
|
262
|
+
// good
|
|
263
|
+
// We poll instead of using a webhook because the provider doesn't guarantee
|
|
264
|
+
// single delivery; downstream dedupe would cost more than polling.
|
|
396
265
|
setInterval(checkPaymentStatus, 5000);
|
|
397
|
-
|
|
398
|
-
// Good — warns of a hazard the name can't carry
|
|
399
|
-
// WARNING: this endpoint is only idempotent if `externalId` comes from
|
|
400
|
-
// the client; if we generate it ourselves, retries create duplicate records.
|
|
401
266
|
```
|
|
402
267
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
### No magic strings
|
|
406
|
-
|
|
407
|
-
Do not hard-code domain or protocol literals inline (status values, roles, path fragments, error codes, event names). Name them once — const, enum, shared contract, or map — and reuse that name. Exception: one-off strings with no reuse and no domain meaning (e.g. a single log label) may stay inline if a named constant would only obscure them.
|
|
408
|
-
|
|
409
|
-
```js
|
|
410
|
-
// Bad — domain literals scattered across the codebase
|
|
411
|
-
if (order.status === 'pending_payment') {
|
|
412
|
-
/* ... */
|
|
413
|
-
}
|
|
414
|
-
// ...elsewhere, in a different file
|
|
415
|
-
if (order.status === 'pending_payment') {
|
|
416
|
-
/* ... */
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// Good — named once, reused everywhere
|
|
420
|
-
const ORDER_STATUS = {
|
|
421
|
-
PENDING_PAYMENT: 'pending_payment',
|
|
422
|
-
PAID: 'paid',
|
|
423
|
-
CANCELLED: 'cancelled',
|
|
424
|
-
};
|
|
425
|
-
if (order.status === ORDER_STATUS.PENDING_PAYMENT) {
|
|
426
|
-
/* ... */
|
|
427
|
-
}
|
|
428
|
-
```
|
|
429
|
-
|
|
430
|
-
### SOLID
|
|
431
|
-
|
|
432
|
-
Apply with judgment. Favor single responsibility and inversion of dependencies (ports/adapters) where variation is real. Do not force every SOLID letter into every small file.
|
|
433
|
-
|
|
434
|
-
```js
|
|
435
|
-
// Bad — the use case depends directly on a concrete implementation
|
|
436
|
-
class SendWelcomeEmail {
|
|
437
|
-
async execute(user) {
|
|
438
|
-
await new SmtpMailer().send(user.email, 'welcome'); // coupled to SMTP
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// Good — depends on an interface (port), not the implementation
|
|
443
|
-
class SendWelcomeEmail {
|
|
444
|
-
constructor(mailer) {
|
|
445
|
-
this.mailer = mailer;
|
|
446
|
-
} // mailer implements MailerPort
|
|
447
|
-
async execute(user) {
|
|
448
|
-
await this.mailer.send(user.email, 'welcome');
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
```
|
|
452
|
-
|
|
453
|
-
### Validate once
|
|
454
|
-
|
|
455
|
-
Validate at the boundary (the first function, layer, or contract that owns the input). Downstream code should trust that contract — do not re-check the same emptiness, type, or range in helpers, use cases, or adapters further in.
|
|
456
|
-
|
|
457
|
-
Bad smell: hand-rolled parsers that trim/null-check/re-parse what the boundary already guarantees, or a second layer validating the same rule again "just in case."
|
|
458
|
-
|
|
459
|
-
When a shared schema (or equivalent single contract) defines the input, that schema is the **single source of truth**. Do not stack a second validator for the same invariants. Documentation types or interface shapes may mirror the contract, but they must not re-enforce the same rules.
|
|
460
|
-
|
|
461
|
-
```php
|
|
462
|
-
// Bad — the FormRequest already validates, and the Service re-checks the same rule
|
|
463
|
-
class UpdateProfileRequest extends FormRequest {
|
|
464
|
-
public function rules() {
|
|
465
|
-
return ['email' => 'required|email'];
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
class ProfileService {
|
|
469
|
-
public function update(array $data) {
|
|
470
|
-
// Redundant re-validation.
|
|
471
|
-
if (empty($data['email'])) {
|
|
472
|
-
throw new \InvalidArgumentException('email required');
|
|
473
|
-
}
|
|
474
|
-
$this->user->update($data);
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
// Good — the Service trusts the contract already validated by the FormRequest
|
|
479
|
-
class ProfileService {
|
|
480
|
-
public function update(array $validated) {
|
|
481
|
-
$this->user->update($validated);
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
```
|
|
268
|
+
---
|
|
485
269
|
|
|
486
|
-
|
|
487
|
-
| ---------------------------------------------------------------------------------- | ---------------------------------------------------------- |
|
|
488
|
-
| Parse once at the edge; pass a typed command/query inward | Re-parse or re-check the same rules in every layer |
|
|
489
|
-
| One validation approach per input | Two mechanisms validating the same body/query |
|
|
490
|
-
| Keep docs/interface metadata separate from the validation contract when both exist | Duplicate the same type/range/required rules in two places |
|
|
270
|
+
**When in doubt:** fail fast, keep it flat, keep it small.
|
|
491
271
|
|
|
492
|
-
|
|
272
|
+
**Enforcement note:** rules 1–2 (mechanical) should be backed by a linter/formatter in the target language, not left to this document alone — text-only compliance from an agent caps out well below what a lint rule enforces. Rules 3–10 (structural) are catchable by targeted lint rules or code review checklists in most languages. Rules 11–19 (architectural) require judgment and stay as review guidance.
|