@rmrdeveloper/sideroom-pi 7.1.0 → 8.0.1

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.
@@ -1,492 +1,272 @@
1
- # Guidelines Template
2
-
3
- How to write code in this repo: the principles and style rules every change must follow.
4
-
5
- ## Quick reference
6
-
7
- Each row is expanded, with examples, in the matching section below.
8
-
9
- | Do | Don't |
10
- | -------------------------------------------------- | ------------------------------------------------------------------------------------------- |
11
- | Use braces around every `if` body | Write an unbraced, single-statement conditional body |
12
- | Early return on bad input | Pyramid `if/else` nesting |
13
- | Explicit error, fail now | Multiple fallbacks that hide the real failure |
14
- | One responsibility per unit | Validate + transform + persist + notify in one place |
15
- | Extract when duplication repeats | Abstract before a second real use |
16
- | Ship the simplest solution for the current problem | Add layers, hooks, or config "just in case" |
17
- | Build only what's needed today | Add fields/params/branches for a future case that hasn't arrived |
18
- | Compose small, focused units | Build deep inheritance chains for unrelated behavior |
19
- | Talk only to immediate collaborators | Reach through several levels of another object's internal structure |
20
- | A function either does or returns, not both | Mix a side effect into what looks like a getter |
21
- | Validate once at the edge | Re-validate the same invariant in every layer |
22
- | One validator per input | Two validators for the same body/query |
23
- | Let errors surface with context | Swallow errors in an empty or generic `catch`, or catch-and-continue as if nothing happened |
24
- | Return new values instead of mutating input | Mutate parameters and hide the side effect from the caller |
25
- | One consistent meaning per null/undefined | Overload null/undefined to mean several different business states |
26
- | Inject dependencies so units are easy to test | Hardcode dependencies that force hitting real infra to test |
27
- | Inner layers depend on nothing outward | Let domain logic import framework/DB/HTTP details directly |
28
- | Names that reveal role or domain meaning | Vague names (`data`, `info`, `temp`, `result`, `obj`) |
29
- | Named const / enum / contract for domain literals | Magic strings scattered through the codebase |
30
- | Depend on interfaces/ports where variation is real | Couple a use case directly to a concrete implementation |
31
- | Comments only for important non-obvious intent | Narrating comments, noise, or stale TODOs |
32
-
33
- ## Principles
34
-
35
- ### Guard clauses
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
- Validate and exit early. Prefer flat control flow over deep nesting. Keep the happy path at the end, at the shallowest indent level.
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
- // Bad — pyramid nesting, happy path buried
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
- ### Braced conditionals
73
-
74
- Every `if` and `else` body must use braces in languages that support them,
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
- // Bad — silent fallback hides a broken invariant
87
- function getShippingCost(order) {
88
- const zone = ZONES[order.zoneId] || ZONES.default; // hides a bad zoneId
89
- return zone.baseCost;
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
- ### SRP
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
- // Bad — validates, transforms, persists, and notifies all in one place
108
- async function saveUser(data) {
109
- if (!data.email) {
110
- throw new Error('email required');
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
- async function createUser(data) {
127
- validateUser(data);
128
- const user = normalizeUser(data);
129
- await db.users.insert(user);
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
- ### DRY
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
- // Bad — configurability nobody asked for, added "just in case"
166
- function formatPrice(
167
- value,
168
- { currency = 'USD', locale = 'en-US', showSymbol = true, roundingStrategy = 'nearest' } = {},
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
- ### YAGNI (You Aren't Gonna Need It)
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
- // Bad — speculative support for a case that doesn't exist yet
185
- function createInvoice(order, { supportsRecurring = false, supportsMultiCurrency = false } = {}) {
186
- // ...branches for features no client uses today
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
- ### Composition over inheritance
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
- // Bad — inheritance forces unrelated behavior onto every subclass
201
- class Animal {
202
- makeSound() {
203
- throw new Error('not implemented');
204
- }
205
- fly() {
206
- throw new Error('not implemented');
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
- ### Law of Demeter (don't talk to strangers)
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
- // Bad — reaches through three levels of internal structure
229
- function getCityName(user) {
230
- return user.address.city.name;
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
- ### Command Query Separation (CQS)
240
-
241
- A function either **does** something (command, causes a side effect) or **returns** something (query) — not both. Mixing the two makes call sites unpredictable: you can't tell if calling something is safe to do twice.
242
-
243
- ```js
244
- // Bad — returns a value AND causes a side effect
245
- function getNextId(counter) {
246
- counter.value++; // side effect hidden inside a "getter"
247
- return counter.value;
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
- function incrementCounter(counter) {
255
- counter.value++;
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
- ### Explicit error handling
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
- // Bad — swallows the error, no context, execution continues as if nothing happened
265
- async function loadUser(id) {
266
- try {
267
- return await api.getUser(id);
268
- } catch (e) {
269
- return null; // caller has no idea a failure occurred
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
- // Good — the error surfaces with context, caller decides how to handle it
274
- async function loadUser(id) {
275
- try {
276
- return await api.getUser(id);
277
- } catch (e) {
278
- throw new Error(`Failed to load user ${id}: ${e.message}`, { cause: e });
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
- ### Immutability by default
284
-
285
- Prefer creating new values over mutating existing ones, especially for data passed as a parameter. Mutation hides side effects and makes state changes hard to trace, particularly in reactive systems.
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
- // Bad — mutates the input, callers get a surprise side effect
289
- function addItem(cart, item) {
290
- cart.items.push(item);
291
- return cart;
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
- ### Null/undefined handling
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
- // Bad — null is overloaded to mean three different things
306
- function getDiscount(user) {
307
- if (!user) {
308
- return null; // no user
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
- ### Testability as a design constraint
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
- // Bad — hardcoded dependency, can't test without hitting the real clock/API
333
- function isSubscriptionExpired(subscription) {
334
- return subscription.expiresAt < new Date();
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
- ### Dependency direction
344
-
345
- Inner layers (domain/business logic) must not depend on outer layers (frameworks, databases, HTTP clients). Outer layers depend inward, never the reverse. This keeps business rules testable and portable independent of infrastructure choices.
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
- // Bad — domain logic imports directly from an infrastructure detail
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
- ### Clear names
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
- // Bad — vague names that hide the domain meaning
367
- function process(data) {
368
- const temp = data.filter((x) => x.val > 0);
369
- return temp;
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
- // Bad — narrates the obvious
259
+ // bad
386
260
  // increment the counter by 1
387
261
  counter++;
388
-
389
- // Bad — restates what the name already says
390
- // get the user by id
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
- **Quick test before writing a comment:** if deleting it leaves the code just as clear, the comment isn't earning its place.
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
- | Do | Don't |
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
- When in doubt: **fail fast, keep it flat, keep it small.**
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.