@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,235 @@
|
|
|
1
|
+
# JavaScript Coding Guidelines
|
|
2
|
+
|
|
3
|
+
This is the JavaScript rendition of `GUIDELINES_TEMPLATE.md`. It keeps every
|
|
4
|
+
principle and teaching scenario from the template, expressed for modern
|
|
5
|
+
ECMAScript and Node.js. Read it together with the shared template before any
|
|
6
|
+
code-writing operation.
|
|
7
|
+
|
|
8
|
+
## Quick reference
|
|
9
|
+
|
|
10
|
+
| Do | Don't |
|
|
11
|
+
| --- | --- |
|
|
12
|
+
| Exit invalid paths early | Bury the happy path in nested `if/else` blocks |
|
|
13
|
+
| Throw on broken invariants | Substitute defaults that hide corrupted state |
|
|
14
|
+
| Separate validation, transformation, persistence, and notification | Put unrelated responsibilities in one function |
|
|
15
|
+
| Extract a repeated domain rule | Generalize a one-off helper |
|
|
16
|
+
| Keep today's invoice and price requirements small | Add options for hypothetical future features |
|
|
17
|
+
| Return replacement values | Mutate parameter-owned arrays or objects |
|
|
18
|
+
| Validate transport input once at the boundary | Repeat the same checks in services and helpers |
|
|
19
|
+
| Name domain literals once | Scatter status strings and event names |
|
|
20
|
+
|
|
21
|
+
## Principles
|
|
22
|
+
|
|
23
|
+
### Guard clauses
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
// Bad — the real path is buried
|
|
27
|
+
function getDiscount(user) {
|
|
28
|
+
if (user) {
|
|
29
|
+
if (user.isActive) {
|
|
30
|
+
if (user.hasSubscription) return 0.2;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Good — flat and explicit
|
|
37
|
+
function getDiscount(user) {
|
|
38
|
+
if (!user) return 0;
|
|
39
|
+
if (!user.isActive) return 0;
|
|
40
|
+
if (!user.hasSubscription) return 0;
|
|
41
|
+
return 0.2;
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Fail fast
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
// Bad — a bad zone id is hidden
|
|
49
|
+
function getShippingCost(order) {
|
|
50
|
+
return ZONES[order.zoneId] ?? ZONES.default;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Good — preserve the violated invariant
|
|
54
|
+
function getShippingCost(order) {
|
|
55
|
+
const zone = ZONES[order.zoneId];
|
|
56
|
+
if (!zone) throw new Error(`Unknown zoneId: ${order.zoneId}`);
|
|
57
|
+
return zone.baseCost;
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### SRP
|
|
62
|
+
|
|
63
|
+
Keep the user scenario split into validation, normalization, persistence, and
|
|
64
|
+
notification. A coordinator may compose these units, but should not conceal
|
|
65
|
+
their responsibilities.
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
function validateUser(data) {
|
|
69
|
+
if (!data.email) throw new Error('email required');
|
|
70
|
+
}
|
|
71
|
+
function normalizeUser(data) {
|
|
72
|
+
return { ...data, email: data.email.trim().toLowerCase() };
|
|
73
|
+
}
|
|
74
|
+
async function createUser(data, users, mailer) {
|
|
75
|
+
validateUser(data);
|
|
76
|
+
const user = await users.insert(normalizeUser(data));
|
|
77
|
+
await mailer.send(user.email, 'welcome');
|
|
78
|
+
return user;
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### DRY
|
|
83
|
+
|
|
84
|
+
Extract ticket-status normalization only after it is a repeated domain rule,
|
|
85
|
+
not because two unrelated strings happen to be trimmed the same way.
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
const TICKET_STATUS = Object.freeze({ OPEN: 'open', CLOSED: 'closed' });
|
|
89
|
+
function normalizeTicketStatus(value) {
|
|
90
|
+
if (Object.values(TICKET_STATUS).includes(value)) return value;
|
|
91
|
+
throw new Error(`Unknown ticket status: ${value}`);
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### KISS
|
|
96
|
+
|
|
97
|
+
The price example solves only the current requirement. Do not add locale
|
|
98
|
+
strategies or configuration objects without a present use.
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
function formatPrice(value) {
|
|
102
|
+
return `$${value.toFixed(2)}`;
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### YAGNI
|
|
107
|
+
|
|
108
|
+
The invoice example solves only the current requirement. Do not add recurring
|
|
109
|
+
billing flags, multi-currency branches, or future configuration without a
|
|
110
|
+
present use.
|
|
111
|
+
|
|
112
|
+
```js
|
|
113
|
+
function createInvoice(order) {
|
|
114
|
+
return { total: order.total, items: order.items };
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Composition, Demeter, and CQS
|
|
119
|
+
|
|
120
|
+
Compose only useful behavior, ask immediate collaborators for their own data,
|
|
121
|
+
and keep a query separate from mutation.
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
const canBark = { makeSound: () => 'Woof' };
|
|
125
|
+
const dog = { ...canBark };
|
|
126
|
+
|
|
127
|
+
function getCityName(user) {
|
|
128
|
+
return user.getCityName();
|
|
129
|
+
}
|
|
130
|
+
function peekNextId(counter) {
|
|
131
|
+
return counter.value + 1;
|
|
132
|
+
}
|
|
133
|
+
function incrementCounter(counter) {
|
|
134
|
+
counter.value += 1;
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Explicit errors, immutability, and nulls
|
|
139
|
+
|
|
140
|
+
```js
|
|
141
|
+
async function loadUser(id, api) {
|
|
142
|
+
try {
|
|
143
|
+
return await api.getUser(id);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
throw new Error(`Failed to load user ${id}`, { cause: error });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function addItem(cart, item) {
|
|
150
|
+
return { ...cart, items: [...cart.items, item] };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function getPlanDiscount(user) {
|
|
154
|
+
return user?.plan?.discount ?? 0;
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Use one documented absence convention. Do not overload `null` or `undefined`
|
|
159
|
+
to represent distinct business states when an explicit value is clearer.
|
|
160
|
+
|
|
161
|
+
### Testability as a design constraint
|
|
162
|
+
|
|
163
|
+
Inject time and infrastructure dependencies rather than reaching for globals.
|
|
164
|
+
|
|
165
|
+
```js
|
|
166
|
+
function isSubscriptionExpired(subscription, now) {
|
|
167
|
+
return subscription.expiresAt < now;
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Dependency direction
|
|
172
|
+
|
|
173
|
+
Domain calculations receive domain data instead of constructing SQL, HTTP, or
|
|
174
|
+
mail clients themselves.
|
|
175
|
+
|
|
176
|
+
```js
|
|
177
|
+
function calculateInvoiceTotal(invoice) {
|
|
178
|
+
return invoice.items.reduce((sum, item) => sum + item.price, 0);
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Clear names
|
|
183
|
+
|
|
184
|
+
Use `activeSubscriptions`, `remainingDays`, and `externalId`, not `data`,
|
|
185
|
+
`temp`, or `x`.
|
|
186
|
+
|
|
187
|
+
### Comments
|
|
188
|
+
|
|
189
|
+
Explain only non-obvious trade-offs and hazards. Do not narrate a line whose
|
|
190
|
+
name and structure already make it clear.
|
|
191
|
+
|
|
192
|
+
### No magic strings
|
|
193
|
+
|
|
194
|
+
Name statuses once rather than scattering literals:
|
|
195
|
+
|
|
196
|
+
```js
|
|
197
|
+
const ORDER_STATUS = Object.freeze({
|
|
198
|
+
PENDING_PAYMENT: 'pending_payment',
|
|
199
|
+
PAID: 'paid',
|
|
200
|
+
});
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### SOLID
|
|
204
|
+
|
|
205
|
+
Depend on a mailer port where the infrastructure can vary:
|
|
206
|
+
|
|
207
|
+
```js
|
|
208
|
+
class SendWelcomeEmail {
|
|
209
|
+
constructor(mailer) {
|
|
210
|
+
this.mailer = mailer;
|
|
211
|
+
}
|
|
212
|
+
execute(user) {
|
|
213
|
+
return this.mailer.send(user.email, 'welcome');
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Validate once
|
|
219
|
+
|
|
220
|
+
Parse and validate a request body at its transport boundary. Services trust the
|
|
221
|
+
validated command and do not repeat required, type, or range checks.
|
|
222
|
+
|
|
223
|
+
```js
|
|
224
|
+
function parseUpdateProfile(body) {
|
|
225
|
+
if (typeof body?.email !== 'string' || body.email.length === 0) {
|
|
226
|
+
throw new Error('email required');
|
|
227
|
+
}
|
|
228
|
+
return { email: body.email };
|
|
229
|
+
}
|
|
230
|
+
async function updateProfile(command, users) {
|
|
231
|
+
await users.update(command);
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
When in doubt: **fail fast, keep it flat, keep it small.**
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# PHP Laravel Coding Guidelines
|
|
2
|
+
|
|
3
|
+
This is the PHP Laravel version of `GUIDELINES_TEMPLATE.md`. It preserves the
|
|
4
|
+
same decisions and teaching scenarios using strict PHP, Laravel boundaries,
|
|
5
|
+
and Laravel's testing conventions. The shared baseline remains mandatory.
|
|
6
|
+
|
|
7
|
+
## Quick reference
|
|
8
|
+
|
|
9
|
+
| Do | Don't |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| Guard invalid input early | Nest the happy path in `if/else` pyramids |
|
|
12
|
+
| Throw precise exceptions | Fall back to a default that hides bad state |
|
|
13
|
+
| Keep controller, use case, and infrastructure separate | Validate, persist, and notify in one method |
|
|
14
|
+
| Extract repeated domain behavior | Build a service for one use |
|
|
15
|
+
| Use immutable value data where practical | Mutate caller-owned arrays or models invisibly |
|
|
16
|
+
| Validate in a Form Request once | Repeat its rules in services |
|
|
17
|
+
| Inject interfaces at real seams | Construct facades or clients in domain logic |
|
|
18
|
+
| Use named enums/constants | Repeat status strings |
|
|
19
|
+
|
|
20
|
+
## Principles
|
|
21
|
+
|
|
22
|
+
### Guard clauses
|
|
23
|
+
|
|
24
|
+
The discount example stays flat.
|
|
25
|
+
|
|
26
|
+
```php
|
|
27
|
+
function getDiscount(?User $user): float
|
|
28
|
+
{
|
|
29
|
+
if ($user === null) return 0.0;
|
|
30
|
+
if (! $user->isActive()) return 0.0;
|
|
31
|
+
if (! $user->hasSubscription()) return 0.0;
|
|
32
|
+
return 0.2;
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Fail fast
|
|
37
|
+
|
|
38
|
+
Do not silently choose a default shipping zone.
|
|
39
|
+
|
|
40
|
+
```php
|
|
41
|
+
function shippingCost(string $zoneId, ZoneRepository $zones): int
|
|
42
|
+
{
|
|
43
|
+
$zone = $zones->find($zoneId);
|
|
44
|
+
if ($zone === null) throw new DomainException("Unknown zoneId: {$zoneId}");
|
|
45
|
+
return $zone->baseCost();
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### SRP
|
|
50
|
+
|
|
51
|
+
The user flow keeps validation, normalization, persistence, and notification
|
|
52
|
+
as distinct responsibilities.
|
|
53
|
+
|
|
54
|
+
```php
|
|
55
|
+
final class CreateUser
|
|
56
|
+
{
|
|
57
|
+
public function __construct(private UserRepository $users, private Mailer $mailer) {}
|
|
58
|
+
public function handle(CreateUserData $data): void
|
|
59
|
+
{
|
|
60
|
+
$user = $this->users->create($data->withEmail(strtolower(trim($data->email))));
|
|
61
|
+
$this->mailer->send($user->email, 'welcome');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### DRY
|
|
67
|
+
|
|
68
|
+
Extract the third repeated ticket-status normalization, not a one-off helper.
|
|
69
|
+
|
|
70
|
+
```php
|
|
71
|
+
enum TicketStatus: string { case Open = 'open'; case Closed = 'closed'; }
|
|
72
|
+
function normalizeTicketStatus(string $value): TicketStatus { return TicketStatus::from($value); }
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### KISS
|
|
76
|
+
|
|
77
|
+
Keep the price and invoice examples limited to their actual requirements.
|
|
78
|
+
|
|
79
|
+
```php
|
|
80
|
+
function formatPrice(float $value): string { return '$'.number_format($value, 2); }
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### YAGNI (You Aren't Gonna Need It)
|
|
84
|
+
|
|
85
|
+
Build the invoice shape needed today; do not add multi-currency, recurrence,
|
|
86
|
+
queues, events, or cache abstractions without a current use case.
|
|
87
|
+
|
|
88
|
+
```php
|
|
89
|
+
function createInvoice(Order $order): array { return ['total' => $order->total, 'items' => $order->items]; }
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Composition over inheritance
|
|
93
|
+
|
|
94
|
+
Use focused services and value objects instead of a base class that forces a
|
|
95
|
+
dog to inherit `fly()`.
|
|
96
|
+
|
|
97
|
+
```php
|
|
98
|
+
final class Dog { public function __construct(private BarkBehavior $bark) {} public function sound(): string { return $this->bark->sound(); } }
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Law of Demeter
|
|
102
|
+
|
|
103
|
+
Ask the user for its city instead of chaining relations.
|
|
104
|
+
|
|
105
|
+
```php
|
|
106
|
+
function cityName(User $user): string { return $user->cityName(); }
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Command Query Separation
|
|
110
|
+
|
|
111
|
+
Keep peeking at an identifier separate from incrementing a counter.
|
|
112
|
+
|
|
113
|
+
```php
|
|
114
|
+
function peekNextId(Counter $counter): int { return $counter->value() + 1; }
|
|
115
|
+
function incrementCounter(Counter $counter): void { $counter->increment(); }
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Explicit error handling
|
|
119
|
+
|
|
120
|
+
Translate infrastructure failures with context; do not catch and return null.
|
|
121
|
+
|
|
122
|
+
```php
|
|
123
|
+
function loadUser(string $id, UserApi $api): User
|
|
124
|
+
{
|
|
125
|
+
try { return $api->get($id); }
|
|
126
|
+
catch (Throwable $error) { throw new RuntimeException("Failed to load user {$id}", 0, $error); }
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Immutability by default
|
|
131
|
+
|
|
132
|
+
Return a replacement cart or DTO rather than changing the caller's collection.
|
|
133
|
+
|
|
134
|
+
```php
|
|
135
|
+
function addItem(CartData $cart, ItemData $item): CartData
|
|
136
|
+
{
|
|
137
|
+
return new CartData([...$cart->items, $item]);
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Null handling
|
|
142
|
+
|
|
143
|
+
Use explicit nullable contracts and preserve a legitimate zero discount.
|
|
144
|
+
|
|
145
|
+
```php
|
|
146
|
+
function getDiscount(?User $user): float { return $user?->plan()?->discount() ?? 0.0; }
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Testability as a design constraint
|
|
150
|
+
|
|
151
|
+
Inject the clock rather than calling `now()` inside domain behavior.
|
|
152
|
+
|
|
153
|
+
```php
|
|
154
|
+
function isSubscriptionExpired(Subscription $subscription, DateTimeImmutable $now): bool
|
|
155
|
+
{
|
|
156
|
+
return $subscription->expiresAt() < $now;
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Dependency direction
|
|
161
|
+
|
|
162
|
+
Use cases receive repositories or domain data; domain code never constructs
|
|
163
|
+
Eloquent queries or reads `Request` globals.
|
|
164
|
+
|
|
165
|
+
```php
|
|
166
|
+
function calculateInvoiceTotal(Invoice $invoice): int
|
|
167
|
+
{
|
|
168
|
+
return array_sum(array_map(fn (InvoiceItem $item) => $item->price(), $invoice->items()));
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Clear names
|
|
173
|
+
|
|
174
|
+
Use `$activeSubscriptions`, `$remainingDays`, and `$externalId`; avoid `$data`,
|
|
175
|
+
`$temp`, and `$result`.
|
|
176
|
+
|
|
177
|
+
### Comments
|
|
178
|
+
|
|
179
|
+
Comments explain a hidden trade-off or an idempotency hazard, never narrate a
|
|
180
|
+
line of PHP.
|
|
181
|
+
|
|
182
|
+
### No magic strings
|
|
183
|
+
|
|
184
|
+
Use a backed enum for the order state scenario.
|
|
185
|
+
|
|
186
|
+
```php
|
|
187
|
+
enum OrderStatus: string { case PendingPayment = 'pending_payment'; case Paid = 'paid'; }
|
|
188
|
+
if ($order->status === OrderStatus::PendingPayment) { $payments->request($order); }
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### SOLID
|
|
192
|
+
|
|
193
|
+
Depend on a mailer contract, not an SMTP implementation in a use case.
|
|
194
|
+
|
|
195
|
+
```php
|
|
196
|
+
interface Mailer { public function send(string $email, string $template): void; }
|
|
197
|
+
final class SendWelcomeEmail { public function __construct(private Mailer $mailer) {} }
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Validate once
|
|
201
|
+
|
|
202
|
+
The Form Request owns HTTP validation; the service trusts its validated DTO.
|
|
203
|
+
|
|
204
|
+
```php
|
|
205
|
+
final class UpdateProfileRequest extends FormRequest { public function rules(): array { return ['email' => ['required', 'email']]; } }
|
|
206
|
+
final class UpdateProfile { public function handle(UpdateProfileData $data): void { /* persist $data */ } }
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Use one validation contract per body/query. When in doubt: **fail fast, keep it
|
|
210
|
+
flat, keep it small.**
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# Python Coding Guidelines
|
|
2
|
+
|
|
3
|
+
This is the Python version of `GUIDELINES_TEMPLATE.md`. It preserves the same
|
|
4
|
+
principles and example scenarios with typed Python, explicit boundaries, and
|
|
5
|
+
Python testing conventions. The shared baseline remains mandatory.
|
|
6
|
+
|
|
7
|
+
## Quick reference
|
|
8
|
+
|
|
9
|
+
| Do | Don't |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| Return early for invalid state | Hide the happy path inside nested branches |
|
|
12
|
+
| Raise precise errors immediately | Use a default to hide a broken invariant |
|
|
13
|
+
| Give each function one reason to change | Validate, transform, save, and notify together |
|
|
14
|
+
| Extract real repeated domain logic | Add abstractions before they earn a second use |
|
|
15
|
+
| Keep inputs immutable by default | Surprise callers by changing their objects |
|
|
16
|
+
| Validate once at transport boundaries | Repeat body/query validation in every layer |
|
|
17
|
+
| Inject time and infrastructure | Reach for global clients in domain code |
|
|
18
|
+
| Use enums and named constants | Spread protocol strings around the codebase |
|
|
19
|
+
|
|
20
|
+
## Principles
|
|
21
|
+
|
|
22
|
+
### Guard clauses
|
|
23
|
+
|
|
24
|
+
The discount scenario is flat and typed.
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
def get_discount(user: User | None) -> float:
|
|
28
|
+
if user is None:
|
|
29
|
+
return 0.0
|
|
30
|
+
if not user.is_active:
|
|
31
|
+
return 0.0
|
|
32
|
+
if not user.has_subscription:
|
|
33
|
+
return 0.0
|
|
34
|
+
return 0.2
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Fail fast
|
|
38
|
+
|
|
39
|
+
Never conceal a bad zone key with a default zone.
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
def shipping_cost(zone_id: str, zones: dict[str, Zone]) -> int:
|
|
43
|
+
zone = zones.get(zone_id)
|
|
44
|
+
if zone is None:
|
|
45
|
+
raise ValueError(f"Unknown zone_id: {zone_id}")
|
|
46
|
+
return zone.base_cost
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### SRP
|
|
50
|
+
|
|
51
|
+
The user flow separates normalization, persistence, and notification.
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
def normalize_email(email: str) -> str:
|
|
55
|
+
return email.strip().lower()
|
|
56
|
+
|
|
57
|
+
def create_user(email: str, users: UserRepository, mailer: Mailer) -> User:
|
|
58
|
+
if not email:
|
|
59
|
+
raise ValueError("email required")
|
|
60
|
+
user = users.insert(email=normalize_email(email))
|
|
61
|
+
mailer.send(user.email, "welcome")
|
|
62
|
+
return user
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### DRY
|
|
66
|
+
|
|
67
|
+
Extract the third repeated ticket-status normalization, not a helper created
|
|
68
|
+
for a single call.
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
class TicketStatus(StrEnum):
|
|
72
|
+
OPEN = "open"
|
|
73
|
+
CLOSED = "closed"
|
|
74
|
+
|
|
75
|
+
def normalize_ticket_status(value: str) -> TicketStatus:
|
|
76
|
+
return TicketStatus(value)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### KISS
|
|
80
|
+
|
|
81
|
+
Keep price and invoice code focused on the current requirement.
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
def format_price(value: float) -> str:
|
|
85
|
+
return f"${value:.2f}"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### YAGNI (You Aren't Gonna Need It)
|
|
89
|
+
|
|
90
|
+
Build the invoice required today. Do not introduce strategy trees, recurring
|
|
91
|
+
invoices, or multiple currencies until the requirement exists.
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
def create_invoice(order: Order) -> Invoice:
|
|
95
|
+
return Invoice(total=order.total, items=order.items)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Composition over inheritance
|
|
99
|
+
|
|
100
|
+
Compose a dog's bark behavior rather than inherit an unrelated `fly` method.
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
@dataclass(frozen=True)
|
|
104
|
+
class Dog:
|
|
105
|
+
bark: Callable[[], str]
|
|
106
|
+
|
|
107
|
+
def make_sound(self) -> str:
|
|
108
|
+
return self.bark()
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Law of Demeter
|
|
112
|
+
|
|
113
|
+
Ask the user for a city instead of traversing `user.address.city.name`.
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
def city_name(user: User) -> str:
|
|
117
|
+
return user.city_name()
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Command Query Separation
|
|
121
|
+
|
|
122
|
+
Querying the next ID does not increment it.
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
def peek_next_id(counter: Counter) -> int: return counter.value + 1
|
|
126
|
+
def increment_counter(counter: Counter) -> None: counter.increment()
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Explicit error handling
|
|
130
|
+
|
|
131
|
+
Attach context and preserve the original exception.
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
def load_user(user_id: str, api: UserApi) -> User:
|
|
135
|
+
try:
|
|
136
|
+
return api.get_user(user_id)
|
|
137
|
+
except ApiError as error:
|
|
138
|
+
raise RuntimeError(f"Failed to load user {user_id}") from error
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Immutability by default
|
|
142
|
+
|
|
143
|
+
The cart example returns a replacement dataclass.
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
def add_item(cart: Cart, item: Item) -> Cart:
|
|
147
|
+
return replace(cart, items=(*cart.items, item))
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Null handling
|
|
151
|
+
|
|
152
|
+
Use `None` consistently and preserve a zero discount.
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
def get_discount(user: User | None) -> float:
|
|
156
|
+
return 0.0 if user is None or user.plan is None else user.plan.discount
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Testability as a design constraint
|
|
160
|
+
|
|
161
|
+
Pass the clock to the subscription check.
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
def is_subscription_expired(subscription: Subscription, now: datetime) -> bool:
|
|
165
|
+
return subscription.expires_at < now
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Dependency direction
|
|
169
|
+
|
|
170
|
+
The invoice calculation receives an invoice, not a database connection.
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
def calculate_invoice_total(invoice: Invoice) -> int:
|
|
174
|
+
return sum(item.price for item in invoice.items)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Clear names
|
|
178
|
+
|
|
179
|
+
Use `active_subscriptions`, `remaining_days`, and `external_id`, never
|
|
180
|
+
`data`, `temp`, or `obj`.
|
|
181
|
+
|
|
182
|
+
### Comments
|
|
183
|
+
|
|
184
|
+
Comments document a provider delivery trade-off or an idempotency hazard,
|
|
185
|
+
never narrate the next statement.
|
|
186
|
+
|
|
187
|
+
### No magic strings
|
|
188
|
+
|
|
189
|
+
Name order states once.
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
class OrderStatus(StrEnum):
|
|
193
|
+
PENDING_PAYMENT = "pending_payment"
|
|
194
|
+
PAID = "paid"
|
|
195
|
+
|
|
196
|
+
if order.status is OrderStatus.PENDING_PAYMENT:
|
|
197
|
+
request_payment(order)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### SOLID
|
|
201
|
+
|
|
202
|
+
The welcome use case depends on a mailer protocol, not SMTP.
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
class Mailer(Protocol):
|
|
206
|
+
def send(self, email: str, template: str) -> None: ...
|
|
207
|
+
|
|
208
|
+
class SendWelcomeEmail:
|
|
209
|
+
def __init__(self, mailer: Mailer) -> None: self._mailer = mailer
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Validate once
|
|
213
|
+
|
|
214
|
+
Parse HTTP/message input into a typed command at the boundary. Application
|
|
215
|
+
services trust that command instead of repeating the same required, type, and
|
|
216
|
+
range checks. Use one schema or validator for each body/query.
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
@dataclass(frozen=True)
|
|
220
|
+
class UpdateProfile:
|
|
221
|
+
email: str
|
|
222
|
+
|
|
223
|
+
def update_profile(command: UpdateProfile, users: UserRepository) -> None:
|
|
224
|
+
users.update(command)
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
When in doubt: **fail fast, keep it flat, keep it small.**
|