@wairon/cli 5.1.1-dev.112 → 5.1.1-dev.113

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.
@@ -114,7 +114,7 @@ schemas:
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.
117
- - **Renames keep a trace**: rename with `sdd_rename_component`, `sdd_rename_method` and `sdd_rename_type`, and move methods with `sdd_move_methods` — never by deleting and re-adding a spec or editing an id through `sdd_update_spec`. The tools rewrite every reference, record the old key in the element's rename trace (`previousIds`, a method's `previousNames`; the design export shows it as `formerly`, which is how a generator keeps its user's code across the rename), and keep a published name through `as`. A retired id or method name cannot be taken again (`id-retired`, `name-retired`), and a hand-edited trace that collides is `RENAME_TRACE_CONFLICT`. Never write a trace yourself; unsetting one releases its names at the cost that consumers read a delete plus an add.
117
+ - **Renames keep a trace**: rename with `sdd_rename_component`, `sdd_rename_method` and `sdd_rename_type` (a contract or an implementation on its own with `sdd_rename_spec`), move methods with `sdd_move_methods` and a component or a type to another subsystem with `sdd_move_spec` — never by deleting and re-adding a spec or editing an id or a subsystem through `sdd_update_spec`. The tools rewrite every reference, record the old key in the element's rename trace (`previousIds`, a method's `previousNames`; the design export shows it as `formerly`, which is how a generator keeps its user's code across the rename), and keep a published name through `as`. A retired id or method name cannot be taken again (`id-retired`, `name-retired`), and a hand-edited trace that collides is `RENAME_TRACE_CONFLICT`. Never write a trace yourself; unsetting one releases its names at the cost that consumers read a delete plus an add.
118
118
  - **targetLanguage**: set on L0 (`sdd_initialize_system`), override per L1 — enables language-aware validation (flow constructs the language lacks are flagged) and the brief's type mapping for implementers. Extension packs (`.wai/project.yaml` → `extensions.packs`) may register additional languages/platforms and custom profiles.
119
119
  - **technologies (L4)**: an external technology (database, vendor SDK, service) is abstracted as a component — wrapped by an Adapter behind an intent-language interface (inside a Repository for persistence; a standalone Adapter for an external API). Declare the binding on that component's L4: `technologies: [mysql]`. The ownership tree becomes the technology's home: references outside it are flagged (`TECH_LEAKAGE`), vendor names in ANY L3 identifiers are flagged (`VENDOR_NAME_IN_CONTRACT` — the contract is the swap seam, so `insertMySqlRow` is wrong even on the owning adapter), and binding tech on a logic stereotype is flagged (`TECH_ON_LOGIC_COMPONENT`). Swapping the technology then touches one L4.
120
120
  - **lint.allow (per-spec suppression)**: any L1–L4/type spec may carry `lint: { allow: [{ code, reason }] }` — silences that WARNING (or NOTICE) code on that spec only (wairon's `#[allow]`). Errors are never locally suppressible; unknown codes and allows that no longer match anything are flagged. Prefer fixing — an allow is for a documented false positive or a deliberate, reviewable exception.
@@ -65,6 +65,13 @@ Point the subagent at that skill and spend the brief on what only you know:
65
65
  it: a paraphrase drifts, and the subagent cannot tell which copy is current.
66
66
  * **The premise you are asking them to act on**, stated *as* a premise, so it can
67
67
  be contradicted.
68
+ * **For a Portal, its handler shape** — the contract's own parameters per verb,
69
+ the handles its framework hands each function (to be named in the
70
+ implementation's `injectedParams`), and its `router` linkage (an entry of its
71
+ own file, `<module>#<name>`, or a central module) — quoted from the
72
+ implementation spec, with a pointer to `sdd-implement`'s **Handler shape** and
73
+ **Routers** rules. A subagent left to guess writes `(req, res)` handlers, and
74
+ the gate reads those as substitutions.
68
75
 
69
76
  One design decision never goes into a brief as an instruction: **an entry**. A
70
77
  subagent that meets an unreached Portal verb (`UNUSED_COMPONENT` /
@@ -184,6 +184,14 @@ All implementation work must strictly adhere to these rules:
184
184
  - A Portal's verbs are reached by the callers the design models or by the outside callers its `invokedBy` entry names. Serve exactly those: the Portal's L4 `router` is the entry its file exports for whatever process hosts it. Never add a verb, route or command the contract does not declare, and never ask for an entry to make an `UNUSED_*` finding go away — report the unreached verb instead.
185
185
  - Another project's library (an `InProcess` Portal) is imported and called directly from the component the narrative names — no wrapper Adapter unless the design has one. Keep pure and read logic to the library verbs whose `effect` the design allows.
186
186
  - Function names follow the contract's method names in the language's casing; a wire name that differs (an HTTP path, a CLI command, a JSON-RPC method) belongs to the endpoint binding — route it there, do not rename the function after it.
187
+ - **Handler shape — the green path for a framework handler** (no design change, no re-lock):
188
+ - A verb's function takes the contract's OWN parameters, in order, under the contract's names. `cancelOrder(req, res)` realizing `cancelOrder(orderId, customerId)` is reported as two substitutions (`UNREALIZED_PARAM` + `UNDECLARED_PARAM`), annotated or not; so is a request object, or any object sharing none of the record's fields, where the contract declares a record.
189
+ - What a framework hands the function besides those — a context, a request, a response, `next` — is wiring: name it in the implementation's `injectedParams` with `sdd_update_spec` (code linkage, outside the approval). It is matched with or without its leading underscore, at the START or the END of the list: `handler(ctx, req, orderId, customerId, res)` with `injectedParams: [ctx, req, res]` is green.
190
+ - Two honest shapes: (a) the router unpacks the path, query and body and calls `cancelOrder(orderId, customerId)` (handles injected around them if the framework insists); (b) the framework hands the handler the request alone — inject it and read each contract parameter off it **by its own name** (`req.params.orderId`, `req.query['limit']`, `const { orderId } = req.params`), which realizes the parameter through the handle. A contract parameter never read is still `UNREALIZED_PARAM`.
191
+ - An injection no handler takes is `UNUSED_INJECTED_PARAM` — drop it. A leading `_` marks a parameter the body provably never reads; a used `_secret` is judged like any other.
192
+ - `conformance: off` (on the implementation or one method) is for code wairon is not meant to compare with its contract — generated or vendored code. It switches the realization checks off (method, parameters, async, forward call claims), never the doctrine: an unnarrated write, a write shortcut and a call the analysis cannot follow are still reported.
193
+ - **Routers**: set the Portal implementation's `router` (code linkage) to the entry its file exports (`handleOrderRequest`), or to a central module's table or entry (`src/routes.ts#ROUTES`), or to the module alone (`src/routes.ts`). Write routes in a readable idiom: guards on `<request>.method` and `parts[i]`, or a route table — an array of `{ method, path }` objects or an object keyed `VERB /path` (template-literal keys over constants, and a table returned in place, are read). Table paths are written under the prefix the router strips, and that prefix must be the Portal's `basePath`.
194
+ - **Another project's code** is reached through what it exports — its member alias or a pinned external — or through a binding module the implementation declares in `bindings`. Importing a name straight out of its source tree that it does not export is `CROSS_PROJECT_SOURCE_IMPORT`.
187
195
  - **Strict Layer Isolation & No Persistence Shortcuts**:
188
196
  - A `Portal` must **never** depend directly on a `Store`, `Registry`, `Adapter` or `Query`. Passthrough READS may go through a `Repository`/`Index` facade; every WRITE must route through an `Orchestrator` (a Portal narrative call or dispatch-table binding that reaches a write-effect facade method is a `PORTAL_WRITE_SHORTCUT` error).
189
197
  - Held domain state always lives in a dedicated data component, never as fields inside an `Orchestrator`. Two sanctioned shapes: the RECOMMENDED `Repository` pattern (owns `Store` + `Registry` + `Index`; consumers depend on the facade), or — for genuinely simple state — a deliberately standalone `Store` (workflow-layer consumers only, acknowledged via `lint.allow` on `UNOWNED_STORE`). Do **not** combine Store/Registry/Index functionality into a single helper component, and never fold state into a consuming component because a link was refused.
package/docs/cli.md CHANGED
@@ -151,14 +151,19 @@ warnings as errors (notices are printed and counted, never fatal).
151
151
  declare the producer's names in the binding itself), or its component reaches
152
152
  neither a pinned external nor a member project that exports something. Both
153
153
  ES module exports and CommonJS exports are read (`exports.X = { … }`,
154
- `module.exports.X = function (…) {…}`, each property of `module.exports =
155
- { … }`). Return types are not compared. A binding into a contained **member**
154
+ `module.exports.X = function (…) {…}` or `= X`, each property of
155
+ `module.exports = { … }` — a shorthand `{ Client }` included —
156
+ `module.exports = class X { … }`, a TypeScript `export = X`, and JSDoc
157
+ `@typedef`/`@property`), a bare identifier through the module's own
158
+ declaration of it; a `_private` class or object member is not compared, and
159
+ an object tagged as a whole has each member judged against its tag. Return types are not compared. A binding into a contained **member**
156
160
  project (declared under `members`, never pinned — named by its alias or its
157
161
  project id) is compared the same way with what the member's L0 exports now
158
162
  (its exported contracts and types, and the types they name), so a
159
163
  `rename-param` or `rename-field` in the member reaches its parent's binding
160
164
  at `validate` time as `BINDING_DRIFT` with the rename to follow; a member is
161
- never told to pin — one that exports nothing is told to export there. A git
165
+ never told to pin — one that exports nothing is told to export there. A
166
+ member in a `../` sibling checkout is a contained one here. A git
162
167
  member is pinned like an external (`wairon externals pin <alias>`) and
163
168
  compared with that pin. A file of another project imported directly from its
164
169
  source tree is not a binding: it is the producer's own code.
@@ -198,7 +203,8 @@ warnings as errors (notices are printed and counted, never fatal).
198
203
  `MISSING_SOURCE_FILE` (error) and a contract method naming no file is
199
204
  `METHOD_SOURCE_PATH_MISSING` (warning). `rules.conformance.requireCode: true`
200
205
  in `project.yaml` reports the two notices as errors, for a CI that must say
201
- every designed implementation has code.
206
+ every designed implementation has code. It is code-conformance configuration,
207
+ outside the approval identity: flipping it never stales a lock (see `lock-check`).
202
208
  - **Types are linked to code too.** A type spec without a `sourcePath` is
203
209
  `MISSING_TYPE_SOURCE_PATH` — a notice before its subsystem has code, a
204
210
  warning after. Plan a type's `sourcePath` when you plan the implementations'.
@@ -263,6 +269,57 @@ warnings as errors (notices are printed and counted, never fatal).
263
269
  that Portal is realized by the link, never reported `UNREALIZED_DEPENDENCY`:
264
270
  never add an import across a network boundary to satisfy a check.
265
271
 
272
+ #### Framework handlers, injected handles and routers
273
+
274
+ The parameter check pairs a verb's contract parameters with the function that
275
+ realizes it, in order. A framework handler is green without any design change:
276
+
277
+ - **Take the contract's own parameters, under its names.** `cancelOrder(req, res)`
278
+ realizing `cancelOrder(orderId, customerId)` is two substitutions
279
+ (`UNREALIZED_PARAM` + `UNDECLARED_PARAM`), annotated or not: a parameter named
280
+ as a transport handle (req, res, ctx, request, response, next, reply) where the
281
+ contract declares an argument of its own is never a silent pairing. Neither is a
282
+ platform transport class (`IncomingMessage`, a fetch `Request`) or an object
283
+ sharing none of a record's fields where the contract declares that record; an
284
+ object holding every field under another name is a rename (`PARAM_NAME_MISMATCH`).
285
+ - **Declare what the framework hands you as wiring.** Name the handles in the
286
+ implementation's `injectedParams` (code linkage: `sdd_update_spec`, no
287
+ re-lock). A name matches with or without its leading `_`, at the start or the
288
+ end of the list — `handler(ctx, req, orderId, customerId, res)` with
289
+ `injectedParams: [ctx, req, res]`. An injection no handler takes is
290
+ `UNUSED_INJECTED_PARAM`.
291
+ - **Or read the parameters off the request.** Where the framework hands the
292
+ handler the request alone, inject it and read each contract parameter off it by
293
+ its own name (`req.params.orderId`, `req.query['limit']`,
294
+ `const { orderId } = req.params`): that realizes the parameter through the
295
+ handle. A parameter never read off it stays `UNREALIZED_PARAM`.
296
+ - **`conformance: off`** on an implementation or a method is for code wairon is
297
+ not meant to compare with its contract (generated or vendored code). It switches
298
+ the realization checks off — method, parameters, async, whether narrated calls
299
+ are found — and never the doctrine: an unnarrated write
300
+ (`UNDECLARED_WRITE_CALL`), a call the analysis cannot follow, a Portal write
301
+ shortcut and route coverage are judged whatever it says.
302
+
303
+ A Portal's `router` (code linkage) names the entry its own file exports
304
+ (`handleOrderRequest`), a central module's entry or table
305
+ (`src/routes.ts#ROUTES`), or a module alone (`src/routes.ts`, every route-bearing
306
+ export read). Several Portals may name one router; a route is declared when any
307
+ of them declares it. Guards on `<request>.method` and `parts[i]` are read, and so
308
+ are route tables — an array of `{ method, path }` objects or an object keyed
309
+ `VERB /path` (template-literal keys over constants included), bound to a const or
310
+ returned in place. A table is read under the ONE prefix its router strips off the
311
+ path (`path.slice(BASE.length)`), so that prefix must be the Portal's `basePath`:
312
+ another one is `UNDECLARED_ROUTE` + `UNROUTED_ENDPOINT`, naming both.
313
+
314
+ Another project's code is reached through what it publishes — its member alias, a
315
+ pinned external — or through a binding module the implementation declares in
316
+ `bindings`. An import that resolves into its source under a name it does not
317
+ export is `CROSS_PROJECT_SOURCE_IMPORT` (warning); one into a module realizing its
318
+ Store is the persistence shortcut past its surface. A claimed call into a
319
+ component whose code is in another language (a Rust member behind a binding) is
320
+ not judged by the TypeScript call graph: it is said once per project and language
321
+ as `CALL_ACROSS_LANGUAGE` (notice), never as an unrealized call.
322
+
266
323
  #### Technologies and their packages
267
324
 
268
325
  `TECH_LEAKAGE_IN_CODE` compares each import's package with the packages of the
@@ -332,8 +389,16 @@ When any spec is still `draft` or `design`, the lock says how many of how many,
332
389
  per kind (subsystems, components, contracts, implementations, types), and that
333
390
  it approves them as they stand. Re-running it when nothing it records moved
334
391
  keeps the record on file — its `lockedAt` included — so `.wai/lock.json` stays
335
- unmodified; a change of approver, verdict, member pin or wairon release writes
336
- a new record.
392
+ unmodified; a change of approver, verdict or member pin writes a new record. A
393
+ new wairon release writes nothing by itself: when its built-in design rules are
394
+ the same as the ones the record was taken under (the record's `gateParts.release`
395
+ is unchanged), the approval simply still matches and the record — its release
396
+ stamp included — is left as it is; when they moved, the approval carries over
397
+ by re-validation and one `wairon lock` refreshes the stamp (see *Upgrading
398
+ wairon* under `lock-check`). Evidence recorded *beside* the approval (the code
399
+ results with the analyzer's version, the design counts, gate parts, a release
400
+ stamp that is missing, no version or newer than the running wairon) is refreshed
401
+ and named; the summary says when this run left the file untouched.
337
402
 
338
403
  **What it certifies is the design.** Only design findings can refuse a lock.
339
404
  Code-conformance findings (the code↔spec checks) are recorded **beside** the
@@ -419,11 +484,28 @@ whole design's, so it is refused as a first approval (run `wairon lock`), and
419
484
  refused whenever a spec outside the subsystem — or an input the identity covers
420
485
  besides the design (rule tuning, packs, network, a pin, a member's approval,
421
486
  the release) — moved since the approval; when it passes, the rest stays approved
422
- exactly as it was, and the record is the one a full lock would write. When
423
- nothing changed, `lock` leaves the approval on record as it is — its `lockedAt`,
424
- release stamp and restamp trace — and `.wai/lock.json` untouched; only evidence
487
+ exactly as it was, and the record is the one a full lock would write. A
488
+ member project's subsystem (`--subsystem payments_svc::payments` at a parent) is
489
+ refused: a member's subsystem is approved at the member's own root
490
+ (`wairon lock --subsystem payments` in its folder), then the parent locks to pin
491
+ its new approval. When
492
+ nothing changed, `lock` leaves the approval on record as it is — its `lockedAt`
493
+ and restamp trace — and `.wai/lock.json` untouched by that run; only evidence
425
494
  recorded *beside* the approval (the code results, the design counts, gate parts
426
- that are missing or do not match the identity) is refreshed, and it says which.
495
+ that are missing or do not match the identity, a release stamp that is missing,
496
+ no version or newer than this wairon — replaced with this release) is refreshed,
497
+ and it says which. When a lock records the project's own inputs for the first
498
+ time (a first approval, or a record from before gate parts) or the rule tuning
499
+ or packs moved, it prints the inputs it records — projectType, each rule
500
+ severity override, the selected packs, a declared network, `composition` — so
501
+ the tuning you approve is shown, not only digested.
502
+
503
+ **In a family, lock bottom-up.** A parent's record pins each member's approval,
504
+ so lock every member that is not approved at its own root first, then the
505
+ parent: three projects cost three locks. A parent locked first records the
506
+ member as drifted, its `lock-check` fails until the member re-locks, and it
507
+ must lock again to pin the new approval — `lock` warns before it writes when a
508
+ direct member is not covered by its own approval.
427
509
  Another approver locking an unchanged design records their own approval. A shell
428
510
  with no terminal and no `--yes` writes nothing.
429
511
 
@@ -439,8 +521,10 @@ record counts too, so commit it before you rely on a local pass.
439
521
  | **`locked`** — the approval still covers this design | pass (0) | pass (0) |
440
522
  | **`stale`** — the design moved past its approval | **fail (1)** | **fail (1)** |
441
523
  | **`unlocked`** — nothing was ever approved | pass, with a notice (0) | **fail (1)** |
442
- | no `.wai/specs` in this directory at all | pass, saying so (0) | **fail (1)** |
443
- | a member project has spec changes nobody approved at its own root | **fail (1)** | **fail (1)** |
524
+ | no `.wai/specs` in this directory at all, and no approval on record | pass, saying so (0) | **fail (1)** |
525
+ | no `.wai/specs` (or none left), while `.wai/lock.json` approves spec files | **fail (1)**: the tree was deleted | **fail (1)** |
526
+ | `.wai/project.yaml` missing while an approval is on record | **fail (1)**, naming the file | **fail (1)** |
527
+ | a member project's own approval does not cover it (spec changes, inputs that moved, a record that predates recorded inputs) | **fail (1)**, in the member's own terms | **fail (1)** |
444
528
  | a member project was never approved by anyone | pass, naming it as *not gated* (0) | **fail (1)** |
445
529
  | only a member's own approval moved (it re-locked) | **fail (1)**, naming the member: re-lock here to pin its new approval | **fail (1)** |
446
530
 
@@ -488,8 +572,30 @@ them; later upgrades then carry over*, and `wairon lock` takes a real approval
488
572
  (never a restamp). The same holds for gate parts that do not break down the
489
573
  `stateId` beside them (they are ignored, with a notice; when the identity still
490
574
  matches, `wairon lock` rewrites them keeping the approval), and for a release
491
- stamp that is no wairon version or is *newer* than the running wairon — a record
492
- *written by a newer wairon* is never re-judged by an older one.
575
+ stamp that is no wairon version or is *newer* than the running wairon when the
576
+ identity no longer matches — a record *written by a newer wairon* is never
577
+ re-judged by an older one. When the identity still matches, the identity
578
+ decides: the approval holds, and every surface (`lock-check`, `status`,
579
+ `sdd_get_status`, `validate`) reports the stamp as a notice (`its release stamp
580
+ "banana" is no wairon version`, `… says wairon 9.9.9 took it, a newer release than
581
+ this one`); `wairon lock` replaces it with the running release. A matching
582
+ record that carries no gate parts at all gets the same notice `{}` gets, and
583
+ `lock` rewrites them. A `lock.json` that names one key twice (what a careless
584
+ conflict resolution leaves) cannot be read, like one with conflict markers.
585
+
586
+ **What the identity covers, and what it leaves out.** The project's own *design*
587
+ rule tuning — `projectType`, `sddRuleSeverity` overrides of design rules, the
588
+ other design settings under `rules` — its pack selection, network declaration
589
+ and `composition` are inputs: changing one stales the approval. A selected pack
590
+ that cannot load (not installed, not vendored) leaves the approval *owed* —
591
+ `lock-check` fails, naming the pack — because the doctrine it was judged under
592
+ cannot be read. **Code-conformance configuration is deliberately outside the
593
+ identity:** `rules.conformance` (`requireCode` included) and severity overrides
594
+ of rules that judge code govern how the *code* is held to the design, not what
595
+ the design is, so flipping `requireCode` changes what `validate --ci` fails on
596
+ and never stales an approval — exactly as adding a `sourcePath` never does. The
597
+ analyzer's doctrine is recorded beside the claim (`code.analyzer.doctrineDigest`)
598
+ instead.
493
599
 
494
600
  **It is optional by construction.** By default only a moved approval (or a
495
601
  member's unapproved changes) refuses, and neither can happen in a project that
@@ -1015,10 +1121,14 @@ content — `as:` only asserts it (a contradiction is `MEMBER_KIND_MISMATCH`):
1015
1121
  `partOf: { project: <parent id>, path: <parent root> }`.
1016
1122
  - A **project** is an independent boundary: its tree declares an id, holds an L0
1017
1123
  or carries a lock. It is reached only as `alias::name` through its L0 exports,
1018
- judged by its own gate and locked at its own root. A project stored outside
1019
- this project (`../x`, git, `hosted:`) is a **referenced** project: this
1020
- project's gate judges it against its pin (`wairon externals pin`), and the
1021
- family run opens it where it is stored and composes it per use.
1124
+ judged by its own gate and locked at its own root. A project contained in this
1125
+ project, or in a `../x` sibling checkout beside it, is followed by the family
1126
+ scan and composed **live**: its exports are read where it is stored, its own
1127
+ gate runs in the family run, and nothing is pinned (`wairon externals pin`
1128
+ says so for a member alias). A project fetched from git or a `hosted:` record
1129
+ is a **referenced** project: this project's gate judges it against its pin
1130
+ (`wairon externals pin`), and the family run opens it where it is stored and
1131
+ composes it per use. A hosted instance never reads a `../` source.
1022
1132
 
1023
1133
  Boundaries are earned: grow a system from subsystems, to parts when a piece
1024
1134
  needs its own folder or repository, to projects only when it needs its own team,
@@ -1097,7 +1207,7 @@ widen it (`EXPORT_WIDENS_AUDIENCE`).
1097
1207
  | `wairon member move <alias> <path>` | Move a contained member's directory and point its `members` entry there (a legacy L1 mount is moved into `members` first) |
1098
1208
  | `wairon member attach <alias> <path> [--description <text>]` | Make the **existing** project at `<path>` a member, keeping its L0, subsystems, packs and lock; its id is declared when it only defaulted one (the id its lock approved, else its effective id). `<path>` reads as `member add` reads a location: inside this project, or a sibling with a leading `../`. Refused when its id collides with a family project's, and for any other path out of this project (`not-contained`) |
1099
1209
  | `wairon member detach <alias> [--widen]` | Take a member out of the family: this project and every family consumer reach it as an external by `source.path`, this project's pinned. Refused (`audience-too-narrow`, each export named with its users) while a family project uses a name the member exports only to the family; `--widen` instead widens exactly those used exports to `instance` in the member's L0, each shown in the plan |
1100
- | `wairon member adopt <alias>` | Make this project's external found by a path inside it a member again — detach's inverse |
1210
+ | `wairon member adopt <alias>` | Make this project's external found by path a member again — detach's inverse. Its source reads as `member attach` reads a location: inside this project, or a sibling with a leading `../` (a sibling member is composed live like a contained one, never pinned) |
1101
1211
  | `wairon member rename-alias <old> <new>` | Rename one alias of this project (a member or an external) and respell this project's references through it; no member or sibling changes |
1102
1212
  | `wairon member internalize <alias> [--into <subsystem>] [--packs adopt\|drop] [--export <name>…]` | Fold a member into this project's own specs folder. A **part** is a storage move: its subsystems move in and nothing else changes. A **project** is demoted first — its own metadata goes to a home (its L0 vision onto the `--into` subsystem, its boundaries, requirements and databases into this L0, its language, profile and depth onto the moved subsystems, its members and externals into this configuration, its packs adopted or dropped), what has no home (its lock, pins, derived outputs) is deleted and listed, and every family project that consumed it is re-pointed here |
1103
1213
  | `wairon project rename <new-id> [--project <alias path>]` | Move a project's id — this project's, or a member's named by its alias path — and every reference to the old id family-wide; the old id is kept in `previousIds`. Lists every project it writes to re-lock |
@@ -1217,14 +1327,14 @@ project, so it holds nothing machine-specific: it runs `wairon mcp serve` from t
1217
1327
  and the server attaches to the project it is started in. A `--global` registration is
1218
1328
  machine-wide and names the running CLI by its absolute path.
1219
1329
 
1220
- The local server offers 47 tools:
1330
+ The local server offers 49 tools:
1221
1331
 
1222
1332
  | Group | Tools |
1223
1333
  |-------|-------|
1224
1334
  | Topology (read) | `listAgents`, `getAgent`, `listDomains`, `validateTopology`, `getProjectConfig` |
1225
- | 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` (dry run), `sdd_delete_spec` (dry run, force) |
1335
+ | Authoring | `sdd_initialize_system`, `sdd_add_subsystem`, `sdd_set_public_interfaces`, `sdd_add_component`, `sdd_define_interface`, `sdd_set_endpoints` (dry run), `sdd_write_narrative`, `sdd_add_type` (dry run), `sdd_update_spec` (dry run, search), `sdd_delete_spec` (dry run, force, search) |
1226
1336
  | Reading and checking | `sdd_get_spec`, `sdd_get_status`, `sdd_validate_tree` |
1227
- | Renames and moves (in this tree) — each with `dryRun` | `sdd_rename_component`, `sdd_rename_method`, `sdd_rename_param`, `sdd_rename_type`, `sdd_rename_field`, `sdd_move_methods` |
1337
+ | Renames and moves (in this tree) — each with `dryRun` | `sdd_rename_component`, `sdd_rename_method`, `sdd_rename_param`, `sdd_rename_type`, `sdd_rename_field`, `sdd_rename_spec` (a contract or an implementation on its own), `sdd_move_methods`, `sdd_move_spec` (a component or a type to another subsystem) |
1228
1338
  | Members | `sdd_add_member`, `sdd_move_member` |
1229
1339
  | Family migrations (each takes `dryRun`) | `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` |
1230
1340
  | Externals | `sdd_add_external`, `sdd_update_external`, `sdd_remove_external`, `sdd_pin_externals`, `sdd_get_externals_status` |
@@ -1233,6 +1343,15 @@ The local server offers 47 tools:
1233
1343
  | Packs | `sdd_pack_impact` |
1234
1344
  | Delegation | `sdd_get_agent_brief` (the live brief for one agent; also served as the `wairon-agent://` resource) |
1235
1345
 
1346
+ Every tool description ends with its argument names (`Arguments: id, newId, dryRun?.`). Every write
1347
+ tool refuses a spec of another project — a member's spec read through this tree is written from the
1348
+ member's own root, and the refusal names its folder and the call to make there. `sdd_delete_spec` and
1349
+ a contract method removed through `sdd_update_spec` report `published` (the public names a consumer
1350
+ reaches what goes through) and `breaks` (the consumers in reach, and in the `search` folders, that use
1351
+ one), as the renames do; a removed contract method takes its implementation entries with it
1352
+ (`cascaded`) and is refused while another spec still calls it. A delta never changes a spec's id or
1353
+ subsystem: `sdd_rename_spec` and `sdd_move_spec` do, with every reference and published entry.
1354
+
1236
1355
  A hosted server adds its own `sdd_host_*` and `sdd_landscape_*` tools; see the
1237
1356
  [hosted server guide](https://github.com/SYW-Apps/Waffle-AIron/blob/main/docs/design/hosted-mcp-server.md).
1238
1357
 
@@ -1347,7 +1466,7 @@ return the impact of every pack they applied in their results.
1347
1466
  | `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) |
1348
1467
  | `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 |
1349
1468
  | `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 |
1350
- | `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 3.1 document per portal), import one, or list them. OpenAPI defaults to the `project` audience, so every HTTP Portal of the project's own is described — one its L0 export table never exports included (a wider `--audience` narrows to what the table shares there); the native snapshot defaults to `instance`. In the OpenAPI document a path placeholder — `{name}`, or the Express spelling `:name`, which is rewritten to `{name}` — is a parameter `in: path`, the other params the query of a GET/DELETE or the JSON body of a POST/PUT/PATCH — the one object-typed param left IS the body, as a client sends it (named under `x-wairon-body-param`, so `surface import` puts it back), and several are the properties of a body object. The Portal's `basePath` is joined into every path (the document carries no `servers` entry: where it is served is deployment, not design). The parameter that carries the credential the Portal's `auth` binds (a bearer `token`, say) is left out of the parameters and the body — `security` describes it — and named under `x-wairon-credential-param` so a wairon reader can restore it. Each operation answers its success code — `204` with no content for a method returning nothing; `201 Created` for a POST that creates: a method whose `effect` is `lifecycle`, or one whose effect is undeclared, `write` or `io` and whose name says it creates (`createHabit`, `placeOrder`, `signUp`); else `200` (a cancel, an archive, a log-in) — and a method returning `result<T, E>` also answers a `default` error response carrying E's schema (`surface import` reads both back). A `custom` auth becomes an `apiKey` scheme only when the Portal's `auth.name` (with `auth.in`, default header) names where the credential travels; naming none, it is an `http` scheme `custom` with the design's description — no header name is invented. Two operations of one Portal on the same verb and path (placeholders compared by position) are refused naming both, never dropped (`validate` reports them as `ENDPOINT_ROUTE_DUPLICATE`). A type another project publishes resolves from its pin, or — for a member project — from what the member exports now; a type nothing resolves is named in a warning (`The document has no schema for N type(s) it names: …`), never passed under a ✔. Every status line goes to stderr, so `surface export --format openapi > api.json` writes the JSON alone; an export that publishes nothing, or of a design `validate` refuses (its error count and codes), is a warning, never a ✔. An unknown `--portal` is refused naming the portals the surface renders |
1469
+ | `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 3.1 document per portal), import one, or list them. OpenAPI defaults to the `project` audience, so every HTTP Portal of the project's own is described — one its L0 export table never exports included (a wider `--audience` narrows to what the table shares there); the native snapshot defaults to `instance`. In the OpenAPI document a path placeholder — `{name}`, or the Express spelling `:name`, which is rewritten to `{name}` — is a parameter `in: path`, the other params the query of a GET/DELETE or the JSON body of a POST/PUT/PATCH — the one object-typed param left IS the body, as a client sends it (named under `x-wairon-body-param`, so `surface import` puts it back), and several are the properties of a body object. The Portal's `basePath` is joined into every path (the document carries no `servers` entry: where it is served is deployment, not design). The parameter that carries the credential the Portal's `auth` binds (a bearer `token`, say) is left out of the parameters and the body — `security` describes it — and named under `x-wairon-credential-param` so a wairon reader can restore it. Each operation answers the success `status` its endpoint states (`sdd_set_endpoints` `status`: `202` Accepted, a `3xx` redirect with a `Location` header and no body, a `201` for a workflow verb that files something), else its conventional code — `204` with no content for a method returning nothing; `201 Created` for a POST that creates: a method whose `effect` is `lifecycle`, or one whose effect is undeclared, `write` or `io` and whose name says it creates (`createHabit`, `placeOrder`, `signUp`); else `200` (a cancel, an archive, a log-in) — and a method returning `result<T, E>` also answers a `default` error response carrying E's schema (`surface import` reads both back). A `custom` auth becomes an `apiKey` scheme only when the Portal's `auth.name` (with `auth.in`, default header) names where the credential travels; naming none, it is an `http` scheme `custom` with the design's description — no header name is invented. Two operations of one Portal on the same verb and path (placeholders compared by position) are refused naming both, never dropped (`validate` reports them as `ENDPOINT_ROUTE_DUPLICATE`). A type another project publishes resolves from its pin, or — for a member project — from what the member exports now; a type nothing resolves is named in a warning (`The document has no schema for N type(s) it names: …`), never passed under a ✔. The components are exactly the types the operations reach, each ONCE — a member's type reached under its alias, its project id or another producer's closure is one component, keyed `<alias>.<id>` (a qualified id's `::` written `.`, so every key matches `^[a-zA-Z0-9._-]+$`); a field or parameter typed `T?` is not `required` (it stays nullable), and a field's description is carried on its property. Every status line goes to stderr, so `surface export --format openapi > api.json` writes the JSON alone; an export that publishes nothing, or of a design `validate` refuses (its error count and codes), is a warning, never a ✔. An unknown `--portal` is refused naming the portals the surface renders |
1351
1470
  | `wairon surface diff [--against <ref\|file>] [--json]` | The public-surface changelog: this project's export table now against the same table at its last **committed** approval (or at a git revision, or in a saved native snapshot from `surface export`) — every exported name and contract method `added`, `removed`, `renamed` (from its rename trace) or `changed` (signature or type shape), and how many a consumer may have to follow. A field renamed per its trace keeps its own row beside its type's rename (traced, or a changed public name of the same definition — never "removed + added"), and an exported type reshaped only by a rename it embeds says which (`embeds renamed field "money.currency" → "currencyCode"`). Against a revision it also covers the project's own HTTP Portals (its service API, exported or not), with every type they name from a pinned external or a member project expanded from the pin or member of each side: a re-pin that reshapes the project's own wire format (`GET /routes/:id/tiles` answering a renamed field) is a change of its own surface, named on the verb. What a producer writes release notes from before it re-locks; `wairon externals consumers --search <dir>` then says who uses what. Read-only. With no approval ever committed it says so (lock and commit first, or name `--against`). An L0 export entry added since that publishes nothing (a wildcard over a subsystem that publishes nothing) is listed as such, never read as no change. `--against` takes a native snapshot or a git revision: an OpenAPI document, or a file the native schema does not read, is refused in one line. `sdd_surface_diff` is the same answer for an assistant |
1352
1471
  | `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) |
1353
1472
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wairon/cli",
3
- "version": "5.1.1-dev.112",
3
+ "version": "5.1.1-dev.113",
4
4
  "description": "SYW Waffle AIron — CLI for managing AI coding agent topology across projects",
5
5
  "keywords": [
6
6
  "ai",