@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.
@@ -1,19 +1,251 @@
1
- # Rust
1
+ # Rust Coding Guidelines
2
2
 
3
- Delta only. Keep the shared table.
3
+ Rules every Rust change must follow.
4
4
 
5
- - Prefer `Result` and `?`. Do not `.unwrap()` / `.expect()` in library or production paths.
6
- - Let errors surface with context (`map_err`, `anyhow`, or an equivalent). Do not swallow with `let _ =`.
7
- - Do not mutate the caller's value when returning a new one is enough.
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
- // Bad
11
- fn load_user(id: &str) -> User {
12
- repo.get(id).unwrap()
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
- // Good
16
- fn load_user(id: &str) -> Result<User, LoadUserError> {
17
- repo.get(id).map_err(|err| LoadUserError::from_repo(id, err))
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
- Delta only. Keep the shared table.
4
-
5
- - Braces on every `if` / `else`, including one-line guards.
6
- - Pick one meaning for `undefined` vs `null` and keep it. Do not use either as a stand-in for a named business state.
7
- - Name domain literals once (`const` / enum / const object). Do not scatter magic strings.
8
-
9
- ```ts
10
- // Bad
11
- function getDiscount(user?: { plan?: { discount: number } }) {
12
- if (!user) {
13
- return null;
14
- }
15
- if (!user.plan) {
16
- return null;
17
- }
18
- if (user.plan.discount === 0) {
19
- return null;
20
- }
21
- }
22
-
23
- // Good
24
- function getDiscount(user: { plan?: { discount: number } } | undefined): number {
25
- if (user === undefined || user.plan === undefined) {
26
- return 0;
27
- }
28
- return user.plan.discount;
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.