@rmrdeveloper/sideroom-pi 5.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.
@@ -0,0 +1,231 @@
1
+ # TypeScript Coding Guidelines
2
+
3
+ This is the TypeScript version of `GUIDELINES_TEMPLATE.md`. It preserves the
4
+ same principles, decisions, and example scenarios, expressed with strict
5
+ TypeScript. The shared baseline remains mandatory.
6
+
7
+ ## Quick reference
8
+
9
+ | Do | Don't |
10
+ | --- | --- |
11
+ | Early return on bad input | Pyramid `if/else` nesting |
12
+ | Explicit error, fail now | Fallbacks that hide a broken invariant |
13
+ | One responsibility per unit | Validate, transform, persist, and notify together |
14
+ | Extract repeated domain logic | Abstract before a second real use |
15
+ | Ship the simplest solution | Add speculative layers or configuration |
16
+ | Return new values | Mutate caller-owned input |
17
+ | Validate once at the edge | Re-validate the same contract in every layer |
18
+ | Use named types and constants | Scatter magic literals |
19
+
20
+ ## Principles
21
+
22
+ ### Guard clauses
23
+
24
+ Keep the same discount scenario flat and typed.
25
+
26
+ ```ts
27
+ // Bad
28
+ function getDiscount(user?: { active: boolean; subscriber: boolean }): number {
29
+ if (user) { if (user.active) { if (user.subscriber) return 0.2; } }
30
+ return 0;
31
+ }
32
+
33
+ // Good
34
+ function getDiscount(user?: { active: boolean; subscriber: boolean }): number {
35
+ if (!user) return 0;
36
+ if (!user.active) return 0;
37
+ if (!user.subscriber) return 0;
38
+ return 0.2;
39
+ }
40
+ ```
41
+
42
+ ### Fail fast
43
+
44
+ Never substitute a default zone for an impossible domain key.
45
+
46
+ ```ts
47
+ const zones: Record<string, { baseCost: number }> = { local: { baseCost: 5 } };
48
+ function shippingCost(zoneId: string): number {
49
+ const zone = zones[zoneId];
50
+ if (zone === undefined) throw new Error(`Unknown zoneId: ${zoneId}`);
51
+ return zone.baseCost;
52
+ }
53
+ ```
54
+
55
+ ### SRP
56
+
57
+ Split validation, normalization, persistence, and notification.
58
+
59
+ ```ts
60
+ function normalizeEmail(email: string): string { return email.trim().toLowerCase(); }
61
+ async function createUser(email: string, users: UserRepository, mailer: Mailer): Promise<void> {
62
+ if (email.length === 0) throw new Error('email required');
63
+ const user = await users.insert({ email: normalizeEmail(email) });
64
+ await mailer.send(user.email, 'welcome');
65
+ }
66
+ ```
67
+
68
+ ### DRY
69
+
70
+ Extract only repeated domain behavior. One formatting call is not a reusable
71
+ abstraction; three identical ticket-status normalizations are.
72
+
73
+ ```ts
74
+ type TicketStatus = 'open' | 'closed';
75
+ function normalizeTicketStatus(value: string): TicketStatus {
76
+ if (value === 'open' || value === 'closed') return value;
77
+ throw new Error(`Unknown ticket status: ${value}`);
78
+ }
79
+ ```
80
+
81
+ ### KISS
82
+
83
+ Keep the price and invoice scenarios limited to today's requirement.
84
+
85
+ ```ts
86
+ function formatPrice(value: number): string { return `$${value.toFixed(2)}`; }
87
+ ```
88
+
89
+ ### YAGNI (You Aren't Gonna Need It)
90
+
91
+ Build the invoice requirement that exists today; do not add recurrence,
92
+ multi-currency branches, or a future configuration object.
93
+
94
+ ```ts
95
+ function createInvoice(order: { total: number; items: readonly string[] }) {
96
+ return { total: order.total, items: order.items };
97
+ }
98
+ ```
99
+
100
+ Do not add locale strategies, recurring options, or multi-currency branches
101
+ without a requirement.
102
+
103
+ ### Composition over inheritance
104
+
105
+ Compose only needed behavior instead of inheriting unrelated methods.
106
+
107
+ ```ts
108
+ const canBark = { makeSound: () => 'Woof' };
109
+ const canFly = { fly: () => 'Flying' };
110
+ const dog = { ...canBark };
111
+ const bird = { ...canBark, ...canFly };
112
+ ```
113
+
114
+ ### Law of Demeter
115
+
116
+ Ask the immediate collaborator for a city name instead of reaching through a
117
+ deep object graph.
118
+
119
+ ```ts
120
+ interface User { cityName(): string; }
121
+ function getCityName(user: User): string { return user.cityName(); }
122
+ ```
123
+
124
+ ### Command Query Separation
125
+
126
+ Keep reading the next identifier separate from incrementing it.
127
+
128
+ ```ts
129
+ function peekNextId(counter: { value: number }): number { return counter.value + 1; }
130
+ function incrementCounter(counter: { value: number }): void { counter.value += 1; }
131
+ ```
132
+
133
+ ### Explicit error handling
134
+
135
+ Add context and retain the cause; never return `null` merely because a request
136
+ failed.
137
+
138
+ ```ts
139
+ async function loadUser(id: string, api: Api): Promise<User> {
140
+ try { return await api.getUser(id); }
141
+ catch (error) { throw new Error(`Failed to load user ${id}`, { cause: error }); }
142
+ }
143
+ ```
144
+
145
+ ### Immutability by default
146
+
147
+ Use readonly data and make the cart scenario return a replacement value.
148
+
149
+ ```ts
150
+ interface Cart { readonly items: readonly string[]; }
151
+ function addItem(cart: Cart, item: string): Cart {
152
+ return { ...cart, items: [...cart.items, item] };
153
+ }
154
+ ```
155
+
156
+ ### Null and undefined handling
157
+
158
+ Use one documented convention and preserve legitimate zero values.
159
+
160
+ ```ts
161
+ function getDiscount(user?: { plan?: { discount: number } }): number {
162
+ return user?.plan?.discount ?? 0;
163
+ }
164
+ ```
165
+
166
+ ### Testability as a design constraint
167
+
168
+ Inject time, APIs, and repositories rather than reaching for globals.
169
+
170
+ ```ts
171
+ function isSubscriptionExpired(expiresAt: Date, now: Date): boolean {
172
+ return expiresAt < now;
173
+ }
174
+ ```
175
+
176
+ ### Dependency direction
177
+
178
+ Domain calculations receive domain data, not a concrete SQL connection.
179
+
180
+ ```ts
181
+ function calculateInvoiceTotal(invoice: { items: readonly { price: number }[] }): number {
182
+ return invoice.items.reduce((sum, item) => sum + item.price, 0);
183
+ }
184
+ ```
185
+
186
+ ### Clear names
187
+
188
+ Use `activeSubscriptions`, `remainingDays`, and `externalId`, never `data`,
189
+ `temp`, or `x`.
190
+
191
+ ### Comments
192
+
193
+ Comments explain a non-obvious trade-off or hazard, not the next line of code.
194
+ A TODO needs a tracked issue.
195
+
196
+ ### No magic strings
197
+
198
+ Name order states once and reuse their contract.
199
+
200
+ ```ts
201
+ const ORDER_STATUS = { pendingPayment: 'pending_payment', paid: 'paid' } as const;
202
+ if (order.status === ORDER_STATUS.pendingPayment) await requestPayment(order);
203
+ ```
204
+
205
+ ### SOLID
206
+
207
+ Use ports where variation is real; do not couple a welcome-email use case to
208
+ SMTP.
209
+
210
+ ```ts
211
+ interface Mailer { send(email: string, template: string): Promise<void>; }
212
+ class SendWelcomeEmail {
213
+ constructor(private readonly mailer: Mailer) {}
214
+ execute(email: string): Promise<void> { return this.mailer.send(email, 'welcome'); }
215
+ }
216
+ ```
217
+
218
+ ### Validate once
219
+
220
+ Parse the transport payload at the boundary into a typed command. Services
221
+ trust that command rather than repeating the same required, type, and range
222
+ checks. Keep one schema or validator per body/query.
223
+
224
+ ```ts
225
+ interface UpdateProfile { readonly email: string; }
226
+ function updateProfile(input: UpdateProfile, users: UserRepository): Promise<void> {
227
+ return users.update(input);
228
+ }
229
+ ```
230
+
231
+ When in doubt: **fail fast, keep it flat, keep it small.**