@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,19 +1,251 @@
|
|
|
1
|
-
# Rust
|
|
1
|
+
# Rust Coding Guidelines
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Rules every Rust 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: Rust requires braces for every `if`/`else` body. Keep even a one-line guard as a full, formatted block rather than compressing it onto one line.
|
|
8
34
|
|
|
9
35
|
```rust
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
36
|
+
// bad
|
|
37
|
+
if user.is_none() { return 0; }
|
|
38
|
+
// good
|
|
39
|
+
if user.is_none() {
|
|
40
|
+
return 0;
|
|
13
41
|
}
|
|
42
|
+
```
|
|
14
43
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
44
|
+
### 2. No magic strings
|
|
45
|
+
|
|
46
|
+
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.
|
|
47
|
+
|
|
48
|
+
```rust
|
|
49
|
+
// bad
|
|
50
|
+
if order.status == "pending_payment" { charge(order); }
|
|
51
|
+
// good
|
|
52
|
+
if order.status == OrderStatus::PendingPayment { charge(order); }
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 3. Guard clauses
|
|
56
|
+
|
|
57
|
+
RULE: Validate and exit early. Keep the happy path last, at the shallowest indent. No pyramid nesting.
|
|
58
|
+
|
|
59
|
+
```rust
|
|
60
|
+
// bad
|
|
61
|
+
if let Some(user) = user { if user.active { return 20; } } 0
|
|
62
|
+
// good
|
|
63
|
+
let Some(user) = user else { return 0; }; if !user.active { return 0; } 20
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 4. Fail fast
|
|
67
|
+
|
|
68
|
+
RULE: Invalid input, impossible state, or a broken dependency returns an error immediately with a clear message. No fallback chains, silent defaults, or production `unwrap`/`expect`.
|
|
69
|
+
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).
|
|
70
|
+
|
|
71
|
+
```rust
|
|
72
|
+
// bad
|
|
73
|
+
let zone = zones.get(&order.zone_id).unwrap_or(&default_zone);
|
|
74
|
+
// good
|
|
75
|
+
let zone = zones.get(&order.zone_id).ok_or_else(|| ZoneError::Unknown(order.zone_id.clone()))?;
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 5. Explicit error handling
|
|
79
|
+
|
|
80
|
+
RULE: Never swallow an error. Return it with context using `Result`, or let it propagate with `?`.
|
|
81
|
+
|
|
82
|
+
```rust
|
|
83
|
+
// bad
|
|
84
|
+
let user = api.get_user(id).ok();
|
|
85
|
+
// good
|
|
86
|
+
let user = api.get_user(id).map_err(|error| LoadUserError::new(id, error))?;
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 6. Clear names
|
|
90
|
+
|
|
91
|
+
RULE: Names reveal role or domain meaning. No `data`, `info`, `temp`, `result`, `obj`, `val`, `x`. A long honest name beats a short vague one.
|
|
92
|
+
|
|
93
|
+
```rust
|
|
94
|
+
// bad
|
|
95
|
+
fn process(data: &[Subscription]) -> Vec<&Subscription> { data.iter().filter(|x| x.days > 0).collect() }
|
|
96
|
+
// good
|
|
97
|
+
fn active_subscriptions(subscriptions: &[Subscription]) -> Vec<&Subscription> { subscriptions.iter().filter(|subscription| subscription.remaining_days > 0).collect() }
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 7. Command Query Separation (CQS)
|
|
101
|
+
|
|
102
|
+
RULE: A function either does something (side effect) or returns something (query) — never both. A "getter" must never mutate.
|
|
103
|
+
|
|
104
|
+
```rust
|
|
105
|
+
// bad
|
|
106
|
+
fn next_id(counter: &mut Counter) -> u64 { counter.value += 1; counter.value }
|
|
107
|
+
// good
|
|
108
|
+
fn peek_next_id(counter: &Counter) -> u64 { counter.value + 1 }
|
|
109
|
+
fn increment_counter(counter: &mut Counter) { counter.value += 1; }
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### 8. Null/undefined handling
|
|
113
|
+
|
|
114
|
+
RULE: Use `Option` only for genuine absence and never overload `None` for several business states. Give each state its own enum variant or value.
|
|
115
|
+
|
|
116
|
+
```rust
|
|
117
|
+
// bad
|
|
118
|
+
fn discount(user: Option<&User>) -> Option<u8> { user.and_then(|u| u.plan.as_ref()).and_then(|p| (p.discount > 0).then_some(p.discount)) }
|
|
119
|
+
// good
|
|
120
|
+
fn discount(user: Option<&User>) -> u8 { user.and_then(|u| u.plan.as_ref()).map_or(0, |plan| plan.discount) }
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 9. Immutability by default
|
|
124
|
+
|
|
125
|
+
RULE: Return new values instead of mutating parameters. Never hide a side effect inside what looks like a pure transform.
|
|
126
|
+
|
|
127
|
+
```rust
|
|
128
|
+
// bad
|
|
129
|
+
fn add_item(cart: &mut Cart, item: Item) { cart.items.push(item); }
|
|
130
|
+
// good
|
|
131
|
+
fn add_item(mut cart: Cart, item: Item) -> Cart { cart.items.push(item); cart }
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### 10. Validate once
|
|
135
|
+
|
|
136
|
+
RULE: Validate at the boundary that owns the input (deserializer, validated type, DTO). Downstream code trusts that contract — no re-checking the same required/type/range rule further in.
|
|
137
|
+
|
|
138
|
+
```rust
|
|
139
|
+
// bad
|
|
140
|
+
fn update(profile: ValidatedProfile, repo: &impl Repo) -> Result<()> { if profile.email.is_empty() { bail!("email required"); } repo.update(profile) }
|
|
141
|
+
// good
|
|
142
|
+
fn update(profile: ValidatedProfile, repo: &impl Repo) -> Result<()> { repo.update(profile) }
|
|
19
143
|
```
|
|
144
|
+
|
|
145
|
+
### 11. Single Responsibility (SRP)
|
|
146
|
+
|
|
147
|
+
RULE: A function, class, or module has one reason to change. If it validates AND transforms AND persists AND notifies, split it.
|
|
148
|
+
|
|
149
|
+
```rust
|
|
150
|
+
// bad
|
|
151
|
+
fn save(input: Input) -> Result<()> { validate(&input)?; let user = normalize(input); repo().save(&user)?; mailer().welcome(&user) }
|
|
152
|
+
// good
|
|
153
|
+
fn create(input: ValidatedInput, repo: &impl Repo) -> Result<User> { let user = normalize(input); repo.save(&user)?; Ok(user) }
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### 12. DRY (real duplication only)
|
|
157
|
+
|
|
158
|
+
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.
|
|
159
|
+
|
|
160
|
+
```rust
|
|
161
|
+
// bad — extracted after one use
|
|
162
|
+
fn format_name(value: &str) -> String { value.trim().to_uppercase() }
|
|
163
|
+
// good — third occurrence confirms one domain concept
|
|
164
|
+
fn normalize_ticket_status(status: &str) -> Result<TicketStatus> { status.trim().parse() }
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### 13. KISS
|
|
168
|
+
|
|
169
|
+
RULE: Ship the simplest solution for the current, real requirement. No configurability, hooks, or layers added "just in case."
|
|
170
|
+
|
|
171
|
+
```rust
|
|
172
|
+
// bad
|
|
173
|
+
fn format_price(value: f64, options: FormatOptions) -> String { Formatter::new(options).format(value) }
|
|
174
|
+
// good
|
|
175
|
+
fn format_price(value: f64) -> String { format!("${value:.2}") }
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### 14. YAGNI
|
|
179
|
+
|
|
180
|
+
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.)
|
|
181
|
+
|
|
182
|
+
```rust
|
|
183
|
+
// bad
|
|
184
|
+
fn create_invoice(order: Order, recurring: bool, multi_currency: bool) -> Invoice { build(order, recurring, multi_currency) }
|
|
185
|
+
// good
|
|
186
|
+
fn create_invoice(order: Order) -> Invoice { Invoice { total: order.total, items: order.items } }
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### 15. Law of Demeter
|
|
190
|
+
|
|
191
|
+
RULE: Talk only to immediate collaborators. Never reach through an object to grab something several levels deep inside its internal structure.
|
|
192
|
+
|
|
193
|
+
```rust
|
|
194
|
+
// bad
|
|
195
|
+
fn city_name(user: &User) -> &str { &user.address.city.name }
|
|
196
|
+
// good
|
|
197
|
+
fn city_name(user: &User) -> &str { user.city_name() }
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### 16. Composition over inheritance
|
|
201
|
+
|
|
202
|
+
RULE: Compose small, focused behaviors instead of building inheritance chains that force unrelated methods onto every subtype.
|
|
203
|
+
|
|
204
|
+
```rust
|
|
205
|
+
// bad
|
|
206
|
+
trait Animal { fn sound(&self) -> &str; fn fly(&self); }
|
|
207
|
+
// good
|
|
208
|
+
struct Dog<S: SoundBehavior> { sound: S }
|
|
209
|
+
struct Bird<S: SoundBehavior, F: FlightBehavior> { sound: S, flight: F }
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### 17. Dependency direction
|
|
213
|
+
|
|
214
|
+
RULE: Inner layers (domain/business logic) never import outer layers (framework, DB, HTTP). Outer layers depend inward, never the reverse.
|
|
215
|
+
|
|
216
|
+
```rust
|
|
217
|
+
// bad
|
|
218
|
+
fn total(id: InvoiceId) -> Money { MysqlInvoiceRepository::new().get(id).total() }
|
|
219
|
+
// good
|
|
220
|
+
fn total(invoice: &Invoice) -> Money { invoice.total() }
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### 18. Testability as a design constraint
|
|
224
|
+
|
|
225
|
+
RULE: Inject dependencies (clock, network, random) instead of hardcoding them. If a function is hard to test, redesign it — don't skip the test.
|
|
226
|
+
|
|
227
|
+
```rust
|
|
228
|
+
// bad
|
|
229
|
+
fn expired(subscription: &Subscription) -> bool { subscription.expires_at < Utc::now() }
|
|
230
|
+
// good
|
|
231
|
+
fn expired(subscription: &Subscription, now: DateTime<Utc>) -> bool { subscription.expires_at < now }
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### 19. Comments
|
|
235
|
+
|
|
236
|
+
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.
|
|
237
|
+
|
|
238
|
+
```rust
|
|
239
|
+
// bad
|
|
240
|
+
// Increment the counter.
|
|
241
|
+
counter += 1;
|
|
242
|
+
// good
|
|
243
|
+
// Poll because the provider cannot guarantee single webhook delivery.
|
|
244
|
+
schedule_poll(Duration::from_secs(5));
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
**When in doubt:** fail fast, keep it flat, keep it small.
|
|
250
|
+
|
|
251
|
+
**Enforcement note:** rules 1–2 are mechanical and should be backed by Rust formatting or linting. Rules 3–10 are structural checks. Rules 11–19 require review judgment.
|
|
@@ -1,30 +1,249 @@
|
|
|
1
|
-
# TypeScript
|
|
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
|
-
|
|
1
|
+
# TypeScript Coding Guidelines
|
|
2
|
+
|
|
3
|
+
Rules every TypeScript change must follow.
|
|
4
|
+
|
|
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
|
+
```ts
|
|
36
|
+
// bad
|
|
37
|
+
if (!user) return 0;
|
|
38
|
+
// good
|
|
39
|
+
if (!user) { 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
|
+
```ts
|
|
47
|
+
// bad
|
|
48
|
+
if (order.status === 'pending_payment') { charge(order); }
|
|
49
|
+
// good
|
|
50
|
+
const ORDER_STATUS = { PENDING_PAYMENT: 'pending_payment' } as const;
|
|
51
|
+
if (order.status === ORDER_STATUS.PENDING_PAYMENT) { charge(order); }
|
|
30
52
|
```
|
|
53
|
+
|
|
54
|
+
### 3. Guard clauses
|
|
55
|
+
|
|
56
|
+
RULE: Validate and exit early. Keep the happy path last, at the shallowest indent. No pyramid nesting.
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
// bad
|
|
60
|
+
function discount(user?: User): number { if (user) { if (user.active) { return user.subscribed ? .2 : 0; } } return 0; }
|
|
61
|
+
// good
|
|
62
|
+
function discount(user?: User): number { if (!user || !user.active || !user.subscribed) { return 0; } return .2; }
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### 4. Fail fast
|
|
66
|
+
|
|
67
|
+
RULE: Invalid input, impossible state, or a broken dependency throws immediately with a clear message. No fallback chains or silent defaults.
|
|
68
|
+
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).
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
// bad
|
|
72
|
+
const zone = zones[order.zoneId] ?? zones.default;
|
|
73
|
+
// good
|
|
74
|
+
const zone = zones[order.zoneId]; if (!zone) { throw new Error(`Unknown zoneId: ${order.zoneId}`); }
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 5. Explicit error handling
|
|
78
|
+
|
|
79
|
+
RULE: Never swallow an error in an empty or generic `catch`. Re-throw with context, or let it propagate.
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
// bad
|
|
83
|
+
async function loadUser(id: string): Promise<User | null> { try { return await api.getUser(id); } catch { return null; } }
|
|
84
|
+
// good
|
|
85
|
+
async function loadUser(id: string): Promise<User> { try { return await api.getUser(id); } catch (error) { throw new Error(`Failed to load user ${id}`, { cause: error }); } }
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 6. Clear names
|
|
89
|
+
|
|
90
|
+
RULE: Names reveal role or domain meaning. No `data`, `info`, `temp`, `result`, `obj`, `val`, `x`. A long honest name beats a short vague one.
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
// bad
|
|
94
|
+
const process = (data: Subscription[]) => data.filter((x) => x.val > 0);
|
|
95
|
+
// good
|
|
96
|
+
const activeSubscriptions = (subscriptions: Subscription[]) => subscriptions.filter((subscription) => subscription.remainingDays > 0);
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 7. Command Query Separation (CQS)
|
|
100
|
+
|
|
101
|
+
RULE: A function either does something (side effect) or returns something (query) — never both. A "getter" must never mutate.
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
// bad
|
|
105
|
+
function nextId(counter: Counter): number { counter.value++; return counter.value; }
|
|
106
|
+
// good
|
|
107
|
+
function peekNextId(counter: Counter): number { return counter.value + 1; }
|
|
108
|
+
function incrementCounter(counter: Counter): void { counter.value++; }
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 8. Null/undefined handling
|
|
112
|
+
|
|
113
|
+
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.
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
// bad
|
|
117
|
+
function discount(user?: User): number | null { return !user?.plan || user.plan.discount === 0 ? null : user.plan.discount; }
|
|
118
|
+
// good
|
|
119
|
+
function discount(user?: User): number { return user?.plan?.discount ?? 0; }
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### 9. Immutability by default
|
|
123
|
+
|
|
124
|
+
RULE: Return new values instead of mutating parameters. Never hide a side effect inside what looks like a pure transform.
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
// bad
|
|
128
|
+
function addItem(cart: Cart, item: Item): Cart { cart.items.push(item); return cart; }
|
|
129
|
+
// good
|
|
130
|
+
function addItem(cart: Cart, item: Item): Cart { return { ...cart, items: [...cart.items, item] }; }
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### 10. Validate once
|
|
134
|
+
|
|
135
|
+
RULE: Validate at the boundary that owns the input (schema, request DTO). Downstream code trusts that contract — no re-checking the same required/type/range rule further in.
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
// bad
|
|
139
|
+
function update(profile: ValidatedProfile): void { if (!profile.email) { throw new Error('email required'); } repository.update(profile); }
|
|
140
|
+
// good
|
|
141
|
+
function update(profile: ValidatedProfile): void { repository.update(profile); }
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### 11. Single Responsibility (SRP)
|
|
145
|
+
|
|
146
|
+
RULE: A function, class, or module has one reason to change. If it validates AND transforms AND persists AND notifies, split it.
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
// bad
|
|
150
|
+
async function save(input: UserInput): Promise<void> { validate(input); const user = normalize(input); await repository.save(user); await mailer.welcome(user); }
|
|
151
|
+
// good
|
|
152
|
+
async function create(input: ValidatedUser): Promise<User> { const user = normalize(input); await repository.save(user); return user; }
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### 12. DRY (real duplication only)
|
|
156
|
+
|
|
157
|
+
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.
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
// bad — extracted after one use
|
|
161
|
+
const formatName = (value: string): string => value.trim().toUpperCase();
|
|
162
|
+
// good — third occurrence confirms one domain concept
|
|
163
|
+
const normalizeTicketStatus = (status: string): TicketStatus => parseTicketStatus(status.trim());
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### 13. KISS
|
|
167
|
+
|
|
168
|
+
RULE: Ship the simplest solution for the current, real requirement. No configurability, hooks, or layers added "just in case."
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
// bad
|
|
172
|
+
function formatPrice(value: number, options: FormatOptions = defaults): string { return formatter.format(value, options); }
|
|
173
|
+
// good
|
|
174
|
+
function formatPrice(value: number): string { return `$${value.toFixed(2)}`; }
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### 14. YAGNI
|
|
178
|
+
|
|
179
|
+
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.)
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
// bad
|
|
183
|
+
function createInvoice(order: Order, recurring = false, multiCurrency = false): Invoice { return build(order, recurring, multiCurrency); }
|
|
184
|
+
// good
|
|
185
|
+
function createInvoice(order: Order): Invoice { return { total: order.total, items: order.items }; }
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### 15. Law of Demeter
|
|
189
|
+
|
|
190
|
+
RULE: Talk only to immediate collaborators. Never reach through an object to grab something several levels deep inside its internal structure.
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
// bad
|
|
194
|
+
const cityName = (user: User): string => user.address.city.name;
|
|
195
|
+
// good
|
|
196
|
+
const cityName = (user: User): string => user.getCityName();
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### 16. Composition over inheritance
|
|
200
|
+
|
|
201
|
+
RULE: Compose small, focused behaviors instead of building inheritance chains that force unrelated methods onto every subclass.
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
// bad
|
|
205
|
+
abstract class Animal { abstract sound(): string; abstract fly(): void; }
|
|
206
|
+
// good
|
|
207
|
+
type Dog = { sound: SoundBehavior }; type Bird = { sound: SoundBehavior; flight: FlightBehavior };
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### 17. Dependency direction
|
|
211
|
+
|
|
212
|
+
RULE: Inner layers (domain/business logic) never import outer layers (framework, DB, HTTP). Outer layers depend inward, never the reverse.
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
// bad
|
|
216
|
+
function total(id: string): Money { return new MysqlInvoiceRepository().get(id).total; }
|
|
217
|
+
// good
|
|
218
|
+
function total(invoice: Invoice): Money { return invoice.total; }
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### 18. Testability as a design constraint
|
|
222
|
+
|
|
223
|
+
RULE: Inject dependencies (clock, network, random) instead of hardcoding them. If a function is hard to test, redesign it — don't skip the test.
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
// bad
|
|
227
|
+
const expired = (subscription: Subscription): boolean => subscription.expiresAt < new Date();
|
|
228
|
+
// good
|
|
229
|
+
const expired = (subscription: Subscription, now: Date): boolean => subscription.expiresAt < now;
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### 19. Comments
|
|
233
|
+
|
|
234
|
+
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.
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
// bad
|
|
238
|
+
// Increment the counter.
|
|
239
|
+
counter++;
|
|
240
|
+
// good
|
|
241
|
+
// Poll because the provider cannot guarantee single webhook delivery.
|
|
242
|
+
setInterval(checkPaymentStatus, 5_000);
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
**When in doubt:** fail fast, keep it flat, keep it small.
|
|
248
|
+
|
|
249
|
+
**Enforcement note:** rules 1–2 are mechanical and should be backed by TypeScript formatting or linting. Rules 3–10 are structural checks. Rules 11–19 require review judgment.
|