@widnyana/design-thinking 0.0.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.
@@ -0,0 +1,156 @@
1
+ > **Source**: Design Thinking by [r17x](https://github.com/r17x) —
2
+ > https://gist.github.com/r17x/90eb2f7be93932b5693753aedb09c01a
3
+ > Bundled verbatim as the original Effect-TS instantiation of the method.
4
+ > The generalized, stack-agnostic version lives in [method.md](method.md);
5
+ > the notation spec lives in [protocol.md](protocol.md).
6
+
7
+ # Design Thinking
8
+
9
+ ```
10
+ X → Graph → Effect<A, E, R>
11
+ │ │ │ │ │
12
+ │ │ │ │ └─ what each node needs (§5)
13
+ │ │ │ └──── where the graph breaks (§4)
14
+ │ │ └─────── what flows through nodes (§2)
15
+ │ │
16
+ │ └─ nodes = functions, edges = data flow
17
+ │
18
+ └─ the problem: what you’re trying to build
19
+
20
+ §1 Shapes the nouns: records, IDs, variants, errors
21
+ §2 A happy path as call graph
22
+ §3 Cardinality one-shot (Effect) or many (Stream)
23
+ §4 E break points: retry, escape, die
24
+ §5 R dependencies: compile-time proof
25
+ §6 Boundary Schema: unknown → trusted at edges
26
+ §7 Behavior .pipe() wraps without changing the graph
27
+ §8 Scope acquire/release tied to lifecycle
28
+ §9 Test swap R, same graph shape
29
+ §10 Code gen body = A, .pipe() = E
30
+ ```
31
+
32
+ Read the problem. Draw the data flow as a call graph. Write code that IS the graph. If the code doesn't match the graph, something is wrong.
33
+
34
+ ## 1. Name the shapes
35
+
36
+ What are the things? Before drawing a graph, define the domain language.
37
+
38
+ - **Records** -- entities that flow through nodes. A User, a Product, an Order.
39
+ - **IDs** -- identity of things. Branded, constrained, never a bare string.
40
+ - **Variants** -- internal state transitions. A step is Continue or Finished. A status is Pending, Active, or Cancelled.
41
+ - **Errors** -- named failure modes. Not strings. Tagged, structured, carrying context.
42
+
43
+ These are the nouns. The graph is the verbs. You cannot draw the graph until you know what flows through it.
44
+
45
+ ## 2. Think A first
46
+
47
+ Map the happy path as a call graph before writing any code.
48
+
49
+ ```
50
+ F1(A) -> F2(A) -> F3(A)
51
+ ```
52
+
53
+ What is the success data flow? What goes in, what comes out, what transforms happen in between? This graph IS your program structure. Draw it first. The code follows.
54
+
55
+ ## 3. One or many?
56
+
57
+ Is each node one-shot or a flow?
58
+
59
+ - **One value** -- the node runs, produces A, done. This is Effect.
60
+ - **Many values over time** -- the node emits A repeatedly. Events, subscriptions, paginated pulls. This is Stream.
61
+ - **Time-bounded** -- the result is valid for a window. Cache it. Deduplicate concurrent lookups.
62
+
63
+ Same three channels (A, E, R) in all cases. Different cardinality. Mark it on the graph so the code matches.
64
+
65
+ ## 4. Think E second
66
+
67
+ Mark where the graph can break. Each break point is one of three things:
68
+
69
+ - **Retry** -- transient failure, try again. Network timeout, rate limit, connection reset.
70
+ - **Escape Hatch** -- recoverable, return an alternative. Fallback value, cached result, default.
71
+ - **Die** -- defect, invariant violation, programmer bug. NOT a domain error. Something is fundamentally wrong.
72
+
73
+ Errors are VALUES in the E channel until you truly cannot handle them. They flow through the graph like data. You decide at each node: retry, escape, or let it propagate. Only `die` when the program's assumptions are violated.
74
+
75
+ ## 5. Think R third
76
+
77
+ Mark what each node in the graph needs to exist. "We cannot do X if we don't have Y."
78
+
79
+ R is compile-time proof that dependencies are satisfied. Every node declares what it requires -- a database connection, a config value, an HTTP client. R shrinks as you provide layers. When R is empty (`never`), the program can run. If R is not empty, the compiler tells you exactly what's missing.
80
+
81
+ ## 6. Trust at boundary
82
+
83
+ Where does untrusted data enter the graph? HTTP requests, file reads, environment variables, user input, third-party API responses.
84
+
85
+ Schema converts `unknown -> trusted` at the edges. One definition = type + validator + transformer. Define it once, use it everywhere.
86
+
87
+ Trust nothing at the boundary. Trust everything inside. The boundary is the only place you parse. After that, the types guarantee shape.
88
+
89
+ ## 7. Layer behavior
90
+
91
+ What cross-cutting concerns wrap nodes? Retry policies, timeouts, logging, tracing, caching.
92
+
93
+ These wrap via `.pipe()` WITHOUT changing the core graph. The happy path stays clean -- you can read it without wading through retry logic and timeout configuration. Behavior is composed orthogonally. The graph says WHAT happens. The layers say HOW it behaves under pressure.
94
+
95
+ ## 8. Scope resources
96
+
97
+ What nodes acquire resources? Database connections, file handles, WebSocket connections, child processes.
98
+
99
+ Acquire/release is a type guarantee. If a node opens a connection, scope ensures it closes -- even on error, even on interrupt. Cleanup is structural, not a TODO comment you hope someone remembers.
100
+
101
+ ## 9. Swap R to prove it
102
+
103
+ The call graph doesn't change between production and tests. Only R changes.
104
+
105
+ Same graph shape. Same A flowing through. Same E possible. Different layer behind R. If the graph can't run with a test R, the design has hidden dependencies. If you have to mock the world to test one node, the node is doing too much.
106
+
107
+ This is the payoff of separating A, E, and R -- you prove the graph correct by swapping what's behind R.
108
+
109
+ ## 10. gen is A, pipe is E
110
+
111
+ The separation of A (step 2) and E (step 4) maps directly to Effect code structure:
112
+
113
+ - **`Effect.gen` body** -- the happy path. Every `yield*` is an A flowing through the graph. No error handling inside.
114
+ - **`.pipe()` after gen** -- the complete E enumeration. Before writing the pipe, read the actual E type of every yielded effect. The pipe catches, retries, or transforms every E that the gen body can produce.
115
+
116
+ This is not a stylistic preference. If error handling lives inside the gen body, the A path and E path are tangled -- you cannot read the happy path without wading through catches and retries. The gen body IS the call graph from step 2. The pipe IS the error annotation from step 4.
117
+
118
+ ### E scoping at graph layers
119
+
120
+ Each layer in the call graph scopes its own E before passing to the next layer:
121
+
122
+ ```
123
+ Services → Auth → Handlers
124
+ E=SqlError E=DatabaseError E=AuthError E=RPC errors
125
+ ↓ scope ↓ scope ↓ scope
126
+ DatabaseError AuthError RPC errors
127
+ ```
128
+
129
+ A service catches SqlError and produces DatabaseError. Auth catches DatabaseError and produces AuthError. The handler catches AuthError and produces the RPC-declared error. Each layer's pipe is a complete enumeration of what IT received -- not what originated three layers down. Consumers never see implementation errors from deeper layers.
130
+
131
+ ### Divergent strategies
132
+
133
+ When two effects in the same gen body need different E handling -- one should fail hard, the other should fall back gracefully -- that is a **divergent strategy**. Handle each effect's E inline within the gen body, because the outer pipe cannot distinguish which yield produced which error.
134
+
135
+ This is the ONE exception to "no error handling in gen." It exists because the graph has a fork: two branches with different failure semantics sharing a node. Mark it clearly -- it should be rare.
136
+
137
+ ---
138
+
139
+ ## The Pipeline
140
+
141
+ ```
142
+ PROBLEM
143
+ -> "What are the shapes?" -> define the domain language
144
+ -> "What is the happy path?" -> draw the call graph (A)
145
+ -> "Is each node one-shot or a flow?" -> mark cardinality
146
+ -> "Where can it break?" -> annotate errors on the graph (E)
147
+ -> "What does each node need?" -> annotate requirements on the graph (R)
148
+ -> "Where does untrusted data enter?" -> Schema at graph boundaries
149
+ -> "What wraps nodes without changing them?" -> pipe behavior orthogonally
150
+ -> "What resources need cleanup?" -> scope lifecycle
151
+ -> "Can I swap R and the graph still works?" -> verify with test layers
152
+ -> "Does my code separate A from E structurally?" -> gen body = A, pipe = E
153
+ -> CODE -> the code IS the graph
154
+ ```
155
+
156
+ **If the code doesn't match the call graph, the implementation is wrong.**
@@ -0,0 +1,229 @@
1
+ # Design Thinking — The Method
2
+
3
+ A stack-agnostic design methodology. Read the problem. Draw the data flow as a
4
+ call graph (the **Design Graph**). Write code that IS the graph. If the code
5
+ doesn't match the graph, something is wrong.
6
+
7
+ The method is language-neutral. Where it says "Effect", "gen", or "pipe" in the
8
+ Effect-TS appendix (`effect-ts.md`), substitute your stack's equivalent: Result
9
+ types, exceptions at layer joins, middleware, RAII, constructor injection —
10
+ whatever your language provides. The discipline never changes, only the vocabulary.
11
+
12
+ Render every result in [Graph Protocol](protocol.md) format.
13
+
14
+ ```
15
+ X → DesignGraph<A, E, R>
16
+ │ │ │ │ │
17
+ │ │ │ │ └─ what each node needs (§5)
18
+ │ │ │ └──── where the graph breaks (§4)
19
+ │ │ └─────── what flows through nodes (§2)
20
+ │ │
21
+ │ └─ nodes = functions, edges = data flow
22
+ │
23
+ └─ the problem: what you're trying to build
24
+
25
+ §1 Shapes the nouns: records, IDs, variants, errors
26
+ §2 A happy path as call graph
27
+ §3 Cardinality one-shot (1), many (N), or time-bounded (T)
28
+ §4 E break points: retry, escape, die
29
+ §5 R dependencies: compile-time or wiring-time proof
30
+ §6 Boundary parse: unknown → trusted at edges
31
+ §7 Behavior orthogonal layers wrap without changing the graph
32
+ §8 Scope acquire/release tied to lifecycle
33
+ §9 Test swap R, same graph shape
34
+ §10 Structure happy path = A, failure handling = E, never tangled
35
+ ```
36
+
37
+ ## 1. Name the shapes
38
+
39
+ What are the things? Before drawing a graph, define the domain language.
40
+
41
+ - **Records** — entities that flow through nodes. A User, a Product, an Order.
42
+ - **IDs** — identity of things. Branded, constrained, never a bare string.
43
+ - **Variants** — internal state transitions. A step is Continue or Finished.
44
+ A status is Pending, Active, or Cancelled.
45
+ - **Errors** — named failure modes. Not strings. Structured, carrying context.
46
+
47
+ These are the nouns. The graph is the verbs. You cannot draw the graph until
48
+ you know what flows through it.
49
+
50
+ → *Protocol section: SHAPES.*
51
+
52
+ ## 2. Think A first
53
+
54
+ Map the happy path as a call graph before writing any code.
55
+
56
+ ```
57
+ F1(A) -> F2(A) -> F3(A)
58
+ ```
59
+
60
+ What is the success data flow? What goes in, what comes out, what transforms
61
+ happen in between? This graph IS your program structure. Draw it first. The
62
+ code follows.
63
+
64
+ → *Protocol section: GRAPH (edges) + header tree (A).*
65
+
66
+ ## 3. One or many?
67
+
68
+ Is each node one-shot or a flow?
69
+
70
+ - **One value** — the node runs, produces A, done. `(1)`
71
+ - **Many values over time** — the node emits A repeatedly: events,
72
+ subscriptions, paginated pulls, batches. `(N)`
73
+ - **Time-bounded** — the result is valid for a window. Cache it. Deduplicate
74
+ concurrent lookups. `(T)`
75
+
76
+ Same three channels (A, E, R) in all cases. Different cardinality. Mark it on
77
+ the graph so the code matches.
78
+
79
+ → *Protocol section: CARDINALITY.*
80
+
81
+ ## 4. Think E second
82
+
83
+ Mark where the graph can break. Each break point is one of three things:
84
+
85
+ - **Retry** `⟳` — transient failure, try again. Network timeout, rate limit,
86
+ connection reset.
87
+ - **Escape Hatch** `↯` — recoverable, return an alternative. Fallback value,
88
+ cached result, default, skip-and-continue.
89
+ - **Die** `☠` — defect, invariant violation, programmer bug. NOT a domain
90
+ error. Something is fundamentally wrong.
91
+
92
+ Failures are first-class values in the E channel until you truly cannot handle
93
+ them. They flow through the graph like data — whether your language expresses
94
+ that as Result types, checked exceptions, error returns, or a strict rule that
95
+ exceptions only cross at layer joins. You decide at each node: retry, escape,
96
+ or propagate. Only `die` when the program's assumptions are violated.
97
+
98
+ → *Protocol section: GRAPH (E annotations) + VERDICT.*
99
+
100
+ ## 5. Think R third
101
+
102
+ Mark what each node in the graph needs to exist. "We cannot do X if we don't
103
+ have Y."
104
+
105
+ R is proof that dependencies are satisfied — at compile time (injected via
106
+ constructors, generics, typeclasses, traits) or at wiring time (composition
107
+ root, DI container, context object). Every node declares what it requires: a
108
+ database connection, a config value, an HTTP client. R shrinks as you provide
109
+ layers. When R is empty, the program can run. If R is not empty, the compiler
110
+ or the wiring tells you exactly what's missing.
111
+
112
+ → *Protocol section: GRAPH (R annotations) + TEST LAYERS.*
113
+
114
+ ## 6. Trust at boundary
115
+
116
+ Where does untrusted data enter the graph? HTTP requests, file reads,
117
+ environment variables, user input, third-party API responses.
118
+
119
+ Convert `unknown → trusted` at the edges — a schema, a parsing constructor, a
120
+ validating factory, a DTO boundary. One definition = type + validator +
121
+ transformer. Define it once, use it everywhere.
122
+
123
+ Trust nothing at the boundary. Trust everything inside. The boundary is the
124
+ only place you parse. After that, the types guarantee shape.
125
+
126
+ → *Protocol section: BOUNDARIES.*
127
+
128
+ ## 7. Layer behavior
129
+
130
+ What cross-cutting concerns wrap nodes? Retry policies, timeouts, logging,
131
+ tracing, caching.
132
+
133
+ These wrap WITHOUT changing the core graph — middleware, decorators,
134
+ higher-order functions, aspects. The happy path stays clean: you can read it
135
+ without wading through retry logic and timeout configuration. Behavior is
136
+ composed orthogonally. The graph says WHAT happens. The layers say HOW it
137
+ behaves under pressure.
138
+
139
+ → *Protocol section: BEHAVIOR.*
140
+
141
+ ## 8. Scope resources
142
+
143
+ What nodes acquire resources? Database connections, file handles, WebSocket
144
+ connections, child processes, locks.
145
+
146
+ Acquire/release must be structurally bound: RAII, `defer`, context managers,
147
+ `try/finally` at the owning node — even on error, even on interrupt. Cleanup
148
+ is structural, not a TODO comment you hope someone remembers.
149
+
150
+ → *Protocol section: SCOPE.*
151
+
152
+ ## 9. Swap R to prove it
153
+
154
+ The call graph doesn't change between production and tests. Only R changes.
155
+
156
+ Same graph shape. Same A flowing through. Same E possible. Different layer
157
+ behind R. If the graph can't run with a test R, the design has hidden
158
+ dependencies. If you have to mock the world to test one node, the node is
159
+ doing too much.
160
+
161
+ This is the payoff of separating A, E, and R — you prove the graph correct by
162
+ swapping what's behind R.
163
+
164
+ → *Protocol section: TEST LAYERS.*
165
+
166
+ ## 10. Separate A from E structurally
167
+
168
+ The separation of A (§2) and E (§4) maps directly onto code structure, in any
169
+ language:
170
+
171
+ - **The happy path body** — one readable place. Every call is an A flowing
172
+ through the graph. No error handling inside. (In Effect-TS: the `Effect.gen`
173
+ body. Elsewhere: the main function, the do-notation block, the straight-line
174
+ pipeline of calls.)
175
+ - **The failure join points** — the complete E enumeration. Before writing
176
+ them, read the actual failure modes of every node on the happy path. The
177
+ join points catch, retry, or translate every E the happy path can produce.
178
+ (In Effect-TS: `.pipe()` after gen. Elsewhere: the middleware stack, the
179
+ Result chain, the try/catch at the layer boundary.)
180
+
181
+ This is not a stylistic preference. If error handling lives inside the happy
182
+ path body, the A path and E path are tangled — you cannot read the happy path
183
+ without wading through catches and retries. The happy path body IS the call
184
+ graph from §2. The join points ARE the error annotations from §4.
185
+
186
+ ### E scoping at graph layers
187
+
188
+ Each layer scopes its own E before passing to the next layer. A service catches
189
+ storage errors and produces a domain error. Auth catches domain errors and
190
+ produces an access error. The handler catches access errors and produces the
191
+ declared interface error. Each layer's handling is a complete enumeration of
192
+ what IT received — not what originated three layers down. Consumers never see
193
+ implementation errors from deeper layers. *(See protocol.md for the diagram.)*
194
+
195
+ ### Divergent strategies
196
+
197
+ When two nodes in the same happy path need different failure handling — one
198
+ should fail hard, the other should fall back gracefully — that is a
199
+ **divergent strategy**. Handle each node's E inline within the happy path at
200
+ the fork, because the outer join point cannot distinguish which node produced
201
+ which error. This is the ONE exception to "no error handling in the happy
202
+ path." It exists because the graph has a fork: two branches with different
203
+ failure semantics sharing a node. Mark it clearly — it should be rare.
204
+
205
+ ---
206
+
207
+ ## The Pipeline
208
+
209
+ ```
210
+ PROBLEM
211
+ -> "What are the shapes?" -> define the domain language
212
+ -> "What is the happy path?" -> draw the call graph (A)
213
+ -> "Is each node one-shot or a flow?" -> mark cardinality
214
+ -> "Where can it break?" -> annotate errors on the graph (E)
215
+ -> "What does each node need?" -> annotate requirements on the graph (R)
216
+ -> "Where does untrusted data enter?" -> parse at graph boundaries
217
+ -> "What wraps nodes without changing them?" -> layer behavior orthogonally
218
+ -> "What resources need cleanup?" -> scope lifecycle
219
+ -> "Can I swap R and the graph still works?" -> verify with test layers
220
+ -> "Does my code separate A from E structurally?" -> happy path = A, joins = E
221
+ -> CODE -> the code IS the graph
222
+ ```
223
+
224
+ **If the code doesn't match the call graph, the implementation is wrong.**
225
+
226
+ ---
227
+
228
+ *For the original Effect-TS instantiation of this method, see
229
+ [effect-ts.md](effect-ts.md).*
@@ -0,0 +1,228 @@
1
+ # Graph Protocol
2
+
3
+ The notation spec for **Design Graphs**. A Design Graph is the annotated call graph
4
+ of a concrete problem: nodes are functions, edges are data flow, and every node
5
+ carries annotations for data (A), cardinality, failure (E), and requirements (R).
6
+
7
+ The Protocol is stack-agnostic. A graph written for Go, Rust, Python, TypeScript,
8
+ SQL, or a spreadsheet renders identically. Only the vocabulary inside the
9
+ annotations changes (e.g. what "retry" means in your language).
10
+
11
+ Every tool in this package (`/cg`, `/cg-plan`, `/cg-review`, `/cg-map`, and the
12
+ `/dt` active mode) MUST emit output in the format below.
13
+
14
+ ---
15
+
16
+ ## Canonical output skeleton
17
+
18
+ Sections appear in this fixed order. Omit a section only when it is genuinely
19
+ empty for the problem at hand (say so: `SCOPE: none`), never to skip thinking.
20
+
21
+ ```
22
+ PROBLEM: <one-line statement of what you're building or examining>
23
+
24
+ X → DesignGraph<A, E, R>
25
+ │ │ │ │ │
26
+ │ │ │ │ └─ R: what each node needs (§5)
27
+ │ │ │ └──── E: where the graph breaks (§4)
28
+ │ │ └─────── A: what flows through nodes (§2)
29
+ │ │
30
+ │ └─ nodes = functions, edges = data flow
31
+ │
32
+ └─ the problem
33
+
34
+ SHAPES: <the nouns: records, IDs, variants, errors>
35
+
36
+ GRAPH:
37
+ <ascii call graph, annotated inline>
38
+
39
+ CARDINALITY: <node (1|N|T) list>
40
+ BOUNDARIES: <every edge crossing untrusted → trusted>
41
+ BEHAVIOR: <cross-cutting layers that wrap nodes>
42
+ SCOPE: <resources: acquire@… → release@…>
43
+ TEST LAYERS: <the R that tests provide — same GRAPH, zero code change>
44
+ VERDICT: <code matches graph? what is missing/tangled?>
45
+ ```
46
+
47
+ ---
48
+
49
+ ## The header tree
50
+
51
+ The header names the problem and anchors the three channels. `X` is your problem;
52
+ everything after it is the graph you build.
53
+
54
+ ```
55
+ X → DesignGraph<A, E, R>
56
+ │ │ │ │ │
57
+ │ │ │ │ └─ R: what each node needs (§5)
58
+ │ │ │ └──── E: where the graph breaks (§4)
59
+ │ │ └─────── A: what flows through nodes (§2)
60
+ │ │
61
+ │ └─ nodes = functions, edges = data flow
62
+ │
63
+ └─ the problem: what you're trying to build
64
+ ```
65
+
66
+ ---
67
+
68
+ ## Section rules
69
+
70
+ ### PROBLEM
71
+
72
+ One line. The thing you're building or the question you're answering. If you
73
+ cannot write it in one line, the problem is not understood — stop and refine.
74
+
75
+ ### SHAPES
76
+
77
+ The domain nouns. Name them before drawing anything:
78
+
79
+ - **Records** — entities that flow through nodes (User, Order, Config).
80
+ - **IDs** — identity, never a bare primitive (UserId, Email).
81
+ - **Variants** — internal state transitions (pending|active|cancelled).
82
+ - **Errors** — named failure modes, structured, carrying context (not strings).
83
+
84
+ Format: comma-separated list. These are the nouns; the graph is the verbs.
85
+
86
+ ### GRAPH
87
+
88
+ ASCII call graph. Nodes are functions/steps; edges are data flow. Annotate
89
+ **inline**, using the branch conventions from the header tree:
90
+
91
+ ```
92
+ parse → Handler → Service → Repo
93
+ │ │ │
94
+ │ │ └─ R: DbConn · (N)
95
+ │ └─ E: timeout ⟳retry×3 → ↯escape(cached)
96
+ └─ 🔒 HTTP body → trusted
97
+ ```
98
+
99
+ Inline annotation grammar:
100
+
101
+ | Annotation | Meaning | Example |
102
+ |---|---|---|
103
+ | `R: a, b` | what the node requires (§5) | `R: DbConn, Config` |
104
+ | `E: <err> ⟳retry[×n] → ↯escape(alt)` | failure handling (§4) | `E: NetErr ⟳retry×3 → ↯escape(fallback)` |
105
+ | `E: <err> ☠die` | defect, not domain error (§4) | `E: NullConfig ☠die` |
106
+ | `🔒 x → y` | untrusted → trusted boundary (§6) | `🔒 JSON → Settings` |
107
+ | `(1) (N) (T)` | cardinality (§3) | `(N)` = many over time |
108
+ | `⛈ name` | behavior layer wrapping the node (§7) | `⛈logging wraps Service` |
109
+ | `@scope` | resource bound to a scope (§8) | `DbConn @main` |
110
+
111
+ ### CARDINALITY
112
+
113
+ One line listing each node's arity:
114
+
115
+ - `(1)` — one-shot: runs once, produces one value.
116
+ - `(N)` — many: emits values over time (stream, iterator, subscription, pages).
117
+ - `(T)` — time-bounded: result valid for a window; cache it, dedupe concurrent lookups.
118
+
119
+ Same three channels (A, E, R) in all cases. Different cardinality. The code must
120
+ match: don't implement a `(N)` node as a one-shot.
121
+
122
+ ### BOUNDARIES
123
+
124
+ Every place untrusted data enters the graph: HTTP requests, file reads, env
125
+ vars, user input, third-party responses. The rule: **parse, don't validate** —
126
+ convert `unknown → trusted` once, at the edge, then trust the types everywhere
127
+ inside. One definition per shape = type + validator + transformer, reused
128
+ everywhere.
129
+
130
+ ### BEHAVIOR
131
+
132
+ Cross-cutting concerns that wrap nodes WITHOUT changing the core graph:
133
+ retry policies, timeouts, logging, tracing, caching. Composed orthogonally
134
+ (middleware, decorators, higher-order wrappers, aspects). The graph says WHAT
135
+ happens; the layers say HOW it behaves under pressure. If removing logging
136
+ forces you to edit business logic, behavior is not orthogonal.
137
+
138
+ ### SCOPE
139
+
140
+ Resources that must be released: connections, file handles, sockets, child
141
+ processes. Acquire/release must be structurally bound (RAII, `defer`, context
142
+ managers, `try/finally`) — never convention or a TODO. Notation:
143
+
144
+ ```
145
+ SCOPE: DbConn acquire@main → release@main · WS acquire@Handler → release@Handler
146
+ ```
147
+
148
+ ### TEST LAYERS
149
+
150
+ The R that tests provide. The call graph does not change between production and
151
+ tests — only R changes. Same graph, same A, same E, different layers behind R.
152
+ If the graph can't run with a test R, the design has hidden dependencies. If
153
+ you must mock the world to test one node, the node does too much.
154
+
155
+ ### VERDICT
156
+
157
+ The structural check. Answer explicitly:
158
+
159
+ - Is the happy path (A) readable on its own, free of failure handling?
160
+ - Is every E the graph can produce handled at a defined join point?
161
+ - Do per-layer E scopes translate errors (inner errors never leak through)?
162
+ - Does the code match the graph? **If the code doesn't match the call graph,
163
+ the implementation is wrong.**
164
+
165
+ ---
166
+
167
+ ## E scoping at graph layers
168
+
169
+ Each layer scopes its own E before passing to the next:
170
+
171
+ ```
172
+ Services → Auth → Handlers
173
+ E=SqlErr E=DbErr E=AuthErr E=RPC errs
174
+ ↓ scope ↓ scope ↓ scope
175
+ DbErr AuthErr RPC errs
176
+ ```
177
+
178
+ A service catches `SqlErr` and produces `DbErr`. Auth catches `DbErr` and
179
+ produces `AuthErr`. The handler catches `AuthErr` and produces the declared
180
+ error. Each layer's handling enumerates what IT receives — never what originated
181
+ three layers down. Consumers never see implementation errors from deeper layers.
182
+
183
+ ## Divergent strategies (the one exception)
184
+
185
+ When two nodes in the same happy path need different failure semantics — one
186
+ must fail hard, another must fall back gracefully — handle that node's E
187
+ inline at the fork, because the outer layer cannot distinguish which node
188
+ produced which error. Mark it clearly; it should be rare.
189
+
190
+ ---
191
+
192
+ ## Worked example (non-TS: Python CLI)
193
+
194
+ ```
195
+ PROBLEM: sync a list of URLs to local files, resumable, with a status line
196
+
197
+ X → DesignGraph<A, E, R>
198
+ │ │ │ │ │
199
+ │ │ │ │ └─ R: Http, Clock, Logger (§5)
200
+ │ │ │ └──── E: NetErr, DiskErr (§4)
201
+ │ │ └─────── A: (url, bytes), Status (§2)
202
+ │ │
203
+ │ └─ nodes = functions, edges = data flow
204
+ │
205
+ └─ the problem
206
+
207
+ SHAPES: Url, Target(path), Downloaded(url, bytes), Status(pending|done|failed), NetErr, DiskErr
208
+
209
+ GRAPH:
210
+ load_state → plan → fetch … download → write → report
211
+ │ │ │ │
212
+ │ │ ├─ E: NetErr ⟳retry×3(-backoff) → ↯escape(skip, mark failed)
213
+ │ │ └─ 🔒 raw bytes → Downloaded (size, content-type checked)
214
+ │ └─ R: StateFile · (1)
215
+ └─ 🔒 state file → trusted (schema-validated; corrupt → ↯escape(empty, warn))
216
+
217
+ CARDINALITY: load_state (1) · plan (1) · fetch (N) · download (1) · write (1) · report (T)
218
+ BOUNDARIES: load_state 🔒 file → State · fetch 🔒 wire → Downloaded
219
+ BEHAVIOR: ⛈retry wraps fetch · ⛈rate-limit wraps fetch · ⛈progress wraps write
220
+ SCOPE: Http acquire@main → release@main · StateFile @main (context manager)
221
+ TEST LAYERS: R = {Http: FakeHttp, Clock: FrozenClock, Logger: Silent} — same GRAPH, no stubs in nodes
222
+ VERDICT: happy path reads as the pipeline above; all NetErr handling lives at the
223
+ write/report join, not inside download; state corruption escapes at the edge.
224
+ Matches graph → implementation valid.
225
+ ```
226
+
227
+ Note the stack vocabulary: `context manager` for scope, exceptions caught at
228
+ layer joins for E, plain functions for nodes. Protocol unchanged.