@martinffx/atelier 3.3.0 → 3.5.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/CHANGELOG.md +25 -16
- package/README.md +9 -4
- package/agents/architect.md +3 -2
- package/package.json +5 -2
- package/skills/atelier-orchestrator/SKILL.md +14 -7
- package/skills/code-review/SKILL.md +55 -30
- package/skills/code-review/references/output.md +128 -46
- package/skills/code-review/references/personas.md +345 -0
- package/skills/code-review/references/roles/author.md +97 -0
- package/skills/code-review/references/roles/critic.md +179 -0
- package/skills/code-review/references/roles/judge.md +96 -0
- package/skills/code-review/references/roles/triage.md +54 -0
- package/skills/code-review/references/rq.md +173 -457
- package/skills/code-review/references/rs.md +28 -7
- package/skills/code-subagents/SKILL.md +5 -4
- package/skills/code-subagents/references/batch-reviewer-prompt.md +2 -2
- package/skills/code-subagents/references/implementor-prompt.md +5 -3
- package/skills/oracle-codebase-design/SKILL.md +129 -0
- package/skills/oracle-codebase-design/references/deepening.md +37 -0
- package/skills/oracle-codebase-design/references/design-it-twice.md +44 -0
- package/skills/oracle-codebase-design/references/principles.md +205 -0
- package/skills/oracle-codebase-design/references/red-flags.md +32 -0
- package/skills/oracle-debug/SKILL.md +2 -2
- package/skills/oracle-grill-me/SKILL.md +2 -1
- package/skills/spec-brainstorm/SKILL.md +8 -4
- package/skills/spec-finish/SKILL.md +4 -3
- package/skills/spec-implement/SKILL.md +4 -1
- package/skills/spec-loop/SKILL.md +91 -0
- package/skills/spec-plan/SKILL.md +2 -2
- package/skills/code-review/references/reviewers.md +0 -921
|
@@ -18,11 +18,31 @@ Ask me questions 1 by 1, multiple choice.
|
|
|
18
18
|
## Behavior
|
|
19
19
|
|
|
20
20
|
1. Load the review findings from the current conversation context
|
|
21
|
-
2.
|
|
22
|
-
3.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
2. Offer every active finding marked **Mechanical** as one batch (see below)
|
|
22
|
+
3. Offer the nitpicks whose Debate field reads `Author took` as one batch: apply all, pick
|
|
23
|
+
which, or interview each
|
|
24
|
+
4. Interview blocking findings first, then non-blocking ones — one question at a time, always
|
|
25
|
+
multiple choice. Answered questions need no interview; list their answers for confirmation.
|
|
26
|
+
5. Classify each finding as fix, intentional/accepted risk, false positive, or deferred.
|
|
27
|
+
Findings marked **Follow-up** go to a follow-up list and are not fixed here.
|
|
28
|
+
6. Capture the rationale and reconsideration condition for every non-fix resolution
|
|
29
|
+
7. For a finding under Withdrawn in debate whose evidence is `intent`, `sdd` or `test`, offer a
|
|
30
|
+
`review-decision:` comment recording that context. The user approves each one. Do not offer a
|
|
31
|
+
comment when a plain code refutation withdrew it.
|
|
32
|
+
8. Present one plan containing the exact fixes and proposed decision comments
|
|
33
|
+
9. **Always ask the user before applying fixes or adding, changing, or removing comments**
|
|
34
|
+
|
|
35
|
+
## Mechanical Batch
|
|
36
|
+
|
|
37
|
+
Findings marked **Mechanical** keep behavior identical, stay local, and need no judgment. List
|
|
38
|
+
them together, each with its location and exact change, and ask one question:
|
|
39
|
+
|
|
40
|
+
- a) Apply all
|
|
41
|
+
- b) Pick which to apply (the rest join the interview)
|
|
42
|
+
- c) Interview each one
|
|
43
|
+
|
|
44
|
+
Applied mechanical fixes join the final plan like any other fix. A finding that requires user
|
|
45
|
+
approval or reopens a recorded decision is never batched.
|
|
26
46
|
|
|
27
47
|
## Resolution Rules
|
|
28
48
|
|
|
@@ -56,10 +76,11 @@ For example:
|
|
|
56
76
|
|
|
57
77
|
## Guidelines
|
|
58
78
|
|
|
59
|
-
- One question at a time — never batch questions
|
|
79
|
+
- One question at a time — never batch questions (the mechanical batch is a single question)
|
|
60
80
|
- Always provide multiple choice answers (a, b, c, d...)
|
|
61
81
|
- Start with the most ambiguous findings first
|
|
62
|
-
- Skip findings that are clearly actionable without input
|
|
82
|
+
- Skip findings that are clearly actionable without input (mechanical findings go through the batch)
|
|
83
|
+
- Skip nitpicks the Author left and findings withdrawn in debate, except as in step 7
|
|
63
84
|
- For a non-fix choice, ask for the rationale or reconsideration condition only when the
|
|
64
85
|
conversation and repository do not already provide it
|
|
65
86
|
- After all questions are answered, show exact proposed comments with the fix plan and confirm
|
|
@@ -69,10 +69,11 @@ For shared-tree parallel work:
|
|
|
69
69
|
|
|
70
70
|
## Agent Selection
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
Select the agent explicitly for every dispatch (see Delegation in `atelier-orchestrator`). Give
|
|
73
|
+
implementers the design excerpt so they make no design decisions: mechanical file creation should
|
|
74
|
+
match a scaffolder; design-sensitive implementation may use the harness's general agent, the one
|
|
75
|
+
permitted exception to selecting from Atelier's agents. The batch reviewer is a read-only
|
|
76
|
+
critical review.
|
|
76
77
|
|
|
77
78
|
---
|
|
78
79
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Batch Reviewer Prompt Template
|
|
2
2
|
|
|
3
3
|
Use this template after all tasks in an implementation batch report completion. Review the
|
|
4
|
-
captured task patches, not a commit range.
|
|
5
|
-
|
|
4
|
+
captured task patches, not a commit range. Select the agent explicitly (see Delegation in
|
|
5
|
+
`atelier-orchestrator`); the work is a read-only critical review.
|
|
6
6
|
|
|
7
7
|
## Template
|
|
8
8
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Implementer Prompt Template
|
|
2
2
|
|
|
3
3
|
Use this template when dispatching an implementer subagent. Paste the complete work-item
|
|
4
|
-
requirements. Do not make the subagent recover context.
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
requirements. Do not make the subagent recover context. Select the agent explicitly (see Delegation in
|
|
5
|
+
`atelier-orchestrator`), for example a scaffolder for boilerplate or pattern-following file
|
|
6
|
+
creation.
|
|
7
7
|
|
|
8
8
|
## Template
|
|
9
9
|
|
|
@@ -63,6 +63,7 @@ Review your work with fresh eyes:
|
|
|
63
63
|
- Is this my best work?
|
|
64
64
|
- Are names clear and accurate?
|
|
65
65
|
- Is the code clean and maintainable?
|
|
66
|
+
- If `oracle-codebase-design` is available, did I check its red flags (shallow modules, leakage, pass-throughs)?
|
|
66
67
|
|
|
67
68
|
**Discipline:**
|
|
68
69
|
- Did I avoid overbuilding (YAGNI)?
|
|
@@ -71,6 +72,7 @@ Review your work with fresh eyes:
|
|
|
71
72
|
|
|
72
73
|
**Testing:**
|
|
73
74
|
- Do tests actually verify behaviour (not just mock behaviour)?
|
|
75
|
+
- Do tests go through the module's interface rather than past it?
|
|
74
76
|
- Did I follow TDD?
|
|
75
77
|
- Are tests comprehensive?
|
|
76
78
|
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: oracle-codebase-design
|
|
3
|
+
description: >
|
|
4
|
+
Shared vocabulary and principles for designing deep modules. Use when designing or changing
|
|
5
|
+
a module's interface, deciding where a seam goes, handling untrusted input at the edges,
|
|
6
|
+
choosing the level to test at, naming modules, or making code more testable. Also use when
|
|
7
|
+
another skill needs the deep-module vocabulary.
|
|
8
|
+
user-invocable: false
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Codebase Design
|
|
12
|
+
|
|
13
|
+
Design **deep modules**: a lot of behaviour behind a small interface, placed at a clean seam, testable through that interface. The aim is leverage for callers, locality for maintainers, and testability for everyone.
|
|
14
|
+
|
|
15
|
+
## Why
|
|
16
|
+
|
|
17
|
+
Complexity is the enemy. It shows up as:
|
|
18
|
+
|
|
19
|
+
- **Change amplification**: a simple change touches many places.
|
|
20
|
+
- **Cognitive load**: you must hold a lot in your head to change anything safely.
|
|
21
|
+
- **Unknown unknowns**: it is not obvious what you need to change or know.
|
|
22
|
+
|
|
23
|
+
It has two causes: **dependencies** and **obscurity**. Good design removes both.
|
|
24
|
+
|
|
25
|
+
Work **strategically, not tactically**. Working code is not enough. Each change is a small, continual investment in the design, not a patch on top of it.
|
|
26
|
+
|
|
27
|
+
## Glossary
|
|
28
|
+
|
|
29
|
+
Use these terms exactly. Don't substitute "component," "service," "API," or "boundary." Consistent language is the whole point.
|
|
30
|
+
|
|
31
|
+
**Module**: anything with an interface and an implementation. Deliberately scale-agnostic: a function, class, package, or tier-spanning slice. _Avoid_: unit, component, service.
|
|
32
|
+
|
|
33
|
+
**Interface**: everything a caller must know to use the module correctly: the type signature, but also invariants, ordering constraints, error modes, required configuration, and performance characteristics. _Avoid_: API, signature (too narrow, they refer only to the type-level surface).
|
|
34
|
+
|
|
35
|
+
**Implementation**: what's inside a module, its body of code. Distinct from **Adapter**: a thing can be a small adapter with a large implementation (a Postgres repo) or a large adapter with a small implementation (an in-memory fake). Reach for "adapter" when the seam is the topic; "implementation" otherwise.
|
|
36
|
+
|
|
37
|
+
**Depth**: leverage at the interface. The amount of behaviour a caller (or test) can exercise per unit of interface they have to learn. A module is **deep** when a large amount of behaviour sits behind a small interface, **shallow** when the interface is nearly as complex as the implementation.
|
|
38
|
+
|
|
39
|
+
**Seam** _(Michael Feathers)_: a place where you can alter behaviour without editing in that place; the *location* at which a module's interface lives. Where to put the seam is its own design decision, distinct from what goes behind it. _Avoid_: boundary (overloaded with DDD's bounded context).
|
|
40
|
+
|
|
41
|
+
**Adapter**: a concrete thing that satisfies an interface at a seam. Describes *role* (what slot it fills), not substance (what's inside).
|
|
42
|
+
|
|
43
|
+
**Leverage**: what callers get from depth. More capability per unit of interface they learn. One implementation pays back across N call sites and M tests.
|
|
44
|
+
|
|
45
|
+
**Locality**: what maintainers get from depth. Change, bugs, knowledge, and verification concentrate in one place rather than spreading across callers. Fix once, fixed everywhere.
|
|
46
|
+
|
|
47
|
+
### Project names
|
|
48
|
+
|
|
49
|
+
When the project's own pattern names a layer (Router, Service, Repository, Entity), keep that name for the concrete thing: say "the `OrderService`". Describe its *design* with the glossary: is that module deep, where is its seam, what is its interface?
|
|
50
|
+
|
|
51
|
+
## Deep vs shallow
|
|
52
|
+
|
|
53
|
+
**Deep module** = small interface + lots of implementation:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
┌─────────────────────┐
|
|
57
|
+
│ Small Interface │ ← Few methods, simple params
|
|
58
|
+
├─────────────────────┤
|
|
59
|
+
│ │
|
|
60
|
+
│ Deep Implementation│ ← Complex logic hidden
|
|
61
|
+
│ │
|
|
62
|
+
└─────────────────────┘
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Shallow module** = large interface + little implementation (avoid):
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
┌─────────────────────────────────┐
|
|
69
|
+
│ Large Interface │ ← Many methods, complex params
|
|
70
|
+
├─────────────────────────────────┤
|
|
71
|
+
│ Thin Implementation │ ← Just passes through
|
|
72
|
+
└─────────────────────────────────┘
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
When designing an interface, ask:
|
|
76
|
+
|
|
77
|
+
- Can I reduce the number of methods?
|
|
78
|
+
- Can I simplify the parameters?
|
|
79
|
+
- Can I hide more complexity inside?
|
|
80
|
+
- Can I define an error out of existence?
|
|
81
|
+
|
|
82
|
+
## Principles
|
|
83
|
+
|
|
84
|
+
Details, rationale, and examples in [principles.md](references/principles.md).
|
|
85
|
+
|
|
86
|
+
- **Deep modules and the deletion test.** If deleting the module makes complexity vanish, it was a pass-through.
|
|
87
|
+
- **Test at the correct level.** The interface is the test surface. Test at seams, not every function; replace tests, don't layer them.
|
|
88
|
+
- **Parse, don't validate.** Turn untrusted input into domain types at the external seam. The core never re-checks.
|
|
89
|
+
- **Make illegal states unrepresentable.** Shape the types so invalid combinations can't be built.
|
|
90
|
+
- **Functional core, imperative shell.** Pure decisions inside, I/O at the edges.
|
|
91
|
+
- **Errors as values.** Expected failures are part of the interface and are returned, not thrown.
|
|
92
|
+
- **Define errors out of existence.** Change the semantics so the error cannot occur. Do this first.
|
|
93
|
+
- **Information hiding.** Each design decision lives in one module. Duplicated knowledge is leakage.
|
|
94
|
+
- **Pull complexity downward.** A simple interface matters more than a simple implementation.
|
|
95
|
+
- **Somewhat general-purpose modules are deeper.** Keep general and special-purpose code apart.
|
|
96
|
+
- **Different layers, different abstractions.** No pass-through methods or variables.
|
|
97
|
+
- **Together or apart.** Combine what shares information; split general mechanism from specific use.
|
|
98
|
+
- **Make the common case simple.** Defaults over configuration.
|
|
99
|
+
- **Comments and names carry the design.** Describe what isn't obvious; write interface comments first.
|
|
100
|
+
- **Seam discipline.** One adapter means a hypothetical seam, two means a real one. Accept dependencies, don't create them.
|
|
101
|
+
|
|
102
|
+
## Red flags
|
|
103
|
+
|
|
104
|
+
Before finalising a design, check it against [red-flags.md](references/red-flags.md): shallow module, information leakage, pass-through method, vague name, and the rest.
|
|
105
|
+
|
|
106
|
+
## Names and domain language
|
|
107
|
+
|
|
108
|
+
Name modules with the project's domain terms from `CONTEXT.md`. A name that is hard to pick or hard to describe is a design smell: the concept is probably unclear. Resolve it with the `oracle-domain-modelling` skill, and take contested design decisions through `oracle-grill-me`.
|
|
109
|
+
|
|
110
|
+
## Relationships
|
|
111
|
+
|
|
112
|
+
- A **Module** has exactly one **Interface** (the surface it presents to callers and tests).
|
|
113
|
+
- **Depth** is a property of a **Module**, measured against its **Interface**.
|
|
114
|
+
- A **Seam** is where a **Module**'s **Interface** lives.
|
|
115
|
+
- An **Adapter** sits at a **Seam** and satisfies the **Interface**.
|
|
116
|
+
- **Depth** produces **Leverage** for callers and **Locality** for maintainers.
|
|
117
|
+
|
|
118
|
+
## Rejected framings
|
|
119
|
+
|
|
120
|
+
- **Depth as ratio of implementation-lines to interface-lines** (Ousterhout): rewards padding the implementation. We use depth-as-leverage instead.
|
|
121
|
+
- **"Interface" as the TypeScript `interface` keyword or a class's public methods**: too narrow: interface here includes every fact a caller must know.
|
|
122
|
+
- **"Boundary"**: overloaded with DDD's bounded context. Say **seam** or **interface**.
|
|
123
|
+
|
|
124
|
+
## Going deeper
|
|
125
|
+
|
|
126
|
+
- **Principles with examples**, see [principles.md](references/principles.md).
|
|
127
|
+
- **Red-flags checklist**, see [red-flags.md](references/red-flags.md).
|
|
128
|
+
- **Deepening a cluster given its dependencies**, see [deepening.md](references/deepening.md): dependency categories, seam discipline, and replace-don't-layer testing.
|
|
129
|
+
- **Exploring alternative interfaces**, see [design-it-twice.md](references/design-it-twice.md): spin up parallel sub-agents to design the interface several radically different ways, then compare.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Deepening
|
|
2
|
+
|
|
3
|
+
How to deepen a cluster of shallow modules safely, given its dependencies. Assumes the vocabulary in [SKILL.md](../SKILL.md): **module**, **interface**, **seam**, **adapter**.
|
|
4
|
+
|
|
5
|
+
## Dependency categories
|
|
6
|
+
|
|
7
|
+
When assessing a candidate for deepening, classify its dependencies. The category determines how the deepened module is tested across its seam.
|
|
8
|
+
|
|
9
|
+
### 1. In-process
|
|
10
|
+
|
|
11
|
+
Pure computation, in-memory state, no I/O. Always deepenable: merge the modules and test through the new interface directly. No adapter needed.
|
|
12
|
+
|
|
13
|
+
### 2. Local-substitutable
|
|
14
|
+
|
|
15
|
+
Dependencies that have local test stand-ins (PGLite for Postgres, in-memory filesystem). Deepenable if the stand-in exists. The deepened module is tested with the stand-in running in the test suite. The seam is internal; no port at the module's external interface.
|
|
16
|
+
|
|
17
|
+
### 3. Remote but owned (Ports & Adapters)
|
|
18
|
+
|
|
19
|
+
Your own services across a network boundary (microservices, internal APIs). Define a **port** (interface) at the seam. The deep module owns the logic; the transport is injected as an **adapter**. Tests use an in-memory adapter. Production uses an HTTP/gRPC/queue adapter.
|
|
20
|
+
|
|
21
|
+
Recommendation shape: *"Define a port at the seam, implement an HTTP adapter for production and an in-memory adapter for testing, so the logic sits in one deep module even though it's deployed across a network."*
|
|
22
|
+
|
|
23
|
+
### 4. True external (Mock)
|
|
24
|
+
|
|
25
|
+
Third-party services (Stripe, Twilio, etc.) you don't control. The deepened module takes the external dependency as an injected port; tests provide a mock adapter.
|
|
26
|
+
|
|
27
|
+
## Seam discipline
|
|
28
|
+
|
|
29
|
+
- **One adapter means a hypothetical seam. Two adapters means a real one.** Don't introduce a port unless at least two adapters are justified (typically production + test). A single-adapter seam is just indirection.
|
|
30
|
+
- **Internal seams vs external seams.** A deep module can have internal seams (private to its implementation, used by its own tests) as well as the external seam at its interface. Don't expose internal seams through the interface just because tests use them.
|
|
31
|
+
|
|
32
|
+
## Testing strategy: replace, don't layer
|
|
33
|
+
|
|
34
|
+
- Old unit tests on shallow modules become waste once tests at the deepened module's interface exist; delete them.
|
|
35
|
+
- Write new tests at the deepened module's interface. The **interface is the test surface**.
|
|
36
|
+
- Tests assert on observable outcomes through the interface, not internal state.
|
|
37
|
+
- Tests should survive internal refactors, since they describe behaviour, not implementation. If a test has to change when the implementation changes, it's testing past the interface.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Design It Twice
|
|
2
|
+
|
|
3
|
+
When the user wants to explore alternative interfaces for a module, use this parallel sub-agent pattern. Based on "Design It Twice" (John Ousterhout, *A Philosophy of Software Design*): your first idea is unlikely to be the best.
|
|
4
|
+
|
|
5
|
+
Uses the vocabulary in [SKILL.md](../SKILL.md): **module**, **interface**, **seam**, **adapter**, **leverage**.
|
|
6
|
+
|
|
7
|
+
## Process
|
|
8
|
+
|
|
9
|
+
### 1. Frame the problem space
|
|
10
|
+
|
|
11
|
+
Before spawning sub-agents, write a user-facing explanation of the problem space:
|
|
12
|
+
|
|
13
|
+
- The constraints any new interface would need to satisfy
|
|
14
|
+
- The dependencies it would rely on, and which category they fall into (see [deepening.md](deepening.md))
|
|
15
|
+
- A rough illustrative code sketch to ground the constraints, not a proposal, just a way to make the constraints concrete
|
|
16
|
+
|
|
17
|
+
Show this to the user, then immediately proceed to Step 2. The user reads and thinks while the sub-agents work in parallel.
|
|
18
|
+
|
|
19
|
+
### 2. Spawn sub-agents
|
|
20
|
+
|
|
21
|
+
Spawn 3+ sub-agents in parallel. Each must produce a **radically different** interface for the module.
|
|
22
|
+
|
|
23
|
+
Prompt each sub-agent with a separate technical brief (file paths, coupling details, dependency category from [deepening.md](deepening.md), what sits behind the seam). The brief is independent of the user-facing problem-space explanation in Step 1. Give each agent a different design constraint:
|
|
24
|
+
|
|
25
|
+
- Agent 1: "Minimize the interface: aim for 1–3 entry points max. Maximise leverage per entry point."
|
|
26
|
+
- Agent 2: "Maximise flexibility: support many use cases and extension."
|
|
27
|
+
- Agent 3: "Optimise for the most common caller: make the default case trivial."
|
|
28
|
+
- Agent 4 (if applicable): "Design around ports & adapters for cross-seam dependencies."
|
|
29
|
+
|
|
30
|
+
Include both [SKILL.md](../SKILL.md) vocabulary and the project's `CONTEXT.md` vocabulary in the brief so each sub-agent names things consistently with the design language and the project's domain language.
|
|
31
|
+
|
|
32
|
+
Each sub-agent outputs:
|
|
33
|
+
|
|
34
|
+
1. Interface (types, methods, params, plus invariants, ordering, error modes)
|
|
35
|
+
2. Usage example showing how callers use it
|
|
36
|
+
3. What the implementation hides behind the seam
|
|
37
|
+
4. Dependency strategy and adapters (see [deepening.md](deepening.md))
|
|
38
|
+
5. Trade-offs: where leverage is high, where it's thin
|
|
39
|
+
|
|
40
|
+
### 3. Present and compare
|
|
41
|
+
|
|
42
|
+
Present designs sequentially so the user can absorb each one, then compare them in prose. Contrast by **depth** (leverage at the interface), **locality** (where change concentrates), and **seam placement**. Check each against [red-flags.md](red-flags.md).
|
|
43
|
+
|
|
44
|
+
After comparing, give your own recommendation: which design you think is strongest and why. If elements from different designs would combine well, propose a hybrid. Be opinionated: the user wants a strong read, not a menu.
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# Principles
|
|
2
|
+
|
|
3
|
+
Each principle: the rule, why it matters in glossary terms (see [SKILL.md](../SKILL.md)), and a short example. Examples are TypeScript.
|
|
4
|
+
|
|
5
|
+
## 1. Deep modules and the deletion test
|
|
6
|
+
|
|
7
|
+
**Rule.** Put a lot of behaviour behind a small interface. Imagine deleting the module: if complexity vanishes, it was a pass-through; if it reappears across N callers, it was earning its keep.
|
|
8
|
+
|
|
9
|
+
**Why.** Depth gives callers leverage and maintainers locality.
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
// Shallow: the interface is the implementation
|
|
13
|
+
class UserRepo {
|
|
14
|
+
getTable() { return this.db.table("users"); }
|
|
15
|
+
runQuery(q: Query) { return this.db.run(q); }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Deep: callers say what they want
|
|
19
|
+
class UserRepo {
|
|
20
|
+
findByEmail(email: Email): Promise<User | undefined> { /* query, mapping, caching */ }
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 2. Test at the correct level
|
|
25
|
+
|
|
26
|
+
**Rule.** The interface is the test surface. Test through the module's interface at its seam, not each internal function. When you deepen a module, replace the old tests; don't layer new ones on top.
|
|
27
|
+
|
|
28
|
+
**Why.** Tests that reach past the interface break on every refactor and prove nothing the interface doesn't. If you want to test past the interface, the module is probably the wrong shape.
|
|
29
|
+
|
|
30
|
+
- Unit-test the functional core directly; it has no I/O.
|
|
31
|
+
- Integration-test the shell with a local stand-in (see [deepening.md](deepening.md)).
|
|
32
|
+
- Don't mock your own modules. Mock only true external dependencies at a port.
|
|
33
|
+
- Assert on observable outcomes, not internal state.
|
|
34
|
+
|
|
35
|
+
**Smell.** A test that changes whenever the implementation changes; many tiny tests of pure helpers while the real bugs hide in how they're called.
|
|
36
|
+
|
|
37
|
+
## 3. Parse, don't validate
|
|
38
|
+
|
|
39
|
+
**Rule.** At the external seam, turn untrusted input into a domain type that can only exist if valid. The core accepts the parsed type and never re-checks.
|
|
40
|
+
|
|
41
|
+
**Why.** Validation returns a boolean and discards what it learned, so every downstream caller must re-validate or hope. Parsing keeps the knowledge in the type. Locality: the check lives in one place.
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
// Validate: knowledge is lost, callers re-check
|
|
45
|
+
function isEmail(s: string): boolean { /* ... */ }
|
|
46
|
+
function sendWelcome(to: string) { if (!isEmail(to)) throw new Error("bad email"); /* ... */ }
|
|
47
|
+
|
|
48
|
+
// Parse: knowledge is kept in the type
|
|
49
|
+
type Email = string & { readonly __brand: "Email" };
|
|
50
|
+
function parseEmail(s: string): Result<Email, InvalidEmail> { /* ... */ }
|
|
51
|
+
function sendWelcome(to: Email) { /* no check needed */ }
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
In a layered codebase this is `Entity.fromRequest`: the router hands raw input to the Entity, which parses it into domain values, and everything below works with parsed types.
|
|
55
|
+
|
|
56
|
+
**Smell.** The same field checked in router, service, and repository; `string` parameters that must be "a valid X".
|
|
57
|
+
|
|
58
|
+
## 4. Make illegal states unrepresentable
|
|
59
|
+
|
|
60
|
+
**Rule.** Shape the types so invalid combinations cannot be constructed. Prefer discriminated unions to a bag of flags and optionals.
|
|
61
|
+
|
|
62
|
+
**Why.** Every representable-but-invalid state is a case every caller must handle, which is interface complexity. Removing it shrinks the interface.
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
// Illegal states possible: loading with an error, data with no result
|
|
66
|
+
type Fetch = { loading: boolean; error?: Error; data?: Data };
|
|
67
|
+
|
|
68
|
+
// Only legal states exist
|
|
69
|
+
type Fetch =
|
|
70
|
+
| { status: "loading" }
|
|
71
|
+
| { status: "failed"; error: Error }
|
|
72
|
+
| { status: "loaded"; data: Data };
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Smell.** Comments like "only set when `kind` is X"; several booleans that can't all be true; optional fields that are required in some states.
|
|
76
|
+
|
|
77
|
+
## 5. Functional core, imperative shell
|
|
78
|
+
|
|
79
|
+
**Rule.** Keep decisions in pure functions that take values and return values. Push I/O (database, network, clock, randomness) to a thin shell that gathers inputs, calls the core, and applies the result.
|
|
80
|
+
|
|
81
|
+
**Why.** The core is testable without mocks and holds the logic, so bugs concentrate there (locality). The shell is simple enough to cover with a few integration tests.
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
// Core: pure decision
|
|
85
|
+
function decideRenewal(sub: Subscription, now: Date): RenewalDecision { /* ... */ }
|
|
86
|
+
|
|
87
|
+
// Shell: I/O around it
|
|
88
|
+
async function renew(id: SubscriptionId) {
|
|
89
|
+
const sub = await repo.get(id);
|
|
90
|
+
const decision = decideRenewal(sub, clock.now());
|
|
91
|
+
await apply(decision);
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Smell.** Business rules interleaved with queries; tests that need a mocked database to check a calculation. Counter-smell: a pure helper extracted only for testability while the real bugs sit in how it's called. Keep decisions and their orchestration close.
|
|
96
|
+
|
|
97
|
+
## 6. Errors as values
|
|
98
|
+
|
|
99
|
+
**Rule.** Expected failures (invalid input, not found, conflict) are part of the interface and are returned as typed values, such as `Result<T, E>`. Exceptions are for bugs and unrecoverable states.
|
|
100
|
+
|
|
101
|
+
**Why.** Error modes are part of the interface. If they're hidden in `throw`, the caller can't see what to handle and the compiler can't help.
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
function placeOrder(cmd: PlaceOrder): Result<Order, OutOfStock | PaymentDeclined> { /* ... */ }
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
But first, try principle 7: if the error can be defined away, it should not be in the interface at all.
|
|
108
|
+
|
|
109
|
+
**Smell.** `catch` blocks that parse error messages; functions documented with "may throw X, Y, Z".
|
|
110
|
+
|
|
111
|
+
## 7. Define errors out of existence
|
|
112
|
+
|
|
113
|
+
**Rule.** Change the semantics so the error condition is not an error. Do this before reaching for `Result`.
|
|
114
|
+
|
|
115
|
+
**Why.** Every error is interface surface callers must handle. The best error handling is none.
|
|
116
|
+
|
|
117
|
+
- `delete(key)` on a missing key succeeds; the postcondition (key absent) holds.
|
|
118
|
+
- `substring(start, end)` clamps out-of-range indices instead of throwing.
|
|
119
|
+
- Return an empty collection, not `null`.
|
|
120
|
+
|
|
121
|
+
**Caution.** Don't swallow errors that matter. Define away only when the caller's intent is satisfied either way.
|
|
122
|
+
|
|
123
|
+
## 8. Information hiding
|
|
124
|
+
|
|
125
|
+
**Rule.** Each design decision (a file format, a protocol, an algorithm, a table layout) lives in exactly one module.
|
|
126
|
+
|
|
127
|
+
**Why.** Knowledge that appears in two modules is a leak: change one and you must change both (change amplification). Locality means a decision changes in one place.
|
|
128
|
+
|
|
129
|
+
**Smell.** Two modules that both know the wire format; callers that must call methods in a particular order (temporal decomposition: structure follows the order of operations rather than the knowledge involved).
|
|
130
|
+
|
|
131
|
+
## 9. Pull complexity downward
|
|
132
|
+
|
|
133
|
+
**Rule.** When complexity must live somewhere, put it in the module, not its callers. A simple interface matters more than a simple implementation.
|
|
134
|
+
|
|
135
|
+
**Why.** There are many callers and one module. Complexity pushed up is paid N times.
|
|
136
|
+
|
|
137
|
+
**Smell.** Config parameters that make callers decide what the module could decide; exceptions the caller can only rethrow.
|
|
138
|
+
|
|
139
|
+
## 10. Somewhat general-purpose modules are deeper
|
|
140
|
+
|
|
141
|
+
**Rule.** Design the interface for the underlying capability, not for today's single use. Keep it general enough to cover related uses, but implement only what's needed now. Keep general-purpose code apart from special-purpose code.
|
|
142
|
+
|
|
143
|
+
**Why.** A general interface is usually smaller and simpler than a stack of special cases, and it survives the next feature.
|
|
144
|
+
|
|
145
|
+
**Smell.** Methods named for one screen or one caller (`deleteSelectedTextFromEditor`); a general mechanism with feature-specific branches inside it.
|
|
146
|
+
|
|
147
|
+
## 11. Different layers, different abstractions
|
|
148
|
+
|
|
149
|
+
**Rule.** Each layer should offer an abstraction distinct from the ones above and below. A method that only forwards to another method with the same signature is a pass-through; so is a variable threaded unchanged through a chain of calls.
|
|
150
|
+
|
|
151
|
+
**Why.** A pass-through adds interface without depth. The deletion test flags it.
|
|
152
|
+
|
|
153
|
+
If a layer exists because the project's architecture demands it (Router → Service → Repository), make it earn its place: give it behaviour the layer below doesn't have, or fold the behaviour down. For a pass-through variable, put it in a context object or shared module.
|
|
154
|
+
|
|
155
|
+
## 12. Together or apart
|
|
156
|
+
|
|
157
|
+
**Rule.** Combine modules that share information, are always used together, or overlap conceptually. Separate general mechanism from specific use. Don't split just to make things short.
|
|
158
|
+
|
|
159
|
+
**Why.** Splitting adds interface and separates things that need each other. Combining adds locality. Judge by interface complexity and dependencies, not by line count.
|
|
160
|
+
|
|
161
|
+
**Smell.** Conjoined methods: you can't understand one without reading the other.
|
|
162
|
+
|
|
163
|
+
## 13. Make the common case simple
|
|
164
|
+
|
|
165
|
+
**Rule.** Give parameters sensible defaults and design the default path to need no configuration. Rare needs may cost more.
|
|
166
|
+
|
|
167
|
+
**Why.** Interface complexity is paid by every caller; the common caller shouldn't pay for the rare one.
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
// Common case: one argument
|
|
171
|
+
createClient(url);
|
|
172
|
+
// Rare case: opt in
|
|
173
|
+
createClient(url, { retries: 5, timeoutMs: 2_000 });
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## 14. Comments and names carry the design
|
|
177
|
+
|
|
178
|
+
**Rule.**
|
|
179
|
+
- Comments describe what isn't obvious from the code. Interface comments state the abstraction (what it does, invariants, error modes); implementation comments explain *why*.
|
|
180
|
+
- Write the interface comment first. If it is hard to write, the design is unclear: fix the design.
|
|
181
|
+
- Names are precise and used consistently for the same thing.
|
|
182
|
+
- Design for ease of reading, not ease of writing.
|
|
183
|
+
|
|
184
|
+
**Why.** A comment that repeats the code adds nothing; a comment that states the abstraction is the interface's documentation. Names are the cheapest interface.
|
|
185
|
+
|
|
186
|
+
**Link.** A name you can't pick, or a concept you can't describe simply, usually means the domain language isn't settled. Resolve it with `oracle-domain-modelling` and record it in `CONTEXT.md`. If the decision is contested, run it through `oracle-grill-me`.
|
|
187
|
+
|
|
188
|
+
## 15. Seam discipline
|
|
189
|
+
|
|
190
|
+
**Rule.**
|
|
191
|
+
- Accept dependencies; don't create them.
|
|
192
|
+
- Return results; don't produce side effects.
|
|
193
|
+
- One adapter means a hypothetical seam. Two means a real one. Don't introduce a port unless something actually varies across it.
|
|
194
|
+
|
|
195
|
+
**Why.** Seams cost interface. Pay only where behaviour varies (typically production and test).
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
// Testable
|
|
199
|
+
function processOrder(order: Order, paymentGateway: PaymentGateway) {}
|
|
200
|
+
|
|
201
|
+
// Hard to test
|
|
202
|
+
function processOrder(order: Order) {
|
|
203
|
+
const gateway = new StripeGateway();
|
|
204
|
+
}
|
|
205
|
+
```
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Red Flags
|
|
2
|
+
|
|
3
|
+
A checklist to run before finalising a design, and when reviewing one. Each flag points to the principle in [principles.md](principles.md) that fixes it. Terms follow [SKILL.md](../SKILL.md).
|
|
4
|
+
|
|
5
|
+
| Red flag | What it looks like | Fix |
|
|
6
|
+
|----------|--------------------|-----|
|
|
7
|
+
| **Shallow module** | The interface is nearly as complex as the implementation. Passes the deletion test the wrong way: deleting it removes nothing. | Deep modules (1). Merge, or move behaviour in. |
|
|
8
|
+
| **Information leakage** | The same design decision (format, protocol, table layout) appears in two modules. | Information hiding (8). |
|
|
9
|
+
| **Temporal decomposition** | Structure follows the order operations run in (read, process, write) instead of the knowledge involved. Shared knowledge ends up in several steps. | Information hiding (8); together or apart (12). |
|
|
10
|
+
| **Overexposure** | The interface forces callers to learn rarely used features to use the common ones. | Make the common case simple (13). |
|
|
11
|
+
| **Pass-through method** | A method that only forwards to another with a similar signature. | Different layers, different abstractions (11). |
|
|
12
|
+
| **Pass-through variable** | An argument threaded unchanged through a chain of calls. | Different layers, different abstractions (11): context object or shared module. |
|
|
13
|
+
| **Repetition** | The same code or check appears in many places. | Parse, don't validate (3); information hiding (8); pull complexity downward (9). |
|
|
14
|
+
| **Special-general mixture** | A general mechanism contains feature-specific branches. | Somewhat general-purpose modules (10). |
|
|
15
|
+
| **Conjoined methods** | You can't understand one method without reading another. | Together or apart (12). |
|
|
16
|
+
| **Comment repeats code** | The comment says what the next line plainly says. | Comments and names (14). |
|
|
17
|
+
| **Implementation contaminates interface** | The interface comment must describe how it works inside. | Information hiding (8); rewrite the interface comment first (14). |
|
|
18
|
+
| **Vague name** | `data`, `info`, `manager`, `handle`, `process`. | Comments and names (14); resolve with `oracle-domain-modelling`. |
|
|
19
|
+
| **Hard to pick a name** | You can't find a precise name for the thing. | The concept is unclear; settle it in `CONTEXT.md`. |
|
|
20
|
+
| **Hard to describe** | The interface comment is long or awkward. | The design is unclear. Redesign before documenting (14). |
|
|
21
|
+
| **Nonobvious code** | A reader can't tell what it does or why without effort. | Names and structure for reading (14). |
|
|
22
|
+
|
|
23
|
+
## Added for this toolkit
|
|
24
|
+
|
|
25
|
+
| Red flag | What it looks like | Fix |
|
|
26
|
+
|----------|--------------------|-----|
|
|
27
|
+
| **Stringly-typed core** | Core logic takes raw `string` or `any` and re-checks it. | Parse, don't validate (3). |
|
|
28
|
+
| **Flag-and-optional bag** | Several booleans and optionals where only some combinations are legal. | Make illegal states unrepresentable (4). |
|
|
29
|
+
| **I/O in the decision** | Business rules interleaved with queries or network calls. | Functional core, imperative shell (5). |
|
|
30
|
+
| **Throwing for expected failure** | `catch` blocks that parse messages; "may throw X, Y, Z". | Define errors out of existence (7), else errors as values (6). |
|
|
31
|
+
| **Testing past the interface** | Tests break on every refactor; many tests of internal helpers. | Test at the correct level (2). |
|
|
32
|
+
| **Single-adapter port** | An interface with one implementation and no test double. | Seam discipline (15). |
|
|
@@ -145,7 +145,7 @@ Fix the root cause, not the symptom.
|
|
|
145
145
|
|
|
146
146
|
1. **Create a failing test case.** The simplest possible reproduction. MUST exist before the fix.
|
|
147
147
|
|
|
148
|
-
A **correct seam** is one where the test exercises the real bug pattern as it occurs at the call site. If the only available seam is too shallow, note that the codebase architecture is preventing the bug from being locked down.
|
|
148
|
+
A **correct seam** is one where the test exercises the real bug pattern as it occurs at the call site. If the only available seam is too shallow, note that the codebase architecture is preventing the bug from being locked down. Use the seam and depth vocabulary from `oracle-codebase-design` when you raise it.
|
|
149
149
|
|
|
150
150
|
2. **Implement a single fix.** Address the root cause. One change at a time. No "while I'm here" improvements. No bundled refactoring.
|
|
151
151
|
3. **Verify the fix.** Does the test pass? Do other tests still pass? Does the original repro no longer reproduce?
|
|
@@ -153,7 +153,7 @@ Fix the root cause, not the symptom.
|
|
|
153
153
|
- STOP
|
|
154
154
|
- Count the failed fix attempts
|
|
155
155
|
- If < 3: return to Phase 1 with the new information
|
|
156
|
-
- If ≥ 3: **question the architecture**. Pattern problems, hidden coupling, and shared state that each fix reveals are signs of a wrong pattern. Discuss with the user before attempting Fix #4.
|
|
156
|
+
- If ≥ 3: **question the architecture**. Pattern problems, hidden coupling, and shared state that each fix reveals are signs of a wrong pattern (see the red flags in `oracle-codebase-design`). Discuss with the user before attempting Fix #4.
|
|
157
157
|
|
|
158
158
|
### Cleanup + post-mortem
|
|
159
159
|
|
|
@@ -20,4 +20,5 @@ understanding. Update domain documentation as terminology and decisions are reso
|
|
|
20
20
|
session is nested in spec-brainstorm, return changed assumptions to that workflow for any required
|
|
21
21
|
design update.
|
|
22
22
|
|
|
23
|
-
Run this session using the `oracle-domain-modelling` skill.
|
|
23
|
+
Run this session using the `oracle-domain-modelling` skill. When the decision is about a module's
|
|
24
|
+
interface, seams, or test level, also use the `oracle-codebase-design` skill.
|
|
@@ -55,7 +55,8 @@ it touches multiple files or takes time. That work belongs in an Inline Plan.
|
|
|
55
55
|
|
|
56
56
|
Break the system into units with one clear purpose each. Well-defined interfaces between
|
|
57
57
|
them. Each unit independently understandable and independently testable. If you can't
|
|
58
|
-
explain a unit's job in one sentence, it's doing too much.
|
|
58
|
+
explain a unit's job in one sentence, it's doing too much. Apply **oracle-codebase-design**:
|
|
59
|
+
deep modules, seams placed where behaviour varies, and the interface as the test surface.
|
|
59
60
|
|
|
60
61
|
### Working in existing codebases
|
|
61
62
|
|
|
@@ -203,6 +204,8 @@ Before settling on a design, present **2-3 approaches** with trade-offs.
|
|
|
203
204
|
|
|
204
205
|
The first approach must keep the existing architecture and make the smallest correct change.
|
|
205
206
|
Present broader approaches only when a current requirement makes their extra cost relevant.
|
|
207
|
+
When approaches differ mainly in interface shape, use the design-it-twice reference in
|
|
208
|
+
**oracle-codebase-design**.
|
|
206
209
|
|
|
207
210
|
For each approach, address:
|
|
208
211
|
|
|
@@ -250,9 +253,10 @@ with acceptance criteria and priorities. If rejected: revise. If the rejection
|
|
|
250
253
|
reveals a scope misunderstanding, loop back to Discovery (Step 2).
|
|
251
254
|
|
|
252
255
|
**Batch B: Architecture** — component design, domain modeling, and layer
|
|
253
|
-
boundaries. Use installed language-specific architecture or
|
|
254
|
-
your stack. Then present: component structure, domain model, where
|
|
255
|
-
logic lives, where IO lives
|
|
256
|
+
boundaries. Use **oracle-codebase-design** and any installed language-specific architecture or
|
|
257
|
+
API-design skills relevant to your stack. Then present: component structure, domain model, where
|
|
258
|
+
business logic lives, where IO lives, where untrusted input is parsed (the external seam), and
|
|
259
|
+
the seam each module is tested through. If rejected: revise. If the rejection undermines
|
|
256
260
|
the chosen approach, offer to return to approach exploration (4a). If it
|
|
257
261
|
reveals a fundamental gap, loop back to Research (Step 3). If the detail reveals
|
|
258
262
|
the work is far more complex than estimated, say so and offer to revisit the
|