@martinffx/atelier 1.1.0 → 1.2.1
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/CHANGELOG.md +25 -0
- package/README.md +7 -2
- package/dist/atelier.js +16 -15
- package/package.json +1 -1
- package/skills/code-debug/SKILL.md +119 -187
- package/skills/code-debug/references/bug-patterns.md +71 -0
- package/skills/code-debug/references/techniques.md +44 -4
- package/skills/code-handoff/SKILL.md +16 -0
- package/skills/oracle-architect/SKILL.md +82 -1
- package/skills/oracle-architect/references/ddd-patterns.md +101 -0
- package/skills/oracle-architect/references/interface-design.md +203 -0
- package/skills/oracle-doubt/SKILL.md +252 -0
- package/skills/oracle-doubt/references/adversarial-prompt.md +47 -0
- package/skills/oracle-security/SKILL.md +225 -0
- package/skills/oracle-security/references/security-patterns.md +236 -0
- package/skills/oracle-security/references/threat-modeling-guide.md +137 -0
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
**Do:**
|
|
14
14
|
- Print at function entry/exit to trace execution
|
|
15
15
|
- Print variable values at key decision points
|
|
16
|
-
- Use descriptive labels: console.log('userId:', userId)
|
|
17
|
-
- Print full objects for inspection: console.log(JSON.stringify(data, null, 2))
|
|
16
|
+
- Use descriptive labels: `console.log('userId:', userId)`
|
|
17
|
+
- Print full objects for inspection: `console.log(JSON.stringify(data, null, 2))`
|
|
18
18
|
|
|
19
19
|
**Don't:**
|
|
20
20
|
- Leave print statements in production code
|
|
@@ -22,9 +22,25 @@
|
|
|
22
22
|
- Over-print (noise obscures signal)
|
|
23
23
|
- Print in tight loops (use breakpoints instead)
|
|
24
24
|
|
|
25
|
-
###
|
|
25
|
+
### Tagged Debug Logs
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
When instrumenting during a debugging session, tag every temporary log with a
|
|
28
|
+
unique prefix so cleanup is trivial:
|
|
29
|
+
|
|
30
|
+
```javascript
|
|
31
|
+
console.log('[DEBUG-a4f2] userId:', userId);
|
|
32
|
+
console.log('[DEBUG-a4f2] payload:', payload);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Cleanup: `grep -r '\[DEBUG-' .` → delete all matches.
|
|
36
|
+
|
|
37
|
+
Untagged logs survive; tagged logs die.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Structured Logging
|
|
42
|
+
|
|
43
|
+
Better than `console.log` for production:
|
|
28
44
|
|
|
29
45
|
```javascript
|
|
30
46
|
const logger = {
|
|
@@ -115,3 +131,27 @@ logger.error('Request failed', {
|
|
|
115
131
|
| INFO | Normal operation events |
|
|
116
132
|
| WARN | Something unexpected, but handled |
|
|
117
133
|
| ERROR | Operation failed |
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Performance Regression Techniques
|
|
138
|
+
|
|
139
|
+
### The Perf Branch
|
|
140
|
+
|
|
141
|
+
For performance regressions, logs are usually wrong. Instead:
|
|
142
|
+
|
|
143
|
+
1. **Establish a baseline measurement** using one of:
|
|
144
|
+
- Timing harness around the suspect code path
|
|
145
|
+
- `performance.now()` in browser / `process.hrtime()` in Node
|
|
146
|
+
- CPU profiler (Chrome DevTools, `node --prof`, py-spy, etc.)
|
|
147
|
+
- Database query planner (`EXPLAIN ANALYZE`)
|
|
148
|
+
|
|
149
|
+
2. **Bisect** — narrow down which commit / change introduced the regression.
|
|
150
|
+
|
|
151
|
+
3. **Measure first, fix second.** Do not optimise without data.
|
|
152
|
+
|
|
153
|
+
### Hot Path Identification
|
|
154
|
+
|
|
155
|
+
- Profile before guessing
|
|
156
|
+
- Look for O(n²) algorithms, unnecessary renders, blocking operations
|
|
157
|
+
- Check for N+1 queries, unbounded caches, recursive without memoisation
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: code-handoff
|
|
3
|
+
description: Compact the current conversation into a handoff document for another agent to pick up.
|
|
4
|
+
argument-hint: "What will the next session be used for?"
|
|
5
|
+
user-invocable: true
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Write a handoff document summarising the current conversation so a fresh agent can continue the work. Save to the temporary directory of the user's OS - not the current workspace.
|
|
9
|
+
|
|
10
|
+
Include a "suggested skills" section in the document, which suggests skills that the agent should invoke.
|
|
11
|
+
|
|
12
|
+
Do not duplicate content already captured in other artifacts (PRDs, plans, ADRs, issues, commits, diffs). Reference them by path or URL instead.
|
|
13
|
+
|
|
14
|
+
Redact any sensitive information, such as API keys, passwords, or personally identifiable information.
|
|
15
|
+
|
|
16
|
+
If the user passed arguments, treat them as a description of what the next session will focus on and tailor the doc accordingly.
|
|
@@ -6,6 +6,24 @@ user-invocable: false
|
|
|
6
6
|
|
|
7
7
|
# Architect Skill
|
|
8
8
|
|
|
9
|
+
## Glossary
|
|
10
|
+
|
|
11
|
+
Precise vocabulary for every architectural decision. Use these terms exactly — consistency is the point.
|
|
12
|
+
|
|
13
|
+
- **Module** — anything with an interface and an implementation (function, class, package, layer). Router, Service, Entity, and Repository are all modules.
|
|
14
|
+
- **Interface** — everything a caller must know to use the module: types, invariants, error modes, ordering, config. Not just the type signature.
|
|
15
|
+
- **Seam** — where a module's interface lives; a place behaviour can be altered without editing in place. The functional core / effectful edge boundary is the primary seam.
|
|
16
|
+
- **Depth** — leverage at the interface. A module is **deep** when a lot of behaviour sits behind a small interface. A module is **shallow** when the interface is nearly as complex as the implementation.
|
|
17
|
+
- **Adapter** — a concrete thing satisfying an interface at a seam. A Postgres repository is an adapter; an in-memory fake for testing is another adapter at the same seam.
|
|
18
|
+
- **Leverage** — what callers get from depth: more capability per unit of interface they learn.
|
|
19
|
+
- **Locality** — what maintainers get from depth: change, bugs, and knowledge concentrate in one place.
|
|
20
|
+
|
|
21
|
+
Key principles (apply to every decision):
|
|
22
|
+
|
|
23
|
+
- **The deletion test.** Imagine deleting the module. If complexity vanishes, it was a pass-through. If complexity reappears across N callers, it was earning its keep.
|
|
24
|
+
- **The interface is the test surface.** Callers and tests cross the same seam. If you want to test past the interface, the module is probably the wrong shape.
|
|
25
|
+
- **One adapter = hypothetical seam. Two adapters = real seam.** Don't introduce a seam unless something actually varies across it.
|
|
26
|
+
|
|
9
27
|
Domain-Driven Design and hexagonal architecture with functional core pattern for feature design.
|
|
10
28
|
|
|
11
29
|
## Architecture Model
|
|
@@ -25,6 +43,68 @@ Unified view of functional core and effectful edge:
|
|
|
25
43
|
|
|
26
44
|
**Key Principle:** Business logic lives in the functional core (Service + Entity). IO operations live in the effectful edge. Core defines interfaces; edge implements them (dependency inversion).
|
|
27
45
|
|
|
46
|
+
## Evaluating Architecture Decisions
|
|
47
|
+
|
|
48
|
+
Before creating a new module, run these checks:
|
|
49
|
+
|
|
50
|
+
### Deletion Test
|
|
51
|
+
|
|
52
|
+
> *Imagine deleting the module. If complexity vanishes, it was a pass-through. If complexity reappears across N callers, it was earning its keep.*
|
|
53
|
+
|
|
54
|
+
**When to apply:** Before extracting a new Service, Repository, or utility class.
|
|
55
|
+
|
|
56
|
+
**Example:**
|
|
57
|
+
```
|
|
58
|
+
❌ Shallow: OrderFormatterService with 1 method per field
|
|
59
|
+
→ Deleting it just moves 6 lines into the Router
|
|
60
|
+
→ Interface is as complex as implementation
|
|
61
|
+
|
|
62
|
+
✅ Deep: OrderService with validate → reserve → save → publish
|
|
63
|
+
→ Deleting it replicates orchestration across N handlers
|
|
64
|
+
→ Small interface (createOrder), large implementation
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Interface as Test Surface
|
|
68
|
+
|
|
69
|
+
> *Callers and tests cross the same seam. If you want to test past the interface, the module is probably the wrong shape.*
|
|
70
|
+
|
|
71
|
+
**Implication:**
|
|
72
|
+
- Unit tests for Entities test the public interface (validate, transform)
|
|
73
|
+
- Unit tests for Services test through the seam with stub repositories
|
|
74
|
+
- Integration tests for Repositories test the actual adapter
|
|
75
|
+
- If you find yourself testing "internal" methods, the module needs redrawing
|
|
76
|
+
|
|
77
|
+
### Adapter Rule
|
|
78
|
+
|
|
79
|
+
> *One adapter = hypothetical seam. Two adapters = real seam.*
|
|
80
|
+
|
|
81
|
+
**When to apply:** Deciding whether to extract an interface.
|
|
82
|
+
|
|
83
|
+
**Examples:**
|
|
84
|
+
```typescript
|
|
85
|
+
// ❌ Hypothetical seam — only one adapter exists
|
|
86
|
+
interface IEmailClient { send(email: Email): Promise<void>; }
|
|
87
|
+
class SendgridClient implements IEmailClient { ... }
|
|
88
|
+
// No second adapter. The interface adds indirection without value.
|
|
89
|
+
|
|
90
|
+
// ✅ Real seam — two adapters exist
|
|
91
|
+
interface IOrderRepository { save(order: Order): Promise<Order>; }
|
|
92
|
+
class PostgresOrderRepository implements IOrderRepository { ... }
|
|
93
|
+
class InMemoryOrderRepository implements IOrderRepository { ... } // for tests
|
|
94
|
+
// The seam earns its keep because callers vary (production vs test).
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Depth Check
|
|
98
|
+
|
|
99
|
+
Prefer deep modules over shallow ones:
|
|
100
|
+
|
|
101
|
+
| Module | Interface Size | Implementation | Depth |
|
|
102
|
+
|--------|--------------|----------------|-------|
|
|
103
|
+
| `OrderValidator` | 3 methods (validate, validateItems, validateAddress) | 30 lines each | Shallow |
|
|
104
|
+
| `Order` (entity) | 4 methods (fromRequest, toRecord, toResponse, validate) | 200 lines of rules, transforms, invariants | **Deep** |
|
|
105
|
+
|
|
106
|
+
**Rule of thumb:** A module's interface should hide at least 3x the complexity it exposes.
|
|
107
|
+
|
|
28
108
|
## Functional Core
|
|
29
109
|
|
|
30
110
|
Pure, deterministic components containing all business logic.
|
|
@@ -241,6 +321,7 @@ The testing skill uses architectural structure to determine:
|
|
|
241
321
|
|
|
242
322
|
For detailed patterns and examples:
|
|
243
323
|
|
|
244
|
-
- **See [references/ddd-patterns.md](references/ddd-patterns.md)** - Aggregates, Value Objects, Domain Events, Bounded Contexts, composition patterns
|
|
324
|
+
- **See [references/ddd-patterns.md](references/ddd-patterns.md)** - Aggregates, Value Objects, Domain Events, Bounded Contexts, composition patterns, seam placement
|
|
245
325
|
- **See [references/data-modeling.md](references/data-modeling.md)** - Entity design principles, schema patterns, access pattern optimization, data transformation
|
|
246
326
|
- **See [references/api-design.md](references/api-design.md)** - REST conventions, request/response contracts, error handling, versioning patterns
|
|
327
|
+
- **See [references/interface-design.md](references/interface-design.md)** - Designing module interfaces, "Design It Twice" pattern, depth heuristics, dependency categories, anti-patterns
|
|
@@ -245,3 +245,104 @@ Bounded Context: "Orders"
|
|
|
245
245
|
3. **Aggregate → Repository**: Service uses repository to persist aggregate
|
|
246
246
|
4. **Service → Producer**: Service publishes domain events after state change
|
|
247
247
|
5. **Consumer → Service**: Events from other contexts trigger service operations
|
|
248
|
+
|
|
249
|
+
## Seam Placement
|
|
250
|
+
|
|
251
|
+
Where you put the seam matters as much as what goes behind it.
|
|
252
|
+
|
|
253
|
+
### Core/Edge Seam
|
|
254
|
+
|
|
255
|
+
The primary seam runs between the functional core and the effectful edge:
|
|
256
|
+
|
|
257
|
+
```
|
|
258
|
+
┌─────────────────────────────────┐
|
|
259
|
+
│ Functional Core (pure) │
|
|
260
|
+
│ ┌──────────────────────────┐ │
|
|
261
|
+
│ │ OrderService │ │
|
|
262
|
+
│ │ createOrder() ────────┼───┼──▶ (seam)
|
|
263
|
+
│ │ processPayment() │ │
|
|
264
|
+
│ └──────────────────────────┘ │
|
|
265
|
+
└─────────────────────────────────┘
|
|
266
|
+
│
|
|
267
|
+
▼ seam
|
|
268
|
+
┌─────────────────────────────────┐
|
|
269
|
+
│ Effectful Edge (IO) │
|
|
270
|
+
│ ┌──────────────────────────┐ │
|
|
271
|
+
│ │ PostgresOrderRepository │◀──┼── adapter
|
|
272
|
+
│ │ HttpPaymentClient │ │ adapter
|
|
273
|
+
│ │ InMemoryOrderRepository │ │ adapter (test)
|
|
274
|
+
│ └──────────────────────────┘ │
|
|
275
|
+
└─────────────────────────────────┘
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
**Rules:**
|
|
279
|
+
- Core defines the interface; edge implements it (dependency inversion)
|
|
280
|
+
- The seam must have **at least two adapters** to be real (production + test)
|
|
281
|
+
- Core never imports edge code; edge imports core interfaces
|
|
282
|
+
|
|
283
|
+
### Internal Seams
|
|
284
|
+
|
|
285
|
+
A deep module can have **internal seams** — private to its implementation, used by its own tests:
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
class Order {
|
|
289
|
+
// External seam (public interface)
|
|
290
|
+
validate(): Result<Order> { ... }
|
|
291
|
+
|
|
292
|
+
// Internal seam (private, tested directly)
|
|
293
|
+
private calculateTax(subtotal: number): Money { ... }
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Internal seams let you test complex logic without exposing it in the public interface.
|
|
298
|
+
|
|
299
|
+
### When to Create a Seam
|
|
300
|
+
|
|
301
|
+
Use the **adapter rule**: one adapter = hypothetical; two = real.
|
|
302
|
+
|
|
303
|
+
| Scenario | Action |
|
|
304
|
+
|----------|--------|
|
|
305
|
+
| One database implementation, no test fake | No seam needed. Direct dependency is simpler. |
|
|
306
|
+
| One database implementation, test fake exists | Extract interface. The fake is the second adapter. |
|
|
307
|
+
| Multiple database implementations (Postgres + DynamoDB) | Extract interface. The implementations are the adapters. |
|
|
308
|
+
| External API with one client | Direct dependency. Introduce seam only when you need a mock/stub for testing. |
|
|
309
|
+
|
|
310
|
+
### Anti-Patterns
|
|
311
|
+
|
|
312
|
+
**Seam at every dependency:**
|
|
313
|
+
```typescript
|
|
314
|
+
// ❌ Too many seams — every dependency gets an interface
|
|
315
|
+
interface IValidator { ... }
|
|
316
|
+
interface IFormatter { ... }
|
|
317
|
+
interface IConverter { ... }
|
|
318
|
+
// Each has one adapter. Indirection without value.
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
**No seam at the core/edge boundary:**
|
|
322
|
+
```typescript
|
|
323
|
+
// ❌ Core imports edge directly
|
|
324
|
+
import { db } from '../edge/postgres';
|
|
325
|
+
class OrderService {
|
|
326
|
+
async createOrder(req) {
|
|
327
|
+
await db.orders.create(...); // Core knows about Postgres
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
**Leaky seam:**
|
|
333
|
+
```typescript
|
|
334
|
+
// ❌ Interface exposes implementation details
|
|
335
|
+
interface OrderRepository {
|
|
336
|
+
queryRaw(sql: string): Promise<Row[]>; // Caller knows SQL
|
|
337
|
+
beginTransaction(): Promise<Transaction>; // Caller manages transactions
|
|
338
|
+
}
|
|
339
|
+
// The seam leaks. Callers become coupled to the database.
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Seam Placement Checklist
|
|
343
|
+
|
|
344
|
+
Before introducing a new seam, verify:
|
|
345
|
+
- [ ] At least two adapters will exist (production + test, or multiple production)
|
|
346
|
+
- [ ] The interface hides implementation details (no SQL, no HTTP, no framework types)
|
|
347
|
+
- [ ] The core doesn't import edge code
|
|
348
|
+
- [ ] Deleting the module would re-appear complexity across N callers
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# Interface Design Reference
|
|
2
|
+
|
|
3
|
+
Patterns for designing module interfaces that maximize depth, leverage, and locality.
|
|
4
|
+
|
|
5
|
+
Based on "Design It Twice" (Ousterhout) — your first idea is unlikely to be the best.
|
|
6
|
+
|
|
7
|
+
## Design It Twice
|
|
8
|
+
|
|
9
|
+
Before committing to a module's interface, sketch at least two alternatives with different trade-offs.
|
|
10
|
+
|
|
11
|
+
### Process
|
|
12
|
+
|
|
13
|
+
1. **Frame the problem space**
|
|
14
|
+
- List constraints any interface must satisfy
|
|
15
|
+
- Identify dependencies and which category they fall into
|
|
16
|
+
- Write a rough illustrative code sketch to ground the constraints
|
|
17
|
+
|
|
18
|
+
2. **Sketch alternatives**
|
|
19
|
+
- Alternative A: Minimize the interface (1–3 entry points max)
|
|
20
|
+
- Alternative B: Maximize flexibility (support many use cases)
|
|
21
|
+
- Alternative C: Optimize for the most common caller (default case trivial)
|
|
22
|
+
|
|
23
|
+
3. **Compare by depth**
|
|
24
|
+
- Which alternative has the smallest interface for the most behaviour?
|
|
25
|
+
- Which concentrates the most knowledge in one place?
|
|
26
|
+
- Which has the best test surface?
|
|
27
|
+
|
|
28
|
+
### Example: Order Creation Interface
|
|
29
|
+
|
|
30
|
+
**Problem:** Design the interface for creating an order.
|
|
31
|
+
|
|
32
|
+
**Alternative A — Minimal interface:**
|
|
33
|
+
```typescript
|
|
34
|
+
interface OrderService {
|
|
35
|
+
createOrder(request: CreateOrderRequest): Promise<Result<Order>>;
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
- **Pros:** Tiny surface. One seam. Easy to mock.
|
|
39
|
+
- **Cons:** All complexity hidden inside. Hard to test partial flows (e.g. validation without persistence).
|
|
40
|
+
- **Depth:** Very deep. High leverage, high locality.
|
|
41
|
+
|
|
42
|
+
**Alternative B — Flexible interface:**
|
|
43
|
+
```typescript
|
|
44
|
+
interface OrderService {
|
|
45
|
+
validate(request: CreateOrderRequest): Promise<Result<Order>>;
|
|
46
|
+
reserveInventory(order: Order): Promise<Result<Order>>;
|
|
47
|
+
save(order: Order): Promise<Result<Order>>;
|
|
48
|
+
publishEvents(order: Order): Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
- **Pros:** Callers compose their own flows. Easy to test each step.
|
|
52
|
+
- **Cons:** Interface is as complex as implementation. Callers must know ordering constraints (reserve before save).
|
|
53
|
+
- **Depth:** Shallow. Low leverage — callers replicate orchestration.
|
|
54
|
+
|
|
55
|
+
**Alternative C — Optimized for common case:**
|
|
56
|
+
```typescript
|
|
57
|
+
interface OrderService {
|
|
58
|
+
createOrder(request: CreateOrderRequest): Promise<Result<Order>>;
|
|
59
|
+
createOrderDraft(request: CreateOrderRequest): Promise<Result<Order>>; // no inventory check
|
|
60
|
+
confirmOrder(orderId: string): Promise<Result<Order>>; // draft → confirmed
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
- **Pros:** Common case (createOrder) is trivial. Draft pattern supports async workflows.
|
|
64
|
+
- **Cons:** More surface area than Alternative A. Callers must understand draft vs confirmed.
|
|
65
|
+
- **Depth:** Moderate. Good leverage for the common case.
|
|
66
|
+
|
|
67
|
+
**Recommendation:** Alternative C. It balances depth with real workflow needs. The draft/confirm split is a real seam (two callers: interactive vs batch).
|
|
68
|
+
|
|
69
|
+
## Interface Size Heuristics
|
|
70
|
+
|
|
71
|
+
### The Rule of Three
|
|
72
|
+
|
|
73
|
+
A module's public interface should have no more than 3 primary entry points for each responsibility:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
// ❌ Too wide
|
|
77
|
+
interface OrderService {
|
|
78
|
+
createOrder(...): Promise<Order>;
|
|
79
|
+
updateOrder(...): Promise<Order>;
|
|
80
|
+
deleteOrder(...): Promise<void>;
|
|
81
|
+
getOrder(...): Promise<Order>;
|
|
82
|
+
listOrders(...): Promise<Order[]>;
|
|
83
|
+
searchOrders(...): Promise<Order[]>;
|
|
84
|
+
cancelOrder(...): Promise<Order>;
|
|
85
|
+
shipOrder(...): Promise<Order>;
|
|
86
|
+
refundOrder(...): Promise<Order>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ✅ Split by responsibility (2 modules, each with ~3 methods)
|
|
90
|
+
interface OrderLifecycleService {
|
|
91
|
+
create(...): Promise<Order>;
|
|
92
|
+
cancel(...): Promise<Order>;
|
|
93
|
+
ship(...): Promise<Order>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface OrderQueryService {
|
|
97
|
+
get(id: string): Promise<Order>;
|
|
98
|
+
list(filters: OrderFilters): Promise<Order[]>;
|
|
99
|
+
search(query: string): Promise<Order[]>;
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Invariants in the Interface
|
|
104
|
+
|
|
105
|
+
The interface includes not just types, but everything a caller must know:
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
interface PaymentService {
|
|
109
|
+
// ❌ Interface hides invariants
|
|
110
|
+
processPayment(orderId: string, amount: number): Promise<Receipt>;
|
|
111
|
+
// Caller must discover: amount must match order total, order must be confirmed,
|
|
112
|
+
// can't process twice, throws if payment gateway is down.
|
|
113
|
+
|
|
114
|
+
// ✅ Interface encodes invariants in types
|
|
115
|
+
processPayment(
|
|
116
|
+
order: ConfirmedOrder, // type encodes "must be confirmed"
|
|
117
|
+
payment: PaymentMethod // type encodes valid methods
|
|
118
|
+
): Promise<Result<Receipt, PaymentError>>;
|
|
119
|
+
// Error mode explicit in return type. Ordering implicit in input types.
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Dependency Categories
|
|
123
|
+
|
|
124
|
+
The shape of an interface depends on what it depends on.
|
|
125
|
+
|
|
126
|
+
| Category | Description | Interface Impact |
|
|
127
|
+
|----------|-------------|------------------|
|
|
128
|
+
| **In-process** | Same memory space, no serialization | Can pass rich types, callbacks, streams |
|
|
129
|
+
| **Local-substitutable** | Same interface, different implementations (real vs test) | Seam is real. Interface must hide implementation |
|
|
130
|
+
| **Ports & adapters** | Crosses a real boundary (DB, HTTP, message queue) | Interface must be narrow. Errors are part of the contract |
|
|
131
|
+
| **Mock** | Test-only dependency, no production equivalent | Don't create a seam. Test the real module |
|
|
132
|
+
|
|
133
|
+
## Common Interface Anti-Patterns
|
|
134
|
+
|
|
135
|
+
### Leaky Abstraction
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
// ❌ Interface exposes implementation
|
|
139
|
+
interface UserRepository {
|
|
140
|
+
query(sql: string, params: any[]): Promise<Row[]>;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ✅ Interface hides implementation
|
|
144
|
+
interface UserRepository {
|
|
145
|
+
findByEmail(email: Email): Promise<User | null>;
|
|
146
|
+
save(user: User): Promise<User>;
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Pass-Through Interface
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
// ❌ Every method delegates 1:1 to another module
|
|
154
|
+
class OrderService {
|
|
155
|
+
async findById(id: string) { return this.repo.findById(id); }
|
|
156
|
+
async save(order: Order) { return this.repo.save(order); }
|
|
157
|
+
async delete(id: string) { return this.repo.delete(id); }
|
|
158
|
+
}
|
|
159
|
+
// Deletion test: delete OrderService → complexity doesn't reappear.
|
|
160
|
+
// It's a pass-through. Remove it and call repo directly.
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### God Interface
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
// ❌ One module does everything
|
|
167
|
+
interface PlatformService {
|
|
168
|
+
createUser(...): Promise<User>;
|
|
169
|
+
sendEmail(...): Promise<void>;
|
|
170
|
+
processPayment(...): Promise<Receipt>;
|
|
171
|
+
generateReport(...): Promise<PDF>;
|
|
172
|
+
}
|
|
173
|
+
// No cohesion. Deleting it would reappear complexity, but in unrelated places.
|
|
174
|
+
// Split by domain responsibility instead.
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Chatty Interface
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
// ❌ Many small methods that must be called in sequence
|
|
181
|
+
interface OrderBuilder {
|
|
182
|
+
setCustomer(customerId: string): this;
|
|
183
|
+
addItem(item: OrderItem): this;
|
|
184
|
+
setShipping(address: Address): this;
|
|
185
|
+
setPayment(method: PaymentMethod): this;
|
|
186
|
+
validate(): Result<void>;
|
|
187
|
+
build(): Order;
|
|
188
|
+
}
|
|
189
|
+
// Callers must know ordering. State machine hidden in method calls.
|
|
190
|
+
// Better: single method that takes a complete request.
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Decision Checklist
|
|
194
|
+
|
|
195
|
+
Before finalizing an interface:
|
|
196
|
+
|
|
197
|
+
- [ ] Run the deletion test — does complexity reappear across N callers?
|
|
198
|
+
- [ ] Check depth — is the interface at least 3x smaller than the implementation?
|
|
199
|
+
- [ ] Verify seam — are there at least two adapters?
|
|
200
|
+
- [ ] Encode invariants — are error modes, ordering, and constraints visible in types?
|
|
201
|
+
- [ ] Test surface — can you test all behaviour through the public interface?
|
|
202
|
+
- [ ] Hide details — does the interface leak SQL, HTTP, frameworks, or implementation?
|
|
203
|
+
- [ ] Common case — is the most frequent caller's experience trivial?
|