@wairon/cli 5.1.1-dev.11 → 5.1.1-dev.110

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.
Files changed (37) hide show
  1. package/CONTRIBUTING.md +193 -0
  2. package/README.md +154 -41
  3. package/dist/cli/index.js +66474 -29775
  4. package/dist/cli/index.js.map +1 -1
  5. package/dist/index.d.ts +43671 -0
  6. package/dist/index.js +53581 -21797
  7. package/dist/index.js.map +1 -1
  8. package/dist/templates/architect.yaml +15 -13
  9. package/dist/templates/domain-owner.yaml +5 -4
  10. package/dist/templates/skills/sdd-architect.md +44 -26
  11. package/dist/templates/skills/sdd-auditor.md +5 -4
  12. package/dist/templates/skills/sdd-delegate.md +43 -4
  13. package/dist/templates/skills/sdd-implement.md +83 -13
  14. package/dist/templates/skills/sdd-narrative.md +7 -4
  15. package/dist/templates/variants/logic-shapes.yaml +59 -0
  16. package/dist/templates/variants/portal-shapes.yaml +13 -0
  17. package/dist/webapp.html +771 -0
  18. package/docs/architecture.md +83 -0
  19. package/docs/cli.md +1208 -0
  20. package/docs/design/approval-baseline.md +257 -0
  21. package/docs/design/connecting-agent-entrypoint.md +167 -0
  22. package/docs/design/execution-budgets.md +199 -0
  23. package/docs/design/pack-scoping.md +581 -0
  24. package/docs/design/supervisor-doctrine.md +110 -0
  25. package/docs/design-export.md +313 -0
  26. package/docs/extending-wairon.md +616 -0
  27. package/docs/network.md +109 -0
  28. package/docs/requirements.md +65 -0
  29. package/docs/roadmap.md +133 -0
  30. package/docs/standards/INDEX.md +23 -0
  31. package/docs/standards/architecture.md +1042 -0
  32. package/docs/standards/language-bindings.md +120 -0
  33. package/docs/templates.md +61 -0
  34. package/docs/vision.md +59 -0
  35. package/docs/wairon-feature-requests-hosted-mcp.md +74 -0
  36. package/package.json +18 -5
  37. package/schemas/design-export-2.json +1186 -0
@@ -0,0 +1,1042 @@
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. One edge needs no
303
+ Adapter: a call into another project's **library** (an `InProcess` Portal) is
304
+ made directly, from any component (§9, *Libraries are called directly*).
305
+
306
+ **Emission has no special block, and is optional.** Events are a pattern for
307
+ **decoupled reactions** — used when a producer should not know its consumers.
308
+ Realizations, smallest-first:
309
+ - *No bus (in-process):* "emit" is a **fire-and-forget** call — spawn a task/thread
310
+ that invokes the handler/Observer; the producer does not await it.
311
+ - *Message bus:* emit by calling a **message-bus Adapter** (`publish(topic, event)`)
312
+ — a normal `dependsOn → Adapter` edge; an Observer subscribes via a bus Adapter.
313
+ - *Simplest systems* may skip events entirely and call the next step directly.
314
+
315
+ There is no "EventPublisher" block in any case. Event topology is declared, not
316
+ inferred: a component lists the topics it `emits` and `subscribesTo` (L2
317
+ fields), and a MessageBus public interface's endpoints carry a
318
+ `direction: publish | subscribe`. The validator pairs the two by topic —
319
+ an emitted topic nobody consumes is `UNCONSUMED_TOPIC`, a subscription with no
320
+ source is `UNSOURCED_SUBSCRIPTION` (topics with external ends are acknowledged
321
+ via `lint.allow`). Trees that use no events see neither check.
322
+
323
+ **Ports + implementations (the Carrier rule).** When a capability has multiple
324
+ external providers, define an **interface (a port)** and one Adapter per provider:
325
+ `Carrier` interface ← `DhlAdapter`, `UpsAdapter`. Callers depend on the `Carrier`
326
+ interface; the concrete Adapter is injected at bootstrap. This is composition +
327
+ polymorphism via interfaces — **not** inheritance.
328
+
329
+ ---
330
+
331
+ ## 5. Object modeling: where behavior lives
332
+
333
+ Entities are **passive state**; behavior belongs to the object that can perform it.
334
+ The test: **can the object do this autonomously, using only its own state?**
335
+
336
+ - **Yes → an intrinsic method on the entity.** `order.total()`, `order.is_valid()`,
337
+ `dog.bark()` — pure logic over the entity's own fields (computed values,
338
+ invariants, self-contained state transitions).
339
+ - **No (it needs an external actor or system) → a method on the acting *block*,
340
+ taking the entity as an argument.** An order cannot ship itself → a `Carrier`
341
+ (interface) with `DhlAdapter.ship(order)`. A dog cannot take itself out → a
342
+ `Caretaker` (an Orchestrator) with `take_out(dog)`.
343
+
344
+ This is the rich-vs-anemic balance, and it maps onto the blocks: **the Store holds
345
+ the entity; Registries/Orchestrators/Actors/Adapters act *on* it.** It keeps
346
+ domain logic out of passive data and prevents "god entities" that secretly
347
+ orchestrate. Behavior that belongs to **one live instance**, message by message,
348
+ lives on an entity Actor (§3), which still keeps the entity itself passive.
349
+
350
+ **Writes follow the aggregate.** An owner's value objects (a customer's address)
351
+ are saved with it in one Store call. A separate aggregate (the customer's
352
+ credentials) is written by the workflow, in one transaction when both must land
353
+ together (§10), with the ids generated in the workflow and the dependent
354
+ referencing its owner.
355
+
356
+ **Composition over inheritance.** The model has no class hierarchies. Variation is
357
+ expressed by **interfaces (ports) with multiple implementations** (the Carrier
358
+ example) and by **composition** (a pattern owns blocks). Inheritance is avoided;
359
+ "is-a" relationships that tempt a hierarchy are almost always "implements-an-
360
+ interface" or "is-composed-of."
361
+
362
+ ---
363
+
364
+ ## 6. Entities and types
365
+
366
+ Entities and value objects are **first-class type specs** (not one of the ten
367
+ blocks — they are the *data* the blocks operate on). Each is defined **exactly
368
+ once**, scoped by ownership:
369
+
370
+ - **Shared value objects** with no single owner (Money, Address, Email) → the
371
+ **system-level `types/`** (sibling to subsystems, under L0).
372
+ - **Entities / aggregates** (Order, Customer) → defined in the **subsystem that
373
+ owns their lifecycle** (the bounded context whose Store is authoritative):
374
+ `<subsystem>/types/`.
375
+
376
+ A type is **never** redefined in multiple places and is **not** a sibling to all
377
+ subsystems. When other subsystems process an Order, they **reference the owner's
378
+ contract** (its L1 public interface) or hold a **local projection/DTO** (an
379
+ anti-corruption view) — they do not share or redefine the owning aggregate. An L3
380
+ interface references types by id, so signatures stay structured (which also makes
381
+ data-model/ER diagrams derivable — see §14).
382
+
383
+ ### Type expressions: one neutral grammar
384
+
385
+ Every structured type position — a param's `type`, a method's `returns`, a
386
+ field's `type`, a type method's and a signature type's params and returns — is
387
+ written in one language-neutral grammar, so a contract means the same thing to
388
+ every implementation language:
389
+
390
+ | Form | Meaning |
391
+ |---|---|
392
+ | `string`, `int`, `float`, `bool`, `bytes` | the scalar primitives (`int` and `float` are distinct; width is an L4 concern) |
393
+ | `date`, `datetime`, `duration` | a calendar day, an instant, an elapsed time |
394
+ | `void`, `any` | no value (a whole returns only; `()` is another spelling), and an unconstrained value |
395
+ | `list<T>`, `set<T>`, `map<K, V>` | ordered, unique, keyed; a map key is `string`, `int` or an enum |
396
+ | `T?` | T, or no value |
397
+ | `A \| B` | exactly one of the named types (a primitive or collection never joins a union) |
398
+ | `async T` | completes later with T — at the top of a returns only (`async void`) |
399
+ | `result<T, E>` | completes with T or fails with E — a whole returns only, or under its `async`; T may be `void` |
400
+ | `Invoice`, `billing::invoice`, `Page<T>` | a named type: an entity, value-object, enum or signature type |
401
+
402
+ "No value" (`T?`) belongs to a type; "may be left out" belongs to a position
403
+ and stays the `optional` flag on a param or field. Both together mean "may be
404
+ left out, and may be explicitly none".
405
+
406
+ An **enum** is a type of its own kind: a closed, ordered set of named values
407
+ (`values: [{ name, description? }]`), optionally with pure methods. A field
408
+ typed by an enum is a value domain, not a relation, so the ER diagram draws no
409
+ edge to it, and OpenAPI renders it as a string `enum`.
410
+
411
+ A **named scalar** is a value-object that declares `holds: <primitive>` in
412
+ place of fields: one value under a name (`order_id` holds `string`), a newtype
413
+ or type alias in every language (`type OrderId = string`). It holds one of
414
+ `string`, `int`, `float`, `bool`, `bytes`, `date`, `datetime` or `duration` —
415
+ not a union, not a named type, not `any` — and carries no fields beside it
416
+ (`NAMED_SCALAR_MEMBERS`). It is how a value that needs its own meaning gets a
417
+ name instead of staying a bare `string`, and how a choice that mixed in a
418
+ primitive (`PackSelection | string`) becomes a union of named types
419
+ (`PackSelection | PackPath`). Like an enum it is a value domain: the ER diagram
420
+ draws it compactly with no edge to it, OpenAPI renders it as its primitive's
421
+ schema under its name, and type-shape conformance compares it with the code's
422
+ alias through the language's dialect (`TYPE_HOLDS_MISMATCH`).
423
+
424
+ `result<T, E>` is the success-or-failure type the mainstream languages with
425
+ typed failures share (Rust's and Swift's `Result`, Kotlin's `Result`, F#'s
426
+ `Result`). In a language without typed failures it is written as its success
427
+ type: in TypeScript the function returns T and throws E, so code conformance
428
+ compares the annotation with T and never with E.
429
+
430
+ TypeScript spellings (`string[]`, `boolean`, `T | null`, `Promise<T>`,
431
+ `Record<K, V>`) and Rust's `Result<T, E>` and `()` are accepted as input and
432
+ stored in the canonical spelling; the write answers with each respelling.
433
+ `number` is not accepted ("int or float?"). Inline object shapes, inline
434
+ function types, string-literal unions, intersections, utility types and unions
435
+ mixing in a primitive are not in the grammar: each has a named replacement (a
436
+ value-object, a signature type, an enum). The writer refuses each of them; a
437
+ tree that already holds one still loads and stays lockable, with a warning per
438
+ position, and `wairon doctor --fix` rewrites every alias and proposes an `int`
439
+ or an enum where it can. How each form is written in a given language is in
440
+ [Language bindings](language-bindings.md#type-mapping).
441
+
442
+ ---
443
+
444
+ ## 7. Patterns (named compositions)
445
+
446
+ A **pattern** is a *named, rule-bound composition of building blocks, exposed as
447
+ one component (a facade)*. A pattern owns **only building blocks** — never another
448
+ pattern (§9).
449
+
450
+ ### Repository
451
+ > The data-access-layer component for **one aggregate**. It **owns** exactly one
452
+ > **Store** — the aggregate's entity and everything that must stay consistent with
453
+ > it, across as many tables as that takes — and the faces over it: a **Registry**
454
+ > (write face), one-or-more **Indexes** (read faces), any **Queries** (computed
455
+ > reads), and optionally one **Adapter** (the backend). It contains **no logic of
456
+ > its own**. The Store and Adapter are **private behind** the Registry, Indexes and
457
+ > Queries. Its facade **forwards 1:1** to the Registry (writes) and to the Indexes
458
+ > and Queries (reads); **consumers depend on the Repository facade only** — never
459
+ > the inner blocks.
460
+
461
+ The Registry's write is `validate → store.write (→ optionally publish)`; it does
462
+ **not** touch the Indexes. Indexes are **projections over the Store** that share its
463
+ per-entry references — a value-content update is seen through the shared reference
464
+ with no index change; only create/delete, or an update that changes an *indexed
465
+ field*, propagates structurally from the Store to its Indexes. Write-time
466
+ constraints (uniqueness, validation) are checked against the **Store** (the
467
+ authoritative source) with pure validation logic — not via an Index.
468
+
469
+ **Index or Query.** An Index shares the Store's references, so it is never stale.
470
+ A read the backend computes on every call — an aggregate, a ranking, a report the
471
+ database assembles — is not a projection: it is a **Query** member, depending on
472
+ its Store or the backend Adapter (and pure logic). A custom computed query is a
473
+ Query, never an Adapter-backed Index method, and never a consumer→Adapter shortcut.
474
+
475
+ **Reads across aggregates.** A Repository stays a self-contained unit: a Query
476
+ never depends on another Repository's facade (two such Queries would also form a
477
+ cycle). A join falls into one of three cases:
478
+ - **within one aggregate** — a Store or Query method whose join is its implementation;
479
+ - **across aggregates** with small data or in different databases — a **read
480
+ Orchestrator** joining the facades' results;
481
+ - **across aggregates in one database, as one statement** — a **read-model
482
+ Repository** whose Query runs the join through the shared database Adapter.
483
+ Declaring which aggregates a read model reads is not modelled yet; name them in
484
+ its description.
485
+
486
+ **Mapping an ORM.** An object-relational mapper fits the model: the database
487
+ context (Entity Framework's `DbContext`, for one) is the database **Adapter** —
488
+ the connection, change tracking and the transaction context §10 has it issue;
489
+ entity mappings, navigation properties and `Include` joins implement a **Store**;
490
+ cross-aggregate projections and view-mapped keyless entities are read-model
491
+ **Queries**. The one habit the model pushes against is querying the context
492
+ straight from a workflow.
493
+
494
+ **When the full pattern is overkill — the two sanctioned shapes for held state.**
495
+ The Repository is the RECOMMENDED shape wherever read and write consumers diverge
496
+ or write-time constraints exist. For genuinely simple held state, a **deliberately
497
+ standalone Store** is the sanctioned lightweight form: the state stays visible as a
498
+ component, reachable from the workflow layer (a workflow Orchestrator or an Actor),
499
+ acknowledged with a `lint.allow` reason on the `UNOWNED_STORE` warning — and it
500
+ declares its `durability` like every Store (`durable` | `read-through` |
501
+ `ram-projection` | `cache`). The durability axis is **orthogonal** to the shape:
502
+ an in-memory Repository (`ram-projection` Store inside) and a persisted bare Store
503
+ (`read-through` file backing) are both legal quadrants. What is NEVER sanctioned is
504
+ the third path — folding held state into an Orchestrator's fields; hidden state
505
+ is invisible to every rule in this standard. And a standalone **Registry** is not a
506
+ shape at all: a Registry is the write path to a Store (`REGISTRY_WITHOUT_STORE`) —
507
+ a component that itself holds persisted state is a *Store*, whatever its file I/O
508
+ looks like. A **Query** has no standalone shape either: it lives only inside the
509
+ Repository whose Store it reads (`UNOWNED_QUERY`).
510
+
511
+ ### gateway: a Portal variant
512
+ > A gateway is a **Portal** with the built-in `gateway` variant: a Portal that
513
+ > **authenticates, authorizes, validates or rate-limits before it dispatches**, by
514
+ > calling that logic, and declares its inbound authentication in `auth`. The
515
+ > checks are Orchestrators it depends on — typically read logic that gathers the
516
+ > caller's grants and pure logic that rules on them — never members it owns. A
517
+ > Portal with no ingress checks needs no variant.
518
+ >
519
+ > Inside a declared **network**, the gateway is also the only Portal that may be
520
+ > entered from outside it, and the only one a modelled call from outside may land
521
+ > on (`GATEWAY_BYPASSED`; §9, *Networks and gateways*).
522
+
523
+ The variant carries the implementation guidance (§8). The Gateway *pattern*
524
+ (`Portal + ingress Orchestrator + interceptor Specialists`) is retired
525
+ (`STEREOTYPE_RETIRED`); §8 lists its migration.
526
+
527
+ ### The facade rule (mechanically enforced: `FACADE_FORWARDING`)
528
+ A pattern's facade does **pure 1:1 forwarding with no logic**: **every facade
529
+ method's authored narrative is exactly one `call` step targeting an owned
530
+ member.** More than one step, a `local` step, or a call that leaves the pattern
531
+ means the facade contains logic — a violation. The validator enforces this on
532
+ Repository facades as the `FACADE_FORWARDING` warning
533
+ (lint.allow-suppressible for deliberate exceptions); methods without an
534
+ authored narrative are governed by the detail dial, not this rule.
535
+
536
+ ### No wildcard block
537
+ The model has no residual block for what fits nowhere else. A need that seems to
538
+ fit no block is a decomposition not yet made: held or derived state → a Store,
539
+ Index or Repository; behavior over a value's own fields → a type method; external
540
+ I/O → an Adapter; logic → an Orchestrator with the narrowest `dependencyClass`
541
+ that holds; one live thing → an Actor.
542
+
543
+ ---
544
+
545
+ ## 8. Roles (naming vocabulary)
546
+
547
+ Below patterns are **roles** — conventional names for Orchestrators (and some
548
+ Indexes) that share a block's rules but carry a documented name + recommended
549
+ interface. They are vocabulary, **not** distinct types.
550
+
551
+ | Role | Recommended interface | Underlying block |
552
+ |---|---|---|
553
+ | Router | `route(input) → destination` | Index (flat key→target) or a pure Orchestrator (rule-based) |
554
+ | Validator | `validate(x) → Result` | a pure Orchestrator |
555
+ | Scanner | `scan(scope) → findings` | a pure Orchestrator handed the scope, or a read Orchestrator that loads it |
556
+ | Mapper / Compiler / Evaluator | one transform method | a pure Orchestrator |
557
+
558
+ ### The built-in variants
559
+
560
+ One step above roles sit **variants** — recurring shapes with a machine-recognized
561
+ identity (`variant:` on the component, resolved against the variant registry)
562
+ whose guidance travels into every agent brief. wairon ships a **built-in layer**.
563
+ The registry loads it first, then the global variants (`WAIRON_VARIANTS_DIR`, else
564
+ `~/.wairon/variants`), then the project's `.wai/variants/`; a later layer
565
+ **overrides a variant with the same id**, so a project can reword the built-in
566
+ guidance for its own team. A component's stereotype must equal its variant's
567
+ `base` (`VARIANT_BASE_MISMATCH`).
568
+
569
+ Variants carry **identity and guidance, not rules**. What an Orchestrator may
570
+ depend on is its `dependencyClass` field, which the validator enforces; one shape
571
+ can sit in either class, which is why the class is a field and not a variant.
572
+
573
+ | Variant | Base | Class | Shape | Discipline (guidance-enforced) |
574
+ |---|---|---|---|---|
575
+ | `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 |
576
+ | `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) |
577
+ | `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 |
578
+ | `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 |
579
+ | `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 |
580
+
581
+ A variant is **promoted to a first-class stereotype** only when independent
582
+ projects/packs keep re-registering it, or when its edge rules exceed what guidance
583
+ can express.
584
+
585
+ ### Retired stereotypes
586
+
587
+ `Specialist` and `Gateway` are retired. A component typed with either is
588
+ `STEREOTYPE_RETIRED` (error) until it is migrated; meanwhile the rules that judge a
589
+ stereotype's shape skip it, so it reports once.
590
+
591
+ | Retired | Becomes | Migration |
592
+ |---|---|---|
593
+ | `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 |
594
+ | `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 |
595
+
596
+ **Router vs Portal vs Facade:** a **Router** selects a destination from *dynamic
597
+ input values*; a **Portal** is the *transport* boundary that *uses* a Router/Index
598
+ to dispatch; a **Facade** does *no* routing (the caller already chose the method).
599
+
600
+ ---
601
+
602
+ ## 9. Relationships, composition, and layering
603
+
604
+ - **`owns` (composition):** a pattern owns its member blocks — **exactly one hop**;
605
+ patterns never own patterns, blocks own nothing. This makes depth finite (no
606
+ "L2.5").
607
+ - **`dependsOn` (collaboration):** any component uses others.
608
+ - **Visibility rule:** a component may depend on (a) blocks within its own group,
609
+ (b) the *facade* of any other group, or (c) any standalone block — **never** a
610
+ block private to another group.
611
+ - **"Leaf" is graph-specific:** an *ownership-leaf* owns no sub-components (every
612
+ block); a *dependency-sink* depends on nothing (typically a Store or Adapter). A
613
+ read Orchestrator is an ownership-leaf that may still depend on a Repository —
614
+ so it is not a dependency-sink. The two are orthogonal.
615
+
616
+ **Layering inside a subsystem:**
617
+ ```
618
+ Ingress: Portal (with the gateway variant when it authenticates, authorizes, validates or rate-limits first)
619
+ Workflow: Orchestrators: workflows, over read and pure logic
620
+ Data: Repository = Store (one aggregate) + Registry (write+constraints) + Indexes/Queries (read) + Adapter (private)
621
+ Process: a Supervisor tree + Actors (sessions, connections, timers, live entity instances)
622
+ ```
623
+
624
+ Workflow Orchestrators **use** Repositories; Repositories never contain a Portal or
625
+ an Orchestrator. **When you are tempted to nest a pattern inside a
626
+ component, promote it to a subsystem (L1)** — composition of *patterns* is an L1
627
+ concern. A **Saga** is therefore an L1-level arrangement (an Orchestrator + an
628
+ Observer + a sibling Repository for its persisted progress), not an L2 pattern.
629
+
630
+ ### Crossing a subsystem boundary: published surfaces and their consumers
631
+ A component in another subsystem may depend only on what that subsystem
632
+ **publishes** in its `publicInterfaces` (`CROSS_SUBSYSTEM_PRIVATE_ACCESS`
633
+ otherwise), and the hop enters through a Portal (`CROSS_SUBSYSTEM_TARGET_NON_PORTAL`).
634
+ An entry may narrow who may use it with **`consumers`**: a list of subsystem ids.
635
+ Without it, any subsystem may depend on the entry; with it, only those listed
636
+ may — a dependency from any other subsystem is `CROSS_SUBSYSTEM_UNLISTED_CONSUMER`
637
+ (an error). When several entries publish the same component, each naming
638
+ consumers, the union of their lists is the whole set.
639
+
640
+ ### Transports: how callers reach a Portal
641
+ A Portal states its **`transport`**, one vocabulary shared with its endpoint
642
+ bindings: **network** (`HTTP`, `gRPC`, `GraphQL`, `MessageBus`, and `Custom` as
643
+ the escape hatch), **local** (`CLI`, `IPC`, `NamedPipe`, and `JSONRPC` for
644
+ JSON-RPC over stdio — a language server, a stdio MCP tool surface) or
645
+ **`InProcess`**: a library, whose verbs are its contract methods and bind no
646
+ endpoint. A Portal has one transport, one `auth` and one entry posture; a surface
647
+ that needs two is two Portals. JSON-RPC over HTTP is an `HTTP` verb with a
648
+ dispatch table. Which process hosts which Portal is implementation: the Portal's
649
+ L4 **`router`** names the router entry its own file exports, through which
650
+ whatever serves it hands it requests (route coverage reads the routes there).
651
+
652
+ ### Entries: every verb is reached
653
+ Every Portal verb must be **reached**. Either a modelled caller reaches it — a
654
+ `call` step along a `dependsOn` edge, `alias::portal.verb` across projects, a
655
+ `dispatch` table, a `register` step — or the verb is an **entry**: callers
656
+ outside the design reach it over the Portal's transport. A Portal is not reached
657
+ merely by existing, so one nothing calls and nothing enters is
658
+ `UNUSED_COMPONENT` / `UNUSED_METHOD`, with both remedies named.
659
+
660
+ - Declare an entry with `invokedBy: { kind: entry, caller: "…" }` — on the
661
+ Portal (every verb inherits it) or on one verb. The `caller` says who, as
662
+ reviewable prose (browsers of the shop, the operator's terminal, an AI tool
663
+ over stdio, the applications that link the crate).
664
+ - `kind: runtime` is for the process's own runtime invoking a method — a
665
+ composition root, a timer, a signal, a framework callback — on any component.
666
+ - **Never invent an entry to silence a finding.** An entry says real callers
667
+ exist outside the design. When the caller is inside the design, model the
668
+ call; when there is none, remove the verb.
669
+ - An `Observer`'s subscriptions and the subsystem `lifecycle` roots reach their
670
+ methods as before, and a contract that `implements` an imported extension
671
+ point is reached by its producer.
672
+
673
+ ### Networks and gateways
674
+ A project may declare **`network`** in `.wai/project.yaml` (`true`, or
675
+ `{ description }`): it and every member below it sit inside that network.
676
+ Networks nest, one level per declaring project; parts cannot declare one.
677
+
678
+ - An entry's **`scope`** is relative: `outside` (the default: callers from
679
+ outside the innermost network containing the Portal) or `network` (sibling
680
+ services inside it). A scope on a local or in-process verb means nothing
681
+ (`ENTRY_SCOPE_NOT_NETWORK`).
682
+ - **Gateways.** Inside a declared network, only a Portal with the `gateway`
683
+ variant may be entered from `outside`, and a modelled call crossing into a
684
+ network must land on its gateway (`GATEWAY_BYPASSED`, an error). Traffic from
685
+ the outermost network to the innermost passes one gateway per level.
686
+ - **Proof.** A `network` entry claims sibling callers, so the family run at the
687
+ root proves each one has a modelled caller inside its boundary
688
+ (`ENTRY_UNPROVEN`). A member validated alone counts it as declared.
689
+ - **Audience is a different axis.** An export's `audience` says who may design
690
+ against a contract; scope says who can reach a verb at runtime. They meet in
691
+ one place: a network verb exported beyond the project must be the outermost
692
+ gateway, entered from `outside` (`EXPORT_BEYOND_NETWORK`).
693
+ - A project that declares no network never sees any of this: its entries are
694
+ `outside` and any Portal may take them. Addresses never appear in specs —
695
+ `wairon network` derives flows, policy and diagrams from this reach.
696
+
697
+ ### Libraries are called directly
698
+ A library — a crate, a package, a jar, or an FFI/DLL surface — is a project whose
699
+ Portals are `InProcess`. Another project calls its exported verbs **directly**,
700
+ from any component: `alias::portal` in `dependsOn` and `alias::portal.verb` in a
701
+ `call` step. No client Adapter is required (`CROSS_SUBSYSTEM_NON_ADAPTER` and
702
+ `ARCHITECTURE_VIOLATION_PORTAL_DEP` exempt exactly this edge). Two checks take
703
+ the Adapter's place:
704
+
705
+ - **Purity** (`LIBRARY_CALL_IMPURE`): `pure` logic may call only library verbs
706
+ declared `effect: none`, and `read` logic also `effect: read`; a verb that does
707
+ I/O or writes (`io`, `write`, `lifecycle`), or declares no effect, is for
708
+ workflows.
709
+ - **The language bridge** (`LANGUAGE_BRIDGE_MISSING`): an `InProcess` Portal
710
+ with no `abi` is a native API in its project's `targetLanguage`. Called from
711
+ another language it needs `abi: c` (a C-ABI shared library, DLL or FFI) or
712
+ `abi: wasm`, or a network Portal in front.
713
+
714
+ Within one project nothing changes: a sibling subsystem's Portal is reached
715
+ through a client Adapter or a `trustedLink`. Wrapping a volatile third-party
716
+ API in an Adapter of your own — an anti-corruption layer that absorbs upstream
717
+ contract changes — remains a recommended pattern; it is never enforced.
718
+
719
+ ### Extension points: contracts consumers implement
720
+ A producer that *calls* a contract its consumers *supply* — a trait, a
721
+ callback, a webhook — exports it with **`role: implement`** (typically the
722
+ Adapter holding the port). A consumer's L3 interface declares
723
+ **`implements: alias::name`** and states every method of it with the same
724
+ signature (`IMPLEMENTS_MISMATCH` otherwise); those methods count as reached,
725
+ because the producer calls them. In-process, the consumer registers its
726
+ implementation with a `register` step; over a network, the implementing
727
+ component is a Portal the producer calls through an Adapter.
728
+
729
+ ### Forwarding a contract: `signatureFrom`
730
+ A method that takes its params and returns from somewhere else names the source
731
+ in **`signatureFrom`** instead of restating them: a signature type
732
+ (`billing.change_listener`, or `alias::name` across projects), or a contract
733
+ method `component.method` that its component reaches along a `dependsOn` or
734
+ `owns` edge — the shape of a facade or a forwarding Portal. The loader resolves
735
+ the source on every read, so the forwarding method can never drift from it.
736
+
737
+ ---
738
+
739
+ ## 10. Implementation variants (L4) — language-neutral rule
740
+
741
+ Structure (L2/L3) is *what it is*; the concurrency strategy (L4) is *how it is
742
+ realized*. **Concurrency is only a concern when shared mutable state is actually
743
+ accessed concurrently** — a single-threaded or simple system uses plain ownership
744
+ and skips this section entirely; do not add concurrency machinery a system does not
745
+ need. When it *is* needed, the rule, stated neutrally:
746
+
747
+ | Updating… | Strategy |
748
+ |---|---|
749
+ | one value, simple read-modify-write (counter, flag, single reference) | a single **atomic operation** — wait-free, ordered, cheaper than a lock |
750
+ | one location, compound update with no atomic equivalent (rare) | a compare-and-set retry (race-free; avoid over large/variable values) |
751
+ | multiple locations that must stay mutually consistent (a map; or two fields) | **serialize writes (in order) + wait-free snapshot reads** (the default) |
752
+
753
+ **Default = wait-free reads / in-order serialized writes:** readers read a snapshot
754
+ reference without blocking; writers are serialized so they apply in order; on
755
+ commit the snapshot reference is swapped for all readers. Values are held by
756
+ reference so the snapshot swap is pointer-only (zero-copy).
757
+
758
+ The concrete primitives per language (atomics, swappable references, sharded maps,
759
+ persistent structures, transactional backends) live in
760
+ [language-bindings.md](language-bindings.md). The standard itself prescribes only
761
+ the *strategy*, so it remains language-agnostic.
762
+
763
+ **Exceptions to the default:** write-heavy large collections → sharded locks or a
764
+ persistent structure; cross-store atomic transactions → a transactional backend via
765
+ the Adapter (see the unit-of-work rule below); single-writer (Actor) state →
766
+ wait-free reads, no write lock; non-shared / write-once state → plain ownership.
767
+
768
+ ### Transactions & the unit of work
769
+
770
+ A transaction is a **value, not a place**. The doctrine, in five rules:
771
+
772
+ 1. **The backend Adapter issues the transaction context.** The Adapter over the
773
+ transactional technology (declared via `technologies` on its L4) exposes
774
+ begin/commit/rollback as ordinary contract methods; `begin` returns an opaque
775
+ transaction-context value. No other block ever creates one.
776
+ 2. **The method that owns the workflow owns the scope** — an Orchestrator's
777
+ method, or an Actor's own method when the Actor runs the workflow. Begin,
778
+ commit, and rollback are narrative steps of that method — begin, then a
779
+ `try` region whose calls carry the context, commit as the body's last step,
780
+ rollback in the `catch` handler. The transaction boundary is thereby VISIBLE
781
+ in the L5 narrative, reviewable like any other flow.
782
+ 3. **The context travels as an explicit method argument.** Repository facades
783
+ (and their inner Registry/Store) MAY accept an optional transaction-context
784
+ parameter on write-face methods and pass it through to the backend Adapter.
785
+ Never store the context in component state — a held transaction is hidden
786
+ state and a concurrency hazard in one.
787
+ 4. **In-memory state changes only after commit.** An Actor applies a decision to
788
+ its own state after the commit step, never before: a rollback leaves nothing in
789
+ memory to undo, and no reader sees a state the Store does not hold.
790
+ 5. **Guarantee tags stay honest.** A method declaring `transactional`/`atomic`
791
+ should either bind a transactional backend through its technology Adapter or
792
+ accept the context parameter. The validator checks claim↔declaration
793
+ consistency (a narrative step asserting a guarantee must call a method
794
+ declaring it) — whether the transaction actually holds is implementation
795
+ correctness, proven by tests, not by the gate.
796
+
797
+ **Publish-and-persist atomically = the outbox pattern.** When a workflow must
798
+ persist domain state AND emit an event without a gap, do not call the bus in the
799
+ same breath as the write: append an outbox entry in the SAME transaction as the
800
+ domain write, through a **sibling outbox Repository** (the outbox is an aggregate
801
+ of its own, so it has its own Store), and let an Actor (or Observer on the
802
+ backend's change feed) drain the outbox, publish through the bus Adapter, and mark
803
+ entries delivered. A relay that restarts republishes, so consumers drop duplicates
804
+ by event id. The Saga arrangement (§9) composes with this: the outbox is its
805
+ delivery half, and the saga's progress Repository its memory.
806
+
807
+ ---
808
+
809
+ ## 11. Zero-copy / reference semantics (language-neutral)
810
+
811
+ The **Store** owns the authoritative per-entry reference. **Indexes** hold the
812
+ **same** references (shared, not value-copied), keyed differently — so reads are
813
+ zero-copy and **value-content updates are transparent**: updating the entity behind
814
+ its shared reference is seen by the Store and every Index at once, with no index
815
+ change. Only **structural** changes — create, delete, or an update that changes an
816
+ indexed field — propagate from the Store to its Indexes (an internal change-
817
+ propagation mechanism, *not* the domain Observer block). The **Registry** writes
818
+ only the Store; it never updates Indexes. (Where a language can't share references
819
+ safely, the binding appendix gives the equivalent.)
820
+
821
+ A derived Index — the exceptional Index over another Index of the same
822
+ Repository (§3) — projects that Index's references rather than the Store's,
823
+ and is dropped and rebuilt with it, so the same holds for it.
824
+
825
+ A coherent Index defined this way is **never stale**. A deliberately **evicting /
826
+ TTL cache** is a different thing — it is for *external or expensive-to-compute*
827
+ data, not a projection of an in-process Store. An external cache service (e.g.
828
+ Redis) is a caching **Adapter**; an in-process memo/TTL cache is a **Store with
829
+ `durability: cache`** — evictable, loss-safe, hydration-exempt, and VISIBLE as
830
+ held state. It is never a memoizing Orchestrator (an Orchestrator holds only its
831
+ collaborators) and never a private field inside a logic component (hidden state).
832
+
833
+ ---
834
+
835
+ ## 12. Infrastructure (not building blocks)
836
+
837
+ - **Message broker** — external infra (like a database). *Our client to it is an
838
+ Adapter* (so publish/subscribe edges are visible in the graph — see §4).
839
+ - **Composition root / wiring (DI)** — the bootstrap that constructs the graph and
840
+ injects dependencies. Infrastructure, not a domain block.
841
+ - **Cross-cutting** (authz, logging/metrics/tracing, config): coarse checks run at
842
+ the ingress — a Portal with the `gateway` variant calls authentication,
843
+ authorization, validation and rate-limit logic before it dispatches — plus infra
844
+ facilities; fine-grained authz lives in Registries/Orchestrators. Config loaded
845
+ at boot is infra injected at the composition root.
846
+
847
+ ---
848
+
849
+ ## 13. Right-sizing the model
850
+
851
+ The standard upholds good structure at **any scale** — it must not force
852
+ microservice / distributed / concurrent complexity on a system that doesn't need it.
853
+
854
+ - **L1 is optional.** A small system (a restaurant reservation app, a single
855
+ webapp) can be **one subsystem ≈ the whole system** — the L1 layer collapses into
856
+ L0. Use multiple subsystems only when there are real bounded-context or deployment
857
+ boundaries.
858
+ - **Concurrency is optional** (§10) — only when shared mutable state is actually
859
+ accessed concurrently. A simple single-threaded app uses plain ownership.
860
+ - **Events are optional** (§4) — a pattern for decoupled reactions, not a mandate.
861
+ Without a message bus, in-process events are fire-and-forget calls; the simplest
862
+ systems call the next step directly.
863
+ - **Use the smallest set of blocks/patterns the system needs.** A tiny app might be
864
+ one Portal + a couple of Orchestrators + one Repository. Don't add blocks,
865
+ patterns, or layers a system doesn't require.
866
+
867
+ The *discipline* is the same everywhere (right block for the job, correct
868
+ dependencies, behavior on the right object); only the *amount* of structure scales.
869
+
870
+ ---
871
+
872
+ ## 14. Diagram generation
873
+
874
+ The spec tree is a typed graph, so it renders to diagrams with no extra
875
+ modeling. These surfaces are SHIPPED (interactive canvas, Mermaid, draw.io,
876
+ Excalidraw exports, and the flow modal):
877
+ - component + `dependsOn` → C4 / component diagrams
878
+ - `owns` tree → containment / module diagrams
879
+ - typed entities (§6) → data-model / ER diagrams
880
+ - L3 interfaces → contract views; L1 public interfaces → API maps
881
+ - **L5 narratives → sequence diagrams and flowcharts** (each `call` is a
882
+ message; `parallel` fans out, `detach` renders fire-and-forget)
883
+
884
+ Designs should stay faithful to the typed model so the diagrams stay free.
885
+
886
+ ---
887
+
888
+ ## 15. Worked example: a live auction
889
+
890
+ Sellers list lots. Bidders watch a lot live and place bids; a bid in the last
891
+ seconds extends the lot's end. An open lot closes at its deadline, a payment feed
892
+ marks sold lots paid, and each seller has a dashboard of their lots, bids and
893
+ bidders.
894
+
895
+ | Component | Block | Responsible for |
896
+ |---|---|---|
897
+ | `bidding_portal` | Portal (HTTP and WebSocket) | bids, watch subscriptions, admin commands |
898
+ | `bidding` | Orchestrator (workflow) | `place(lotId, bid)`: load the bidder, hand the bid to the live lot |
899
+ | `lot_lifecycle` | Orchestrator (workflow) | `open`, `close`, `cancel`, `markPaid` — shared by the deadline sweep, admins and the payment feed |
900
+ | `bid_rules` | Orchestrator, `pure` | `decide(lot, bid, bidder, policy, now)`, including the last-second extension |
901
+ | `seller_dashboard` | Orchestrator, `read` | a seller's lots with their bids and bidders, joined across two Repositories |
902
+ | `live_lot` | Actor | one open lot's working state, its watchers, its heartbeat |
903
+ | `live_lots` | Supervisor | find-or-start a live lot by id, stop it, unload idle ones |
904
+ | `deadline_sweep` | Actor | the process-wide tick that closes lots whose deadline has passed |
905
+ | `outbox_relay` | Actor | publishing outbox entries through the event bus and marking them delivered |
906
+ | `auction_runtime` | Supervisor (root) | starting and stopping `live_lots`, `deadline_sweep` and `outbox_relay` |
907
+ | `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` |
908
+ | `bidder_repository` | Repository | the bidder aggregate |
909
+ | `outbox_repository` | Repository | the outbox aggregate |
910
+ | `auction_database` | Adapter | the database connection; issues transactions |
911
+ | `event_bus` | Adapter | publishing and subscribing to events |
912
+ | `watcher_sockets` | Adapter | pushing updates to watching clients over WebSocket |
913
+ | `payment_feed` | Observer | forwarding payment events to `lot_lifecycle.markPaid` |
914
+
915
+ ```
916
+ bidding_portal → bidding, lot_lifecycle, seller_dashboard, live_lots, live_lot
917
+ bidding → bidder_repository, live_lots, live_lot
918
+ lot_lifecycle → live_lots, live_lot
919
+ seller_dashboard → lot_repository, bidder_repository
920
+ payment_feed → lot_lifecycle, event_bus
921
+ auction_runtime → live_lots, deadline_sweep, outbox_relay
922
+ live_lots → live_lot
923
+ live_lot → bid_rules, lot_repository, outbox_repository, auction_database, watcher_sockets
924
+ deadline_sweep → lot_repository, lot_lifecycle
925
+ outbox_relay → outbox_repository, event_bus
926
+ ```
927
+
928
+ **Placing a bid.** `bidding_portal` hands the bid to `bidding.place`, which loads
929
+ the bidder through `bidder_repository`, asks `live_lots` for the live lot by id
930
+ (starting it, with its state loaded through `lot_repository`, when it is not
931
+ running) and calls `live_lot.placeBid(bid, bidder)`. That method is the Decider
932
+ shape (§3):
933
+ 1. **decide** — `bid_rules.decide(lot, bid, bidder, policy, now)` accepts or
934
+ rejects the bid, and moves `endsAt` when the bid lands in the last seconds; a
935
+ rejection returns at once;
936
+ 2. **persist** — begin a transaction through `auction_database`; in a `try`
937
+ region, write the bid and the lot's `endsAt` through `lot_repository` and
938
+ append a `bid_placed` entry through `outbox_repository`, both carrying the
939
+ context; commit; on failure, roll back and rethrow;
940
+ 3. **apply** — only after the commit, update its own leading bid and `endsAt`;
941
+ 4. **act** — push the new standing to the lot's watchers through `watcher_sockets`.
942
+
943
+ `outbox_relay` later publishes `bid_placed` through `event_bus` and marks the
944
+ entry delivered.
945
+
946
+ **Closing.** The deadline is the field `lot.endsAt`, which the lot Index keeps in
947
+ order. On each tick `deadline_sweep` reads the lots that are due through
948
+ `lot_repository` and calls `lot_lifecycle.close` for each; `close` reaches the live
949
+ lot through `live_lots`, the same single writer a bid goes through. `live_lot`
950
+ checks `endsAt` against its own state before it closes, because a last-second bid
951
+ it handled first may have moved the deadline.
952
+
953
+ **After a restart.** Nothing is preloaded. A lot's Actor starts again on its next
954
+ bid, watch or close; lots that fell due while the process was down close within one
955
+ tick; the relay republishes entries it had not marked delivered, and consumers drop
956
+ duplicates by event id.
957
+
958
+ **The names.** Each component is named for what it is responsible for: a process
959
+ (`bidding`, `lot_lifecycle`), a live thing (`live_lot`), a set of them
960
+ (`live_lots`), an aggregate's data (`lot_repository`), an external system
961
+ (`auction_database`, `event_bus`), an audience or a feed (`seller_dashboard`,
962
+ `payment_feed`). Logic without collaborators is named for the rules it holds
963
+ (`bid_rules`). A method is a verb phrase that does not repeat its component's
964
+ words — `bidding.place`, not `bidding.placeBid` — and arithmetic over one value's
965
+ own fields is a method on its type (`lot.isOpen(now)`). A role word appears only to
966
+ tell apart components that share a noun, and it matches the block:
967
+ `lot_repository` is a Repository.
968
+
969
+ **The rules it demonstrates.**
970
+ 1. **A live Actor is addressed through its Supervisor.** `bidding`,
971
+ `lot_lifecycle` and `bidding_portal` reach a lot by id through `live_lots` and
972
+ never hold it; each lists `live_lot` together with `live_lots` in `dependsOn`,
973
+ since a call must target a declared dependency.
974
+ 2. **One writer per live aggregate.** `live_lot` is the only writer of its lot in
975
+ the process: a close from the sweep or an admin goes through `live_lots` just as
976
+ a bid does. A cluster needs a lease or sharded routing to keep it so.
977
+ 3. **The method that owns the workflow owns the transaction.** `live_lot.placeBid`
978
+ begins, commits and rolls back, and changes its in-memory state only after the
979
+ commit.
980
+ 4. **Supervisors form a tree.** `auction_runtime` supervises `live_lots` and the
981
+ two process-wide Actors.
982
+ 5. **A Portal messages a Supervisor by id**, as an Observer may: `bidding_portal`
983
+ adds a watcher to a live lot through `live_lots`.
984
+ 6. **Deadlines are aggregate fields read by an Index.** A schedule aggregate is
985
+ only for timers that span aggregates.
986
+ 7. **Reads across aggregates go through read logic.** `seller_dashboard` joins
987
+ `lot_repository` (its `bid_history` Query computes a lot's bids per call) with
988
+ `bidder_repository`; the outbox is a sibling Repository, written in the bid's
989
+ transaction.
990
+
991
+ ---
992
+
993
+ ## 16. Summary of binding rules
994
+
995
+ 1. Two axes: containment (folders) and refinement (the three files per component).
996
+ 2. Ten building blocks only; dependency rules fixed; the **Adapter** is the single
997
+ external-I/O boundary.
998
+ 3. A pattern owns **only blocks**, **one hop**, **never a pattern**; compose
999
+ patterns at L1. The one block that owns is a Supervisor, owning its
1000
+ supervision state (its own Stores and Registries), private to it.
1001
+ 4. `owns` ≠ `dependsOn`; cross-group access is via facades only.
1002
+ 5. A Repository facade forwards 1:1 with no logic (single-`call`-to-owned-member
1003
+ narratives; enforced as `FACADE_FORWARDING`).
1004
+ 6. State-owners (Store/Index for domain state; Supervisor/Actor for runtime state) ≠
1005
+ components holding only collaborators (Orchestrator/Registry/Query/Portal/Observer).
1006
+ 7. All logic is a flowchart: an Orchestrator's methods, or an Actor's or
1007
+ Supervisor's own methods over its runtime state. An Orchestrator's
1008
+ `dependencyClass` bounds its dependencies — `pure` on pure logic only, `read` also
1009
+ on read logic, Repositories, Indexes and Adapters, unset a workflow — and pure
1010
+ logic may be used by every block. Local control flow → anywhere.
1011
+ 8. Behavior lives where it can be performed autonomously; coordinated/external
1012
+ behavior lives on the acting block, taking the entity as an argument.
1013
+ Composition + interfaces over inheritance.
1014
+ 9. Entities are first-class types, defined once by their owner; referenced, never
1015
+ redefined, elsewhere. Interfaces are method contracts; fields are implementation.
1016
+ 10. The Actor owns one live thing and its methods are full flowcharts; the
1017
+ Supervisor owns the set of Actors, may supervise Supervisors, owns its
1018
+ supervision state, and reaches shared data only through read and lifecycle
1019
+ methods — writes go through a workflow. A live Actor is reached through its
1020
+ supervision — looked up by id in a Registry its Supervisor maintains — and is
1021
+ the only writer of its aggregate; the method that owns a workflow owns its
1022
+ transaction, and in-memory state changes only after commit. A lifecycle
1023
+ method changes what exists, never domain fields, and calls only read and
1024
+ lifecycle methods.
1025
+ 11. Structure is L2/L3; the concurrency variant is L4 (neutral strategy here,
1026
+ primitives in the language-bindings appendix). The bus client, composition root,
1027
+ and cross-cutting concerns are infrastructure, not blocks.
1028
+ 12. A Store covers one aggregate. Registry (write), Index and Query (read) are
1029
+ decoupled — all work on the Store; Indexes are reference-sharing projections,
1030
+ Queries compute per call inside their Repository, and the Registry updates
1031
+ neither. Reads across aggregates go through read logic or a read-model Repository.
1032
+ 13. Right-size: L1, concurrency, and events are all optional; use the smallest set
1033
+ of blocks a system needs, while upholding the same discipline at any scale.
1034
+ 14. A gateway is a Portal with the `gateway` variant. Variants load built-in →
1035
+ global → project and carry guidance, not rules. `Specialist` and `Gateway` are
1036
+ retired (`STEREOTYPE_RETIRED`) until migrated.
1037
+ 15. Every Portal verb is reached: by a modelled caller, or as a declared entry
1038
+ (`invokedBy: { kind: entry }`) whose callers are outside the design — never an
1039
+ entry invented to silence a finding. A Portal states one `transport`; entries
1040
+ are scoped `outside | network` by the networks projects declare, and inside a
1041
+ network only a gateway is entered from outside. A library (`InProcess`) of
1042
+ another project is called directly, checked for purity and language bridge.