@wairon/cli 5.1.1-dev.102 → 5.1.1-dev.104

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,953 @@
1
+ # wairon — Architecture Standard
2
+
3
+ > The canonical, language-neutral definition of wairon's architecture model: the
4
+ > building blocks, the patterns, the entity/behavior rules, the spec-tree layout,
5
+ > and the rules a conformant design must obey. The schema, the validator, the MCP
6
+ > tools, and the SDD skills all reconcile to this document.
7
+ >
8
+ > This standard describes *how to structure any system designed with wairon*. It
9
+ > is language-agnostic — the structural rules apply to any OOP or structured
10
+ > language. Language-specific realization (concurrency, memory) lives in
11
+ > [language-bindings.md](language-bindings.md).
12
+
13
+ ---
14
+
15
+ ## 1. Two orthogonal axes: containment and refinement
16
+
17
+ Everything is positioned on **two independent axes**. Conflating them is the most
18
+ common source of confusion, so they come first.
19
+
20
+ - **Containment (the tree → folders):** what contains what —
21
+ `System ⊃ Subsystem ⊃ Component ⊃ owned blocks`.
22
+ - **Refinement (level of detail → files):** for *one* component, increasing
23
+ detail — `structure → interface → implementation + narrative`.
24
+
25
+ The labels L0–L5 walk down containment first, then refinement:
26
+
27
+ | Label | Axis | Artifact |
28
+ |---|---|---|
29
+ | **L0 System** | containment | `.index.yaml` (specs root) |
30
+ | **L1 Subsystem** | containment | `<subsystem>/.index.yaml` |
31
+ | **L2 Component** | containment leaf **and** refinement root (structure) | `<component>/.index.yaml` |
32
+ | **L3 Interface** | refinement | `<component>/.interface.yaml` |
33
+ | **L4 Implementation** | refinement | `<component>/.implementation.yaml` |
34
+ | **L5 Narrative** | refinement (a *sub-field* of L4, not a separate file) | steps inside `.implementation.yaml` |
35
+
36
+ ---
37
+
38
+ ## 2. The spec tree on disk
39
+
40
+ ```
41
+ .wai/specs/
42
+ .index.yaml ← L0 system: vision, boundaries, global requirements
43
+ types/ ← shared value objects (Money, Address, Email …)
44
+ money.yaml
45
+ <subsystem>/ ← L1 a bounded context / service
46
+ .index.yaml ← L1 public interface(s) the service exposes
47
+ types/ ← entities/aggregates owned by THIS subsystem (e.g. order.yaml)
48
+ <component>/ ← L2 a standalone block OR a pattern facade
49
+ .index.yaml ← L2 structure: role/stereotype, responsibility, owns, dependsOn
50
+ .interface.yaml ← L3 contract: method signatures (referencing types by id)
51
+ .implementation.yaml ← L4+L5 sourcePath, concurrency variant, per-method narrative
52
+ <owned-block>/ ← (only if this component is a pattern) a private owned block
53
+ .index.yaml
54
+ .interface.yaml
55
+ .implementation.yaml
56
+ ```
57
+
58
+ **File contents:**
59
+ - `.index.yaml` (L2) — identity, **role/stereotype**, **responsibility description**, `owns` (member blocks; patterns only), `dependsOn` (collaborators). No methods, no narratives.
60
+ - `.interface.yaml` (L3) — **method signatures** (name, params/returns referencing types, optional HTTP/gRPC/event binding). Interfaces are **method contracts only** — fields are implementation, never part of the contract.
61
+ - `.implementation.yaml` (L4+L5) — `sourcePath`, the **concurrency variant**, and the **per-method narrative** (L5 steps).
62
+
63
+ Different agents own different refinement files: the **architect** owns
64
+ `.index.yaml` + `.interface.yaml`; the **implementer** owns `.implementation.yaml`. (Legacy undotted names — `system.yaml`, `subsystem.yaml`, `component.yaml`, `interface.yaml`, `implementation.yaml` — still load; `wairon doctor --fix` migrates them.)
65
+
66
+ ---
67
+
68
+ ## 3. Building blocks (the primitives)
69
+
70
+ These ten are the **only** atomic component types.
71
+
72
+ | Block | Role | Owns | Default interface |
73
+ |---|---|---|---|
74
+ | **Portal** | The service's **inbound** front door — receives external commands and dispatches them | the inbound transport binding + dependencies | `dispatch(command) → result` |
75
+ | **Orchestrator** | **Logic as a flowchart** — ordered steps, conditions, branches, loops and parallel dispatch — calling its collaborators, other Orchestrators among them, nested down to leaf logic. One or more cohesive methods sharing those collaborators, realized as a class or a module of functions. Its `dependencyClass` (`pure` or `read`) bounds what it may depend on; unset, it is a **workflow** | dependencies only | domain-specific methods |
76
+ | **Supervisor** | Owns the **set** of live Actors and their lifecycle; may supervise other Supervisors | runtime state (the set of Actors) | `start/stop`, find-or-start by id |
77
+ | **Actor** | Owns **one** live thing — a session, a connection, a timer, or an entity instance — and handles its events one at a time. Its methods are full flowcharts, not shims | that thing's runtime state | `start/stop` + its messages |
78
+ | **Store** | Authoritative state boundary for **one aggregate** | the aggregate's entities | `get`, `list`, `write`, `remove` |
79
+ | **Index** | Read path; optimized derived lookup maps over a Store | derived lookup state | `get_by_<key>`, `list_by_<key>` |
80
+ | **Query** | A **Repository member** for computed reads over its Store — results the backend computes per call (an aggregate, a ranking, a report) | dependencies only | one method per computed read |
81
+ | **Registry** | Write path (CUD) for one aggregate | dependencies only | `create`, `update`, `delete` |
82
+ | **Adapter** | A **client to an external system/protocol** (DB, FS, HTTP, gRPC, WS, message bus). The **only** block doing raw external I/O | a connection/resource handle | protocol-shaped (`query`/`execute`, `send`/`receive`, `publish`…) |
83
+ | **Observer** | Subscribes to events and forwards them to one workflow | a subscription | `on_event(event)` |
84
+
85
+ ### State vs dependencies vs workflow
86
+
87
+ Every component holds **dependencies** (its injected collaborators). What differs
88
+ is whether it also owns **state** or **workflow**:
89
+
90
+ | Owns domain/runtime state | Owns workflow | Owns only dependencies (stateless paths and boundaries) |
91
+ |---|---|---|
92
+ | Store (authoritative), Index (derived); Supervisor, Actor (runtime) | Orchestrator | Registry, Query, Portal, Observer, Adapter* |
93
+
94
+ \*The Adapter holds a *resource handle* (a connection), not domain state.
95
+ "Stateless" means **no domain state** — not "no fields."
96
+
97
+ An **Orchestrator holds only its collaborators**: whatever one call computes lives
98
+ on that call's stack. Runtime state — a live session, a timer, the set of running
99
+ instances — belongs to an **Actor** or a **Supervisor**, whose own methods run
100
+ logic over it; domain state belongs to a **Store**. State an Orchestrator keeps
101
+ for itself between calls is hidden state, invisible to every rule in this
102
+ standard.
103
+
104
+ ### Dependency rules
105
+
106
+ - **Pure logic** — an Orchestrator with `dependencyClass: pure` — may be used by
107
+ **every** block: a Store, Registry, Index or Query validating or deriving
108
+ through it, an Adapter translating with it. It depends only on other pure
109
+ Orchestrators, so it never drags data, I/O or held state into its caller.
110
+ - **Portal** is the inbound boundary; no *local* component may depend on it. It
111
+ dispatches to Orchestrators and may consult Repository and Index **read** faces
112
+ for passthrough reads; a write reached from a Portal's narrative or dispatch
113
+ table — or a lifecycle change — routes through an Orchestrator
114
+ (`PORTAL_WRITE_SHORTCUT`). It never depends
115
+ on a Store, Registry, Adapter or Query. A Portal may **message a Supervisor by
116
+ id** to reach a live Actor (below).
117
+ The **one** exception to "no component depends on a Portal" is a
118
+ **cross-subsystem client Adapter** (see below): from its side the remote
119
+ subsystem's Portal is an external front door, so it depends on that Portal —
120
+ never on the remote subsystem's internals.
121
+ - **Observer** may depend on exactly **one** Orchestrator *or* one Supervisor
122
+ entry; it may use a message-bus **Adapter** to subscribe. No component depends on it.
123
+ - **Orchestrator** — its `dependencyClass` bounds its dependencies. The class is
124
+ checked on every hop, so by induction nothing below pure logic reaches data or I/O:
125
+ - `pure` depends only on pure Orchestrators;
126
+ - `read` also depends on read Orchestrators, Repositories, Indexes and Adapters,
127
+ and never calls their write methods (judged once facade methods carry effect tags);
128
+ - unset, it is a **workflow**: it may depend on whatever the other rules allow —
129
+ Repositories, other Orchestrators, Adapters, and Supervisors to reach live Actors.
130
+ - **Supervisor** depends on its Actors, Orchestrators, Adapters and other
131
+ Supervisors, and never on a presentation block
132
+ (`ARCHITECTURE_VIOLATION_SUPERVISOR_DEP`). Its held state has two homes:
133
+ - its **supervision state** — restart counts, live sets, run brackets — is a
134
+ Store or Registry it **owns** (`owns`, one hop): private, read and written in
135
+ full. A Supervisor owns nothing else (`SUPERVISOR_CONTAINMENT`), and nobody
136
+ else depends on that state: a component that does is the intruder, and the
137
+ finding is reported on it, naming the Supervisor and the state
138
+ (`SUPERVISION_STATE_INTRUSION`);
139
+ - **shared data** — a data component it does not own — it reaches only through
140
+ `read`- and `lifecycle`-effect methods. A write to an entity's fields goes
141
+ through a workflow: the Supervisor depends on the Orchestrator that does it
142
+ (`SUPERVISOR_WRITE_SHORTCUT`, judged from its narrative call steps against the
143
+ callee's declared effect; a callee that declares no effect is refused too).
144
+ A read made on behalf of a request still passes; in practice the paired write
145
+ forces the workflow out, and the read moves with it.
146
+ - **Actor** — a live Actor is **reached through its supervision**. A caller finds
147
+ it by id through a **Registry its Supervisor maintains** — owns, or keeps
148
+ through lifecycle calls (register and unregister a live handle) — so the
149
+ caller's `dependsOn` lists that Registry and the Actor: the lookup hop the code
150
+ really takes. Depending on a Supervisor that supervises the Actor (messaging it
151
+ by id) is reach too; anything else is `ACTOR_REACHED_WITHOUT_SUPERVISOR`. A
152
+ Supervisor depending on the Actor supervises it. The Actor itself may use
153
+ Repository facades, Indexes, Adapters and Orchestrators, and never depends on
154
+ its own Supervisor.
155
+ - **Method effects.** A contract method's `effect` says what it does to held
156
+ state: `read` observes it; `write` modifies an entity's domain fields;
157
+ `lifecycle` creates, destroys, or (un)registers an entity's existence or
158
+ membership and never modifies its domain fields. The lifecycle effect is
159
+ **closed under composition**: a lifecycle method calls only read and lifecycle
160
+ methods besides its construction and local steps (`LIFECYCLE_CALLS_WRITE`).
161
+ Both `write` and `lifecycle` are mutations: a Portal's read shortcut covers
162
+ neither, and a durable Store needs a hydration read-back for both.
163
+ - **Store** may depend on an **Adapter** (its backend), another Store, or pure
164
+ logic. It is depended *upon* by Registries, Indexes and Queries — never the reverse.
165
+ - **Index** is a read-only **projection over a Store**: it shares the Store's
166
+ per-entry references. It may depend on its Store, an Adapter for cold reads, and
167
+ pure logic. It **never** depends on a Registry — read and write paths are decoupled.
168
+ As an **exceptional case**, when one Index's projection is itself worth
169
+ re-presenting another way, a derived Index may depend on another Index — only
170
+ one **owned by the same Repository**, and never in a cycle of Index edges
171
+ (`ARCHITECTURE_VIOLATION_INDEX_DEP` otherwise). It is not a way to chain lookups:
172
+ reach for it only when a second parse of the Store would duplicate state the
173
+ first Index already holds (wairon's export tables over its scanned specs).
174
+ An Index that **scans** a tree of project roots may read the configuration of
175
+ the root it is walking, through that root's own binding — the same per-root
176
+ binding it already resolves that root's files through — and nothing else of
177
+ it (wairon's spec scan reads each root's `project.yaml` for the project graph).
178
+ - **Query** computes reads over its own Repository's Store: it depends **only on
179
+ its Store, a backend Adapter or pure logic**, and it lives only inside a
180
+ Repository (`UNOWNED_QUERY`).
181
+ - **Registry** is the write path to a Store; it may depend on its Store, Adapters
182
+ (e.g. a message-bus Adapter to publish), and pure logic for validation. It does
183
+ **not** depend on Indexes, and it drives no workflow.
184
+ - **Adapter** is a dependency-sink toward the system (its other side is external).
185
+ It may use pure logic, but never a Store or any other Orchestrator. Workflows,
186
+ read logic, Actors, Supervisors and the data blocks may depend on an Adapter to
187
+ reach an external system. **Cross-subsystem client Adapter** (the one
188
+ Adapter that depends *outward* on a Portal): a sibling subsystem is "potentially
189
+ external" — designed to be swappable for a network service — so a local client
190
+ Adapter is its only reachable surface, and it depends on the **remote subsystem's
191
+ Portal** (its front door), which dispatches inward. It still may not depend on the
192
+ remote subsystem's Orchestrators or Stores directly.
193
+ - The narrative `call` graph and the `dependsOn` graph must both be **DAGs** (no
194
+ cycles), and every narrative `call` must target a method on a declared dependency.
195
+
196
+ ### The Orchestrator, precisely
197
+
198
+ An Orchestrator is **logic as a flowchart**: ordered steps, conditions, branches,
199
+ loops, parallel dispatch and calls, where a call to another Orchestrator nests its
200
+ flowchart, down to leaf Orchestrators of native logic. It holds its collaborators
201
+ (constructor-injected, or closed over by a module of functions — see
202
+ [language-bindings.md](language-bindings.md)) and **no domain state**: each
203
+ call's state lives on the stack, so a shared instance is concurrency-safe and its
204
+ instantiation lifecycle is irrelevant to correctness.
205
+
206
+ - **All logic is a flowchart.** It is an Orchestrator's, an Actor's or
207
+ Supervisor's own method over its own runtime state, or a type method for
208
+ arithmetic over one value's own fields (§5). A verdict, a translation, a derived
209
+ view and a multi-step workflow are all Orchestrators; what tells them apart is
210
+ what they may depend on. **Local control flow** (guard clauses, a Registry's
211
+ `exists?` check) is fine anywhere.
212
+ - **The dependency class** says what an Orchestrator may reach:
213
+
214
+ | `dependencyClass` | May depend on | Typical shapes |
215
+ |---|---|---|
216
+ | `pure` | pure Orchestrators only | a verdict over supplied facts, a format translation, a view or text derived from values it is handed |
217
+ | `read` | pure and read Orchestrators, Repositories, Indexes, Adapters — reading, never writing | a derivation that loads its own source, a lookup that gathers the facts for a verdict |
218
+ | unset (a workflow) | whatever the other rules allow | writes, sequences, transactions, reaching live Actors |
219
+
220
+ Choose the narrowest class that holds, and split to reach it: loading in the
221
+ workflow and deriving in pure logic makes the derivation replayable and
222
+ testable without fakes.
223
+ - **One or more cohesive methods.** An Orchestrator is the unit that holds one
224
+ set of collaborators. Its methods belong together when they serve one purpose
225
+ and share those collaborators; groups of methods that call no common component
226
+ are two Orchestrators in one.
227
+ - Dependencies are injected at construction; method arguments carry only the
228
+ **workflow input**. For "same workflow over many repositories," prefer one
229
+ instance per concrete dependency behind a shared interface; use dynamic dispatch
230
+ only when the choice is genuinely per-call.
231
+
232
+ ### Self-initiated and long-running processes
233
+
234
+ There is **no separate "Process/Daemon" block** — owning one live thing *is* the
235
+ **Actor's** defining job. An Actor holds the runtime state of one live thing — a
236
+ session, a connection, a timer, or an entity instance — and handles its events one
237
+ at a time, so that state has a single writer. Its **methods are full
238
+ flowcharts**: an Actor method may call Repository facades, Indexes, Adapters and
239
+ Orchestrators itself. A part moves into an Orchestrator only when something else
240
+ also triggers it, or when it is too big to read in place. An Actor never passes
241
+ itself to an Orchestrator; it passes values from its state and applies the result.
242
+ The **Supervisor** owns the *set* of Actors and their lifecycle (find-or-start by
243
+ id, stop, restart with backoff, unload idle instances) and may supervise other
244
+ Supervisors, so a runtime is a **tree** under one root Supervisor. The state that
245
+ bookkeeping needs is the Supervisor's own supervision state — a Store or Registry
246
+ it owns — and the live handles callers look Actors up by are a Registry it
247
+ maintains.
248
+
249
+ | Need | Modeled as |
250
+ |---|---|
251
+ | cron / scheduler | an Actor owning the timer; each tick is its own method, calling the Orchestrator that does the work |
252
+ | batch job | an Actor owning the iteration, handling each item itself or through an Orchestrator |
253
+ | stream / websocket consumer | an Actor owning the read-loop + an Adapter owning the connection |
254
+ | message-bus consumer | an Observer subscribing via a bus Adapter, or an Actor owning a poll-loop |
255
+ | daemon | an Actor owning a long-running loop |
256
+
257
+ **Entity Actors.** An Actor may be the live instance of an entity — an auction
258
+ lot, a match, a device twin. The entity type stays data with pure methods; the
259
+ Actor holds one instance's working state and handles its messages; the **Store
260
+ stays authoritative**, loaded when the Actor starts and written through the
261
+ Repository. Use an entity Actor when live in-memory interaction is the point; use
262
+ persisted status plus event-driven workflows when steps are minutes or days
263
+ apart. A live entity Actor is the **only writer of its aggregate** in its
264
+ process: every other writer messages it through its Supervisor (a cluster needs a
265
+ lease or sharded routing to keep one writer).
266
+
267
+ **Large decisions: the Decider shape.** When an Actor's handling of a message
268
+ holds real decision logic, split it: *decide* — a pure Orchestrator, given the
269
+ state and the message, returns a decision; *persist* — the Actor's method writes
270
+ it in the transaction it owns (§10); *apply* — only after the commit, the Actor
271
+ updates its own state; *act* — it notifies or emits through its collaborators.
272
+ The decision logic is then replayable and testable without the Actor.
273
+
274
+ **Timers.**
275
+
276
+ | Timer | Modeled as |
277
+ |---|---|
278
+ | per-instance and loss-safe (a heartbeat, an idle timeout) | state of the Actor that owns the instance |
279
+ | a deadline on an aggregate (a lot's end, an order's payment due) | a **field of the aggregate** (`lot.endsAt`), read by its Index; a sweep Actor handles what falls due |
280
+ | a timer that spans several aggregates | a schedule aggregate with its own Repository, fired by an Actor |
281
+ | a process-wide schedule (a backup every minute) | its own Actor |
282
+ | restart backoff, passivation | the Supervisor |
283
+ | an external scheduler | an Adapter |
284
+
285
+ A deadline kept as a field survives a restart with nothing preloaded: the sweep
286
+ finds whatever fell due while the process was down.
287
+
288
+ ---
289
+
290
+ ## 4. External I/O and emission — the Adapter
291
+
292
+ The **Adapter** is the single boundary to the outside world, in two shapes:
293
+ 1. **Behind a Store** — a DB/FS Adapter the Store translates domain calls into
294
+ (`store.get` → `adapter.query(...)` → deserialize → entity).
295
+ 2. **Standalone outbound** — an Orchestrator, Actor or Registry calling an
296
+ HTTP/gRPC/email/message-bus Adapter directly for an external effect. An
297
+ external tool — a linter run as a process, a policy engine behind an API — has
298
+ the same shape: an Adapter runs it, and an Orchestrator interprets its output.
299
+
300
+ **Inbound vs outbound:** the **Portal** is *our* front door (others call us); the
301
+ **Adapter** is *our* client to external systems (we call/connect out, including
302
+ publishing). They are different roles, not duplicates.
303
+
304
+ **Emission has no special block, and is optional.** Events are a pattern for
305
+ **decoupled reactions** — used when a producer should not know its consumers.
306
+ Realizations, smallest-first:
307
+ - *No bus (in-process):* "emit" is a **fire-and-forget** call — spawn a task/thread
308
+ that invokes the handler/Observer; the producer does not await it.
309
+ - *Message bus:* emit by calling a **message-bus Adapter** (`publish(topic, event)`)
310
+ — a normal `dependsOn → Adapter` edge; an Observer subscribes via a bus Adapter.
311
+ - *Simplest systems* may skip events entirely and call the next step directly.
312
+
313
+ There is no "EventPublisher" block in any case. Event topology is declared, not
314
+ inferred: a component lists the topics it `emits` and `subscribesTo` (L2
315
+ fields), and a MessageBus public interface's endpoints carry a
316
+ `direction: publish | subscribe`. The validator pairs the two by topic —
317
+ an emitted topic nobody consumes is `UNCONSUMED_TOPIC`, a subscription with no
318
+ source is `UNSOURCED_SUBSCRIPTION` (topics with external ends are acknowledged
319
+ via `lint.allow`). Trees that use no events see neither check.
320
+
321
+ **Ports + implementations (the Carrier rule).** When a capability has multiple
322
+ external providers, define an **interface (a port)** and one Adapter per provider:
323
+ `Carrier` interface ← `DhlAdapter`, `UpsAdapter`. Callers depend on the `Carrier`
324
+ interface; the concrete Adapter is injected at bootstrap. This is composition +
325
+ polymorphism via interfaces — **not** inheritance.
326
+
327
+ ---
328
+
329
+ ## 5. Object modeling: where behavior lives
330
+
331
+ Entities are **passive state**; behavior belongs to the object that can perform it.
332
+ The test: **can the object do this autonomously, using only its own state?**
333
+
334
+ - **Yes → an intrinsic method on the entity.** `order.total()`, `order.is_valid()`,
335
+ `dog.bark()` — pure logic over the entity's own fields (computed values,
336
+ invariants, self-contained state transitions).
337
+ - **No (it needs an external actor or system) → a method on the acting *block*,
338
+ taking the entity as an argument.** An order cannot ship itself → a `Carrier`
339
+ (interface) with `DhlAdapter.ship(order)`. A dog cannot take itself out → a
340
+ `Caretaker` (an Orchestrator) with `take_out(dog)`.
341
+
342
+ This is the rich-vs-anemic balance, and it maps onto the blocks: **the Store holds
343
+ the entity; Registries/Orchestrators/Actors/Adapters act *on* it.** It keeps
344
+ domain logic out of passive data and prevents "god entities" that secretly
345
+ orchestrate. Behavior that belongs to **one live instance**, message by message,
346
+ lives on an entity Actor (§3), which still keeps the entity itself passive.
347
+
348
+ **Writes follow the aggregate.** An owner's value objects (a customer's address)
349
+ are saved with it in one Store call. A separate aggregate (the customer's
350
+ credentials) is written by the workflow, in one transaction when both must land
351
+ together (§10), with the ids generated in the workflow and the dependent
352
+ referencing its owner.
353
+
354
+ **Composition over inheritance.** The model has no class hierarchies. Variation is
355
+ expressed by **interfaces (ports) with multiple implementations** (the Carrier
356
+ example) and by **composition** (a pattern owns blocks). Inheritance is avoided;
357
+ "is-a" relationships that tempt a hierarchy are almost always "implements-an-
358
+ interface" or "is-composed-of."
359
+
360
+ ---
361
+
362
+ ## 6. Entities and types
363
+
364
+ Entities and value objects are **first-class type specs** (not one of the ten
365
+ blocks — they are the *data* the blocks operate on). Each is defined **exactly
366
+ once**, scoped by ownership:
367
+
368
+ - **Shared value objects** with no single owner (Money, Address, Email) → the
369
+ **system-level `types/`** (sibling to subsystems, under L0).
370
+ - **Entities / aggregates** (Order, Customer) → defined in the **subsystem that
371
+ owns their lifecycle** (the bounded context whose Store is authoritative):
372
+ `<subsystem>/types/`.
373
+
374
+ A type is **never** redefined in multiple places and is **not** a sibling to all
375
+ subsystems. When other subsystems process an Order, they **reference the owner's
376
+ contract** (its L1 public interface) or hold a **local projection/DTO** (an
377
+ anti-corruption view) — they do not share or redefine the owning aggregate. An L3
378
+ interface references types by id, so signatures stay structured (which also makes
379
+ data-model/ER diagrams derivable — see §14).
380
+
381
+ ### Type expressions: one neutral grammar
382
+
383
+ Every structured type position — a param's `type`, a method's `returns`, a
384
+ field's `type`, a type method's and a signature type's params and returns — is
385
+ written in one language-neutral grammar, so a contract means the same thing to
386
+ every implementation language:
387
+
388
+ | Form | Meaning |
389
+ |---|---|
390
+ | `string`, `int`, `float`, `bool`, `bytes` | the scalar primitives (`int` and `float` are distinct; width is an L4 concern) |
391
+ | `date`, `datetime`, `duration` | a calendar day, an instant, an elapsed time |
392
+ | `void`, `any` | no value (a whole returns only; `()` is another spelling), and an unconstrained value |
393
+ | `list<T>`, `set<T>`, `map<K, V>` | ordered, unique, keyed; a map key is `string`, `int` or an enum |
394
+ | `T?` | T, or no value |
395
+ | `A \| B` | exactly one of the named types (a primitive or collection never joins a union) |
396
+ | `async T` | completes later with T — at the top of a returns only (`async void`) |
397
+ | `result<T, E>` | completes with T or fails with E — a whole returns only, or under its `async`; T may be `void` |
398
+ | `Invoice`, `billing::invoice`, `Page<T>` | a named type: an entity, value-object, enum or signature type |
399
+
400
+ "No value" (`T?`) belongs to a type; "may be left out" belongs to a position
401
+ and stays the `optional` flag on a param or field. Both together mean "may be
402
+ left out, and may be explicitly none".
403
+
404
+ An **enum** is a type of its own kind: a closed, ordered set of named values
405
+ (`values: [{ name, description? }]`), optionally with pure methods. A field
406
+ typed by an enum is a value domain, not a relation, so the ER diagram draws no
407
+ edge to it, and OpenAPI renders it as a string `enum`.
408
+
409
+ A **named scalar** is a value-object that declares `holds: <primitive>` in
410
+ place of fields: one value under a name (`order_id` holds `string`), a newtype
411
+ or type alias in every language (`type OrderId = string`). It holds one of
412
+ `string`, `int`, `float`, `bool`, `bytes`, `date`, `datetime` or `duration` —
413
+ not a union, not a named type, not `any` — and carries no fields beside it
414
+ (`NAMED_SCALAR_MEMBERS`). It is how a value that needs its own meaning gets a
415
+ name instead of staying a bare `string`, and how a choice that mixed in a
416
+ primitive (`PackSelection | string`) becomes a union of named types
417
+ (`PackSelection | PackPath`). Like an enum it is a value domain: the ER diagram
418
+ draws it compactly with no edge to it, OpenAPI renders it as its primitive's
419
+ schema under its name, and type-shape conformance compares it with the code's
420
+ alias through the language's dialect (`TYPE_HOLDS_MISMATCH`).
421
+
422
+ `result<T, E>` is the success-or-failure type the mainstream languages with
423
+ typed failures share (Rust's and Swift's `Result`, Kotlin's `Result`, F#'s
424
+ `Result`). In a language without typed failures it is written as its success
425
+ type: in TypeScript the function returns T and throws E, so code conformance
426
+ compares the annotation with T and never with E.
427
+
428
+ TypeScript spellings (`string[]`, `boolean`, `T | null`, `Promise<T>`,
429
+ `Record<K, V>`) and Rust's `Result<T, E>` and `()` are accepted as input and
430
+ stored in the canonical spelling; the write answers with each respelling.
431
+ `number` is not accepted ("int or float?"). Inline object shapes, inline
432
+ function types, string-literal unions, intersections, utility types and unions
433
+ mixing in a primitive are not in the grammar: each has a named replacement (a
434
+ value-object, a signature type, an enum). The writer refuses each of them; a
435
+ tree that already holds one still loads and stays lockable, with a warning per
436
+ position, and `wairon doctor --fix` rewrites every alias and proposes an `int`
437
+ or an enum where it can. How each form is written in a given language is in
438
+ [Language bindings](language-bindings.md#type-mapping).
439
+
440
+ ---
441
+
442
+ ## 7. Patterns (named compositions)
443
+
444
+ A **pattern** is a *named, rule-bound composition of building blocks, exposed as
445
+ one component (a facade)*. A pattern owns **only building blocks** — never another
446
+ pattern (§9).
447
+
448
+ ### Repository
449
+ > The data-access-layer component for **one aggregate**. It **owns** exactly one
450
+ > **Store** — the aggregate's entity and everything that must stay consistent with
451
+ > it, across as many tables as that takes — and the faces over it: a **Registry**
452
+ > (write face), one-or-more **Indexes** (read faces), any **Queries** (computed
453
+ > reads), and optionally one **Adapter** (the backend). It contains **no logic of
454
+ > its own**. The Store and Adapter are **private behind** the Registry, Indexes and
455
+ > Queries. Its facade **forwards 1:1** to the Registry (writes) and to the Indexes
456
+ > and Queries (reads); **consumers depend on the Repository facade only** — never
457
+ > the inner blocks.
458
+
459
+ The Registry's write is `validate → store.write (→ optionally publish)`; it does
460
+ **not** touch the Indexes. Indexes are **projections over the Store** that share its
461
+ per-entry references — a value-content update is seen through the shared reference
462
+ with no index change; only create/delete, or an update that changes an *indexed
463
+ field*, propagates structurally from the Store to its Indexes. Write-time
464
+ constraints (uniqueness, validation) are checked against the **Store** (the
465
+ authoritative source) with pure validation logic — not via an Index.
466
+
467
+ **Index or Query.** An Index shares the Store's references, so it is never stale.
468
+ A read the backend computes on every call — an aggregate, a ranking, a report the
469
+ database assembles — is not a projection: it is a **Query** member, depending on
470
+ its Store or the backend Adapter (and pure logic). A custom computed query is a
471
+ Query, never an Adapter-backed Index method, and never a consumer→Adapter shortcut.
472
+
473
+ **Reads across aggregates.** A Repository stays a self-contained unit: a Query
474
+ never depends on another Repository's facade (two such Queries would also form a
475
+ cycle). A join falls into one of three cases:
476
+ - **within one aggregate** — a Store or Query method whose join is its implementation;
477
+ - **across aggregates** with small data or in different databases — a **read
478
+ Orchestrator** joining the facades' results;
479
+ - **across aggregates in one database, as one statement** — a **read-model
480
+ Repository** whose Query runs the join through the shared database Adapter.
481
+ Declaring which aggregates a read model reads is not modelled yet; name them in
482
+ its description.
483
+
484
+ **Mapping an ORM.** An object-relational mapper fits the model: the database
485
+ context (Entity Framework's `DbContext`, for one) is the database **Adapter** —
486
+ the connection, change tracking and the transaction context §10 has it issue;
487
+ entity mappings, navigation properties and `Include` joins implement a **Store**;
488
+ cross-aggregate projections and view-mapped keyless entities are read-model
489
+ **Queries**. The one habit the model pushes against is querying the context
490
+ straight from a workflow.
491
+
492
+ **When the full pattern is overkill — the two sanctioned shapes for held state.**
493
+ The Repository is the RECOMMENDED shape wherever read and write consumers diverge
494
+ or write-time constraints exist. For genuinely simple held state, a **deliberately
495
+ standalone Store** is the sanctioned lightweight form: the state stays visible as a
496
+ component, reachable from the workflow layer (a workflow Orchestrator or an Actor),
497
+ acknowledged with a `lint.allow` reason on the `UNOWNED_STORE` warning — and it
498
+ declares its `durability` like every Store (`durable` | `read-through` |
499
+ `ram-projection` | `cache`). The durability axis is **orthogonal** to the shape:
500
+ an in-memory Repository (`ram-projection` Store inside) and a persisted bare Store
501
+ (`read-through` file backing) are both legal quadrants. What is NEVER sanctioned is
502
+ the third path — folding held state into an Orchestrator's fields; hidden state
503
+ is invisible to every rule in this standard. And a standalone **Registry** is not a
504
+ shape at all: a Registry is the write path to a Store (`REGISTRY_WITHOUT_STORE`) —
505
+ a component that itself holds persisted state is a *Store*, whatever its file I/O
506
+ looks like. A **Query** has no standalone shape either: it lives only inside the
507
+ Repository whose Store it reads (`UNOWNED_QUERY`).
508
+
509
+ ### gateway: a Portal variant
510
+ > A gateway is a **Portal** with the built-in `gateway` variant: a Portal that
511
+ > **authenticates, authorizes, validates or rate-limits before it dispatches**, by
512
+ > calling that logic, and declares its inbound authentication in `auth`. The
513
+ > checks are Orchestrators it depends on — typically read logic that gathers the
514
+ > caller's grants and pure logic that rules on them — never members it owns. A
515
+ > Portal with no ingress checks needs no variant.
516
+
517
+ The variant carries the implementation guidance (§8). The Gateway *pattern*
518
+ (`Portal + ingress Orchestrator + interceptor Specialists`) is retired
519
+ (`STEREOTYPE_RETIRED`); §8 lists its migration.
520
+
521
+ ### The facade rule (mechanically enforced: `FACADE_FORWARDING`)
522
+ A pattern's facade does **pure 1:1 forwarding with no logic**: **every facade
523
+ method's authored narrative is exactly one `call` step targeting an owned
524
+ member.** More than one step, a `local` step, or a call that leaves the pattern
525
+ means the facade contains logic — a violation. The validator enforces this on
526
+ Repository facades as the `FACADE_FORWARDING` warning
527
+ (lint.allow-suppressible for deliberate exceptions); methods without an
528
+ authored narrative are governed by the detail dial, not this rule.
529
+
530
+ ### No wildcard block
531
+ The model has no residual block for what fits nowhere else. A need that seems to
532
+ fit no block is a decomposition not yet made: held or derived state → a Store,
533
+ Index or Repository; behavior over a value's own fields → a type method; external
534
+ I/O → an Adapter; logic → an Orchestrator with the narrowest `dependencyClass`
535
+ that holds; one live thing → an Actor.
536
+
537
+ ---
538
+
539
+ ## 8. Roles (naming vocabulary)
540
+
541
+ Below patterns are **roles** — conventional names for Orchestrators (and some
542
+ Indexes) that share a block's rules but carry a documented name + recommended
543
+ interface. They are vocabulary, **not** distinct types.
544
+
545
+ | Role | Recommended interface | Underlying block |
546
+ |---|---|---|
547
+ | Router | `route(input) → destination` | Index (flat key→target) or a pure Orchestrator (rule-based) |
548
+ | Validator | `validate(x) → Result` | a pure Orchestrator |
549
+ | Scanner | `scan(scope) → findings` | a pure Orchestrator handed the scope, or a read Orchestrator that loads it |
550
+ | Mapper / Compiler / Evaluator | one transform method | a pure Orchestrator |
551
+
552
+ ### The built-in variants
553
+
554
+ One step above roles sit **variants** — recurring shapes with a machine-recognized
555
+ identity (`variant:` on the component, resolved against the variant registry)
556
+ whose guidance travels into every agent brief. wairon ships a **built-in layer**.
557
+ The registry loads it first, then the global variants (`WAIRON_VARIANTS_DIR`, else
558
+ `~/.wairon/variants`), then the project's `.wai/variants/`; a later layer
559
+ **overrides a variant with the same id**, so a project can reword the built-in
560
+ guidance for its own team. A component's stereotype must equal its variant's
561
+ `base` (`VARIANT_BASE_MISMATCH`).
562
+
563
+ Variants carry **identity and guidance, not rules**. What an Orchestrator may
564
+ depend on is its `dependencyClass` field, which the validator enforces; one shape
565
+ can sit in either class, which is why the class is a field and not a variant.
566
+
567
+ | Variant | Base | Class | Shape | Discipline (guidance-enforced) |
568
+ |---|---|---|---|---|
569
+ | `arbiter` | Orchestrator | `pure` | subject + supplied world → deterministic verdict + reasons | NO I/O, no state deps — the caller gathers the world (companion shape: read logic or a workflow gathers, the arbiter rules); no clock/randomness; `idempotent` where it holds |
570
+ | `projector` | Orchestrator | `pure` when handed its source, `read` when it loads it | source model → self-contained derived view (snapshot, graph, artifact, digest) | ≤1 read facade or parameters-only; recomputed per call, owns nothing, writes nothing. NOT an Index: an Index is a maintained read model over an owned Store (or, exceptionally, over another Index of the same Repository) |
571
+ | `composer` | Orchestrator | `pure` when handed its values, `read` when it loads them | templates + values → authored text/file map | returns content, never writes or executes it; degrades gracefully on missing optional inputs |
572
+ | `codec` | Orchestrator | `pure` | format ↔ format, bidirectional | pure whole-value translation; inbound half validates + safety-checks; both directions in one component so the round-trip stays testable |
573
+ | `gateway` | Portal | — | a Portal that authenticates, authorizes, validates or rate-limits before dispatching | calls that logic before it dispatches; declares its inbound auth in `auth`; writes still route through Orchestrators |
574
+
575
+ A variant is **promoted to a first-class stereotype** only when independent
576
+ projects/packs keep re-registering it, or when its edge rules exceed what guidance
577
+ can express.
578
+
579
+ ### Retired stereotypes
580
+
581
+ `Specialist` and `Gateway` are retired. A component typed with either is
582
+ `STEREOTYPE_RETIRED` (error) until it is migrated; meanwhile the rules that judge a
583
+ stereotype's shape skip it, so it reports once.
584
+
585
+ | Retired | Becomes | Migration |
586
+ |---|---|---|
587
+ | `Specialist` | an Orchestrator with the `dependencyClass` its dependencies give it | `wairon doctor --fix` retypes it with that class, and rebases each Specialist-based variant onto Orchestrator |
588
+ | `Gateway` | the Portal it owned, with the `gateway` variant | by hand: (1) the owned Portal becomes the front door, with `variant: gateway`; (2) the Gateway's other members become that Portal's dependencies; (3) its consumers depend on the Portal; (4) delete the Gateway spec — `sdd_rename_component` can then give the Portal the Gateway's id |
589
+
590
+ **Router vs Portal vs Facade:** a **Router** selects a destination from *dynamic
591
+ input values*; a **Portal** is the *transport* boundary that *uses* a Router/Index
592
+ to dispatch; a **Facade** does *no* routing (the caller already chose the method).
593
+
594
+ ---
595
+
596
+ ## 9. Relationships, composition, and layering
597
+
598
+ - **`owns` (composition):** a pattern owns its member blocks — **exactly one hop**;
599
+ patterns never own patterns, blocks own nothing. This makes depth finite (no
600
+ "L2.5").
601
+ - **`dependsOn` (collaboration):** any component uses others.
602
+ - **Visibility rule:** a component may depend on (a) blocks within its own group,
603
+ (b) the *facade* of any other group, or (c) any standalone block — **never** a
604
+ block private to another group.
605
+ - **"Leaf" is graph-specific:** an *ownership-leaf* owns no sub-components (every
606
+ block); a *dependency-sink* depends on nothing (typically a Store or Adapter). A
607
+ read Orchestrator is an ownership-leaf that may still depend on a Repository —
608
+ so it is not a dependency-sink. The two are orthogonal.
609
+
610
+ **Layering inside a subsystem:**
611
+ ```
612
+ Ingress: Portal (with the gateway variant when it authenticates, authorizes, validates or rate-limits first)
613
+ Workflow: Orchestrators: workflows, over read and pure logic
614
+ Data: Repository = Store (one aggregate) + Registry (write+constraints) + Indexes/Queries (read) + Adapter (private)
615
+ Process: a Supervisor tree + Actors (sessions, connections, timers, live entity instances)
616
+ ```
617
+
618
+ Workflow Orchestrators **use** Repositories; Repositories never contain a Portal or
619
+ an Orchestrator. **When you are tempted to nest a pattern inside a
620
+ component, promote it to a subsystem (L1)** — composition of *patterns* is an L1
621
+ concern. A **Saga** is therefore an L1-level arrangement (an Orchestrator + an
622
+ Observer + a sibling Repository for its persisted progress), not an L2 pattern.
623
+
624
+ ### Crossing a subsystem boundary: published surfaces and their consumers
625
+ A component in another subsystem may depend only on what that subsystem
626
+ **publishes** in its `publicInterfaces` (`CROSS_SUBSYSTEM_PRIVATE_ACCESS`
627
+ otherwise), and the hop enters through a Portal (`CROSS_SUBSYSTEM_TARGET_NON_PORTAL`).
628
+ An entry may narrow who may use it with **`consumers`**: a list of subsystem ids.
629
+ Without it, any subsystem may depend on the entry; with it, only those listed
630
+ may — a dependency from any other subsystem is `CROSS_SUBSYSTEM_UNLISTED_CONSUMER`
631
+ (an error). When several entries publish the same component, each naming
632
+ consumers, the union of their lists is the whole set.
633
+
634
+ ### Listeners mount Portals
635
+ A Portal's routes are its methods' endpoint bindings; the **listener** that
636
+ hands the Portal its requests declares that with **`mounts`** on its own L2
637
+ spec — each mount names the `portal`, the path `prefixes` it routes there, and
638
+ optionally `via`, the router entry the Portal's file exports for it. An HTTP
639
+ Portal that no listener mounts, and that declares no `mounts` itself (which
640
+ marks it as a listener, even with `mounts: []`), is `UNMOUNTED_PORTAL`: nothing
641
+ would ever reach it. `mounts: []` is honest only on a Portal that IS the whole
642
+ HTTP server and serves just its own routes; a service with several HTTP
643
+ Portals has one listener that mounts each of them. A prefix matches whole path segments (`/web` covers
644
+ `/web/admin`, not `/webhooks`). Only HTTP endpoints are judged.
645
+
646
+ ### Forwarding a contract: `signatureFrom`
647
+ A method that takes its params and returns from somewhere else names the source
648
+ in **`signatureFrom`** instead of restating them: a signature type
649
+ (`billing.change_listener`, or `alias::name` across projects), or a contract
650
+ method `component.method` that its component reaches along a `dependsOn` or
651
+ `owns` edge — the shape of a facade or a forwarding Portal. The loader resolves
652
+ the source on every read, so the forwarding method can never drift from it.
653
+
654
+ ---
655
+
656
+ ## 10. Implementation variants (L4) — language-neutral rule
657
+
658
+ Structure (L2/L3) is *what it is*; the concurrency strategy (L4) is *how it is
659
+ realized*. **Concurrency is only a concern when shared mutable state is actually
660
+ accessed concurrently** — a single-threaded or simple system uses plain ownership
661
+ and skips this section entirely; do not add concurrency machinery a system does not
662
+ need. When it *is* needed, the rule, stated neutrally:
663
+
664
+ | Updating… | Strategy |
665
+ |---|---|
666
+ | one value, simple read-modify-write (counter, flag, single reference) | a single **atomic operation** — wait-free, ordered, cheaper than a lock |
667
+ | one location, compound update with no atomic equivalent (rare) | a compare-and-set retry (race-free; avoid over large/variable values) |
668
+ | multiple locations that must stay mutually consistent (a map; or two fields) | **serialize writes (in order) + wait-free snapshot reads** (the default) |
669
+
670
+ **Default = wait-free reads / in-order serialized writes:** readers read a snapshot
671
+ reference without blocking; writers are serialized so they apply in order; on
672
+ commit the snapshot reference is swapped for all readers. Values are held by
673
+ reference so the snapshot swap is pointer-only (zero-copy).
674
+
675
+ The concrete primitives per language (atomics, swappable references, sharded maps,
676
+ persistent structures, transactional backends) live in
677
+ [language-bindings.md](language-bindings.md). The standard itself prescribes only
678
+ the *strategy*, so it remains language-agnostic.
679
+
680
+ **Exceptions to the default:** write-heavy large collections → sharded locks or a
681
+ persistent structure; cross-store atomic transactions → a transactional backend via
682
+ the Adapter (see the unit-of-work rule below); single-writer (Actor) state →
683
+ wait-free reads, no write lock; non-shared / write-once state → plain ownership.
684
+
685
+ ### Transactions & the unit of work
686
+
687
+ A transaction is a **value, not a place**. The doctrine, in five rules:
688
+
689
+ 1. **The backend Adapter issues the transaction context.** The Adapter over the
690
+ transactional technology (declared via `technologies` on its L4) exposes
691
+ begin/commit/rollback as ordinary contract methods; `begin` returns an opaque
692
+ transaction-context value. No other block ever creates one.
693
+ 2. **The method that owns the workflow owns the scope** — an Orchestrator's
694
+ method, or an Actor's own method when the Actor runs the workflow. Begin,
695
+ commit, and rollback are narrative steps of that method — begin, then a
696
+ `try` region whose calls carry the context, commit as the body's last step,
697
+ rollback in the `catch` handler. The transaction boundary is thereby VISIBLE
698
+ in the L5 narrative, reviewable like any other flow.
699
+ 3. **The context travels as an explicit method argument.** Repository facades
700
+ (and their inner Registry/Store) MAY accept an optional transaction-context
701
+ parameter on write-face methods and pass it through to the backend Adapter.
702
+ Never store the context in component state — a held transaction is hidden
703
+ state and a concurrency hazard in one.
704
+ 4. **In-memory state changes only after commit.** An Actor applies a decision to
705
+ its own state after the commit step, never before: a rollback leaves nothing in
706
+ memory to undo, and no reader sees a state the Store does not hold.
707
+ 5. **Guarantee tags stay honest.** A method declaring `transactional`/`atomic`
708
+ should either bind a transactional backend through its technology Adapter or
709
+ accept the context parameter. The validator checks claim↔declaration
710
+ consistency (a narrative step asserting a guarantee must call a method
711
+ declaring it) — whether the transaction actually holds is implementation
712
+ correctness, proven by tests, not by the gate.
713
+
714
+ **Publish-and-persist atomically = the outbox pattern.** When a workflow must
715
+ persist domain state AND emit an event without a gap, do not call the bus in the
716
+ same breath as the write: append an outbox entry in the SAME transaction as the
717
+ domain write, through a **sibling outbox Repository** (the outbox is an aggregate
718
+ of its own, so it has its own Store), and let an Actor (or Observer on the
719
+ backend's change feed) drain the outbox, publish through the bus Adapter, and mark
720
+ entries delivered. A relay that restarts republishes, so consumers drop duplicates
721
+ by event id. The Saga arrangement (§9) composes with this: the outbox is its
722
+ delivery half, and the saga's progress Repository its memory.
723
+
724
+ ---
725
+
726
+ ## 11. Zero-copy / reference semantics (language-neutral)
727
+
728
+ The **Store** owns the authoritative per-entry reference. **Indexes** hold the
729
+ **same** references (shared, not value-copied), keyed differently — so reads are
730
+ zero-copy and **value-content updates are transparent**: updating the entity behind
731
+ its shared reference is seen by the Store and every Index at once, with no index
732
+ change. Only **structural** changes — create, delete, or an update that changes an
733
+ indexed field — propagate from the Store to its Indexes (an internal change-
734
+ propagation mechanism, *not* the domain Observer block). The **Registry** writes
735
+ only the Store; it never updates Indexes. (Where a language can't share references
736
+ safely, the binding appendix gives the equivalent.)
737
+
738
+ A derived Index — the exceptional Index over another Index of the same
739
+ Repository (§3) — projects that Index's references rather than the Store's,
740
+ and is dropped and rebuilt with it, so the same holds for it.
741
+
742
+ A coherent Index defined this way is **never stale**. A deliberately **evicting /
743
+ TTL cache** is a different thing — it is for *external or expensive-to-compute*
744
+ data, not a projection of an in-process Store. An external cache service (e.g.
745
+ Redis) is a caching **Adapter**; an in-process memo/TTL cache is a **Store with
746
+ `durability: cache`** — evictable, loss-safe, hydration-exempt, and VISIBLE as
747
+ held state. It is never a memoizing Orchestrator (an Orchestrator holds only its
748
+ collaborators) and never a private field inside a logic component (hidden state).
749
+
750
+ ---
751
+
752
+ ## 12. Infrastructure (not building blocks)
753
+
754
+ - **Message broker** — external infra (like a database). *Our client to it is an
755
+ Adapter* (so publish/subscribe edges are visible in the graph — see §4).
756
+ - **Composition root / wiring (DI)** — the bootstrap that constructs the graph and
757
+ injects dependencies. Infrastructure, not a domain block.
758
+ - **Cross-cutting** (authz, logging/metrics/tracing, config): coarse checks run at
759
+ the ingress — a Portal with the `gateway` variant calls authentication,
760
+ authorization, validation and rate-limit logic before it dispatches — plus infra
761
+ facilities; fine-grained authz lives in Registries/Orchestrators. Config loaded
762
+ at boot is infra injected at the composition root.
763
+
764
+ ---
765
+
766
+ ## 13. Right-sizing the model
767
+
768
+ The standard upholds good structure at **any scale** — it must not force
769
+ microservice / distributed / concurrent complexity on a system that doesn't need it.
770
+
771
+ - **L1 is optional.** A small system (a restaurant reservation app, a single
772
+ webapp) can be **one subsystem ≈ the whole system** — the L1 layer collapses into
773
+ L0. Use multiple subsystems only when there are real bounded-context or deployment
774
+ boundaries.
775
+ - **Concurrency is optional** (§10) — only when shared mutable state is actually
776
+ accessed concurrently. A simple single-threaded app uses plain ownership.
777
+ - **Events are optional** (§4) — a pattern for decoupled reactions, not a mandate.
778
+ Without a message bus, in-process events are fire-and-forget calls; the simplest
779
+ systems call the next step directly.
780
+ - **Use the smallest set of blocks/patterns the system needs.** A tiny app might be
781
+ one Portal + a couple of Orchestrators + one Repository. Don't add blocks,
782
+ patterns, or layers a system doesn't require.
783
+
784
+ The *discipline* is the same everywhere (right block for the job, correct
785
+ dependencies, behavior on the right object); only the *amount* of structure scales.
786
+
787
+ ---
788
+
789
+ ## 14. Diagram generation
790
+
791
+ The spec tree is a typed graph, so it renders to diagrams with no extra
792
+ modeling. These surfaces are SHIPPED (interactive canvas, Mermaid, draw.io,
793
+ Excalidraw exports, and the flow modal):
794
+ - component + `dependsOn` → C4 / component diagrams
795
+ - `owns` tree → containment / module diagrams
796
+ - typed entities (§6) → data-model / ER diagrams
797
+ - L3 interfaces → contract views; L1 public interfaces → API maps
798
+ - **L5 narratives → sequence diagrams and flowcharts** (each `call` is a
799
+ message; `parallel` fans out, `detach` renders fire-and-forget)
800
+
801
+ Designs should stay faithful to the typed model so the diagrams stay free.
802
+
803
+ ---
804
+
805
+ ## 15. Worked example: a live auction
806
+
807
+ Sellers list lots. Bidders watch a lot live and place bids; a bid in the last
808
+ seconds extends the lot's end. An open lot closes at its deadline, a payment feed
809
+ marks sold lots paid, and each seller has a dashboard of their lots, bids and
810
+ bidders.
811
+
812
+ | Component | Block | Responsible for |
813
+ |---|---|---|
814
+ | `bidding_portal` | Portal (HTTP and WebSocket) | bids, watch subscriptions, admin commands |
815
+ | `bidding` | Orchestrator (workflow) | `place(lotId, bid)`: load the bidder, hand the bid to the live lot |
816
+ | `lot_lifecycle` | Orchestrator (workflow) | `open`, `close`, `cancel`, `markPaid` — shared by the deadline sweep, admins and the payment feed |
817
+ | `bid_rules` | Orchestrator, `pure` | `decide(lot, bid, bidder, policy, now)`, including the last-second extension |
818
+ | `seller_dashboard` | Orchestrator, `read` | a seller's lots with their bids and bidders, joined across two Repositories |
819
+ | `live_lot` | Actor | one open lot's working state, its watchers, its heartbeat |
820
+ | `live_lots` | Supervisor | find-or-start a live lot by id, stop it, unload idle ones |
821
+ | `deadline_sweep` | Actor | the process-wide tick that closes lots whose deadline has passed |
822
+ | `outbox_relay` | Actor | publishing outbox entries through the event bus and marking them delivered |
823
+ | `auction_runtime` | Supervisor (root) | starting and stopping `live_lots`, `deadline_sweep` and `outbox_relay` |
824
+ | `lot_repository` | Repository | the lot aggregate: a Store over the tables `lots` and `bids`, a Registry, an Index by id and by `endsAt`, and the Query `bid_history` |
825
+ | `bidder_repository` | Repository | the bidder aggregate |
826
+ | `outbox_repository` | Repository | the outbox aggregate |
827
+ | `auction_database` | Adapter | the database connection; issues transactions |
828
+ | `event_bus` | Adapter | publishing and subscribing to events |
829
+ | `watcher_sockets` | Adapter | pushing updates to watching clients over WebSocket |
830
+ | `payment_feed` | Observer | forwarding payment events to `lot_lifecycle.markPaid` |
831
+
832
+ ```
833
+ bidding_portal → bidding, lot_lifecycle, seller_dashboard, live_lots, live_lot
834
+ bidding → bidder_repository, live_lots, live_lot
835
+ lot_lifecycle → live_lots, live_lot
836
+ seller_dashboard → lot_repository, bidder_repository
837
+ payment_feed → lot_lifecycle, event_bus
838
+ auction_runtime → live_lots, deadline_sweep, outbox_relay
839
+ live_lots → live_lot
840
+ live_lot → bid_rules, lot_repository, outbox_repository, auction_database, watcher_sockets
841
+ deadline_sweep → lot_repository, lot_lifecycle
842
+ outbox_relay → outbox_repository, event_bus
843
+ ```
844
+
845
+ **Placing a bid.** `bidding_portal` hands the bid to `bidding.place`, which loads
846
+ the bidder through `bidder_repository`, asks `live_lots` for the live lot by id
847
+ (starting it, with its state loaded through `lot_repository`, when it is not
848
+ running) and calls `live_lot.placeBid(bid, bidder)`. That method is the Decider
849
+ shape (§3):
850
+ 1. **decide** — `bid_rules.decide(lot, bid, bidder, policy, now)` accepts or
851
+ rejects the bid, and moves `endsAt` when the bid lands in the last seconds; a
852
+ rejection returns at once;
853
+ 2. **persist** — begin a transaction through `auction_database`; in a `try`
854
+ region, write the bid and the lot's `endsAt` through `lot_repository` and
855
+ append a `bid_placed` entry through `outbox_repository`, both carrying the
856
+ context; commit; on failure, roll back and rethrow;
857
+ 3. **apply** — only after the commit, update its own leading bid and `endsAt`;
858
+ 4. **act** — push the new standing to the lot's watchers through `watcher_sockets`.
859
+
860
+ `outbox_relay` later publishes `bid_placed` through `event_bus` and marks the
861
+ entry delivered.
862
+
863
+ **Closing.** The deadline is the field `lot.endsAt`, which the lot Index keeps in
864
+ order. On each tick `deadline_sweep` reads the lots that are due through
865
+ `lot_repository` and calls `lot_lifecycle.close` for each; `close` reaches the live
866
+ lot through `live_lots`, the same single writer a bid goes through. `live_lot`
867
+ checks `endsAt` against its own state before it closes, because a last-second bid
868
+ it handled first may have moved the deadline.
869
+
870
+ **After a restart.** Nothing is preloaded. A lot's Actor starts again on its next
871
+ bid, watch or close; lots that fell due while the process was down close within one
872
+ tick; the relay republishes entries it had not marked delivered, and consumers drop
873
+ duplicates by event id.
874
+
875
+ **The names.** Each component is named for what it is responsible for: a process
876
+ (`bidding`, `lot_lifecycle`), a live thing (`live_lot`), a set of them
877
+ (`live_lots`), an aggregate's data (`lot_repository`), an external system
878
+ (`auction_database`, `event_bus`), an audience or a feed (`seller_dashboard`,
879
+ `payment_feed`). Logic without collaborators is named for the rules it holds
880
+ (`bid_rules`). A method is a verb phrase that does not repeat its component's
881
+ words — `bidding.place`, not `bidding.placeBid` — and arithmetic over one value's
882
+ own fields is a method on its type (`lot.isOpen(now)`). A role word appears only to
883
+ tell apart components that share a noun, and it matches the block:
884
+ `lot_repository` is a Repository.
885
+
886
+ **The rules it demonstrates.**
887
+ 1. **A live Actor is addressed through its Supervisor.** `bidding`,
888
+ `lot_lifecycle` and `bidding_portal` reach a lot by id through `live_lots` and
889
+ never hold it; each lists `live_lot` together with `live_lots` in `dependsOn`,
890
+ since a call must target a declared dependency.
891
+ 2. **One writer per live aggregate.** `live_lot` is the only writer of its lot in
892
+ the process: a close from the sweep or an admin goes through `live_lots` just as
893
+ a bid does. A cluster needs a lease or sharded routing to keep it so.
894
+ 3. **The method that owns the workflow owns the transaction.** `live_lot.placeBid`
895
+ begins, commits and rolls back, and changes its in-memory state only after the
896
+ commit.
897
+ 4. **Supervisors form a tree.** `auction_runtime` supervises `live_lots` and the
898
+ two process-wide Actors.
899
+ 5. **A Portal messages a Supervisor by id**, as an Observer may: `bidding_portal`
900
+ adds a watcher to a live lot through `live_lots`.
901
+ 6. **Deadlines are aggregate fields read by an Index.** A schedule aggregate is
902
+ only for timers that span aggregates.
903
+ 7. **Reads across aggregates go through read logic.** `seller_dashboard` joins
904
+ `lot_repository` (its `bid_history` Query computes a lot's bids per call) with
905
+ `bidder_repository`; the outbox is a sibling Repository, written in the bid's
906
+ transaction.
907
+
908
+ ---
909
+
910
+ ## 16. Summary of binding rules
911
+
912
+ 1. Two axes: containment (folders) and refinement (the three files per component).
913
+ 2. Ten building blocks only; dependency rules fixed; the **Adapter** is the single
914
+ external-I/O boundary.
915
+ 3. A pattern owns **only blocks**, **one hop**, **never a pattern**; compose
916
+ patterns at L1. The one block that owns is a Supervisor, owning its
917
+ supervision state (its own Stores and Registries), private to it.
918
+ 4. `owns` ≠ `dependsOn`; cross-group access is via facades only.
919
+ 5. A Repository facade forwards 1:1 with no logic (single-`call`-to-owned-member
920
+ narratives; enforced as `FACADE_FORWARDING`).
921
+ 6. State-owners (Store/Index for domain state; Supervisor/Actor for runtime state) ≠
922
+ components holding only collaborators (Orchestrator/Registry/Query/Portal/Observer).
923
+ 7. All logic is a flowchart: an Orchestrator's methods, or an Actor's or
924
+ Supervisor's own methods over its runtime state. An Orchestrator's
925
+ `dependencyClass` bounds its dependencies — `pure` on pure logic only, `read` also
926
+ on read logic, Repositories, Indexes and Adapters, unset a workflow — and pure
927
+ logic may be used by every block. Local control flow → anywhere.
928
+ 8. Behavior lives where it can be performed autonomously; coordinated/external
929
+ behavior lives on the acting block, taking the entity as an argument.
930
+ Composition + interfaces over inheritance.
931
+ 9. Entities are first-class types, defined once by their owner; referenced, never
932
+ redefined, elsewhere. Interfaces are method contracts; fields are implementation.
933
+ 10. The Actor owns one live thing and its methods are full flowcharts; the
934
+ Supervisor owns the set of Actors, may supervise Supervisors, owns its
935
+ supervision state, and reaches shared data only through read and lifecycle
936
+ methods — writes go through a workflow. A live Actor is reached through its
937
+ supervision — looked up by id in a Registry its Supervisor maintains — and is
938
+ the only writer of its aggregate; the method that owns a workflow owns its
939
+ transaction, and in-memory state changes only after commit. A lifecycle
940
+ method changes what exists, never domain fields, and calls only read and
941
+ lifecycle methods.
942
+ 11. Structure is L2/L3; the concurrency variant is L4 (neutral strategy here,
943
+ primitives in the language-bindings appendix). The bus client, composition root,
944
+ and cross-cutting concerns are infrastructure, not blocks.
945
+ 12. A Store covers one aggregate. Registry (write), Index and Query (read) are
946
+ decoupled — all work on the Store; Indexes are reference-sharing projections,
947
+ Queries compute per call inside their Repository, and the Registry updates
948
+ neither. Reads across aggregates go through read logic or a read-model Repository.
949
+ 13. Right-size: L1, concurrency, and events are all optional; use the smallest set
950
+ of blocks a system needs, while upholding the same discipline at any scale.
951
+ 14. A gateway is a Portal with the `gateway` variant. Variants load built-in →
952
+ global → project and carry guidance, not rules. `Specialist` and `Gateway` are
953
+ retired (`STEREOTYPE_RETIRED`) until migrated.