@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.
- package/LICENSE +21 -0
- package/README.md +119 -0
- package/dist/pi-extension.js +1175 -0
- package/package.json +70 -0
- package/skills/sideroom-critic/SKILL.md +32 -0
- package/skills/sideroom-grilling/SKILL.md +81 -0
- package/skills/sideroom-spec/SKILL.md +38 -0
- package/skills/sideroom-transcribe-audio/SKILL.md +38 -0
- package/skills/sideroom-transcribe-audio/scripts/transcribe.py +40 -0
- package/src/assets/agents/sideroom-code-reviewer.md +30 -0
- package/src/assets/agents/sideroom-fixer.md +37 -0
- package/src/assets/agents/sideroom-implementer.md +39 -0
- package/src/assets/agents/sideroom-planner.md +31 -0
- package/src/assets/agents/sideroom-verifier.md +32 -0
- package/src/assets/artifacts/GUIDELINES_TEMPLATE.md +463 -0
- package/src/assets/artifacts/guidelines/java.md +211 -0
- package/src/assets/artifacts/guidelines/javascript.md +235 -0
- package/src/assets/artifacts/guidelines/php-laravel.md +210 -0
- package/src/assets/artifacts/guidelines/python.md +227 -0
- package/src/assets/artifacts/guidelines/typescript.md +231 -0
|
@@ -0,0 +1,463 @@
|
|
|
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
|
+
| Early return on bad input | Pyramid `if/else` nesting |
|
|
12
|
+
| Explicit error, fail now | Multiple fallbacks that hide the real failure |
|
|
13
|
+
| One responsibility per unit | Validate + transform + persist + notify in one place |
|
|
14
|
+
| Extract when duplication repeats | Abstract before a second real use |
|
|
15
|
+
| Ship the simplest solution for the current problem | Add layers, hooks, or config "just in case" |
|
|
16
|
+
| Build only what's needed today | Add fields/params/branches for a future case that hasn't arrived |
|
|
17
|
+
| Compose small, focused units | Build deep inheritance chains for unrelated behavior |
|
|
18
|
+
| Talk only to immediate collaborators | Reach through several levels of another object's internal structure |
|
|
19
|
+
| A function either does or returns, not both | Mix a side effect into what looks like a getter |
|
|
20
|
+
| Validate once at the edge | Re-validate the same invariant in every layer |
|
|
21
|
+
| One validator per input | Two validators for the same body/query |
|
|
22
|
+
| Let errors surface with context | Swallow errors in an empty or generic `catch`, or catch-and-continue as if nothing happened |
|
|
23
|
+
| Return new values instead of mutating input | Mutate parameters and hide the side effect from the caller |
|
|
24
|
+
| One consistent meaning per null/undefined | Overload null/undefined to mean several different business states |
|
|
25
|
+
| Inject dependencies so units are easy to test | Hardcode dependencies that force hitting real infra to test |
|
|
26
|
+
| Inner layers depend on nothing outward | Let domain logic import framework/DB/HTTP details directly |
|
|
27
|
+
| Names that reveal role or domain meaning | Vague names (`data`, `info`, `temp`, `result`, `obj`) |
|
|
28
|
+
| Named const / enum / contract for domain literals | Magic strings scattered through the codebase |
|
|
29
|
+
| Depend on interfaces/ports where variation is real | Couple a use case directly to a concrete implementation |
|
|
30
|
+
| Comments only for important non-obvious intent | Narrating comments, noise, or stale TODOs |
|
|
31
|
+
|
|
32
|
+
## Principles
|
|
33
|
+
|
|
34
|
+
### Guard clauses
|
|
35
|
+
|
|
36
|
+
Validate and exit early. Prefer flat control flow over deep nesting. Keep the happy path at the end, at the shallowest indent level.
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
// Bad — pyramid nesting, happy path buried
|
|
40
|
+
function getDiscount(user) {
|
|
41
|
+
if (user) {
|
|
42
|
+
if (user.isActive) {
|
|
43
|
+
if (user.hasSubscription) {
|
|
44
|
+
return 0.2;
|
|
45
|
+
} else {
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Good — guard clauses, happy path at the end, shallow indent
|
|
57
|
+
function getDiscount(user) {
|
|
58
|
+
if (!user) return 0;
|
|
59
|
+
if (!user.isActive) return 0;
|
|
60
|
+
if (!user.hasSubscription) return 0;
|
|
61
|
+
return 0.2;
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Fail fast
|
|
66
|
+
|
|
67
|
+
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."
|
|
68
|
+
|
|
69
|
+
**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.
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
// Bad — silent fallback hides a broken invariant
|
|
73
|
+
function getShippingCost(order) {
|
|
74
|
+
const zone = ZONES[order.zoneId] || ZONES.default; // hides a bad zoneId
|
|
75
|
+
return zone.baseCost;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Good — fails immediately with a clear error
|
|
79
|
+
function getShippingCost(order) {
|
|
80
|
+
const zone = ZONES[order.zoneId];
|
|
81
|
+
if (!zone) throw new Error(`Unknown zoneId: ${order.zoneId}`);
|
|
82
|
+
return zone.baseCost;
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### SRP
|
|
87
|
+
|
|
88
|
+
A function, class, or module has one reason to change. If it does two jobs, split it.
|
|
89
|
+
|
|
90
|
+
```js
|
|
91
|
+
// Bad — validates, transforms, persists, and notifies all in one place
|
|
92
|
+
async function saveUser(data) {
|
|
93
|
+
if (!data.email) throw new Error('email required');
|
|
94
|
+
const normalized = { ...data, email: data.email.trim().toLowerCase() };
|
|
95
|
+
await db.users.insert(normalized);
|
|
96
|
+
await mailer.send(normalized.email, 'welcome');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Good — each unit has a single reason to change
|
|
100
|
+
function validateUser(data) {
|
|
101
|
+
if (!data.email) throw new Error('email required');
|
|
102
|
+
}
|
|
103
|
+
function normalizeUser(data) {
|
|
104
|
+
return { ...data, email: data.email.trim().toLowerCase() };
|
|
105
|
+
}
|
|
106
|
+
async function createUser(data) {
|
|
107
|
+
validateUser(data);
|
|
108
|
+
const user = normalizeUser(data);
|
|
109
|
+
await db.users.insert(user);
|
|
110
|
+
await notifyWelcome(user.email);
|
|
111
|
+
return user;
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### DRY
|
|
116
|
+
|
|
117
|
+
Do not copy logic that means the same thing. Extract only when duplication is real — not as premature abstraction.
|
|
118
|
+
|
|
119
|
+
**Practical rule:** extract when at least 2 of these 3 conditions hold:
|
|
120
|
+
|
|
121
|
+
- The same logic appears **3 or more times** (twice is tolerated; the third confirms the pattern).
|
|
122
|
+
- 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).
|
|
123
|
+
- The copied code represents the **same domain concept**, not just similar-looking code.
|
|
124
|
+
|
|
125
|
+
**Not real duplication (do not extract):**
|
|
126
|
+
|
|
127
|
+
- 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.
|
|
128
|
+
- 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.
|
|
129
|
+
|
|
130
|
+
**Example:**
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
// Bad: extracted prematurely, only one real use
|
|
134
|
+
function formatName(x: string) { return x.trim().toUpperCase(); }
|
|
135
|
+
|
|
136
|
+
// Good: appears 3 times with the same domain meaning → extract
|
|
137
|
+
function normalizeTicketStatus(status: string): TicketStatus { ... }
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### KISS
|
|
141
|
+
|
|
142
|
+
Ship the simplest solution that solves the current problem. No extra layers, hooks, or configurability "just in case."
|
|
143
|
+
|
|
144
|
+
```js
|
|
145
|
+
// Bad — configurability nobody asked for, added "just in case"
|
|
146
|
+
function formatPrice(
|
|
147
|
+
value,
|
|
148
|
+
{ currency = 'USD', locale = 'en-US', showSymbol = true, roundingStrategy = 'nearest' } = {},
|
|
149
|
+
) {
|
|
150
|
+
// ...unneeded logic for a single real use case
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Good — solves the current problem, nothing more
|
|
154
|
+
function formatPrice(value) {
|
|
155
|
+
return `$${value.toFixed(2)}`;
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### YAGNI (You Aren't Gonna Need It)
|
|
160
|
+
|
|
161
|
+
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.
|
|
162
|
+
|
|
163
|
+
```js
|
|
164
|
+
// Bad — speculative support for a case that doesn't exist yet
|
|
165
|
+
function createInvoice(order, { supportsRecurring = false, supportsMultiCurrency = false } = {}) {
|
|
166
|
+
// ...branches for features no client uses today
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Good — build for the requirement that actually exists
|
|
170
|
+
function createInvoice(order) {
|
|
171
|
+
return { total: order.total, items: order.items };
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Composition over inheritance
|
|
176
|
+
|
|
177
|
+
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.
|
|
178
|
+
|
|
179
|
+
```js
|
|
180
|
+
// Bad — inheritance forces unrelated behavior onto every subclass
|
|
181
|
+
class Animal {
|
|
182
|
+
makeSound() {
|
|
183
|
+
throw new Error('not implemented');
|
|
184
|
+
}
|
|
185
|
+
fly() {
|
|
186
|
+
throw new Error('not implemented');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
class Dog extends Animal {
|
|
190
|
+
makeSound() {
|
|
191
|
+
return 'Woof';
|
|
192
|
+
}
|
|
193
|
+
// forced to inherit `fly`, which makes no sense for a Dog
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Good — compose only the behaviors that apply
|
|
197
|
+
const canBark = { makeSound: () => 'Woof' };
|
|
198
|
+
const canFly = { fly: () => 'Flying' };
|
|
199
|
+
const dog = { ...canBark };
|
|
200
|
+
const bird = { ...canBark, ...canFly };
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Law of Demeter (don't talk to strangers)
|
|
204
|
+
|
|
205
|
+
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.
|
|
206
|
+
|
|
207
|
+
```js
|
|
208
|
+
// Bad — reaches through three levels of internal structure
|
|
209
|
+
function getCityName(user) {
|
|
210
|
+
return user.address.city.name;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Good — ask the object for what you need, let it own its structure
|
|
214
|
+
function getCityName(user) {
|
|
215
|
+
return user.getCityName();
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Command Query Separation (CQS)
|
|
220
|
+
|
|
221
|
+
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.
|
|
222
|
+
|
|
223
|
+
```js
|
|
224
|
+
// Bad — returns a value AND causes a side effect
|
|
225
|
+
function getNextId(counter) {
|
|
226
|
+
counter.value++; // side effect hidden inside a "getter"
|
|
227
|
+
return counter.value;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Good — separate the query from the command
|
|
231
|
+
function peekNextId(counter) {
|
|
232
|
+
return counter.value + 1;
|
|
233
|
+
}
|
|
234
|
+
function incrementCounter(counter) {
|
|
235
|
+
counter.value++;
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Explicit error handling
|
|
240
|
+
|
|
241
|
+
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.
|
|
242
|
+
|
|
243
|
+
```js
|
|
244
|
+
// Bad — swallows the error, no context, execution continues as if nothing happened
|
|
245
|
+
async function loadUser(id) {
|
|
246
|
+
try {
|
|
247
|
+
return await api.getUser(id);
|
|
248
|
+
} catch (e) {
|
|
249
|
+
return null; // caller has no idea a failure occurred
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Good — the error surfaces with context, caller decides how to handle it
|
|
254
|
+
async function loadUser(id) {
|
|
255
|
+
try {
|
|
256
|
+
return await api.getUser(id);
|
|
257
|
+
} catch (e) {
|
|
258
|
+
throw new Error(`Failed to load user ${id}: ${e.message}`, { cause: e });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Immutability by default
|
|
264
|
+
|
|
265
|
+
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.
|
|
266
|
+
|
|
267
|
+
```js
|
|
268
|
+
// Bad — mutates the input, callers get a surprise side effect
|
|
269
|
+
function addItem(cart, item) {
|
|
270
|
+
cart.items.push(item);
|
|
271
|
+
return cart;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Good — returns a new value, caller's original data stays untouched
|
|
275
|
+
function addItem(cart, item) {
|
|
276
|
+
return { ...cart, items: [...cart.items, item] };
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Null/undefined handling
|
|
281
|
+
|
|
282
|
+
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.
|
|
283
|
+
|
|
284
|
+
```js
|
|
285
|
+
// Bad — null is overloaded to mean three different things
|
|
286
|
+
function getDiscount(user) {
|
|
287
|
+
if (!user) return null; // no user
|
|
288
|
+
if (!user.plan) return null; // no plan
|
|
289
|
+
if (user.plan.discount === 0) return null; // legitimately zero discount
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Good — each case is explicit, zero is a real value
|
|
293
|
+
function getDiscount(user) {
|
|
294
|
+
if (!user || !user.plan) return 0;
|
|
295
|
+
return user.plan.discount;
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Testability as a design constraint
|
|
300
|
+
|
|
301
|
+
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.
|
|
302
|
+
|
|
303
|
+
```js
|
|
304
|
+
// Bad — hardcoded dependency, can't test without hitting the real clock/API
|
|
305
|
+
function isSubscriptionExpired(subscription) {
|
|
306
|
+
return subscription.expiresAt < new Date();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Good — dependency is injected, trivial to test with a fixed date
|
|
310
|
+
function isSubscriptionExpired(subscription, now = new Date()) {
|
|
311
|
+
return subscription.expiresAt < now;
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Dependency direction
|
|
316
|
+
|
|
317
|
+
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.
|
|
318
|
+
|
|
319
|
+
```js
|
|
320
|
+
// Bad — domain logic imports directly from an infrastructure detail
|
|
321
|
+
import { MysqlConnection } from '../infra/mysql';
|
|
322
|
+
function calculateInvoiceTotal(invoiceId) {
|
|
323
|
+
const invoice = new MysqlConnection().query('SELECT * FROM invoices WHERE id = ?', [invoiceId]);
|
|
324
|
+
return invoice.items.reduce((sum, item) => sum + item.price, 0);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Good — domain logic depends only on the shape of the data, not its source
|
|
328
|
+
function calculateInvoiceTotal(invoice) {
|
|
329
|
+
return invoice.items.reduce((sum, item) => sum + item.price, 0);
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### Clear names
|
|
334
|
+
|
|
335
|
+
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.
|
|
336
|
+
|
|
337
|
+
```js
|
|
338
|
+
// Bad — vague names that hide the domain meaning
|
|
339
|
+
function process(data) {
|
|
340
|
+
const temp = data.filter((x) => x.val > 0);
|
|
341
|
+
return temp;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Good — names reveal role and domain meaning
|
|
345
|
+
function getActiveSubscriptions(subscriptions) {
|
|
346
|
+
return subscriptions.filter((subscription) => subscription.remainingDays > 0);
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### Comments
|
|
351
|
+
|
|
352
|
+
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.
|
|
353
|
+
|
|
354
|
+
**Examples:**
|
|
355
|
+
|
|
356
|
+
```ts
|
|
357
|
+
// Bad — narrates the obvious
|
|
358
|
+
// increment the counter by 1
|
|
359
|
+
counter++;
|
|
360
|
+
|
|
361
|
+
// Bad — restates what the name already says
|
|
362
|
+
// get the user by id
|
|
363
|
+
const user = getUserById(id);
|
|
364
|
+
|
|
365
|
+
// Good — explains a non-obvious trade-off
|
|
366
|
+
// We poll instead of using a webhook because the provider doesn't
|
|
367
|
+
// guarantee single delivery; downstream dedupe would cost more than polling.
|
|
368
|
+
setInterval(checkPaymentStatus, 5000);
|
|
369
|
+
|
|
370
|
+
// Good — warns of a hazard the name can't carry
|
|
371
|
+
// WARNING: this endpoint is only idempotent if `externalId` comes from
|
|
372
|
+
// the client; if we generate it ourselves, retries create duplicate records.
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
**Quick test before writing a comment:** if deleting it leaves the code just as clear, the comment isn't earning its place.
|
|
376
|
+
|
|
377
|
+
### No magic strings
|
|
378
|
+
|
|
379
|
+
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.
|
|
380
|
+
|
|
381
|
+
```js
|
|
382
|
+
// Bad — domain literals scattered across the codebase
|
|
383
|
+
if (order.status === 'pending_payment') {
|
|
384
|
+
/* ... */
|
|
385
|
+
}
|
|
386
|
+
// ...elsewhere, in a different file
|
|
387
|
+
if (order.status === 'pending_payment') {
|
|
388
|
+
/* ... */
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Good — named once, reused everywhere
|
|
392
|
+
const ORDER_STATUS = {
|
|
393
|
+
PENDING_PAYMENT: 'pending_payment',
|
|
394
|
+
PAID: 'paid',
|
|
395
|
+
CANCELLED: 'cancelled',
|
|
396
|
+
};
|
|
397
|
+
if (order.status === ORDER_STATUS.PENDING_PAYMENT) {
|
|
398
|
+
/* ... */
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### SOLID
|
|
403
|
+
|
|
404
|
+
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.
|
|
405
|
+
|
|
406
|
+
```js
|
|
407
|
+
// Bad — the use case depends directly on a concrete implementation
|
|
408
|
+
class SendWelcomeEmail {
|
|
409
|
+
async execute(user) {
|
|
410
|
+
await new SmtpMailer().send(user.email, 'welcome'); // coupled to SMTP
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Good — depends on an interface (port), not the implementation
|
|
415
|
+
class SendWelcomeEmail {
|
|
416
|
+
constructor(mailer) {
|
|
417
|
+
this.mailer = mailer;
|
|
418
|
+
} // mailer implements MailerPort
|
|
419
|
+
async execute(user) {
|
|
420
|
+
await this.mailer.send(user.email, 'welcome');
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
### Validate once
|
|
426
|
+
|
|
427
|
+
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.
|
|
428
|
+
|
|
429
|
+
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."
|
|
430
|
+
|
|
431
|
+
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.
|
|
432
|
+
|
|
433
|
+
```php
|
|
434
|
+
// Bad — the FormRequest already validates, and the Service re-checks the same rule
|
|
435
|
+
class UpdateProfileRequest extends FormRequest {
|
|
436
|
+
public function rules() {
|
|
437
|
+
return ['email' => 'required|email'];
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
class ProfileService {
|
|
441
|
+
public function update(array $data) {
|
|
442
|
+
if (empty($data['email'])) { // redundant re-validation
|
|
443
|
+
throw new \InvalidArgumentException('email required');
|
|
444
|
+
}
|
|
445
|
+
$this->user->update($data);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Good — the Service trusts the contract already validated by the FormRequest
|
|
450
|
+
class ProfileService {
|
|
451
|
+
public function update(array $validated) {
|
|
452
|
+
$this->user->update($validated);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
| Do | Don't |
|
|
458
|
+
| ---------------------------------------------------------------------------------- | ---------------------------------------------------------- |
|
|
459
|
+
| Parse once at the edge; pass a typed command/query inward | Re-parse or re-check the same rules in every layer |
|
|
460
|
+
| One validation approach per input | Two mechanisms validating the same body/query |
|
|
461
|
+
| Keep docs/interface metadata separate from the validation contract when both exist | Duplicate the same type/range/required rules in two places |
|
|
462
|
+
|
|
463
|
+
When in doubt: **fail fast, keep it flat, keep it small.**
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# Java Coding Guidelines
|
|
2
|
+
|
|
3
|
+
This is the Java version of `GUIDELINES_TEMPLATE.md`. It preserves the same
|
|
4
|
+
principles and example scenarios using explicit Java types, immutable values,
|
|
5
|
+
and layered application boundaries. The shared baseline remains mandatory.
|
|
6
|
+
|
|
7
|
+
## Quick reference
|
|
8
|
+
|
|
9
|
+
| Do | Don't |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| Guard invalid input early | Bury happy paths in nested `if/else` branches |
|
|
12
|
+
| Throw a precise exception now | Substitute a default and hide invalid state |
|
|
13
|
+
| Give a class one responsibility | Validate, transform, persist, and notify together |
|
|
14
|
+
| Extract repeated domain behavior | Add a hierarchy for a one-off method |
|
|
15
|
+
| Build only current behavior | Add speculative flags and configuration |
|
|
16
|
+
| Prefer immutable records/value objects | Mutate data passed by a caller |
|
|
17
|
+
| Validate once at an adapter | Re-validate the same command in every layer |
|
|
18
|
+
| Depend on ports at real seams | Construct database/HTTP clients in domain code |
|
|
19
|
+
|
|
20
|
+
## Principles
|
|
21
|
+
|
|
22
|
+
### Guard clauses
|
|
23
|
+
|
|
24
|
+
The discount example keeps the happy path shallow.
|
|
25
|
+
|
|
26
|
+
```java
|
|
27
|
+
double getDiscount(User user) {
|
|
28
|
+
if (user == null) return 0.0;
|
|
29
|
+
if (!user.active()) return 0.0;
|
|
30
|
+
if (!user.hasSubscription()) return 0.0;
|
|
31
|
+
return 0.2;
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Fail fast
|
|
36
|
+
|
|
37
|
+
An unknown shipping zone is invalid state, not a reason to use a default.
|
|
38
|
+
|
|
39
|
+
```java
|
|
40
|
+
int shippingCost(String zoneId, Map<String, Zone> zones) {
|
|
41
|
+
var zone = zones.get(zoneId);
|
|
42
|
+
if (zone == null) throw new IllegalArgumentException("Unknown zoneId: " + zoneId);
|
|
43
|
+
return zone.baseCost();
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### SRP
|
|
48
|
+
|
|
49
|
+
The user flow separates normalization, repository work, and notification.
|
|
50
|
+
|
|
51
|
+
```java
|
|
52
|
+
final class CreateUser {
|
|
53
|
+
private final UserRepository users;
|
|
54
|
+
private final Mailer mailer;
|
|
55
|
+
User create(String email) {
|
|
56
|
+
if (email.isBlank()) throw new IllegalArgumentException("email required");
|
|
57
|
+
var user = users.save(new User(normalizeEmail(email)));
|
|
58
|
+
mailer.send(user.email(), "welcome");
|
|
59
|
+
return user;
|
|
60
|
+
}
|
|
61
|
+
private String normalizeEmail(String email) { return email.trim().toLowerCase(Locale.ROOT); }
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### DRY
|
|
66
|
+
|
|
67
|
+
Extract the third repeated ticket-status normalization, not a premature helper.
|
|
68
|
+
|
|
69
|
+
```java
|
|
70
|
+
enum TicketStatus { OPEN, CLOSED }
|
|
71
|
+
TicketStatus normalizeTicketStatus(String value) { return TicketStatus.valueOf(value.toUpperCase(Locale.ROOT)); }
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### KISS
|
|
75
|
+
|
|
76
|
+
Keep price and invoice examples limited to today's scope.
|
|
77
|
+
|
|
78
|
+
```java
|
|
79
|
+
String formatPrice(BigDecimal value) { return "$" + value.setScale(2); }
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### YAGNI (You Aren't Gonna Need It)
|
|
83
|
+
|
|
84
|
+
Build the invoice requirement that exists today. Do not add currency strategies,
|
|
85
|
+
recurring branches, event listeners, or cache layers without a real requirement.
|
|
86
|
+
|
|
87
|
+
```java
|
|
88
|
+
Invoice createInvoice(Order order) { return new Invoice(order.total(), order.items()); }
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Composition over inheritance
|
|
92
|
+
|
|
93
|
+
Compose behavior rather than inherit an irrelevant `fly()` operation.
|
|
94
|
+
|
|
95
|
+
```java
|
|
96
|
+
record Dog(BarkBehavior bark) { String makeSound() { return bark.sound(); } }
|
|
97
|
+
interface BarkBehavior { String sound(); }
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Law of Demeter
|
|
101
|
+
|
|
102
|
+
Ask the user for its city, not its address's city's name.
|
|
103
|
+
|
|
104
|
+
```java
|
|
105
|
+
String cityName(User user) { return user.cityName(); }
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Command Query Separation
|
|
109
|
+
|
|
110
|
+
Peeking and incrementing a counter are separate methods.
|
|
111
|
+
|
|
112
|
+
```java
|
|
113
|
+
int peekNextId(Counter counter) { return counter.value() + 1; }
|
|
114
|
+
void incrementCounter(Counter counter) { counter.increment(); }
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Explicit error handling
|
|
118
|
+
|
|
119
|
+
Retain a cause and add user-specific context; do not catch and return null.
|
|
120
|
+
|
|
121
|
+
```java
|
|
122
|
+
User loadUser(String id, UserApi api) {
|
|
123
|
+
try { return api.get(id); }
|
|
124
|
+
catch (ApiException error) { throw new IllegalStateException("Failed to load user " + id, error); }
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Immutability by default
|
|
129
|
+
|
|
130
|
+
The cart scenario returns a new record.
|
|
131
|
+
|
|
132
|
+
```java
|
|
133
|
+
record Cart(List<Item> items) {}
|
|
134
|
+
Cart addItem(Cart cart, Item item) {
|
|
135
|
+
var items = new ArrayList<>(cart.items());
|
|
136
|
+
items.add(item);
|
|
137
|
+
return new Cart(List.copyOf(items));
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Null handling
|
|
142
|
+
|
|
143
|
+
Prefer explicit absence with `Optional` at boundaries and preserve a zero
|
|
144
|
+
discount as a real value.
|
|
145
|
+
|
|
146
|
+
```java
|
|
147
|
+
double getDiscount(Optional<User> user) {
|
|
148
|
+
return user.flatMap(User::plan).map(Plan::discount).orElse(0.0);
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Testability as a design constraint
|
|
153
|
+
|
|
154
|
+
Inject `Clock` instead of asking the system clock inside domain behavior.
|
|
155
|
+
|
|
156
|
+
```java
|
|
157
|
+
boolean isSubscriptionExpired(Subscription subscription, Clock clock) {
|
|
158
|
+
return subscription.expiresAt().isBefore(Instant.now(clock));
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Dependency direction
|
|
163
|
+
|
|
164
|
+
Invoice calculation receives an invoice rather than an SQL/JPA dependency.
|
|
165
|
+
|
|
166
|
+
```java
|
|
167
|
+
int calculateInvoiceTotal(Invoice invoice) {
|
|
168
|
+
return invoice.items().stream().mapToInt(InvoiceItem::price).sum();
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Clear names
|
|
173
|
+
|
|
174
|
+
Use `activeSubscriptions`, `remainingDays`, and `externalId`, never `data`,
|
|
175
|
+
`temp`, or `obj`.
|
|
176
|
+
|
|
177
|
+
### Comments
|
|
178
|
+
|
|
179
|
+
Comments explain a delivery trade-off or an idempotency hazard, not a statement
|
|
180
|
+
whose name already explains it.
|
|
181
|
+
|
|
182
|
+
### No magic strings
|
|
183
|
+
|
|
184
|
+
Model order states as an enum.
|
|
185
|
+
|
|
186
|
+
```java
|
|
187
|
+
enum OrderStatus { PENDING_PAYMENT, PAID }
|
|
188
|
+
if (order.status() == OrderStatus.PENDING_PAYMENT) payments.request(order);
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### SOLID
|
|
192
|
+
|
|
193
|
+
The welcome use case depends on a mailer port, not SMTP.
|
|
194
|
+
|
|
195
|
+
```java
|
|
196
|
+
interface Mailer { void send(String email, String template); }
|
|
197
|
+
final class SendWelcomeEmail { private final Mailer mailer; SendWelcomeEmail(Mailer mailer) { this.mailer = mailer; } }
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Validate once
|
|
201
|
+
|
|
202
|
+
An HTTP/message adapter builds a validated command. The application service
|
|
203
|
+
trusts it and never repeats the same null, type, and range checks.
|
|
204
|
+
|
|
205
|
+
```java
|
|
206
|
+
record UpdateProfile(String email) {}
|
|
207
|
+
void updateProfile(UpdateProfile command, UserRepository users) { users.update(command); }
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Use one validation contract per body/query. When in doubt: **fail fast, keep it
|
|
211
|
+
flat, keep it small.**
|