@rmrdeveloper/sideroom-pi 7.1.0 → 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 +32 -15
- 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-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,26 +1,248 @@
|
|
|
1
|
-
# PHP / Laravel
|
|
1
|
+
# PHP / Laravel Coding Guidelines
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Rules every PHP or Laravel change must follow.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
## Index
|
|
6
|
+
|
|
7
|
+
| # | Rule | Tier |
|
|
8
|
+
| --- | ------ | ------ |
|
|
9
|
+
| 1 | [Braced conditionals](#1-braced-conditionals) | mechanical |
|
|
10
|
+
| 2 | [No magic strings](#2-no-magic-strings) | mechanical |
|
|
11
|
+
| 3 | [Guard clauses](#3-guard-clauses) | structural |
|
|
12
|
+
| 4 | [Fail fast](#4-fail-fast) | structural |
|
|
13
|
+
| 5 | [Explicit error handling](#5-explicit-error-handling) | structural |
|
|
14
|
+
| 6 | [Clear names](#6-clear-names) | structural |
|
|
15
|
+
| 7 | [Command Query Separation](#7-command-query-separation-cqs) | structural |
|
|
16
|
+
| 8 | [Null/undefined handling](#8-nullundefined-handling) | structural |
|
|
17
|
+
| 9 | [Immutability by default](#9-immutability-by-default) | structural |
|
|
18
|
+
| 10 | [Validate once](#10-validate-once) | structural |
|
|
19
|
+
| 11 | [Single Responsibility](#11-single-responsibility-srp) | architectural |
|
|
20
|
+
| 12 | [DRY (real duplication only)](#12-dry-real-duplication-only) | architectural |
|
|
21
|
+
| 13 | [KISS](#13-kiss) | architectural |
|
|
22
|
+
| 14 | [YAGNI](#14-yagni) | architectural |
|
|
23
|
+
| 15 | [Law of Demeter](#15-law-of-demeter) | architectural |
|
|
24
|
+
| 16 | [Composition over inheritance](#16-composition-over-inheritance) | architectural |
|
|
25
|
+
| 17 | [Dependency direction](#17-dependency-direction) | architectural |
|
|
26
|
+
| 18 | [Testability](#18-testability-as-a-design-constraint) | architectural |
|
|
27
|
+
| 19 | [Comments](#19-comments) | architectural |
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
### 1. Braced conditionals
|
|
32
|
+
|
|
33
|
+
RULE: Every `if`/`else` body uses braces, even a one-line guard. Never bind a statement to a conditional by indentation alone.
|
|
34
|
+
|
|
35
|
+
```php
|
|
36
|
+
// bad
|
|
37
|
+
if ($user === null) return 0;
|
|
38
|
+
// good
|
|
39
|
+
if ($user === null) { return 0; }
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 2. No magic strings
|
|
43
|
+
|
|
44
|
+
RULE: Name domain/protocol literals once (const, enum, shared contract) and reuse the name. One-off strings with no reuse and no domain meaning may stay inline.
|
|
45
|
+
|
|
46
|
+
```php
|
|
47
|
+
// bad
|
|
48
|
+
if ($order->status === 'pending_payment') { $this->charge($order); }
|
|
49
|
+
// good
|
|
50
|
+
if ($order->status === OrderStatus::PendingPayment) { $this->charge($order); }
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 3. Guard clauses
|
|
54
|
+
|
|
55
|
+
RULE: Validate and exit early. Keep the happy path last, at the shallowest indent. No pyramid nesting.
|
|
56
|
+
|
|
57
|
+
```php
|
|
58
|
+
// bad
|
|
59
|
+
if ($user !== null) { if ($user->isActive()) { return $user->hasSubscription() ? .2 : 0; } } return 0;
|
|
60
|
+
// good
|
|
61
|
+
if ($user === null || !$user->isActive() || !$user->hasSubscription()) { return 0; } return .2;
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 4. Fail fast
|
|
65
|
+
|
|
66
|
+
RULE: Invalid input, impossible state, or a broken dependency throws immediately with a clear message. No fallback chains or silent defaults.
|
|
67
|
+
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).
|
|
68
|
+
|
|
69
|
+
```php
|
|
70
|
+
// bad
|
|
71
|
+
$zone = $zones[$order->zoneId] ?? $zones['default'];
|
|
72
|
+
// good
|
|
73
|
+
$zone = $zones[$order->zoneId] ?? throw new DomainException("Unknown zoneId: {$order->zoneId}");
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 5. Explicit error handling
|
|
77
|
+
|
|
78
|
+
RULE: Never swallow an error in an empty or generic `catch`. Re-throw with context, or let it propagate.
|
|
79
|
+
|
|
80
|
+
```php
|
|
81
|
+
// bad
|
|
82
|
+
try { return $api->getUser($id); } catch (Throwable $error) { return null; }
|
|
83
|
+
// good
|
|
84
|
+
try { return $api->getUser($id); } catch (ApiException $error) { throw new UserLoadException("Failed to load user {$id}", previous: $error); }
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 6. Clear names
|
|
88
|
+
|
|
89
|
+
RULE: Names reveal role or domain meaning. No `data`, `info`, `temp`, `result`, `obj`, `val`, `x`. A long honest name beats a short vague one.
|
|
90
|
+
|
|
91
|
+
```php
|
|
92
|
+
// bad
|
|
93
|
+
function process(array $data): array { return array_filter($data, fn ($x) => $x->val > 0); }
|
|
94
|
+
// good
|
|
95
|
+
function activeSubscriptions(array $subscriptions): array { return array_filter($subscriptions, fn ($subscription) => $subscription->remainingDays > 0); }
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 7. Command Query Separation (CQS)
|
|
99
|
+
|
|
100
|
+
RULE: A function either does something (side effect) or returns something (query) — never both. A "getter" must never mutate.
|
|
101
|
+
|
|
102
|
+
```php
|
|
103
|
+
// bad
|
|
104
|
+
function getNextId(Counter $counter): int { return ++$counter->value; }
|
|
105
|
+
// good
|
|
106
|
+
function peekNextId(Counter $counter): int { return $counter->value + 1; }
|
|
107
|
+
function incrementCounter(Counter $counter): void { ++$counter->value; }
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### 8. Null/undefined handling
|
|
111
|
+
|
|
112
|
+
RULE: Pick one convention for `null` and never mix meanings. Never overload null to mean several business states — give each state its own explicit value.
|
|
113
|
+
|
|
114
|
+
```php
|
|
115
|
+
// bad
|
|
116
|
+
function discount(?User $user): ?int { return (!$user?->plan || $user->plan->discount === 0) ? null : $user->plan->discount; }
|
|
117
|
+
// good
|
|
118
|
+
function discount(?User $user): int { return $user?->plan?->discount ?? 0; }
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 9. Immutability by default
|
|
122
|
+
|
|
123
|
+
RULE: Return new values instead of mutating parameters. Never hide a side effect inside what looks like a pure transform.
|
|
124
|
+
|
|
125
|
+
```php
|
|
126
|
+
// bad
|
|
127
|
+
function addItem(Cart $cart, Item $item): Cart { $cart->items[] = $item; return $cart; }
|
|
128
|
+
// good
|
|
129
|
+
function addItem(Cart $cart, Item $item): Cart { return $cart->withItems([...$cart->items, $item]); }
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### 10. Validate once
|
|
133
|
+
|
|
134
|
+
RULE: Validate at the boundary that owns the input (`FormRequest`, schema, DTO). Downstream code trusts that contract — no re-checking the same required/type/range rule further in.
|
|
135
|
+
|
|
136
|
+
```php
|
|
137
|
+
// bad
|
|
138
|
+
function update(array $validated): void { if (empty($validated['email'])) { throw new InvalidArgumentException(); } $this->user->update($validated); }
|
|
139
|
+
// good
|
|
140
|
+
function update(array $validated): void { $this->user->update($validated); }
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### 11. Single Responsibility (SRP)
|
|
144
|
+
|
|
145
|
+
RULE: A function, class, or module has one reason to change. If it validates AND transforms AND persists AND notifies, split it.
|
|
146
|
+
|
|
147
|
+
```php
|
|
148
|
+
// bad
|
|
149
|
+
function save(array $input): void { $this->validate($input); $user = $this->normalize($input); $this->repo->save($user); $this->mailer->welcome($user); }
|
|
150
|
+
// good
|
|
151
|
+
function create(ValidatedUser $input): User { $user = $this->normalizer->normalize($input); $this->repo->save($user); return $user; }
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### 12. DRY (real duplication only)
|
|
155
|
+
|
|
156
|
+
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, or extract look-alike code from different concepts.
|
|
157
|
+
|
|
158
|
+
```php
|
|
159
|
+
// bad — extracted after one use
|
|
160
|
+
function formatName(string $value): string { return strtoupper(trim($value)); }
|
|
161
|
+
// good — third occurrence confirms one domain concept
|
|
162
|
+
function normalizeTicketStatus(string $status): TicketStatus { return TicketStatus::from(trim($status)); }
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### 13. KISS
|
|
166
|
+
|
|
167
|
+
RULE: Ship the simplest solution for the current, real requirement. No configurability, hooks, or layers added "just in case."
|
|
168
|
+
|
|
169
|
+
```php
|
|
170
|
+
// bad
|
|
171
|
+
function formatPrice(float $value, array $options = []): string { return $this->formatter->format($value, $options); }
|
|
172
|
+
// good
|
|
173
|
+
function formatPrice(float $value): string { return '$'.number_format($value, 2); }
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### 14. YAGNI
|
|
177
|
+
|
|
178
|
+
RULE: Don't add fields, params, or branches for a use case that hasn't arrived. (KISS simplifies the chosen solution; YAGNI stops you building the unrequested one.)
|
|
179
|
+
|
|
180
|
+
```php
|
|
181
|
+
// bad
|
|
182
|
+
function createInvoice(Order $order, bool $recurring = false, bool $multiCurrency = false): Invoice { return $this->build($order, $recurring, $multiCurrency); }
|
|
183
|
+
// good
|
|
184
|
+
function createInvoice(Order $order): Invoice { return new Invoice($order->total, $order->items); }
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### 15. Law of Demeter
|
|
188
|
+
|
|
189
|
+
RULE: Talk only to immediate collaborators. Never reach through an object to grab something several levels deep inside its internal structure.
|
|
190
|
+
|
|
191
|
+
```php
|
|
192
|
+
// bad
|
|
193
|
+
function cityName(User $user): string { return $user->address->city->name; }
|
|
194
|
+
// good
|
|
195
|
+
function cityName(User $user): string { return $user->cityName(); }
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### 16. Composition over inheritance
|
|
199
|
+
|
|
200
|
+
RULE: Compose small, focused behaviors instead of building inheritance chains that force unrelated methods onto every subclass.
|
|
201
|
+
|
|
202
|
+
```php
|
|
203
|
+
// bad
|
|
204
|
+
abstract class Animal { abstract public function sound(): string; abstract public function fly(): void; }
|
|
205
|
+
// good
|
|
206
|
+
final class Dog { public function __construct(private SoundBehavior $sound) {} }
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### 17. Dependency direction
|
|
210
|
+
|
|
211
|
+
RULE: Inner layers (domain/business logic) never import outer layers (framework, DB, HTTP). Outer layers depend inward, never the reverse.
|
|
8
212
|
|
|
9
213
|
```php
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
214
|
+
// bad
|
|
215
|
+
function total(string $id): Money { return (new EloquentInvoiceRepository())->find($id)->total(); }
|
|
216
|
+
// good
|
|
217
|
+
function total(Invoice $invoice): Money { return $invoice->total(); }
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### 18. Testability as a design constraint
|
|
221
|
+
|
|
222
|
+
RULE: Inject dependencies (clock, network, random) instead of hardcoding them. If a function is hard to test, redesign it — don't skip the test.
|
|
223
|
+
|
|
224
|
+
```php
|
|
225
|
+
// bad
|
|
226
|
+
function expired(Subscription $subscription): bool { return $subscription->expiresAt < new DateTimeImmutable(); }
|
|
227
|
+
// good
|
|
228
|
+
function expired(Subscription $subscription, Clock $clock): bool { return $subscription->expiresAt < $clock->now(); }
|
|
229
|
+
```
|
|
19
230
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
231
|
+
### 19. Comments
|
|
232
|
+
|
|
233
|
+
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.
|
|
234
|
+
|
|
235
|
+
```php
|
|
236
|
+
// bad
|
|
237
|
+
// Increment the counter.
|
|
238
|
+
++$counter;
|
|
239
|
+
// good
|
|
240
|
+
// Poll because the provider cannot guarantee single webhook delivery.
|
|
241
|
+
Schedule::call($checkPaymentStatus)->everyFiveSeconds();
|
|
26
242
|
```
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
**When in doubt:** fail fast, keep it flat, keep it small.
|
|
247
|
+
|
|
248
|
+
**Enforcement note:** rules 1–2 are mechanical and should be backed by PHP/Laravel formatting or linting. Rules 3–10 are structural checks. Rules 11–19 require review judgment.
|
|
@@ -1,21 +1,321 @@
|
|
|
1
|
-
# Python
|
|
1
|
+
# Python Coding Guidelines
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Rules every Python change must follow.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
## Index
|
|
6
|
+
|
|
7
|
+
| # | Rule | Tier |
|
|
8
|
+
| --- | ------ | ------ |
|
|
9
|
+
| 1 | [Braced conditionals](#1-braced-conditionals) | mechanical |
|
|
10
|
+
| 2 | [No magic strings](#2-no-magic-strings) | mechanical |
|
|
11
|
+
| 3 | [Guard clauses](#3-guard-clauses) | structural |
|
|
12
|
+
| 4 | [Fail fast](#4-fail-fast) | structural |
|
|
13
|
+
| 5 | [Explicit error handling](#5-explicit-error-handling) | structural |
|
|
14
|
+
| 6 | [Clear names](#6-clear-names) | structural |
|
|
15
|
+
| 7 | [Command Query Separation](#7-command-query-separation-cqs) | structural |
|
|
16
|
+
| 8 | [Null/undefined handling](#8-nullundefined-handling) | structural |
|
|
17
|
+
| 9 | [Immutability by default](#9-immutability-by-default) | structural |
|
|
18
|
+
| 10 | [Validate once](#10-validate-once) | structural |
|
|
19
|
+
| 11 | [Single Responsibility](#11-single-responsibility-srp) | architectural |
|
|
20
|
+
| 12 | [DRY (real duplication only)](#12-dry-real-duplication-only) | architectural |
|
|
21
|
+
| 13 | [KISS](#13-kiss) | architectural |
|
|
22
|
+
| 14 | [YAGNI](#14-yagni) | architectural |
|
|
23
|
+
| 15 | [Law of Demeter](#15-law-of-demeter) | architectural |
|
|
24
|
+
| 16 | [Composition over inheritance](#16-composition-over-inheritance) | architectural |
|
|
25
|
+
| 17 | [Dependency direction](#17-dependency-direction) | architectural |
|
|
26
|
+
| 18 | [Testability](#18-testability-as-a-design-constraint) | architectural |
|
|
27
|
+
| 19 | [Comments](#19-comments) | architectural |
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
### 1. Braced conditionals
|
|
32
|
+
|
|
33
|
+
RULE: Python has no braces: every `if`/`else` body uses a full indented suite, never a one-line body.
|
|
8
34
|
|
|
9
35
|
```python
|
|
10
|
-
#
|
|
11
|
-
|
|
12
|
-
|
|
36
|
+
# bad
|
|
37
|
+
if user is None: return 0
|
|
38
|
+
# good
|
|
39
|
+
if user is None:
|
|
40
|
+
return 0
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 2. No magic strings
|
|
44
|
+
|
|
45
|
+
RULE: Name domain/protocol literals once (constant, 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
|
+
|
|
47
|
+
```python
|
|
48
|
+
# bad
|
|
49
|
+
if order.status == "pending_payment":
|
|
50
|
+
charge(order)
|
|
51
|
+
# good
|
|
52
|
+
PENDING_PAYMENT = "pending_payment"
|
|
53
|
+
if order.status == PENDING_PAYMENT:
|
|
54
|
+
charge(order)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 3. Guard clauses
|
|
58
|
+
|
|
59
|
+
RULE: Validate and exit early. Keep the happy path last, at the shallowest indent. No pyramid nesting.
|
|
13
60
|
|
|
14
|
-
|
|
61
|
+
```python
|
|
62
|
+
# bad
|
|
15
63
|
def get_discount(user):
|
|
16
|
-
if user is None:
|
|
17
|
-
|
|
18
|
-
|
|
64
|
+
if user is not None:
|
|
65
|
+
if user.is_active:
|
|
66
|
+
return 0.2 if user.has_subscription else 0
|
|
67
|
+
return 0
|
|
68
|
+
# good
|
|
69
|
+
def get_discount(user):
|
|
70
|
+
if user is None or not user.is_active or not user.has_subscription:
|
|
19
71
|
return 0
|
|
20
72
|
return 0.2
|
|
21
73
|
```
|
|
74
|
+
|
|
75
|
+
### 4. Fail fast
|
|
76
|
+
|
|
77
|
+
RULE: Invalid input, impossible state, or a broken dependency raises immediately with a clear message. No fallback chains or silent defaults.
|
|
78
|
+
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).
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
# bad
|
|
82
|
+
zone = zones.get(order.zone_id, zones["default"])
|
|
83
|
+
# good
|
|
84
|
+
try:
|
|
85
|
+
zone = zones[order.zone_id]
|
|
86
|
+
except KeyError as error:
|
|
87
|
+
raise ValueError(f"Unknown zone_id: {order.zone_id}") from error
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 5. Explicit error handling
|
|
91
|
+
|
|
92
|
+
RULE: Never swallow an error in an empty or generic `except`. Re-raise with context, or let it propagate.
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
# bad
|
|
96
|
+
def load_user(user_id):
|
|
97
|
+
try:
|
|
98
|
+
return api.get_user(user_id)
|
|
99
|
+
except Exception:
|
|
100
|
+
return None
|
|
101
|
+
# good
|
|
102
|
+
def load_user(user_id):
|
|
103
|
+
try:
|
|
104
|
+
return api.get_user(user_id)
|
|
105
|
+
except ApiError as error:
|
|
106
|
+
raise UserLoadError(f"Failed to load user {user_id}") from error
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### 6. Clear names
|
|
110
|
+
|
|
111
|
+
RULE: Names reveal role or domain meaning. No `data`, `info`, `temp`, `result`, `obj`, `val`, `x`. A long honest name beats a short vague one.
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
# bad
|
|
115
|
+
def process(data):
|
|
116
|
+
return [x for x in data if x.val > 0]
|
|
117
|
+
# good
|
|
118
|
+
def get_active_subscriptions(subscriptions):
|
|
119
|
+
return [subscription for subscription in subscriptions if subscription.remaining_days > 0]
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### 7. Command Query Separation (CQS)
|
|
123
|
+
|
|
124
|
+
RULE: A function either does something (side effect) or returns something (query) — never both. A "getter" must never mutate.
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
# bad
|
|
128
|
+
def get_next_id(counter):
|
|
129
|
+
counter.value += 1
|
|
130
|
+
return counter.value
|
|
131
|
+
# good
|
|
132
|
+
def peek_next_id(counter):
|
|
133
|
+
return counter.value + 1
|
|
134
|
+
|
|
135
|
+
def increment_counter(counter):
|
|
136
|
+
counter.value += 1
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### 8. Null/undefined handling
|
|
140
|
+
|
|
141
|
+
RULE: Give `None` one convention (for example, not set or explicitly empty) and never use it for several meanings. Give each business state its own explicit value.
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
# bad
|
|
145
|
+
def get_discount(user):
|
|
146
|
+
if user is None or user.plan is None or user.plan.discount == 0:
|
|
147
|
+
return None
|
|
148
|
+
# good
|
|
149
|
+
def get_discount(user):
|
|
150
|
+
if user is None or user.plan is None:
|
|
151
|
+
return 0
|
|
152
|
+
return user.plan.discount
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### 9. Immutability by default
|
|
156
|
+
|
|
157
|
+
RULE: Return new values instead of mutating parameters. Never hide a side effect inside what looks like a pure transform.
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
# bad
|
|
161
|
+
def add_item(cart, item):
|
|
162
|
+
cart["items"].append(item)
|
|
163
|
+
return cart
|
|
164
|
+
# good
|
|
165
|
+
def add_item(cart, item):
|
|
166
|
+
return {**cart, "items": [*cart["items"], item]}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### 10. Validate once
|
|
170
|
+
|
|
171
|
+
RULE: Validate at the boundary that owns the input (schema, request model, DTO). Downstream code trusts that contract — no re-checking the same required/type/range rule in a helper, use case, or adapter further in.
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
# bad
|
|
175
|
+
def update_profile(validated_profile, repository):
|
|
176
|
+
if not validated_profile.email:
|
|
177
|
+
raise ValueError("email required")
|
|
178
|
+
repository.update(validated_profile)
|
|
179
|
+
# good
|
|
180
|
+
def update_profile(validated_profile, repository):
|
|
181
|
+
repository.update(validated_profile)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### 11. Single Responsibility (SRP)
|
|
185
|
+
|
|
186
|
+
RULE: A function, class, or module has one reason to change. If it validates AND transforms AND persists AND notifies, split it.
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
# bad
|
|
190
|
+
def save_user(payload, repository, mailer):
|
|
191
|
+
if not payload["email"]:
|
|
192
|
+
raise ValueError("email required")
|
|
193
|
+
user = {**payload, "email": payload["email"].strip().lower()}
|
|
194
|
+
repository.insert(user)
|
|
195
|
+
mailer.send(user["email"], "welcome")
|
|
196
|
+
# good
|
|
197
|
+
def normalize_user(payload):
|
|
198
|
+
return {**payload, "email": payload["email"].strip().lower()}
|
|
199
|
+
|
|
200
|
+
def create_user(validated_payload, repository):
|
|
201
|
+
user = normalize_user(validated_payload)
|
|
202
|
+
repository.insert(user)
|
|
203
|
+
return user
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### 12. DRY (real duplication only)
|
|
207
|
+
|
|
208
|
+
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.
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
# bad — extracted after one real use
|
|
212
|
+
def format_name(value):
|
|
213
|
+
return value.strip().upper()
|
|
214
|
+
# good — same domain concept, third occurrence confirms the pattern
|
|
215
|
+
def normalize_ticket_status(status):
|
|
216
|
+
return TicketStatus(status.strip().lower())
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### 13. KISS
|
|
220
|
+
|
|
221
|
+
RULE: Ship the simplest solution for the current, real requirement. No configurability, hooks, or layers added "just in case."
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
# bad
|
|
225
|
+
def format_price(value, currency="USD", locale="en-US", show_symbol=True):
|
|
226
|
+
return price_formatter(value, currency, locale, show_symbol)
|
|
227
|
+
# good
|
|
228
|
+
def format_price(value):
|
|
229
|
+
return f"${value:.2f}"
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### 14. YAGNI
|
|
233
|
+
|
|
234
|
+
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.)
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
# bad
|
|
238
|
+
def create_invoice(order, supports_recurring=False, supports_multi_currency=False):
|
|
239
|
+
return build_invoice(order, supports_recurring, supports_multi_currency)
|
|
240
|
+
# good
|
|
241
|
+
def create_invoice(order):
|
|
242
|
+
return {"total": order.total, "items": order.items}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### 15. Law of Demeter
|
|
246
|
+
|
|
247
|
+
RULE: Talk only to immediate collaborators. Never reach through an object to grab something several levels deep inside its internal structure.
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
# bad
|
|
251
|
+
def get_city_name(user):
|
|
252
|
+
return user.address.city.name
|
|
253
|
+
# good
|
|
254
|
+
def get_city_name(user):
|
|
255
|
+
return user.get_city_name()
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### 16. Composition over inheritance
|
|
259
|
+
|
|
260
|
+
RULE: Compose small, focused behaviors instead of building inheritance chains that force unrelated methods onto every subclass.
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
# bad
|
|
264
|
+
class Animal:
|
|
265
|
+
def fly(self): ...
|
|
266
|
+
class Dog(Animal):
|
|
267
|
+
pass
|
|
268
|
+
# good
|
|
269
|
+
class Bark:
|
|
270
|
+
def make_sound(self):
|
|
271
|
+
return "Woof"
|
|
272
|
+
class Dog:
|
|
273
|
+
def __init__(self, sound=Bark()):
|
|
274
|
+
self.sound = sound
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### 17. Dependency direction
|
|
278
|
+
|
|
279
|
+
RULE: Inner layers (domain/business logic) never import outer layers (framework, DB, HTTP). Outer layers depend inward, never the reverse.
|
|
280
|
+
|
|
281
|
+
```python
|
|
282
|
+
# bad
|
|
283
|
+
def calculate_invoice_total(invoice_id):
|
|
284
|
+
invoice = MysqlInvoiceRepository().get(invoice_id)
|
|
285
|
+
return sum(item.price for item in invoice.items)
|
|
286
|
+
# good
|
|
287
|
+
def calculate_invoice_total(invoice):
|
|
288
|
+
return sum(item.price for item in invoice.items)
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### 18. Testability as a design constraint
|
|
292
|
+
|
|
293
|
+
RULE: Inject dependencies (clock, network, random) instead of hardcoding them. If a function is hard to test, redesign it — don't skip the test.
|
|
294
|
+
|
|
295
|
+
```python
|
|
296
|
+
# bad
|
|
297
|
+
def is_subscription_expired(subscription):
|
|
298
|
+
return subscription.expires_at < datetime.now(timezone.utc)
|
|
299
|
+
# good
|
|
300
|
+
def is_subscription_expired(subscription, now):
|
|
301
|
+
return subscription.expires_at < now
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### 19. Comments
|
|
305
|
+
|
|
306
|
+
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.
|
|
307
|
+
|
|
308
|
+
```python
|
|
309
|
+
# bad
|
|
310
|
+
# Increment the counter by one.
|
|
311
|
+
counter += 1
|
|
312
|
+
# good
|
|
313
|
+
# Polling avoids duplicate webhook delivery that the provider cannot prevent.
|
|
314
|
+
scheduler.every(5).seconds.do(check_payment_status)
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
**When in doubt:** fail fast, keep it flat, keep it small.
|
|
320
|
+
|
|
321
|
+
**Enforcement note:** rules 1–2 are mechanical and should be backed by Python formatting or linting. Rules 3–10 are structural checks. Rules 11–19 require review judgment.
|