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