@wairon/cli 5.1.1-dev.105 → 5.1.1-dev.107
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/README.md +47 -1
- package/dist/cli/index.js +13106 -8767
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +2478 -774
- package/dist/index.js +4721 -1476
- package/dist/index.js.map +1 -1
- package/dist/templates/skills/sdd-architect.md +16 -13
- package/dist/templates/skills/sdd-delegate.md +7 -1
- package/dist/templates/skills/sdd-implement.md +5 -1
- package/dist/webapp.html +53 -52
- package/docs/cli.md +184 -17
- package/docs/design-export.md +13 -8
- package/docs/network.md +104 -0
- package/docs/standards/architecture.md +101 -12
- package/package.json +1 -1
- package/schemas/{design-export-1.json → design-export-2.json} +44 -46
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:
|
|
@@ -88,6 +97,29 @@ warnings as errors (notices are printed and counted, never fatal).
|
|
|
88
97
|
`METHOD_SOURCE_PATH_MISSING` (warning). `rules.conformance.requireCode: true`
|
|
89
98
|
in `project.yaml` reports the two notices as errors, for a CI that must say
|
|
90
99
|
every designed implementation has code.
|
|
100
|
+
- **What the closing line says.** Plain `validate` passes with warnings and
|
|
101
|
+
says so — `Passed with N warning(s)` (`--ci` fails on them); `All checks
|
|
102
|
+
passed` means none. A tree holding no design yet (no L0, or an L0 with nothing
|
|
103
|
+
below it) says there is nothing to check.
|
|
104
|
+
- **Unknown keys are reported.** A key a spec's schema does not know (an
|
|
105
|
+
`exports:` table in the L0 — the export table is `publicInterfaces`; a
|
|
106
|
+
misspelt field) is `UNKNOWN_SPEC_KEY`, and an unknown setting in
|
|
107
|
+
`.wai/project.yaml` is `UNKNOWN_CONFIG_KEY` (both warnings): the parse would
|
|
108
|
+
otherwise drop them without a word.
|
|
109
|
+
- **Code that crosses the design.** A call through a constructor-injected
|
|
110
|
+
collaborator is collaboration whatever the import's form: when its type comes
|
|
111
|
+
from a component the caller never declared, even through `import type`, it is
|
|
112
|
+
`UNDECLARED_DEPENDENCY`; a Portal calling a write- or lifecycle-effect method
|
|
113
|
+
of a Repository, Index, Store or Registry that way is
|
|
114
|
+
`PORTAL_WRITE_SHORTCUT_IN_CODE` (error, the code twin of
|
|
115
|
+
`PORTAL_WRITE_SHORTCUT`); and a file importing a technology's package — a
|
|
116
|
+
package named by one of the technology's declared tokens (its name, or its
|
|
117
|
+
`matches`), never a guessed vendor list — outside the components that bind it
|
|
118
|
+
is `TECH_LEAKAGE_IN_CODE`. An Adapter's call step to a verb of a Portal that
|
|
119
|
+
declares an out-of-process transport (HTTP, gRPC, a database, a bus, a CLI…)
|
|
120
|
+
is the link the design models: it is never resolved to the remote Portal's
|
|
121
|
+
file, so it never reads as `CALL_ORIGIN_UNRESOLVED`; the call to the Adapter
|
|
122
|
+
stays checked.
|
|
91
123
|
|
|
92
124
|
### `wairon generate [--target <name>] [--domain <id>] [--domains <ids>] [--root] [--family] [--no-prune] [--global] [--dry-run]`
|
|
93
125
|
Reconcile the generated guides, skills and context, and — only when the project
|
|
@@ -150,16 +182,32 @@ release and changes nothing: a lock never reaches below its project.)
|
|
|
150
182
|
|
|
151
183
|
**Code linkage is not part of the approval.** The gate identity and the per-spec
|
|
152
184
|
digests read each spec's **design**: where it is realized — `sourcePath`,
|
|
153
|
-
`symbol`, `exportedVia`, `simPath`, `injectedParams`, conformance tiers, a
|
|
154
|
-
`
|
|
185
|
+
`symbol`, `exportedVia`, `simPath`, `injectedParams`, conformance tiers, a Portal's
|
|
186
|
+
`router`, `externalLinks` — and the `createdAt`/`updatedAt` timestamps are left
|
|
155
187
|
out (`specsReading: design`). Linking code to an approved design, or a change and
|
|
156
188
|
its revert, never stales the lock; declare planned `sourcePath`s at design time.
|
|
157
189
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
190
|
+
**Readiness is not part of the approval either.** A spec's `status` (`draft`,
|
|
191
|
+
`design`, `complete`) says how far authoring is, never what the design says, so
|
|
192
|
+
it is left out of the design view: promoting a spec to `complete` after the lock
|
|
193
|
+
never reopens it. A format-3 record taken while the view still carried `status`
|
|
194
|
+
keeps passing when nothing but a status moved — it is judged in the reading it
|
|
195
|
+
was taken in, each spec's approved status recovered from its digest — and
|
|
196
|
+
`wairon doctor --fix` carries it into the current reading without a review.
|
|
197
|
+
|
|
198
|
+
A **format-2** record (written before code linkage left the approval) still passes
|
|
199
|
+
`lock-check` while nothing it covered moved. `wairon doctor --fix` re-expresses it
|
|
200
|
+
as format 3 in place — before any other repair touches a spec — keeping
|
|
201
|
+
`lockedAt`, `lockedBy`, the results and the member pins, and recording
|
|
202
|
+
`reexpressed: { at, fromAlgorithm, fromReading, by }`. When no spec file moved
|
|
203
|
+
but the record still does not match, what moved is the **gate** it was judged
|
|
204
|
+
under — the design rules of a newer wairon release, the project's rule tuning,
|
|
205
|
+
`composition`, the network declaration, consumed contracts or a member's
|
|
206
|
+
approval — never your design: `lock-check` and `doctor` say exactly that, `doctor
|
|
207
|
+
--fix` still carries the record into the design reading (its claim stays as
|
|
208
|
+
certified, so it reads stale for that stated cause), and one `wairon lock`
|
|
209
|
+
re-approves the unchanged design. A format-2 record whose specs moved reads stale
|
|
210
|
+
once; one `wairon lock` clears it for good.
|
|
163
211
|
|
|
164
212
|
It writes **nothing into your spec tree**. The approval is one sha256 per spec
|
|
165
213
|
(over its design view) on `.wai/lock.json`, the record that was always committed — so your
|
|
@@ -276,6 +324,7 @@ With inputs (all optional):
|
|
|
276
324
|
working-directory: packages/api # where the .wai/ tree lives (default: .)
|
|
277
325
|
wairon-version: '6.0.0' # version or npm dist-tag, 6.0.0 or later (default: latest)
|
|
278
326
|
strict: true # fail when nothing was approved — use it once you have locked (default: false)
|
|
327
|
+
validate: true # also run `wairon validate --ci` there (default: true)
|
|
279
328
|
node-version: '20' # (default: '20')
|
|
280
329
|
runs-on: ubuntu-latest # (default: ubuntu-latest)
|
|
281
330
|
```
|
|
@@ -289,6 +338,11 @@ releases, and the default `latest` moves.
|
|
|
289
338
|
On a `pull_request` event the default checkout is the merge commit, so what the
|
|
290
339
|
gate judges is literally the design that would land.
|
|
291
340
|
|
|
341
|
+
After the approval check the job runs `wairon validate --ci` in the same
|
|
342
|
+
directory (`validate: false` turns it off). Point `working-directory` at the
|
|
343
|
+
**family root** — the project that declares the members — so that run is the
|
|
344
|
+
family run, which judges the network proofs no member's own gate can.
|
|
345
|
+
|
|
292
346
|
**A failing job does not block a merge on its own.** A workflow can only fail;
|
|
293
347
|
making a failing job stop a merge is a branch-protection / ruleset setting on
|
|
294
348
|
your repository ("Require status checks to pass" → add this job). No workflow
|
|
@@ -304,7 +358,8 @@ machine-wide MCP config) also needs `--global`; each file it replaces is backed
|
|
|
304
358
|
up beside itself. `--report <section>` prints one section's report and writes
|
|
305
359
|
nothing: `chaining` (the member/reference migration plan) or
|
|
306
360
|
`composed-validation` (what the family run changes about each member's
|
|
307
|
-
findings); it never
|
|
361
|
+
findings) or `reachability` (the reachability migration's plan); it never
|
|
362
|
+
combines with `--fix`.
|
|
308
363
|
|
|
309
364
|
Among its spec repairs, `--fix` rewrites every stored type position that is an
|
|
310
365
|
alias of its canonical spelling (`string[]` becomes `list<string>`, `boolean`
|
|
@@ -316,9 +371,28 @@ literal union, a union mixing in a primitive, a `number` with no proposal) is
|
|
|
316
371
|
listed with its replacement for an author. Plain `wairon doctor` prints the same
|
|
317
372
|
plan without writing it.
|
|
318
373
|
|
|
319
|
-
`--fix` also
|
|
320
|
-
|
|
321
|
-
|
|
374
|
+
**The reachability migration.** `--fix` also moves the tree onto the
|
|
375
|
+
reachability model: `portalType` becomes `transport` (`HTTP_API` → `HTTP`, a
|
|
376
|
+
`Custom` Portal that binds no wire address → `InProcess`), a listener's `mounts`
|
|
377
|
+
become a Portal-level entry on each mounted Portal and its implementation's
|
|
378
|
+
`router` (the mount's `via`), `invokedBy: external` becomes `entry` on a Portal
|
|
379
|
+
verb and `runtime` elsewhere, an authored export `type` that agrees with the derived kind is dropped,
|
|
380
|
+
in-process `Custom` endpoint addresses are removed, and allows of retired codes
|
|
381
|
+
are dropped or renamed. It never invents design: a Portal that was neither a
|
|
382
|
+
listener nor mounted gets no entry, and a `sibling-subsystem` caller, an export
|
|
383
|
+
type that disagrees, an endpoint outside every old prefix and a router already
|
|
384
|
+
set otherwise are listed for an author. Plain `wairon doctor` prints the plan;
|
|
385
|
+
`--report reachability` lists every rewrite and every form left for an author.
|
|
386
|
+
The design changes, so one `wairon lock` follows. After it, declare the entries
|
|
387
|
+
the migration cannot know — a CLI, a stdio tool surface (`JSONRPC`), a library
|
|
388
|
+
(`InProcess`) — and model a caller or remove each verb still reported unreached.
|
|
389
|
+
|
|
390
|
+
`--fix` also re-expresses an earlier lock record — format 2, or format 3 taken
|
|
391
|
+
while `status` was still in the approval — in the current reading, first, before
|
|
392
|
+
any repair touches a spec (see `wairon lock`), keeping who approved it and when.
|
|
393
|
+
A format-2 record over a tree no file of which moved is carried into the design
|
|
394
|
+
reading even when the gate it was judged under moved; doctor then says the cause
|
|
395
|
+
(the gate, not the design) and that one `wairon lock` re-approves it.
|
|
322
396
|
|
|
323
397
|
### `wairon list` (alias `ls`) / `wairon show <id>`
|
|
324
398
|
List, or show full details of, the agents resolved from the spec tree
|
|
@@ -512,6 +586,42 @@ promise and how a consumer follows renames).
|
|
|
512
586
|
- A tree without an L0 is refused. The JSON Schema ships in the package as
|
|
513
587
|
`schemas/design-export-1.json`, and the library twin is `exportDesign()`.
|
|
514
588
|
|
|
589
|
+
### `wairon type rename-field <type> <field> <new-name>`
|
|
590
|
+
Renames a field of a type and respells every reference to it — each foreign
|
|
591
|
+
key `references: <type>.<field>` naming it, the type's own included. The field
|
|
592
|
+
keeps its place, type and description; its old name joins the field's rename
|
|
593
|
+
trace (`previousNames`), which `wairon export` shows as `formerly`, so a
|
|
594
|
+
consumer holding the old name reads a rename, not a removal and an addition.
|
|
595
|
+
Traces and prose are never rewritten. It refuses, writing nothing, a type or
|
|
596
|
+
field that does not exist, a name that is not an identifier, and a name another
|
|
597
|
+
field of the type holds or retired. The MCP twin is `sdd_rename_field`. As with
|
|
598
|
+
any design edit, the next `wairon lock` approves it.
|
|
599
|
+
|
|
600
|
+
### `wairon network flows|policy|diagram|check|why|declare|undeclare`
|
|
601
|
+
Networking derived from the design: the reach the validator already models
|
|
602
|
+
(modelled calls, Portal entries and their scopes, declared networks and their
|
|
603
|
+
gateways), never addresses. Run at the family root.
|
|
604
|
+
|
|
605
|
+
- `flows [--format json|csv|markdown] [--out <file>]` prints the allowed-flows
|
|
606
|
+
matrix, narrowed to the modelled callers wherever the family proves a
|
|
607
|
+
`network` entry.
|
|
608
|
+
- `policy --bindings <file> [--out <file>]` generates Kubernetes
|
|
609
|
+
`NetworkPolicy` from the matrix and a bindings file you keep outside
|
|
610
|
+
`.wai/`. Names it cannot bind are listed, never guessed.
|
|
611
|
+
- `diagram [--out <file>]` prints a Mermaid flowchart of the networks as
|
|
612
|
+
trust boundaries, with the gateways, outside and the flows.
|
|
613
|
+
- `check --observed <file> [--bindings <file>]` compares observed live flows
|
|
614
|
+
(CSV or JSON) with the matrix. It exits 1 on an unexpected flow or an
|
|
615
|
+
unknown verb.
|
|
616
|
+
- `why <from> <to>` prints the modelled chain behind one flow. It exits 1
|
|
617
|
+
when nothing allows it.
|
|
618
|
+
- `declare [--description <text>]` writes the project's `network` declaration
|
|
619
|
+
into `.wai/project.yaml`; `undeclare` removes it (the assistant's twin is
|
|
620
|
+
`sdd_set_network`). It changes which rules fire: validate, then re-lock.
|
|
621
|
+
|
|
622
|
+
[Derived networking](network.md) describes every output, the bindings file
|
|
623
|
+
and the observed-flow format.
|
|
624
|
+
|
|
515
625
|
---
|
|
516
626
|
|
|
517
627
|
## Members
|
|
@@ -557,6 +667,60 @@ then move the pin with `wairon member update`. An uncached git part offline is
|
|
|
557
667
|
declared `externals`, the name a public name in that project's L0 export table.
|
|
558
668
|
An id without `::` is local — a part's subsystems included.
|
|
559
669
|
|
|
670
|
+
**Authoring the L0 export table.** What another project may reference is the
|
|
671
|
+
project's **L0 export table**: the `publicInterfaces` list of the L0 spec
|
|
672
|
+
(`.wai/specs/.index.yaml`). It is authored in two steps, both through the MCP
|
|
673
|
+
tools (an assistant runs them; a human may edit the YAML the same way):
|
|
674
|
+
|
|
675
|
+
1. Each subsystem publishes its own surface — its inbound Portal, and the types
|
|
676
|
+
it shares — with `sdd_set_public_interfaces` (an L1 `publicInterfaces`).
|
|
677
|
+
2. The L0 **re-exports** from those subsystems with `sdd_update_spec` on kind
|
|
678
|
+
`system`, delta `publicInterfaces`. Each entry names a subsystem in `from`;
|
|
679
|
+
`from` alone re-exports everything it publishes, `component` (optionally
|
|
680
|
+
narrowed with `interface`) or `typeDef` picks one item, `as` renames it
|
|
681
|
+
(the public name consumers write after `alias::`), `role: implement` marks
|
|
682
|
+
an extension point consumers implement, and `audience` decides who may see
|
|
683
|
+
it (below).
|
|
684
|
+
|
|
685
|
+
```yaml
|
|
686
|
+
# .wai/specs/.index.yaml (excerpt)
|
|
687
|
+
publicInterfaces:
|
|
688
|
+
- from: distance # everything the distance subsystem publishes
|
|
689
|
+
- from: tiles
|
|
690
|
+
component: tile_portal
|
|
691
|
+
as: tiles # consumers write geo::tiles
|
|
692
|
+
audience: external
|
|
693
|
+
- from: shared
|
|
694
|
+
typeDef: coordinate # a shared type, as geo::coordinate
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
The key is `publicInterfaces` — an `exports:` (or any other key the schema does
|
|
698
|
+
not know) is reported as `UNKNOWN_SPEC_KEY`, never silently ignored, and an
|
|
699
|
+
unknown setting in `.wai/project.yaml` as `UNKNOWN_CONFIG_KEY`. A reference to a
|
|
700
|
+
name the producer does not export is `EXTERNAL_NOT_EXPORTED`.
|
|
701
|
+
|
|
702
|
+
**Export audiences.** Each L0 entry carries an `audience` (default `instance`)
|
|
703
|
+
that decides who may see the public name. Narrowest first:
|
|
704
|
+
|
|
705
|
+
| Audience | Who sees it |
|
|
706
|
+
|----------|-------------|
|
|
707
|
+
| `project` | Only this project's family: its members, its parent and their siblings. |
|
|
708
|
+
| `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. |
|
|
709
|
+
| `instance` | The default. Any project on the instance, and any local consumer by path or git. |
|
|
710
|
+
| `partner` | Also consumers outside the instance that a partner grant admits. |
|
|
711
|
+
| `external` | Publicly consumable. |
|
|
712
|
+
|
|
713
|
+
A consumer is read at one audience: a family project at `project`, so it sees
|
|
714
|
+
everything; a path, git or hosted consumer outside the family at `instance`, so
|
|
715
|
+
it sees `instance`, `partner` and `external` entries only. A name exported to a
|
|
716
|
+
narrower audience is refused by `externals add` (and `externals use`), naming
|
|
717
|
+
the export's audience, the consumer's and this ranking. Because a project cannot
|
|
718
|
+
tell from its own tree whether its consumers are hosted department units,
|
|
719
|
+
`validate` notes every `department` export (`EXPORT_AUDIENCE_NARROW`, a
|
|
720
|
+
notice): widen it to `instance` when a sibling checkout or a git consumer uses
|
|
721
|
+
it, or keep it and allow the notice. A re-export can narrow an audience, never
|
|
722
|
+
widen it (`EXPORT_WIDENS_AUDIENCE`).
|
|
723
|
+
|
|
560
724
|
| Command | Description |
|
|
561
725
|
|---------|-------------|
|
|
562
726
|
| `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 |
|
|
@@ -686,7 +850,7 @@ The local server offers 38 tools:
|
|
|
686
850
|
| Reading and checking | `sdd_get_spec`, `sdd_get_status`, `sdd_validate_tree` |
|
|
687
851
|
| Renames and moves (in this tree) | `sdd_rename_component`, `sdd_rename_method`, `sdd_rename_type`, `sdd_move_methods` |
|
|
688
852
|
| 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` |
|
|
689
|
-
| Externals | `sdd_add_external`, `sdd_pin_externals`, `sdd_get_externals_status` |
|
|
853
|
+
| Externals | `sdd_add_external`, `sdd_update_external`, `sdd_remove_external`, `sdd_pin_externals`, `sdd_get_externals_status` |
|
|
690
854
|
| Packs | `sdd_pack_impact` |
|
|
691
855
|
| Delegation | `sdd_get_agent_brief` (the live brief for one agent; also served as the `wairon-agent://` resource) |
|
|
692
856
|
|
|
@@ -762,7 +926,7 @@ principal.
|
|
|
762
926
|
| `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
927
|
| `wairon host permission list [--user <userId>] [scope flags]` / `remove --id <assignmentId>` | List or remove assignments |
|
|
764
928
|
| `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 |
|
|
929
|
+
| `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
930
|
| `wairon host key list [--project <id>]` | List API keys |
|
|
767
931
|
| `wairon host key revoke --id <id>` | Revoke a key |
|
|
768
932
|
| `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 |
|
|
@@ -797,10 +961,13 @@ return the impact of every pack they applied in their results.
|
|
|
797
961
|
|
|
798
962
|
| Command | Description |
|
|
799
963
|
|---------|-------------|
|
|
800
|
-
| `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.
|
|
801
|
-
| `wairon externals
|
|
802
|
-
| `wairon externals
|
|
803
|
-
| `wairon externals
|
|
964
|
+
| `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 |
|
|
965
|
+
| `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 |
|
|
966
|
+
| `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 |
|
|
967
|
+
| `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 |
|
|
968
|
+
| `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) |
|
|
969
|
+
| `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 |
|
|
970
|
+
| `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 |
|
|
804
971
|
| `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 |
|
|
805
972
|
| `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) |
|
|
806
973
|
|
package/docs/design-export.md
CHANGED
|
@@ -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-
|
|
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 `
|
|
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-
|
|
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
|
|
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
|
-
- **
|
|
260
|
-
|
|
261
|
-
|
|
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
|
package/docs/network.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
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` 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.
|
|
6
|
+
|
|
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.
|
|
8
|
+
|
|
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. Run them at the family root: only the family run can narrow a `network` entry to the callers it really has.
|
|
10
|
+
|
|
11
|
+
## The outputs, and what each is for
|
|
12
|
+
|
|
13
|
+
| Command | Output | What a team does with it |
|
|
14
|
+
|---|---|---|
|
|
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. |
|
|
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. |
|
|
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). |
|
|
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. |
|
|
21
|
+
|
|
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=…`.
|
|
23
|
+
|
|
24
|
+
### What becomes a flow
|
|
25
|
+
|
|
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.
|
|
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.
|
|
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.
|
|
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".
|
|
32
|
+
|
|
33
|
+
### Naming parties
|
|
34
|
+
|
|
35
|
+
`from`, `to`, the observed-flow names and the binding keys all use design names:
|
|
36
|
+
|
|
37
|
+
- `outside`.
|
|
38
|
+
- `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.
|
|
39
|
+
- A project key such as `orders`.
|
|
40
|
+
- A subsystem, as `project::subsystem` or bare for the root's own.
|
|
41
|
+
- A component, as `project::component` or bare for the root's own.
|
|
42
|
+
- On the receiving end, also `project::portal.verb`.
|
|
43
|
+
|
|
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.
|
|
45
|
+
|
|
46
|
+
## The bindings file
|
|
47
|
+
|
|
48
|
+
YAML, kept wherever your deployment config lives (never in `.wai/`):
|
|
49
|
+
|
|
50
|
+
```yaml
|
|
51
|
+
workloads:
|
|
52
|
+
orders: # a project
|
|
53
|
+
selector: { app.kubernetes.io/name: orders }
|
|
54
|
+
namespace: shop
|
|
55
|
+
port: 8080 # the port its Portals listen on (optional)
|
|
56
|
+
platform::api_gateway: # a Portal: more specific than its project
|
|
57
|
+
selector: { app.kubernetes.io/name: edge }
|
|
58
|
+
namespace: edge
|
|
59
|
+
port: 8443
|
|
60
|
+
edge: # a subsystem of the root project
|
|
61
|
+
selector: { app.kubernetes.io/name: edge }
|
|
62
|
+
network: # "anywhere in the root network": a label every member pod carries
|
|
63
|
+
selector: { platform.example/member: "true" }
|
|
64
|
+
outside: # what "outside" means for a policy
|
|
65
|
+
- 0.0.0.0/0
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- `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.
|
|
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.
|
|
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: {}`).
|
|
71
|
+
|
|
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:`. 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
|
+
## The observed-flow format
|
|
75
|
+
|
|
76
|
+
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).
|
|
77
|
+
|
|
78
|
+
CSV with a header row; `source` and `destination` are required, the rest optional:
|
|
79
|
+
|
|
80
|
+
```csv
|
|
81
|
+
source,destination,transport,method,path,count
|
|
82
|
+
outside,edge,HTTP,GET,/api/orders,120
|
|
83
|
+
edge,orders,HTTP,POST,/orders,40
|
|
84
|
+
orders,billing,HTTP,POST,/charges,38
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Or JSON with the same fields, as a list or as `{ "flows": [...] }`:
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
[{ "source": "orders", "destination": "billing", "method": "POST", "path": "/charges", "count": 38 }]
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
- A row without `method` and `path` is an L4 observation: it exercises every allowed flow between the pair.
|
|
94
|
+
- 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.
|
|
95
|
+
- A malformed row (no source or destination, or a non-integer count) is refused with its row number, and nothing is half-read.
|
|
96
|
+
|
|
97
|
+
## A typical loop
|
|
98
|
+
|
|
99
|
+
```sh
|
|
100
|
+
wairon network flows --format markdown --out docs/flows.md # review in the PR
|
|
101
|
+
wairon network policy --bindings ../deploy/bindings.yaml --out ../deploy/netpol.yaml
|
|
102
|
+
wairon network check --observed hubble-export.csv --bindings ../deploy/bindings.yaml
|
|
103
|
+
wairon network why shop::checkout_adapter payments::payments_api.charge
|
|
104
|
+
```
|
|
@@ -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
|
-
###
|
|
635
|
-
A Portal
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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.
|