@wairon/cli 5.1.1-dev.106 → 5.1.1-dev.108

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.
@@ -48,7 +48,8 @@ You must read, respect, and update the living quest log file: `.wai/phased_desig
48
48
  - Do NOT design components or interfaces for the entire system all at once. Proceed **one subsystem at a time** to ensure focused, manageable design reviews.
49
49
  - For the active subsystem:
50
50
  1. Design and add all L2 Components (Portals, Orchestrators, Stores, etc.) using `sdd_add_component` (defaulting to `status: draft`).
51
- The moment any need involves **storing, persisting, caching, or tracking state** — a config, a permission set, a session map, anything — apply the **held-state recipe** below BEFORE adding a bare Store.
51
+ The moment any need involves **storing, persisting, caching, or tracking state** — a config, a permission set, a session map, anything — give that state a component of its own, choosing the **smallest sound shape** from the held-state guidance below.
52
+ Design the SMALLEST sound system: count the components against the endpoints and the state they serve. Ten endpoints over two simple tables is a handful of components, not thirty; every block you add must buy a rule it enforces or a seam it opens.
52
53
  2. Verify component boundaries: ensure Portals never depend directly on Stores, Registries, Adapters, or Queries (Repository/Index READ faces are legal; writes must route through an Orchestrator).
53
54
  3. Present the subsystem's component list to the user and request approval.
54
55
  4. Once approved, define the L3 Interfaces (`.interface.yaml`, via `sdd_define_interface`) for each component in this subsystem.
@@ -78,13 +79,12 @@ The full standard is the source of truth; this is the summary you design against
78
79
  - **No wildcard block**: decompose instead. Held or derived state → Store/Index/Repository; behaviour over a value's own fields → a type method; external I/O → Adapter; logic → an Orchestrator with the narrowest `dependencyClass` that holds; one live thing → Actor. `Specialist` and `Gateway` are retired (`STEREOTYPE_RETIRED`): `wairon doctor --fix` retypes a Specialist as an Orchestrator, and a Gateway becomes the Portal it owned, with the `gateway` variant.
79
80
  2. **Patterns** (named compositions; set `owns`): `Repository` — the data-access component for one aggregate: owns one Store + its Registry + Indexes + Queries + optional Adapter; consumers use the facade only, never the inner blocks. A `Query` lives only inside a Repository and depends only on its Store, a backend Adapter or pure logic. Reads across aggregates go through a read Orchestrator (or a read-model Repository whose Query runs the join), and an outbox is a sibling Repository. A pattern owns only building blocks, never another pattern — compose patterns at the subsystem (L1) level.
80
81
  - **Variants** (`variant:` on a component): the built-in `arbiter`, `projector`, `composer` and `codec` (on Orchestrator) and `gateway` (on Portal: authenticates, authorizes, validates or rate-limits before it dispatches, with its inbound auth in `auth`), then any global and project variants, a later layer overriding by id. A variant carries guidance; what an Orchestrator may depend on is its own `dependencyClass`.
81
- - **No Persistence Shortcuts**: Every domain entity requiring state preservation (even simple settings, configurations, permissions, or in-memory rules) **must** utilize a proper `Repository` composed of `Store`, `Registry`, and `Index` blocks. Under no circumstances may you skip repositories/stores, store state inside an `Orchestrator` directly, or combine `Store`, `Registry`, and `Index` roles into a single "storage" or "helper" component.
82
- - **Held-state recipe (apply the moment state appears — do not wait for a refused link):**
83
- 1. `sdd_add_component` the members: `<x>_store` (Store), `<x>_registry` (Registry — write path), `<x>_index` (Index — read path).
84
- 2. `sdd_add_component` the facade: `<x>_repository` (Repository, `owns: [<x>_store, <x>_registry, <x>_index]`).
85
- 3. Point every consumer at the `<x>_repository` facade — never at the inner blocks.
86
- - *Lightweight exception*: for genuinely simple held state, a deliberately **standalone Store** is sanctioned — consumers from the workflow layer only (a workflow Orchestrator or an Actor), acknowledged with a `lint.allow` reason on the `UNOWNED_STORE` warning. The state stays VISIBLE as a component either way.
87
- - *Never*: hold state as fields inside an Orchestrator because a Store link was refused. A refused link means "apply this recipe", not "inline the state" — state hidden inside a logic component is invisible to the spec and unrecoverable.
82
+ - **No Persistence Shortcuts**: held state always lives in a data component of its own — never as fields inside an `Orchestrator`, and never merged with logic into a "storage" or "helper" component. WHICH data shape is the smallest one that is sound for the state at hand:
83
+ - **Held-state guidance (apply the moment state appears — do not wait for a refused link), smallest first:**
84
+ 1. **Genuinely simple state** — a settings record, a table read and written by key, a small set — is **one standalone `Store`**. This is the sanctioned shape, not a shortcut: its consumers are on the workflow layer (a workflow Orchestrator or an Actor; a Portal reaches it through that Orchestrator), and you acknowledge the `UNOWNED_STORE` warning with a `lint.allow` reason ("simple keyed state, no lookups beyond the key"). Do NOT add a Registry, Index or Repository around it to make it look complete.
85
+ 2. **State with real lookup or index needs** — reads by something other than the key (by owner, by date range, by status), several read projections, or a write path that must keep invariants across them — is the **`Repository` pattern**: `<x>_store` (Store), `<x>_registry` (Registry — write path), one `<x>_index` per real lookup (Index — read path), and the `<x>_repository` facade that `owns` them; every consumer points at the facade. Add an Index only for a lookup a narrative actually performs; a Repository with one Index that only re-reads by key is the simple case dressed up — use step 1.
86
+ 3. Never split Store/Registry/Index for state that step 1 covers, and never combine their roles into one component when step 2 applies.
87
+ - *Never*: hold state as fields inside an Orchestrator because a Store link was refused. A refused link means "give the state its component", not "inline the state" — state hidden inside a logic component is invisible to the spec and unrecoverable.
88
88
  3. **owns vs dependsOn**: `owns` = a pattern's private member blocks (exactly one hop), or a Supervisor's supervision state (its own Stores and Registries). `dependsOn` = collaborators (other facades / standalone blocks). Never depend on a block privately owned by another pattern.
89
89
  4. **Decoupling**: Registry (write) and Index (read) are independent — both work on the Store; the Registry never updates Indexes (Indexes share the Store's references and project structural changes). A Store is depended *upon*; it never depends on a Registry/Index. An Index depends on its Store, a backend Adapter or pure logic; only when one projection is itself worth re-presenting another way may a derived Index depend on another Index — owned by the same Repository, never in a cycle (`ARCHITECTURE_VIOLATION_INDEX_DEP` otherwise). Keep it the exception: it is not a way to chain lookups.
90
90
  5. **Behaviour placement**: behaviour lives where it can be performed autonomously over its own state (`order.total()`, `dog.bark()`); behaviour needing an external actor lives on the *acting* component, taking the entity as an argument (a `Carrier` ships an order — not `order.ship()`). Prefer composition + interfaces over inheritance.
@@ -101,7 +101,7 @@ JSON for review. You never hand-author spec YAML. What to know beyond the tool
101
101
  schemas:
102
102
 
103
103
  - **Levels & files** (written by the tools): L0 `.wai/specs/.index.yaml` → L1 `<sub>/.index.yaml` → L2 `<sub>/<comp>/.index.yaml` (owned members nest one level deeper) → L3 `.interface.yaml` → L4+L5 `.implementation.yaml`. Types live under `types/` (system-level shared, or per subsystem).
104
- - **Statuses**: `draft` → `design` → `complete` — authoring readiness. Completeness rules relax to warnings while draft/design. Approval is separate: `wairon lock` (human-run) validates the tree as if complete and records the approval in `.wai/lock.json`; it never rewrites a spec's `status`.
104
+ - **Statuses**: `draft` → `design` → `complete` — authoring readiness. Completeness rules relax to warnings while draft/design. Approval is separate: `wairon lock` (human-run) validates the tree as if complete and records the approval in `.wai/lock.json`; it never rewrites a spec's `status`. A status is readiness, never design: it is left out of the approval, so promoting a spec to `complete` after the lock never reopens it (and never needs a re-lock).
105
105
  - **designDepth** (project `rules.designDepth`, per-subsystem override, or a pack profile's default): how deep the team COMMITS to designing — `components | interfaces | implementations | narratives` (default `narratives` = full depth). Expectation checks below the depth are gated (nothing deeper is demanded to exist); soundness of whatever IS authored always applies. Ask the user their intended depth at Stage 1 if unclear — do not assume a team that stops at L3 is "incomplete".
106
106
  - **publicInterfaces (L1)**: each entry MUST name the `component` realizing it. Its export kind is DERIVED from the backing Portal's `transport` — never author a `type`. Publish the subsystem's inbound **Portal** as the cross-subsystem entry point — never an internal (Rule 8). A contract consumers IMPLEMENT rather than call (a trait, a callback, a webhook) is exported with `role: implement` (typically the Adapter holding the port); the consumer's interface declares `implements: alias::name` and states every method with the same signature. Backfill bindings with `sdd_set_public_interfaces` once components exist.
107
107
  - **Subsystem consumers**: an entry may list `consumers: [<subsystem>, …]` — the subsystems the surface is published to. Absent, any subsystem may depend on it; present, only those may, and a dependency from any other subsystem is `CROSS_SUBSYSTEM_UNLISTED_CONSUMER` (an error). Use it for an internal Portal meant for one or two peers, so the boundary says who may call it.
@@ -110,7 +110,7 @@ schemas:
110
110
  - **Libraries are called directly**: another project's `InProcess` Portal (a crate, a package, a jar, an FFI/DLL surface) is called from ANY component — `alias::portal` in `dependsOn`, `alias::portal.verb` in a call step — with no client Adapter. Pure logic may call only library verbs declared `effect: none`, read logic also `effect: read` (`LIBRARY_CALL_IMPURE`); a native library (no `abi`) called from another `targetLanguage` needs `abi: c` or `abi: wasm` (`LANGUAGE_BRIDGE_MISSING`). Wrapping a volatile third-party API in your own Adapter is a good habit, not a rule. Within one project a sibling subsystem is still reached through a client Adapter or a `trustedLink`.
111
111
  - **Transport and naming**: name a Portal for what it serves, not its wire (`order_portal`, not `http_handler`); the `transport` says the wire. Method names follow the project's method casing — camelCase for TypeScript and Java, snake_case for Rust and Python, from `targetLanguage` unless `rules.naming` says otherwise — and a wire name that differs (an HTTP path, a CLI command like `lock-check`, a JSON-RPC method like `tools/call`) lives on the method's `endpoint`, never in the method name.
112
112
  - **trustedLinks (L1)**: `{ subsystem, reason }` — declared on the SOURCE subsystem, it licenses a direct in-process edge into the named sibling **without the client-Adapter shim** (the deliberate fast lane). The target must still be in the sibling's `publicInterfaces` (its published Portal); a target-side declaration grants nothing. It also acknowledges a mutual-dependency pair.
113
- - **Declare planned code at design time**: give each implementation the `sourcePath` its code will live at (and a method its own `sourcePath` when its body lives elsewhere). Code linkage — `sourcePath`, `symbol`, `exportedVia`, `simPath`, `injectedParams`, conformance tiers — is not part of the approval, so declaring or changing it never asks for a re-lock. A file not written yet is `SOURCE_FILE_PLANNED` (a notice, never a `--ci` failure) until the component's realization begins; never strip a planned path to quiet anything. A team that wants CI to demand code for every designed implementation sets `rules.conformance.requireCode: true`.
113
+ - **Declare planned code at design time**: give each implementation the `sourcePath` its code will live at (and a method its own `sourcePath` when its body lives elsewhere) — and each TYPE the `sourcePath` of the file that will declare it (with `symbol` when the code name differs): a type without one is never compared with the code's shape, which is `MISSING_TYPE_SOURCE_PATH` (a notice while its subsystem has no code, a warning once it has). Code linkage — `sourcePath`, `symbol`, `exportedVia`, `simPath`, `injectedParams`, conformance tiers — is not part of the approval, so declaring or changing it never asks for a re-lock. A file not written yet is `SOURCE_FILE_PLANNED` (a notice, never a `--ci` failure) until the component's realization begins; never strip a planned path to quiet anything. A team that wants CI to demand code for every designed implementation sets `rules.conformance.requireCode: true`.
114
114
  - **Externals — the pin gates, live drift is visible**: declare a project this one consumes with `sdd_add_external` (alias; source `../sibling`, `hosted:<id>`, `<git url>` or `<git url>#<commit>`; optional `project`, `use`, `description`) instead of editing `.wai/project.yaml`. It refuses in one sentence what cannot be declared, takes the declaration back out when the producer contradicts it (another id, an unexported `use` name), and pins it by default. The owner's gate judges each external against its pin; `sdd_validate_tree` and `sdd_get_status` add ADVISORY live-drift findings (`EXTERNAL_LIVE_INCOMPATIBLE`, `EXTERNAL_DRIFTED`, `EXTERNAL_LIVE_UNCOMPARED`) that never make the tree invalid — adapt the uses, then the human re-pins. `wairon externals status` is the human's opt-in live gate.
115
115
  - **Growing a system — subsystems → parts → projects**: boundaries are earned. Keep subsystems in one folder until a piece needs its own folder or repository; then make it a **part** — `members: { scheduler: services/scheduler }` (or `../admin`, or `git-url#<commit>`), created by `sdd_add_member` (a part by default) or `sdd_externalize_subsystem`. A part's subsystems stay this project's own: local ids, the ordinary subsystem rules, this project's lock. Make a **project** — its own id, exports and lock, referenced as `alias::name` — only when it needs its own team, release, approval or public surface: `sdd_promote_member`, and `sdd_demote_member` to undo it. What a member is follows from its content (an id, an L0 or a lock); `as:` only asserts it.
116
116
  - **Members & the family's shape**: relocate a member with `sdd_move_member`. Every other change of a family's shape is a **family migration**: `sdd_attach_member` (an existing project becomes a member), `sdd_detach_member` / `sdd_adopt_member` (out of the family and back), `sdd_rename_project`, `sdd_rename_member_alias`, `sdd_internalize_member` (a member folded back in, its metadata sent to explicit homes) and `sdd_externalize_subsystem`. Call it with `dryRun: true` first and show the human the plan; applied, it writes every project it touches or none, and it never locks — it names the projects to re-lock.
@@ -24,7 +24,7 @@ You are the **Delegation Orchestrator**. Your job is to hand scoped work to a fo
24
24
  - Pick the agent whose `ownedPaths`/domain matches the task. If no agent fits, stop and tell the user the topology has a gap.
25
25
  2. **Fetch the LIVE brief**:
26
26
  - Call `sdd_get_agent_brief(agentId)` (or read the `wairon-agent://<agentId>` resource).
27
- - The brief carries: `agentId`, `name`, `template`, `domainRoot?`, `ownedPaths`, `readPaths?`, `instructions`, `variantGuidance?`, `typeMapping?` (how the contracts' neutral types are spelled in the language the agent's code is written in, also folded into `instructions` under `## Types in <language>`), and — when the project opted into `execution.tier` — `profile` and `budget`.
27
+ - The brief carries: `agentId`, `name`, `template`, `domainRoot?`, `ownedPaths`, `readPaths?`, `instructions`, `variantGuidance?`, `typeMapping?` (how the contracts' neutral types are spelled in the language the agent's code is written in, also folded into `instructions` under `## Types in <language>`; TypeScript's table is the one code conformance reads back, while Rust's and Python's are mapping-only tables the code analyzer does not check, and the table says so), `codeFence?` (where the agent may write code — for a subsystem owner also the project's shared setup files: manifest, compiler settings, crate or package root), an `## Externals used` section for a component reaching another project (each alias, the names it uses, the producer's transport and abi, and the pinned snapshot `.wai/externals/<alias>.yaml`, which joins `readPaths` — code against the pin, never the producer's source), and — when the project opted into `execution.tier` — `profile` and `budget`.
28
28
  - **Never reuse a brief across delegations or after a re-lock** — fetch fresh per delegation; the call is cheap and the brief is always current.
29
29
  3. **Spawn a GENERIC subagent from the brief**:
30
30
  - Prompt: `brief.instructions`, plus the concrete task description.
@@ -71,7 +71,7 @@ to the artifacts the host AI tool consumes:
71
71
  (`runWithProjectRoot`) — reusing the core, validation, and MCP layers unchanged.
72
72
  - **Everything is file-based** under `.wai/` — no database, and no daemon for the
73
73
  core workflow. `wairon serve` is an **opt-in** hosting daemon (see the
74
- [hosted server guide](design/hosted-mcp-server.md)) that serves the same
74
+ [hosted server guide](https://github.com/SYW-Apps/Waffle-AIron/blob/main/docs/design/hosted-mcp-server.md)) that serves the same
75
75
  file-based tools over HTTP.
76
76
 
77
77
  ---
package/docs/cli.md CHANGED
@@ -74,8 +74,11 @@ warnings as errors (notices are printed and counted, never fatal).
74
74
  **family root** (the project that declares the members). A member validated on
75
75
  its own judges its own gate with its `network` entries counted as declared, and
76
76
  says so in one line, so a green member run is never read as a proven one.
77
- **In CI, run `wairon validate --ci` at the family root**; a member in its own
78
- repository adds the root's run to its pipeline or relies on the root
77
+ **In CI, run `wairon validate --ci` at the family root**, after installing
78
+ the project's dependencies (`npm ci` or your package manager's equivalent —
79
+ the gate reads TypeScript/JavaScript code with the project's own TypeScript;
80
+ see [Using it in GitHub Actions](#using-it-in-github-actions)); a member in
81
+ its own repository adds the root's run to its pipeline or relies on the root
79
82
  repository's.
80
83
  - **Externals, compared live (advisory: the pin gates).** The gate judges each
81
84
  external against its pin. Then every external the run did not compose is
@@ -97,6 +100,58 @@ warnings as errors (notices are printed and counted, never fatal).
97
100
  `METHOD_SOURCE_PATH_MISSING` (warning). `rules.conformance.requireCode: true`
98
101
  in `project.yaml` reports the two notices as errors, for a CI that must say
99
102
  every designed implementation has code.
103
+ - **Types are linked to code too.** A type spec without a `sourcePath` is
104
+ `MISSING_TYPE_SOURCE_PATH` — a notice before its subsystem has code, a
105
+ warning after. Plan a type's `sourcePath` when you plan the implementations'.
106
+ - **What the closing line says.** Plain `validate` passes with warnings and
107
+ says so — `Passed with N warning(s)` (`--ci` fails on them); `All checks
108
+ passed` means none. A tree holding no design yet (no L0, or an L0 with nothing
109
+ below it) says there is nothing to check.
110
+ - **Unknown keys are reported.** A key a spec's schema does not know (an
111
+ `exports:` table in the L0 — the export table is `publicInterfaces`; a
112
+ misspelt field) is `UNKNOWN_SPEC_KEY`, and an unknown setting in
113
+ `.wai/project.yaml` is `UNKNOWN_CONFIG_KEY` (both warnings): the parse would
114
+ otherwise drop them without a word.
115
+ - **Code that crosses the design.** A call through a constructor-injected
116
+ collaborator is collaboration whatever the import's form: when its type comes
117
+ from a component the caller never declared, even through `import type`, it is
118
+ `UNDECLARED_DEPENDENCY`; a Portal calling a write- or lifecycle-effect method
119
+ of a Repository, Index, Store or Registry that way is
120
+ `PORTAL_WRITE_SHORTCUT_IN_CODE` (error, the code twin of
121
+ `PORTAL_WRITE_SHORTCUT`); and a file importing a technology's package
122
+ outside the components that bind it is `TECH_LEAKAGE_IN_CODE`. A technology's
123
+ packages are its declared tokens (its name, or its `matches`) plus the common
124
+ packages wairon knows for it — see [Technologies and their
125
+ packages](#technologies-and-their-packages). An Adapter's call step to a verb
126
+ of a Portal that declares an out-of-process transport (HTTP, gRPC, a
127
+ database, a bus, a CLI…) is the link the design models: it is never resolved
128
+ to the remote Portal's file, so it never reads as `CALL_ORIGIN_UNRESOLVED`;
129
+ the call to the Adapter stays checked. Likewise the Adapter's `dependsOn` to
130
+ that Portal is realized by the link, never reported `UNREALIZED_DEPENDENCY`:
131
+ never add an import across a network boundary to satisfy a check.
132
+
133
+ #### Technologies and their packages
134
+
135
+ `TECH_LEAKAGE_IN_CODE` compares each import's package with the packages of the
136
+ technologies an implementation binds. A design writes the technology the way
137
+ people say it (`postgres`) while the code imports its driver (`pg`), so wairon
138
+ knows the common packages of the common technologies by default:
139
+
140
+ | Technology (any of these names) | Packages |
141
+ | --- | --- |
142
+ | `postgres`, `postgresql`, `pg` | `pg`, `postgres`, `pg-promise`, `@neondatabase/serverless`, `@vercel/postgres` |
143
+ | `mysql`, `mariadb` | `mysql2`, `mysql`, `mariadb` |
144
+ | `redis`, `valkey` | `redis`, `ioredis`, `@redis/client` |
145
+ | `mongodb`, `mongo` | `mongodb`, `mongoose` |
146
+ | `sqlite`, `sqlite3` | `better-sqlite3`, `sqlite3`, `sqlite` |
147
+ | `kafka` | `kafkajs`, `node-rdkafka` |
148
+ | `rabbitmq`, `amqp` | `amqplib`, `amqp-connection-manager` |
149
+
150
+ An HTTP client (`fetch`, `axios`, `got`, `undici`) is never a technology — it is
151
+ how an Adapter reaches anything — so no entry names one. To extend the list,
152
+ write the technology as `{ name, matches }` (each `matches` token is a package
153
+ too), or have a pack contribute `technologyPackages: { <technology>: [<package>,
154
+ …] }`.
100
155
 
101
156
  ### `wairon generate [--target <name>] [--domain <id>] [--domains <ids>] [--root] [--family] [--no-prune] [--global] [--dry-run]`
102
157
  Reconcile the generated guides, skills and context, and — only when the project
@@ -164,11 +219,27 @@ digests read each spec's **design**: where it is realized — `sourcePath`,
164
219
  out (`specsReading: design`). Linking code to an approved design, or a change and
165
220
  its revert, never stales the lock; declare planned `sourcePath`s at design time.
166
221
 
167
- A **format-2** record (written before this reading) still passes `lock-check`
168
- while nothing it covered moved. `wairon doctor --fix` re-expresses it as format 3
169
- in place — keeping `lockedAt`, `lockedBy`, the results and the member pins, and
170
- recording `reexpressed: { at, fromAlgorithm, fromReading, by }`. A format-2 record
171
- that no longer matches reads stale once; one `wairon lock` clears it for good.
222
+ **Readiness is not part of the approval either.** A spec's `status` (`draft`,
223
+ `design`, `complete`) says how far authoring is, never what the design says, so
224
+ it is left out of the design view: promoting a spec to `complete` after the lock
225
+ never reopens it. A format-3 record taken while the view still carried `status`
226
+ keeps passing when nothing but a status moved — it is judged in the reading it
227
+ was taken in, each spec's approved status recovered from its digest — and
228
+ `wairon doctor --fix` carries it into the current reading without a review.
229
+
230
+ A **format-2** record (written before code linkage left the approval) still passes
231
+ `lock-check` while nothing it covered moved. `wairon doctor --fix` re-expresses it
232
+ as format 3 in place — before any other repair touches a spec — keeping
233
+ `lockedAt`, `lockedBy`, the results and the member pins, and recording
234
+ `reexpressed: { at, fromAlgorithm, fromReading, by }`. When no spec file moved
235
+ but the record still does not match, what moved is the **gate** it was judged
236
+ under — the design rules of a newer wairon release, the project's rule tuning,
237
+ `composition`, the network declaration, consumed contracts or a member's
238
+ approval — never your design: `lock-check` and `doctor` say exactly that, `doctor
239
+ --fix` still carries the record into the design reading (its claim stays as
240
+ certified, so it reads stale for that stated cause), and one `wairon lock`
241
+ re-approves the unchanged design. A format-2 record whose specs moved reads stale
242
+ once; one `wairon lock` clears it for good.
172
243
 
173
244
  It writes **nothing into your spec tree**. The approval is one sha256 per spec
174
245
  (over its design view) on `.wai/lock.json`, the record that was always committed — so your
@@ -260,9 +331,12 @@ merging is the thing that was reviewed.
260
331
 
261
332
  #### Using it in GitHub Actions
262
333
 
263
- This repository publishes it as a **reusable workflow** (from v6.0.0 — the
264
- workflow and the `lock-check` command do not exist in earlier releases). Add one
265
- job to your own workflow:
334
+ This repository publishes it as a **reusable workflow**. **6.0.0 is the first
335
+ release that ships the workflow and the `lock-check` command** — earlier releases
336
+ have neither, and no `v6.0.0` tag exists before that release. On a dev build (and
337
+ until 6.0.0 is out), pin the dev tag matching `wairon --version` instead: a CLI
338
+ reporting `5.1.1-dev.107` pins `@v5.1.1-dev.107` with `wairon-version:
339
+ '5.1.1-dev.107'`. Add one job to your own workflow:
266
340
 
267
341
  ```yaml
268
342
  # .github/workflows/ci.yml in YOUR repository
@@ -286,6 +360,7 @@ With inputs (all optional):
286
360
  wairon-version: '6.0.0' # version or npm dist-tag, 6.0.0 or later (default: latest)
287
361
  strict: true # fail when nothing was approved — use it once you have locked (default: false)
288
362
  validate: true # also run `wairon validate --ci` there (default: true)
363
+ install: auto # the project's dependencies before validate: auto | none | <command> (default: auto)
289
364
  node-version: '20' # (default: '20')
290
365
  runs-on: ubuntu-latest # (default: ubuntu-latest)
291
366
  ```
@@ -304,6 +379,44 @@ directory (`validate: false` turns it off). Point `working-directory` at the
304
379
  **family root** — the project that declares the members — so that run is the
305
380
  family run, which judges the network proofs no member's own gate can.
306
381
 
382
+ **The project's dependencies come first.** The conformance gate reads
383
+ TypeScript/JavaScript code with the project's own TypeScript compiler. A fresh
384
+ checkout has none until the project's dependencies are installed, and then every
385
+ file is analyzed below exact grade and `--ci` fails on `CONFORMANCE_DEGRADED` —
386
+ a finding about the runner, not your design. So before `validate --ci` the job
387
+ installs them with the project's own package manager (`install: auto`, the
388
+ default): nothing without a `package.json`; otherwise the nearest lockfile from
389
+ `working-directory` up to the repository root decides — `package-lock.json` →
390
+ `npm ci`, `pnpm-lock.yaml` → `pnpm install --frozen-lockfile`, `yarn.lock` →
391
+ `yarn install` (`--immutable`, or `--frozen-lockfile` for Yarn 1). `install:
392
+ none` skips it; any other value is run as the install command. The approval
393
+ check alone (`validate: false`) installs nothing.
394
+
395
+ #### Without the reusable workflow
396
+
397
+ The same gate as plain steps — for another CI system, or a job of your own. For
398
+ a TypeScript/JavaScript project with code, **install the project's dependencies
399
+ before `wairon validate --ci`** (`npm ci`, `pnpm install --frozen-lockfile` or
400
+ `yarn install --immutable`, whichever your lockfile is for):
401
+
402
+ ```yaml
403
+ # .github/workflows/design.yml in YOUR repository
404
+ on: [pull_request]
405
+
406
+ jobs:
407
+ design:
408
+ runs-on: ubuntu-latest
409
+ steps:
410
+ - uses: actions/checkout@v4
411
+ - uses: actions/setup-node@v4
412
+ with:
413
+ node-version: '20'
414
+ - run: npm install --global @wairon/cli@6.0.0 # the version you lock with
415
+ - run: wairon lock-check --strict
416
+ - run: npm ci # the project's dependencies: the gate reads your code with your TypeScript
417
+ - run: wairon validate --ci
418
+ ```
419
+
307
420
  **A failing job does not block a merge on its own.** A workflow can only fail;
308
421
  making a failing job stop a merge is a branch-protection / ruleset setting on
309
422
  your repository ("Require status checks to pass" → add this job). No workflow
@@ -348,9 +461,12 @@ The design changes, so one `wairon lock` follows. After it, declare the entries
348
461
  the migration cannot know — a CLI, a stdio tool surface (`JSONRPC`), a library
349
462
  (`InProcess`) — and model a caller or remove each verb still reported unreached.
350
463
 
351
- `--fix` also re-expresses a format-2 lock record that still provably covers the
352
- tree as format 3 (see `wairon lock`), keeping who approved it and when; when it
353
- no longer covers the tree, doctor says that one `wairon lock` is needed.
464
+ `--fix` also re-expresses an earlier lock record — format 2, or format 3 taken
465
+ while `status` was still in the approval — in the current reading, first, before
466
+ any repair touches a spec (see `wairon lock`), keeping who approved it and when.
467
+ A format-2 record over a tree no file of which moved is carried into the design
468
+ reading even when the gate it was judged under moved; doctor then says the cause
469
+ (the gate, not the design) and that one `wairon lock` re-approves it.
354
470
 
355
471
  ### `wairon list` (alias `ls`) / `wairon show <id>`
356
472
  List, or show full details of, the agents resolved from the spec tree
@@ -544,7 +660,32 @@ promise and how a consumer follows renames).
544
660
  - A tree without an L0 is refused. The JSON Schema ships in the package as
545
661
  `schemas/design-export-1.json`, and the library twin is `exportDesign()`.
546
662
 
547
- ### `wairon network flows|policy|diagram|check|why`
663
+ ### `wairon type rename-field <type> <field> <new-name>`
664
+ Renames a field of a type and respells every reference to it — each foreign
665
+ key `references: <type>.<field>` naming it, the type's own included. The field
666
+ keeps its place, type and description; its old name joins the field's rename
667
+ trace (`previousNames`), which `wairon export` shows as `formerly`, so a
668
+ consumer holding the old name reads a rename, not a removal and an addition.
669
+ Traces and prose are never rewritten. It refuses, writing nothing, a type or
670
+ field that does not exist, a name that is not an identifier, and a name another
671
+ field of the type holds or retired. The MCP twin is `sdd_rename_field`. As with
672
+ any design edit, the next `wairon lock` approves it.
673
+
674
+ ### `wairon method rename-param <component> <method> <param> <new-name>`
675
+ Renames a parameter of a contract method. The parameter moves on every
676
+ interface of the component that declares the method, keeping its place, type,
677
+ description and optionality; its old name joins the parameter's rename trace
678
+ (`previousNames`), which `wairon export` shows as `formerly`, so a generator or
679
+ consumer reads a rename, not a signature change. Each signature is re-derived
680
+ from the params (a method whose `signatureFrom` names this one follows by
681
+ itself), and an HTTP endpoint path placeholder `{old}` on the method's own
682
+ binding is respelled — the URL a caller sends is unchanged. Prose is never
683
+ rewritten. It refuses, writing nothing, a component, method or parameter that
684
+ does not exist (or a parameter the method takes from a `signatureFrom`), a name
685
+ that is not an identifier, and a name another parameter of the method holds or
686
+ retired. The MCP twin is `sdd_rename_param`. The next `wairon lock` approves it.
687
+
688
+ ### `wairon network flows|policy|diagram|check|why|declare|undeclare`
548
689
  Networking derived from the design: the reach the validator already models
549
690
  (modelled calls, Portal entries and their scopes, declared networks and their
550
691
  gateways), never addresses. Run at the family root.
@@ -562,6 +703,9 @@ gateways), never addresses. Run at the family root.
562
703
  unknown verb.
563
704
  - `why <from> <to>` prints the modelled chain behind one flow. It exits 1
564
705
  when nothing allows it.
706
+ - `declare [--description <text>]` writes the project's `network` declaration
707
+ into `.wai/project.yaml`; `undeclare` removes it (the assistant's twin is
708
+ `sdd_set_network`). It changes which rules fire: validate, then re-lock.
565
709
 
566
710
  [Derived networking](network.md) describes every output, the bindings file
567
711
  and the observed-flow format.
@@ -611,6 +755,60 @@ then move the pin with `wairon member update`. An uncached git part offline is
611
755
  declared `externals`, the name a public name in that project's L0 export table.
612
756
  An id without `::` is local — a part's subsystems included.
613
757
 
758
+ **Authoring the L0 export table.** What another project may reference is the
759
+ project's **L0 export table**: the `publicInterfaces` list of the L0 spec
760
+ (`.wai/specs/.index.yaml`). It is authored in two steps, both through the MCP
761
+ tools (an assistant runs them; a human may edit the YAML the same way):
762
+
763
+ 1. Each subsystem publishes its own surface — its inbound Portal, and the types
764
+ it shares — with `sdd_set_public_interfaces` (an L1 `publicInterfaces`).
765
+ 2. The L0 **re-exports** from those subsystems with `sdd_update_spec` on kind
766
+ `system`, delta `publicInterfaces`. Each entry names a subsystem in `from`;
767
+ `from` alone re-exports everything it publishes, `component` (optionally
768
+ narrowed with `interface`) or `typeDef` picks one item, `as` renames it
769
+ (the public name consumers write after `alias::`), `role: implement` marks
770
+ an extension point consumers implement, and `audience` decides who may see
771
+ it (below).
772
+
773
+ ```yaml
774
+ # .wai/specs/.index.yaml (excerpt)
775
+ publicInterfaces:
776
+ - from: distance # everything the distance subsystem publishes
777
+ - from: tiles
778
+ component: tile_portal
779
+ as: tiles # consumers write geo::tiles
780
+ audience: external
781
+ - from: shared
782
+ typeDef: coordinate # a shared type, as geo::coordinate
783
+ ```
784
+
785
+ The key is `publicInterfaces` — an `exports:` (or any other key the schema does
786
+ not know) is reported as `UNKNOWN_SPEC_KEY`, never silently ignored, and an
787
+ unknown setting in `.wai/project.yaml` as `UNKNOWN_CONFIG_KEY`. A reference to a
788
+ name the producer does not export is `EXTERNAL_NOT_EXPORTED`.
789
+
790
+ **Export audiences.** Each L0 entry carries an `audience` (default `instance`)
791
+ that decides who may see the public name. Narrowest first:
792
+
793
+ | Audience | Who sees it |
794
+ |----------|-------------|
795
+ | `project` | Only this project's family: its members, its parent and their siblings. |
796
+ | `department` | The family, and — on a hosted instance — the projects of the owning department. A local sibling checkout or git consumer does **not** see it. |
797
+ | `instance` | The default. Any project on the instance, and any local consumer by path or git. |
798
+ | `partner` | Also consumers outside the instance that a partner grant admits. |
799
+ | `external` | Publicly consumable. |
800
+
801
+ A consumer is read at one audience: a family project at `project`, so it sees
802
+ everything; a path, git or hosted consumer outside the family at `instance`, so
803
+ it sees `instance`, `partner` and `external` entries only. A name exported to a
804
+ narrower audience is refused by `externals add` (and `externals use`), naming
805
+ the export's audience, the consumer's and this ranking. Because a project cannot
806
+ tell from its own tree whether its consumers are hosted department units,
807
+ `validate` notes every `department` export (`EXPORT_AUDIENCE_NARROW`, a
808
+ notice): widen it to `instance` when a sibling checkout or a git consumer uses
809
+ it, or keep it and allow the notice. A re-export can narrow an audience, never
810
+ widen it (`EXPORT_WIDENS_AUDIENCE`).
811
+
614
812
  | Command | Description |
615
813
  |---------|-------------|
616
814
  | `wairon member add <alias> <source> [--project] [--description <text>]` | Create a member at `<source>` (`path`, `../path` or `git-url[#commit]`) and declare it by the shorthand. A **part** by default: a specs folder (and, outside this project, its `partOf`). With `--project` a project: its `project.yaml` declaring `<alias>` as its id and its L0, each only when absent. A git member is never scaffolded: it is pinned at the commit given, else the default branch head, and its content decides what it is |
@@ -657,7 +855,13 @@ included); applying it stages every change with a backup under each project's
657
855
  `.wai/transactions/<id>/` (never committed) and swaps them in, restoring every
658
856
  backup on any failure. A crash mid-swap leaves a journal: `wairon status` and
659
857
  `wairon validate` show it as a notice, and `wairon doctor --fix` rolls it back.
660
- No verb ever locks — each names the projects to re-lock. A member stored in a
858
+ No verb ever locks — each names the projects to re-lock. A promote (or `externalize --as project`) re-roots every
859
+ source path that moves with the new project: a path inside the new project's folder becomes
860
+ member-relative, and one outside it (source code you have not moved yet) becomes a member-relative
861
+ *planned* path — `SOURCE_FILE_PLANNED`, never a path that escapes the root — so the new project
862
+ validates on day one and you move the code when you are ready. A topic finding
863
+ (`UNCONSUMED_TOPIC`, `UNSOURCED_SUBSCRIPTION`) is sited at its topic: an allow names it
864
+ with `at: <topic>`. A member stored in a
661
865
  `../` sibling checkout on the same volume joins the same transaction: its files
662
866
  change all-or-nothing with the family's, the coordinator's journal names every
663
867
  owner (so `wairon doctor --fix` recovers from either root), and the plan says
@@ -731,21 +935,23 @@ project, so it holds nothing machine-specific: it runs `wairon mcp serve` from t
731
935
  and the server attaches to the project it is started in. A `--global` registration is
732
936
  machine-wide and names the running CLI by its absolute path.
733
937
 
734
- The local server offers 38 tools:
938
+ The local server offers 47 tools:
735
939
 
736
940
  | Group | Tools |
737
941
  |-------|-------|
738
942
  | Topology (read) | `listAgents`, `getAgent`, `listDomains`, `validateTopology`, `getProjectConfig` |
739
943
  | Authoring | `sdd_initialize_system`, `sdd_add_subsystem`, `sdd_set_public_interfaces`, `sdd_add_component`, `sdd_define_interface`, `sdd_set_endpoints`, `sdd_write_narrative`, `sdd_add_type`, `sdd_update_spec`, `sdd_delete_spec` |
740
944
  | Reading and checking | `sdd_get_spec`, `sdd_get_status`, `sdd_validate_tree` |
741
- | Renames and moves (in this tree) | `sdd_rename_component`, `sdd_rename_method`, `sdd_rename_type`, `sdd_move_methods` |
945
+ | Renames and moves (in this tree) | `sdd_rename_component`, `sdd_rename_method`, `sdd_rename_param`, `sdd_rename_type`, `sdd_rename_field`, `sdd_move_methods` |
742
946
  | Members and family migrations (each takes `dryRun`) | `sdd_add_member`, `sdd_move_member`, `sdd_externalize_subsystem`, `sdd_promote_member`, `sdd_demote_member`, `sdd_internalize_member`, `sdd_attach_member`, `sdd_detach_member`, `sdd_adopt_member`, `sdd_rename_project`, `sdd_rename_member_alias` |
743
- | Externals | `sdd_add_external`, `sdd_pin_externals`, `sdd_get_externals_status` |
947
+ | Externals | `sdd_add_external`, `sdd_update_external`, `sdd_remove_external`, `sdd_pin_externals`, `sdd_get_externals_status` |
948
+ | Public surface (the producer's side) | `sdd_list_consumers`, `sdd_surface_diff` |
949
+ | Network | `sdd_set_network`, `sdd_get_network_flows`, `sdd_explain_flow` |
744
950
  | Packs | `sdd_pack_impact` |
745
951
  | Delegation | `sdd_get_agent_brief` (the live brief for one agent; also served as the `wairon-agent://` resource) |
746
952
 
747
953
  A hosted server adds its own `sdd_host_*` and `sdd_landscape_*` tools; see the
748
- [hosted server guide](design/hosted-mcp-server.md).
954
+ [hosted server guide](https://github.com/SYW-Apps/Waffle-AIron/blob/main/docs/design/hosted-mcp-server.md).
749
955
 
750
956
  ---
751
957
 
@@ -755,7 +961,7 @@ A hosted server adds its own `sdd_host_*` and `sdd_landscape_*` tools; see the
755
961
  projects behind one endpoint — a public **data plane** (the `sdd_*` tools over
756
962
  streamable HTTP, per-project API-key scoped) and an admin **control plane**
757
963
  (project & key lifecycle, state-scoped lock). See the
758
- [hosted server guide](design/hosted-mcp-server.md) for the architecture, Docker
964
+ [hosted server guide](https://github.com/SYW-Apps/Waffle-AIron/blob/main/docs/design/hosted-mcp-server.md) for the architecture, Docker
759
965
  self-hosting, auth, and sizing.
760
966
 
761
967
  ### `wairon serve [--host <h>] [--port <p>] [--admin-host <h>] [--admin-port <p>] [--data-dir <path>] [--no-auth]`
@@ -851,10 +1057,13 @@ return the impact of every pack they applied in their results.
851
1057
 
852
1058
  | Command | Description |
853
1059
  |---------|-------------|
854
- | `wairon externals add <alias> [<source>] [--project <id>] [--ref <ref>] [--dir <dir>] [--use a,b\|'*'] [--description <text>] [--no-pin] [--dry-run] [--json]` | Declare one external in `.wai/project.yaml`. The source is the location grammar members use: `../sibling`, `hosted:<id>`, `<git url>` or `<git url>#<commit>` (the commit is the ref the pin follows, fixed there); omit it when the family provides the producer. It is checked against the producer it reaches: one answering to another id, or not exporting a `use` name to this project, takes the declaration back out and names the id or the closest exported names. Pins by default; a producer that cannot be read leaves it declared and unpinned, saying why. A refusal is one sentence naming the accepted form, and exits 1 |
855
- | `wairon externals pin [alias…] [--json]` | Pin declared externals into `.wai/externals/<alias>.yaml` and `.wai/externals.lock.yaml`; exits 1 when an alias could not be pinned (unresolved or unreachable — its previous pin stays) |
856
- | `wairon externals status [--json]` | Each pin compared with its live producer per used member — `unchanged`, `changed`, `renamed` (with the new name), `removed`, `unlocked`, `unavailable` — and each external's health (`incompatible`, `not compared`, `drifted`, `ok`). Git producers are fetched. The opt-in **live** gate: exits 1 when any external is incompatible, 2 when nothing is incompatible but something could not be compared (never a pass), 0 otherwise |
857
- | `wairon externals list [--json]` | The declared externals, how each resolves and what is pinned; a malformed declaration is listed with its problem, never hidden |
1060
+ | `wairon externals add <alias> [<source>] [--project <id>] [--ref <ref>] [--dir <dir>] [--use a,b\|'*'] [--description <text>] [--no-pin] [--dry-run] [--json]` | Declare one external in `.wai/project.yaml`. The source is the location grammar members use: `../sibling`, `hosted:<id>`, `<git url>` or `<git url>#<commit>` (the commit is the ref the pin follows, fixed there); omit it when the family provides the producer. Text that is no location (braces, quotes, whitespace) is refused. The declaration is checked against the producer it reaches **before** anything is written — `--dry-run` included: one answering to another id, or not exporting a `use` name to this project, is refused naming the id, the closest exported names, or the narrower audience the name is exported to (see Export audiences). Pins by default; a producer that cannot be read leaves it declared and unpinned, saying why, and exits 2 (nothing was pinned). A refusal is one sentence naming the accepted form, and exits 1 |
1061
+ | `wairon externals use <alias> [--add a,b\|'*'] [--remove c] [--dry-run] [--json]` | Change one declared external's `use` imports, so the specs may name the producer's public names bare (e.g. so an `implements` of the producer's trait spells its types as the producer does — though `alias::name` and the bare imported name compare as one type either way). An added name the producer does not export to this project is refused. The pin is untouched. Exits 1 on a refusal |
1062
+ | `wairon externals remove <alias> [--dry-run] [--json]` | Remove one external: its declaration and its pin (`.wai/externals/<alias>.yaml` and its lock entry) together. An orphaned pin — one whose declaration was deleted by hand, which `externals list` shows — is removed too. Exits 1 when the alias is neither declared nor pinned |
1063
+ | `wairon externals consumers [--json]` | From a producer's root: the family projects in reach that consume it — each with the alias and section it declares it under and the public names its specs use — so a producer sees who breaks before it changes its surface. A sibling checkout, git or hosted consumer outside the family declares its dependency on its own side and is not visible here |
1064
+ | `wairon externals pin [alias…] [--json]` | Pin declared externals into `.wai/externals/<alias>.yaml` and `.wai/externals.lock.yaml`. The snapshot is rewritten whenever anything it carries moved — not only the signatures the digest covers: a producer that added `abi: c`, changed a transport or a role, or recorded a rename is refreshed by a re-pin. Exits 1 when an alias could not be pinned (unresolved or unreachable — its previous pin stays) |
1065
+ | `wairon externals status [--json]` | Each pin compared with its live producer per used member — `unchanged`, `changed`, `renamed` (with the new name), `removed`, `unlocked`, `unavailable` — and each external's health (`incompatible`, `not compared`, `drifted`, `ok`); a pinned snapshot that no longer carries what the producer says (a stale `abi`, transport or role) is `drifted`, never `ok`, and names the stale facts. A use the lock does not hold is still compared with the live producer: gone from it, it is `removed` or `renamed`. Git producers are fetched. The opt-in **live** gate: exits 1 when any external is incompatible, 2 when nothing is incompatible but something could not be compared (never a pass), 0 otherwise |
1066
+ | `wairon externals list [--json]` | The declared externals, how each resolves and what is pinned; a malformed declaration, and an orphaned pin whose declaration is gone, are listed with their problem, never hidden |
858
1067
  | `wairon surface export \| import \| list [--audience <level>] [--format native\|openapi] [--portal <id>] [--out <path>] [--source <path>]` | Exchange a public surface document: export this project's (native snapshot or one OpenAPI document per portal), import one, or list them |
859
1068
  | `wairon produce <notion\|miro> [--page <id>] [--token <token>]` | Project the local spec tree to Notion or Miro (the token comes from `--token`, the environment, else a prompt; nothing is stored) |
860
1069
 
@@ -9,7 +9,7 @@ rules) that wairon reads and imports. A product built this way (a
9
9
  wairon installation: no fork, no build step, and wairon core never learns
10
10
  the platform exists.
11
11
 
12
- A working wrapper lives at [`examples/wrapper/`](../examples/wrapper/) and
12
+ A working wrapper lives at [`examples/wrapper/`](https://github.com/SYW-Apps/Waffle-AIron/blob/main/examples/wrapper/) and
13
13
  is guarded by `tests/examples/wrapper-example.test.ts`, so it cannot
14
14
  silently rot.
15
15
 
@@ -17,7 +17,7 @@ silently rot.
17
17
 
18
18
  > **New model (preferred).** Installing a pack makes it *available*; a project
19
19
  > then *selects* it. Installing no longer grants a pack authority over every
20
- > project on the machine — see [pack scoping](design/pack-scoping.md).
20
+ > project on the machine — see [pack scoping](https://github.com/SYW-Apps/Waffle-AIron/blob/main/docs/design/pack-scoping.md).
21
21
 
22
22
  ```sh
23
23
  wairon pack install ./appenser-1.2.0.wpack # into this wairon install's store
@@ -101,6 +101,7 @@ project's doctrine anywhere:
101
101
  - uses: SYW-Apps/Waffle-AIron/.github/actions/setup-wairon@v5
102
102
  with:
103
103
  packs: sync # sync (default) | none (bundled repos) | explicit sources
104
+ - run: npm ci # a TS/JS project with code: its own dependencies first (see docs/cli.md)
104
105
  - run: wairon validate --ci
105
106
  ```
106
107
 
@@ -267,7 +268,7 @@ wairon pack add ./my-product-pack --global # machine-wide
267
268
 
268
269
  The installer injects once and leaves — from then on plain `wairon
269
270
  validate` enforces the doctrine, and `wairon pack remove` undoes it. See
270
- [`examples/wrapper/install.js`](../examples/wrapper/install.js) for the
271
+ [`examples/wrapper/install.js`](https://github.com/SYW-Apps/Waffle-AIron/blob/main/examples/wrapper/install.js) for the
271
272
  template. A spec-only product (implementation lives on a cloud platform)
272
273
  needs nothing more: the wrapped repository holds specs + docs, wairon holds
273
274
  the gate.
@@ -595,7 +596,7 @@ project's externals against their live producers, and the family checks),
595
596
  `globalPacksDir`, `discoverPacks`, `setProjectRoot`, the `SddRule` /
596
597
  `RuleContext` / `LoadedExtensions` / `DeclarativePack` types, `SDD_RULES`,
597
598
  `composeRuleSequence`, and the spec loaders/savers. See
598
- [`examples/wrapper/wrapper.js`](../examples/wrapper/wrapper.js).
599
+ [`examples/wrapper/wrapper.js`](https://github.com/SYW-Apps/Waffle-AIron/blob/main/examples/wrapper/wrapper.js).
599
600
 
600
601
  ## Spec-only wrappers
601
602
 
package/docs/network.md CHANGED
@@ -1,27 +1,32 @@
1
1
  # Derived networking: `wairon network`
2
2
 
3
- wairon derives a system's network from its design. The inputs are the calls you model, the entries you declare on Portals (`invokedBy: {kind: entry, scope: outside | network}`), the networks your projects declare (`network: true` in `.wai/project.yaml`) and the gateways on them (`variant: gateway`). From these it answers who may talk to whom, and on what grounds.
3
+ wairon derives a system's network from its design. The inputs are the calls you model, the entries you declare on Portals (`invokedBy: {kind: entry, scope: outside | network}`), the networks your projects declare (`network` in `.wai/project.yaml`, written by `wairon network declare [--description <text>]` — `undeclare` removes it — or by the assistant's `sdd_set_network` tool) and the gateways on them (`variant: gateway`). From these it answers who may talk to whom, and on what grounds.
4
+
5
+ **The gate comes first.** Every network command reads the design as the validator judges it. A flow an error sits on (`GATEWAY_BYPASSED`, ...) is **refused**: the matrix marks its row `REFUSED` with the findings, a policy never admits it (and `policy` exits 1, naming it at the head of the file), `why` answers that the gate refuses it, and `check` files an observed flow that only a refused flow would allow as disallowed. A warning (`ENTRY_UNPROVEN`, ...) only marks its row. Each command prints the findings it was judged against.
4
6
 
5
7
  Specs never hold an address, a port, a namespace or a label. Those deployment facts come from a **bindings file** that the team keeps outside `.wai/` and passes when generating policy. The design stays logical, and the deployment stays yours.
6
8
 
7
- Every command runs at the project you are in. At a project that declares members, the commands cover its whole family; `--no-recursive` limits them to that project alone. Run them at the family root: only the family run can narrow a `network` entry to the callers it really has.
9
+ Every command runs at the project you are in. At a project that declares members, the commands cover its whole family; `--no-recursive` limits them to that project alone. Inside a member, they answer from the enclosing family — the parent's network declaration and the callers the parent models, the same answer the family root gives — and `--no-recursive` judges the member alone. Only the family run can narrow a `network` entry to the callers it really has.
8
10
 
9
11
  ## The outputs, and what each is for
10
12
 
11
13
  | Command | Output | What a team does with it |
12
14
  |---|---|---|
13
- | `wairon network flows [--format json\|csv\|markdown] [--out <file>]` | The **allowed-flows matrix**: one row per flow (from, to, transport, binding, networks crossed, gateway, evidence) | Review it in pull requests as the authoritative "who talks to whom". A design change that adds a dependency shows up as a new row before any code or firewall change exists. The CSV suits spreadsheets and audits, and the Markdown suits review comments. |
14
- | `wairon network policy --bindings <file> [--out <file>]` | **Kubernetes `NetworkPolicy`** YAML: one ingress policy per called workload | Commit it to the deployment repository and diff it in CI. A design change becomes a policy diff that the security reviewer reads in the same PR. |
15
+ | `wairon network flows [--format json\|csv\|markdown] [--out <file>]` | The **allowed-flows matrix**: one row per flow (from, to, transport, binding, networks crossed, gateway, evidence) | Review it in pull requests as the authoritative "who talks to whom". A design change that adds a dependency shows up as a new row before any code or firewall change exists. The CSV suits spreadsheets and audits, and the Markdown suits review comments. A design that fails the gate exits 1. |
16
+ | `wairon network policy --bindings <file> [--out <file>]` | **Kubernetes `NetworkPolicy`** YAML: one ingress policy per called workload | Commit it to the deployment repository and diff it in CI. A design change becomes a policy diff that the security reviewer reads in the same PR. A design that fails the gate exits 1: the refused flows are left out and named at the head of the file. A design name the bindings leave unbound also exits 1. |
15
17
  | `wairon network diagram [--out <file>]` | A **Mermaid** flowchart: declared networks as nested boundaries, gateways on them, an `outside` node, and flows aggregated per workload pair | Use it as the data-flow diagram with trust boundaries that threat modelling starts from: each boundary an edge crosses is a place to ask the STRIDE questions. It also works for onboarding and on architecture pages. The hosted canvas draws the same picture in its **Network** view (`/canvas/<project>/network`; the system spec's `diagram.defaultView: network` opens the canvas there). |
16
- | `wairon network check --observed <file> [--bindings <file>]` | **Unexpected** flows (seen, but nothing allows them), **unknown verbs** (an allowed pair, but a method and path no contract declares) and **unexercised** flows (allowed, never seen) | Audit a live system against its design. Exits 1 on anything unexpected or unknown, so it can gate a pipeline. Unexercised flows are review items, such as dead dependencies or rules to tighten, and never fail the command. |
17
- | `wairon network why <from> <to>` | The modelled chain behind a flow: the entry or call step, each network entered, the gateway, and the verb with its binding | Answer a firewall change request or an incident question ("why does checkout talk to payments?") from the design, in one command. Exits 1 when nothing allows the flow, and names what does reach the target instead. |
18
+ | `wairon network check --observed <file> [--bindings <file>]` | **Unexpected** flows (seen, but nothing allows the pair), **disallowed** flows (a verb the destination declares, but not allowed from that source, or only by a flow the gate refuses), **unknown verbs** (an allowed pair, but a method and path no contract of the destination declares) and **unexercised** flows (allowed, never seen) | Audit a live system against its design. Exits 1 on anything unexpected, disallowed or unknown, so it can gate a pipeline. Unexercised flows are review items, such as dead dependencies or rules to tighten, and never fail the command. |
19
+ | `wairon network why <from> <to>` | The modelled chain behind a flow: the entry or call step, each network entered, the gateway, and the verb with its binding | Answer a firewall change request or an incident question ("why does checkout talk to payments?") from the design, in one command. Exits 1 when nothing allows the flow, and names what does reach the target instead; says so when the gate refuses the flow, when a name is unknown (a typo, not a refusal), and — exiting 0 — when the callee is reached in-process, which is never a network flow. |
20
+ | `wairon network declare [--description <text>]` / `undeclare` | Writes (or removes) `network` in `.wai/project.yaml` | Declare the boundary without a hand edit. It changes which rules fire, so validate, then re-lock. |
18
21
 
19
- The MCP server offers the same reads to an assistant while it designs: `sdd_get_network_flows` (optionally `to: <project | portal | portal.verb>`) and `sdd_explain_flow` (`from`, `to`). The hosted server serves the view model at `GET /web/projects/network?projectId=…`.
22
+ The MCP server offers the same reads to an assistant while it designs: `sdd_get_network_flows` (optionally `to: <project | portal | portal.verb>`) and `sdd_explain_flow` (`from`, `to`), and the declaration as `sdd_set_network` (`declared`, `description`). The hosted server serves the view model at `GET /web/projects/network?projectId=…`.
20
23
 
21
24
  ### What becomes a flow
22
25
 
23
26
  - Only **network transports** produce flows: HTTP, gRPC, GraphQL, MessageBus and Custom. In-process libraries, CLI commands, IPC and JSON-RPC over stdio never do.
24
27
  - A **modelled call** into a network verb from another subsystem or project is a flow from the calling component. It names the networks it enters and the gateway it passes.
28
+ - A verb's **binding** is the path a cluster sees: an HTTP endpoint is joined under its Portal's `basePath` (`basePath: /v1` and `POST /orders` bind `POST /v1/orders`), so L7 checks match real paths.
29
+ - An **Adapter's transport** is its target Portal's: the Adapter calls the Portal's verbs over it. An Adapter may state one (`sdd_add_component` fills it in from the Portal its `dependsOn` names); a stated transport that disagrees with the target's is `ADAPTER_TRANSPORT_MISMATCH`, so changing a Portal's transport shows its impact on every Adapter that calls it.
25
30
  - An **`outside` entry** is a flow from outside the verb's innermost network. For an outermost network that is the world (`outside`); for a nested network it is the next network out (`network:<parent>`). Scopes are relative.
26
31
  - A **`network` entry** is a flow from anywhere inside the network. At the family root, when modelled callers prove the entry, the blanket row is replaced by those callers. The matrix is therefore least-privilege by construction: "the network may reach this" becomes "these two Adapters reach this".
27
32
 
@@ -36,7 +41,7 @@ The MCP server offers the same reads to an assistant while it designs: `sdd_get_
36
41
  - A component, as `project::component` or bare for the root's own.
37
42
  - On the receiving end, also `project::portal.verb`.
38
43
 
39
- Each party has a **default workload name**: a member's project key, or for the root project's own components their subsystem. When the root's component reaches into another project, the caller is named by its component.
44
+ Each party has a **default workload name**: a member's project key, or for the root project's own components their subsystem — also when the root's component reaches into another project: the caller is named by its workload (its subsystem, the pod it runs in), with the component kept for the evidence, so a bindings file written per service keeps covering it after a callee is promoted.
40
45
 
41
46
  ## The bindings file
42
47
 
@@ -64,7 +69,11 @@ outside: # what "outside" means for a policy
64
69
  - `outside` lists the CIDR blocks admitted for callers from outside, for example the internet, a load balancer range or an intranet. Without it, nothing from outside is admitted.
65
70
  - A source in another namespace gets a `namespaceSelector` on `kubernetes.io/metadata.name`. A network-wide source bound without a namespace matches in any namespace (`namespaceSelector: {}`).
66
71
 
67
- **Nothing is guessed.** A design name the bindings do not map is selected by the placeholder label `wairon.dev/workload: <name>`, which matches no pod until you bind it or label one. It is listed in the document and on stderr, and at the top of the output under `# UNBOUND:`. MessageBus flows travel through the broker rather than pod to pod, so they are listed as `# NOT EXPRESSED` instead of opening ingress. Default deny is implied: each policy selects its workload and admits only what the design allows.
72
+ **Nothing is guessed.** A design name the bindings do not map is selected by the placeholder label `wairon.dev/workload: <name>`, which matches no pod until you bind it or label one. It is listed in the document and on stderr, and at the top of the output under `# UNBOUND:`, and `policy` exits 1 so a pipeline notices. MessageBus flows travel through the broker rather than pod to pod, so they are listed as `# NOT EXPRESSED` instead of opening ingress. Default deny is implied: each policy selects its workload and admits only what the design allows.
73
+
74
+ **Renamed projects.** A bindings key, or a name in an observed-flow file, that is a project's former id (its rename trace, `previousIds`) still resolves to the renamed project, and the output carries a `# NOTE` naming the rename, so you can update the key at your pace instead of finding a policy that selects nothing or a legitimate flow filed as unexpected. `wairon project rename` lists the binding and telemetry keys to rename in its plan.
75
+
76
+ **Mixed reach.** A NetworkPolicy cannot split traffic by path. When one workload serves verbs of different reach on one port — a verb entered from `outside` and a verb only the network may call — its policy has to admit the union, and `policy` says so in a `# NOTE` naming the verbs. To keep the narrower verb narrow at L4, bind the Portal on its own (a `<project>::<portal>` key with its own selector or port): it then gets a policy of its own.
68
77
 
69
78
  ## The observed-flow format
70
79
 
@@ -88,6 +97,7 @@ Or JSON with the same fields, as a list or as `{ "flows": [...] }`:
88
97
  - A row without `method` and `path` is an L4 observation: it exercises every allowed flow between the pair.
89
98
  - A row with both is checked at L7 against the verbs' bindings. `{id}` and `:id` path segments match any value, and gRPC paths (`/pkg.Service/Method`) match the `service/method` binding.
90
99
  - A malformed row (no source or destination, or a non-integer count) is refused with its row number, and nothing is half-read.
100
+ - A name that is a project's former id is mapped to the renamed project, with a `# NOTE` (see **Renamed projects** above).
91
101
 
92
102
  ## A typical loop
93
103