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

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.
package/docs/cli.md CHANGED
@@ -68,6 +68,15 @@ warnings as errors (notices are printed and counted, never fatal).
68
68
  member's own gate, and this project's externals composed against their live
69
69
  producers. `--no-recursive` runs this project's gate alone; `--family` runs
70
70
  the family run from a member.
71
+ - **Where the network proofs run.** The proofs that need the whole family —
72
+ `ENTRY_UNPROVEN` (a `network` entry no modelled caller reaches), the gateway
73
+ rules for members, cross-project reach — are judged by the family run at the
74
+ **family root** (the project that declares the members). A member validated on
75
+ its own judges its own gate with its `network` entries counted as declared, and
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
79
+ repository's.
71
80
  - **Externals, compared live (advisory: the pin gates).** The gate judges each
72
81
  external against its pin. Then every external the run did not compose is
73
82
  compared with its **live** producer, offline, and printed in its own section:
@@ -150,8 +159,8 @@ release and changes nothing: a lock never reaches below its project.)
150
159
 
151
160
  **Code linkage is not part of the approval.** The gate identity and the per-spec
152
161
  digests read each spec's **design**: where it is realized — `sourcePath`,
153
- `symbol`, `exportedVia`, `simPath`, `injectedParams`, conformance tiers, a mount's
154
- `via`, `externalLinks` — and the `createdAt`/`updatedAt` timestamps are left
162
+ `symbol`, `exportedVia`, `simPath`, `injectedParams`, conformance tiers, a Portal's
163
+ `router`, `externalLinks` — and the `createdAt`/`updatedAt` timestamps are left
155
164
  out (`specsReading: design`). Linking code to an approved design, or a change and
156
165
  its revert, never stales the lock; declare planned `sourcePath`s at design time.
157
166
 
@@ -276,6 +285,7 @@ With inputs (all optional):
276
285
  working-directory: packages/api # where the .wai/ tree lives (default: .)
277
286
  wairon-version: '6.0.0' # version or npm dist-tag, 6.0.0 or later (default: latest)
278
287
  strict: true # fail when nothing was approved — use it once you have locked (default: false)
288
+ validate: true # also run `wairon validate --ci` there (default: true)
279
289
  node-version: '20' # (default: '20')
280
290
  runs-on: ubuntu-latest # (default: ubuntu-latest)
281
291
  ```
@@ -289,6 +299,11 @@ releases, and the default `latest` moves.
289
299
  On a `pull_request` event the default checkout is the merge commit, so what the
290
300
  gate judges is literally the design that would land.
291
301
 
302
+ After the approval check the job runs `wairon validate --ci` in the same
303
+ directory (`validate: false` turns it off). Point `working-directory` at the
304
+ **family root** — the project that declares the members — so that run is the
305
+ family run, which judges the network proofs no member's own gate can.
306
+
292
307
  **A failing job does not block a merge on its own.** A workflow can only fail;
293
308
  making a failing job stop a merge is a branch-protection / ruleset setting on
294
309
  your repository ("Require status checks to pass" → add this job). No workflow
@@ -304,7 +319,8 @@ machine-wide MCP config) also needs `--global`; each file it replaces is backed
304
319
  up beside itself. `--report <section>` prints one section's report and writes
305
320
  nothing: `chaining` (the member/reference migration plan) or
306
321
  `composed-validation` (what the family run changes about each member's
307
- findings); it never combines with `--fix`.
322
+ findings) or `reachability` (the reachability migration's plan); it never
323
+ combines with `--fix`.
308
324
 
309
325
  Among its spec repairs, `--fix` rewrites every stored type position that is an
310
326
  alias of its canonical spelling (`string[]` becomes `list<string>`, `boolean`
@@ -316,6 +332,22 @@ literal union, a union mixing in a primitive, a `number` with no proposal) is
316
332
  listed with its replacement for an author. Plain `wairon doctor` prints the same
317
333
  plan without writing it.
318
334
 
335
+ **The reachability migration.** `--fix` also moves the tree onto the
336
+ reachability model: `portalType` becomes `transport` (`HTTP_API` → `HTTP`, a
337
+ `Custom` Portal that binds no wire address → `InProcess`), a listener's `mounts`
338
+ become a Portal-level entry on each mounted Portal and its implementation's
339
+ `router` (the mount's `via`), `invokedBy: external` becomes `entry` on a Portal
340
+ verb and `runtime` elsewhere, an authored export `type` that agrees with the derived kind is dropped,
341
+ in-process `Custom` endpoint addresses are removed, and allows of retired codes
342
+ are dropped or renamed. It never invents design: a Portal that was neither a
343
+ listener nor mounted gets no entry, and a `sibling-subsystem` caller, an export
344
+ type that disagrees, an endpoint outside every old prefix and a router already
345
+ set otherwise are listed for an author. Plain `wairon doctor` prints the plan;
346
+ `--report reachability` lists every rewrite and every form left for an author.
347
+ The design changes, so one `wairon lock` follows. After it, declare the entries
348
+ the migration cannot know — a CLI, a stdio tool surface (`JSONRPC`), a library
349
+ (`InProcess`) — and model a caller or remove each verb still reported unreached.
350
+
319
351
  `--fix` also re-expresses a format-2 lock record that still provably covers the
320
352
  tree as format 3 (see `wairon lock`), keeping who approved it and when; when it
321
353
  no longer covers the tree, doctor says that one `wairon lock` is needed.
@@ -512,6 +544,28 @@ promise and how a consumer follows renames).
512
544
  - A tree without an L0 is refused. The JSON Schema ships in the package as
513
545
  `schemas/design-export-1.json`, and the library twin is `exportDesign()`.
514
546
 
547
+ ### `wairon network flows|policy|diagram|check|why`
548
+ Networking derived from the design: the reach the validator already models
549
+ (modelled calls, Portal entries and their scopes, declared networks and their
550
+ gateways), never addresses. Run at the family root.
551
+
552
+ - `flows [--format json|csv|markdown] [--out <file>]` prints the allowed-flows
553
+ matrix, narrowed to the modelled callers wherever the family proves a
554
+ `network` entry.
555
+ - `policy --bindings <file> [--out <file>]` generates Kubernetes
556
+ `NetworkPolicy` from the matrix and a bindings file you keep outside
557
+ `.wai/`. Names it cannot bind are listed, never guessed.
558
+ - `diagram [--out <file>]` prints a Mermaid flowchart of the networks as
559
+ trust boundaries, with the gateways, outside and the flows.
560
+ - `check --observed <file> [--bindings <file>]` compares observed live flows
561
+ (CSV or JSON) with the matrix. It exits 1 on an unexpected flow or an
562
+ unknown verb.
563
+ - `why <from> <to>` prints the modelled chain behind one flow. It exits 1
564
+ when nothing allows it.
565
+
566
+ [Derived networking](network.md) describes every output, the bindings file
567
+ and the observed-flow format.
568
+
515
569
  ---
516
570
 
517
571
  ## Members
@@ -762,7 +816,7 @@ principal.
762
816
  | `wairon host permission set --user <userId> --capability <cap> [--value yes\|approval\|no\|inherit] [--project <id> [--subsystem <id>] \| --unit <unitId> \| --instance]` | Set one assignment. Capabilities: `project:read`, `project:create`, `project:write`, `project:admin`, `approval:decide`. The scope defaults to the instance |
763
817
  | `wairon host permission list [--user <userId>] [scope flags]` / `remove --id <assignmentId>` | List or remove assignments |
764
818
  | `wairon host key mint --project <id\|*> --owner <userId> [--label <label>]` | Mint an API key acting as the owner's live permissions (plaintext shown once). The project must exist (`*` aside); a token naming a project covers its members. Without `--owner` (the legacy `--role editor\|admin` mint) the key resolves to **zero** permissions and the command warns |
765
- | `wairon host doctor [--fix]` | Inspect the data dir and, with `--fix`, migrate it: roll back a transaction a crash left unfinished there, apply the permission-model migration, then register every hosted family's members as records of their own (no grant written — access is inherited through the parent chain — and every member-qualified key entry rewritten to a record id), all or nothing, audited |
819
+ | `wairon host doctor [--fix]` | Inspect the data dir and, with `--fix`, migrate it: roll back a transaction a crash left unfinished there, apply the permission-model migration, migrate every hosted project's spec tree onto the reachability model (each rewritten project is audited as `migration.reachability` and owes one re-lock by its approvers; the server never does this on its own when it binds a project, because the rewrite moves the approved design and a read-only request must not write), then register every hosted family's members as records of their own (no grant written — access is inherited through the parent chain — and every member-qualified key entry rewritten to a record id), all or nothing, audited |
766
820
  | `wairon host key list [--project <id>]` | List API keys |
767
821
  | `wairon host key revoke --id <id>` | Revoke a key |
768
822
  | `wairon host lock --project <id>` | The same lock flow as `wairon lock` (design gate, `members`, `code` beside the claim, format 3) against the hosted project |
@@ -22,7 +22,7 @@ read back into a tree.
22
22
  |---|---|
23
23
  | CLI | `wairon export` prints the JSON to stdout, and nothing else. `wairon export --out design.json` writes the file and reports the path and the approval state. A project with member projects names them (on stderr without `--out`, when it holds no design of its own): they are dependencies, exported from their own roots. See [cli.md](cli.md#wairon-export---out-file). |
24
24
  | Library | `const { exportDesign } = require('@wairon/cli')` (or `import` from an ES module), then `exportDesign(outPath?, approval?)`, which returns the document and writes it when `outPath` is given. The package ships TypeScript declarations for the library entry (`dist/index.d.ts`, the `types` field), so `DesignExport` and `exportDesign` are typed. |
25
- | JSON Schema | `schemas/design-export-1.json`, shipped in the package (`require.resolve('@wairon/cli/schemas/design-export-1.json')`). It is a JSON Schema draft-07 document (it declares `$schema`) and carries no `$id`: reference it by its path in the package. It is generated from the zod schema in `src/models/design-export.ts` at build time, and a test fails if the two drift. |
25
+ | JSON Schema | `schemas/design-export-2.json`, shipped in the package (`require.resolve('@wairon/cli/schemas/design-export-2.json')`). It is a JSON Schema draft-07 document (it declares `$schema`) and carries no `$id`: reference it by its path in the package. It is generated from the zod schema in `src/models/design-export.ts` at build time, and a test fails if the two drift. |
26
26
 
27
27
  The CLI decides the approval verdict first, the same way `wairon lock-check` does (not strict),
28
28
  and stamps it. It also exports a tree that is not approved, stamped with its state.
@@ -30,10 +30,13 @@ and stamps it. It also exports a tree that is not approved, stamped with its sta
30
30
  ## Compatibility promise
31
31
 
32
32
  - **`format`** is always `"wairon-design"`.
33
- - **`formatVersion`** is `MAJOR.MINOR` and is currently `1.0`.
33
+ - **`formatVersion`** is `MAJOR.MINOR` and is currently `2.0`. 2.0 is the reachability
34
+ model's: `portalType` became `transport`, listener `mounts` are gone, a Portal may carry
35
+ the `invokedBy` entry its verbs inherit, and `invokedBy` kinds are `entry | runtime` with a
36
+ `scope`.
34
37
  - **A minor version only adds.** It adds new fields, or new members of a closed set (a
35
38
  `role`, an approval state). A consumer must **ignore fields it does not know**. A 1.x
36
- document validates against `design-export-1.json`.
39
+ document validates against `design-export-2.json`.
37
40
  - **A major version breaks.** It removes or renames a field, or changes what one means. Every
38
41
  major version is named in the CHANGELOG, and wairon emits one major version at a time.
39
42
  - **A design change is never a format change.** Two exports of different trees in the same
@@ -162,11 +165,11 @@ the resolved L0 export table, as `design_export_entry` rows.
162
165
  | Field | Meaning |
163
166
  |---|---|
164
167
  | `key`, `name`, `description`, `status`, `subsystem` | As declared. |
165
- | `stereotype`, `variant`, `dependencyClass`, `durability`, `portalType` | As declared. |
168
+ | `stereotype`, `variant`, `dependencyClass`, `durability` | As declared. |
169
+ | `transport`, `abi`, `invokedBy` | On a Portal: its transport, the `abi` of an `InProcess` Portal that declares one, and the entry its verbs inherit. |
166
170
  | `owns`, `dependsOn` | Keys. |
167
171
  | `emits`, `subscribesTo` | The topics the component publishes to and consumes from. |
168
172
  | `auth`, `basePath`, `dispatch` | A Portal's authentication, base path and dispatch table. Dispatch targets are keys. |
169
- | `mounts` | A listener's mounts. Each portal is a key. |
170
173
  | `patterns`, `externalLinks` | As declared. |
171
174
  | `formerly` | The rename trace. See [Renames](#renames-formerly). |
172
175
  | `ext` | Pack extension data. |
@@ -256,9 +259,11 @@ deployment. The export carries the facts a consumer needs to decide for its own
256
259
 
257
260
  - **Lifecycle roots** (`subsystems[].lifecycle`): what the runtime invokes, with the phase
258
261
  (`init`, `shutdown`, `cyclic`, `interrupt` or `scheduled`).
259
- - **Endpoints and transports** (`interfaces[].methods[].endpoint`, `components[].portalType`):
260
- calls that arrive from outside the process. A `Custom` endpoint can be an in-process binding.
261
- - **Listener mounts** (`components[].mounts`): which listener serves which portal.
262
+ - **Transports and endpoints** (`components[].transport`, `interfaces[].methods[].endpoint`):
263
+ how callers reach each Portal. An `InProcess` Portal is a library and binds no endpoint;
264
+ its `abi` says how a foreign language links it.
265
+ - **Entries** (`components[].invokedBy`, `interfaces[].methods[].invokedBy`): the verbs callers
266
+ outside the design reach, with their `scope`.
262
267
  - **The export table** (`project.exports`): what other projects link against.
263
268
 
264
269
  For example, one generator might emit a `main` that runs the `init` roots and serves the HTTP
@@ -0,0 +1,99 @@
1
+ # Derived networking: `wairon network`
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.
4
+
5
+ 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
+
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.
8
+
9
+ ## The outputs, and what each is for
10
+
11
+ | Command | Output | What a team does with it |
12
+ |---|---|---|
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 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
+
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=…`.
20
+
21
+ ### What becomes a flow
22
+
23
+ - 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
+ - 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.
25
+ - 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
+ - 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
+
28
+ ### Naming parties
29
+
30
+ `from`, `to`, the observed-flow names and the binding keys all use design names:
31
+
32
+ - `outside`.
33
+ - `network` or `network:<id>`, meaning anywhere inside a declared network. A network's id is the key of the project that declares it; plain `network` is the family root's.
34
+ - A project key such as `orders`.
35
+ - A subsystem, as `project::subsystem` or bare for the root's own.
36
+ - A component, as `project::component` or bare for the root's own.
37
+ - On the receiving end, also `project::portal.verb`.
38
+
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.
40
+
41
+ ## The bindings file
42
+
43
+ YAML, kept wherever your deployment config lives (never in `.wai/`):
44
+
45
+ ```yaml
46
+ workloads:
47
+ orders: # a project
48
+ selector: { app.kubernetes.io/name: orders }
49
+ namespace: shop
50
+ port: 8080 # the port its Portals listen on (optional)
51
+ platform::api_gateway: # a Portal: more specific than its project
52
+ selector: { app.kubernetes.io/name: edge }
53
+ namespace: edge
54
+ port: 8443
55
+ edge: # a subsystem of the root project
56
+ selector: { app.kubernetes.io/name: edge }
57
+ network: # "anywhere in the root network": a label every member pod carries
58
+ selector: { platform.example/member: "true" }
59
+ outside: # what "outside" means for a policy
60
+ - 0.0.0.0/0
61
+ ```
62
+
63
+ - `workloads` maps a design name to a `selector` (labels, required and non-empty), a `namespace` (optional) and a `port` (optional, 1–65535). The most specific key a party has wins, in the order component, subsystem, project, default workload name.
64
+ - `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
+ - 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
+
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.
68
+
69
+ ## The observed-flow format
70
+
71
+ Export observed flows from whatever sees them: a service-mesh telemetry export, an eBPF flow log (Cilium/Hubble, Retina), or VPC flow logs once your tooling has mapped addresses to workloads. wairon never sees an IP address. Name each end by a design name, or by a label value that one of your bindings' selectors carries (pass `--bindings` so it can be mapped back).
72
+
73
+ CSV with a header row; `source` and `destination` are required, the rest optional:
74
+
75
+ ```csv
76
+ source,destination,transport,method,path,count
77
+ outside,edge,HTTP,GET,/api/orders,120
78
+ edge,orders,HTTP,POST,/orders,40
79
+ orders,billing,HTTP,POST,/charges,38
80
+ ```
81
+
82
+ Or JSON with the same fields, as a list or as `{ "flows": [...] }`:
83
+
84
+ ```json
85
+ [{ "source": "orders", "destination": "billing", "method": "POST", "path": "/charges", "count": 38 }]
86
+ ```
87
+
88
+ - A row without `method` and `path` is an L4 observation: it exercises every allowed flow between the pair.
89
+ - 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
+ - A malformed row (no source or destination, or a non-integer count) is refused with its row number, and nothing is half-read.
91
+
92
+ ## A typical loop
93
+
94
+ ```sh
95
+ wairon network flows --format markdown --out docs/flows.md # review in the PR
96
+ wairon network policy --bindings ../deploy/bindings.yaml --out ../deploy/netpol.yaml
97
+ wairon network check --observed hubble-export.csv --bindings ../deploy/bindings.yaml
98
+ wairon network why shop::checkout_adapter payments::payments_api.charge
99
+ ```
@@ -299,7 +299,9 @@ The **Adapter** is the single boundary to the outside world, in two shapes:
299
299
 
300
300
  **Inbound vs outbound:** the **Portal** is *our* front door (others call us); the
301
301
  **Adapter** is *our* client to external systems (we call/connect out, including
302
- publishing). They are different roles, not duplicates.
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*).
303
305
 
304
306
  **Emission has no special block, and is optional.** Events are a pattern for
305
307
  **decoupled reactions** — used when a producer should not know its consumers.
@@ -513,6 +515,10 @@ Repository whose Store it reads (`UNOWNED_QUERY`).
513
515
  > checks are Orchestrators it depends on — typically read logic that gathers the
514
516
  > caller's grants and pure logic that rules on them — never members it owns. A
515
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*).
516
522
 
517
523
  The variant carries the implementation guidance (§8). The Gateway *pattern*
518
524
  (`Portal + ingress Orchestrator + interceptor Specialists`) is retired
@@ -631,17 +637,94 @@ may — a dependency from any other subsystem is `CROSS_SUBSYSTEM_UNLISTED_CONSU
631
637
  (an error). When several entries publish the same component, each naming
632
638
  consumers, the union of their lists is the whole set.
633
639
 
634
- ### Listeners mount Portals
635
- A Portal's routes are its methods' endpoint bindings; the **listener** that
636
- hands the Portal its requests declares that with **`mounts`** on its own L2
637
- spec — each mount names the `portal`, the path `prefixes` it routes there, and
638
- optionally `via`, the router entry the Portal's file exports for it. An HTTP
639
- Portal that no listener mounts, and that declares no `mounts` itself (which
640
- marks it as a listener, even with `mounts: []`), is `UNMOUNTED_PORTAL`: nothing
641
- would ever reach it. `mounts: []` is honest only on a Portal that IS the whole
642
- HTTP server and serves just its own routes; a service with several HTTP
643
- Portals has one listener that mounts each of them. A prefix matches whole path segments (`/web` covers
644
- `/web/admin`, not `/webhooks`). Only HTTP endpoints are judged.
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.
645
728
 
646
729
  ### Forwarding a contract: `signatureFrom`
647
730
  A method that takes its params and returns from somewhere else names the source
@@ -951,3 +1034,9 @@ tell apart components that share a noun, and it matches the block:
951
1034
  14. A gateway is a Portal with the `gateway` variant. Variants load built-in →
952
1035
  global → project and carry guidance, not rules. `Specialist` and `Gateway` are
953
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wairon/cli",
3
- "version": "5.1.1-dev.105",
3
+ "version": "5.1.1-dev.106",
4
4
  "description": "SYW Waffle AIron — CLI for managing AI coding agent topology across projects",
5
5
  "keywords": [
6
6
  "ai",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
- "title": "wairon design export, format 1.x",
3
+ "title": "wairon design export, format 2.x",
4
4
  "description": "One project's whole design, resolved (format `wairon-design`). A MINOR formatVersion only adds a field or a member of a closed set, and consumers ignore what they do not know; a MAJOR removes, renames or changes a meaning and is named in the CHANGELOG. Generated from the zod schema in src/models/design-export.ts — never edit by hand.",
5
5
  "$ref": "#/definitions/DesignExport",
6
6
  "definitions": {
@@ -94,6 +94,12 @@
94
94
  },
95
95
  "stability": {
96
96
  "type": "string"
97
+ },
98
+ "type": {
99
+ "type": "string"
100
+ },
101
+ "role": {
102
+ "type": "string"
97
103
  }
98
104
  },
99
105
  "required": [
@@ -268,9 +274,38 @@
268
274
  "durability": {
269
275
  "type": "string"
270
276
  },
271
- "portalType": {
277
+ "transport": {
272
278
  "type": "string"
273
279
  },
280
+ "abi": {
281
+ "type": "string"
282
+ },
283
+ "invokedBy": {
284
+ "type": "object",
285
+ "properties": {
286
+ "kind": {
287
+ "type": "string",
288
+ "enum": [
289
+ "entry",
290
+ "runtime"
291
+ ]
292
+ },
293
+ "caller": {
294
+ "type": "string"
295
+ },
296
+ "scope": {
297
+ "type": "string",
298
+ "enum": [
299
+ "outside",
300
+ "network"
301
+ ]
302
+ }
303
+ },
304
+ "required": [
305
+ "kind"
306
+ ],
307
+ "additionalProperties": true
308
+ },
274
309
  "owns": {
275
310
  "type": "array",
276
311
  "items": {
@@ -437,31 +472,6 @@
437
472
  "additionalProperties": true
438
473
  }
439
474
  },
440
- "mounts": {
441
- "type": "array",
442
- "items": {
443
- "type": "object",
444
- "properties": {
445
- "portal": {
446
- "type": "string"
447
- },
448
- "prefixes": {
449
- "type": "array",
450
- "items": {
451
- "type": "string"
452
- }
453
- },
454
- "via": {
455
- "type": "string"
456
- }
457
- },
458
- "required": [
459
- "portal",
460
- "prefixes"
461
- ],
462
- "additionalProperties": true
463
- }
464
- },
465
475
  "patterns": {
466
476
  "type": "array",
467
477
  "items": {
@@ -528,7 +538,6 @@
528
538
  "emits",
529
539
  "subscribesTo",
530
540
  "dispatch",
531
- "mounts",
532
541
  "patterns",
533
542
  "externalLinks",
534
543
  "formerly"
@@ -658,24 +667,7 @@
658
667
  }
659
668
  },
660
669
  "invokedBy": {
661
- "type": "object",
662
- "properties": {
663
- "kind": {
664
- "type": "string",
665
- "enum": [
666
- "runtime",
667
- "external",
668
- "sibling-subsystem"
669
- ]
670
- },
671
- "caller": {
672
- "type": "string"
673
- }
674
- },
675
- "required": [
676
- "kind"
677
- ],
678
- "additionalProperties": true
670
+ "$ref": "#/definitions/DesignExport/properties/components/items/properties/invokedBy"
679
671
  },
680
672
  "endpoint": {
681
673
  "type": "object",