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

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.
@@ -51,6 +51,7 @@ You must read, respect, and update the living quest log file: `.wai/phased_desig
51
51
  The moment any need involves **storing, persisting, caching, or tracking state** — a config, a permission set, a session map, anything — give that state a component of its own, choosing the **smallest sound shape** from the held-state guidance below.
52
52
  Design the SMALLEST sound system: count the components against the endpoints and the state they serve. Ten endpoints over two simple tables is a handful of components, not thirty; every block you add must buy a rule it enforces or a seam it opens.
53
53
  2. Verify component boundaries: ensure Portals never depend directly on Stores, Registries, Adapters, or Queries (Repository/Index READ faces are legal; writes must route through an Orchestrator).
54
+ When a Portal needs what an Adapter does (a client Adapter to a sibling subsystem, an external API), the call goes in the Orchestrator the Portal already dispatches to — never a new Orchestrator whose only job is to forward one call. A forwarder is the price only when nothing else is in the path; for a sibling subsystem in the same process, prefer a `trustedLink` on the calling subsystem, which lets the Portal (or any component) depend on the sibling's published Portal directly, with no client Adapter and no forwarder.
54
55
  3. Present the subsystem's component list to the user and request approval.
55
56
  4. Once approved, define the L3 Interfaces (`.interface.yaml`, via `sdd_define_interface`) for each component in this subsystem.
56
57
  5. Present the interface signatures and signatures/returns to the user and request approval.
@@ -110,11 +111,13 @@ schemas:
110
111
  - **Libraries are called directly**: another project's `InProcess` Portal (a crate, a package, a jar, an FFI/DLL surface) is called from ANY component — `alias::portal` in `dependsOn`, `alias::portal.verb` in a call step — with no client Adapter. Pure logic may call only library verbs declared `effect: none`, read logic also `effect: read` (`LIBRARY_CALL_IMPURE`); a native library (no `abi`) called from another `targetLanguage` needs `abi: c` or `abi: wasm` (`LANGUAGE_BRIDGE_MISSING`). Wrapping a volatile third-party API in your own Adapter is a good habit, not a rule. Within one project a sibling subsystem is still reached through a client Adapter or a `trustedLink`.
111
112
  - **Transport and naming**: name a Portal for what it serves, not its wire (`order_portal`, not `http_handler`); the `transport` says the wire. Method names follow the project's method casing — camelCase for TypeScript and Java, snake_case for Rust and Python, from `targetLanguage` unless `rules.naming` says otherwise — and a wire name that differs (an HTTP path, a CLI command like `lock-check`, a JSON-RPC method like `tools/call`) lives on the method's `endpoint`, never in the method name.
112
113
  - **trustedLinks (L1)**: `{ subsystem, reason }` — declared on the SOURCE subsystem, it licenses a direct in-process edge into the named sibling **without the client-Adapter shim** (the deliberate fast lane). The target must still be in the sibling's `publicInterfaces` (its published Portal); a target-side declaration grants nothing. It also acknowledges a mutual-dependency pair.
113
- - **Declare planned code at design time**: give each implementation the `sourcePath` its code will live at (and a method its own `sourcePath` when its body lives elsewhere) — and each TYPE the `sourcePath` of the file that will declare it (with `symbol` when the code name differs): a type without one is never compared with the code's shape, which is `MISSING_TYPE_SOURCE_PATH` (a notice while its subsystem has no code, a warning once it has). Code linkage — `sourcePath`, `symbol`, `exportedVia`, `simPath`, `injectedParams`, conformance tiers — is not part of the approval, so declaring or changing it never asks for a re-lock. A file not written yet is `SOURCE_FILE_PLANNED` (a notice, never a `--ci` failure) until the component's realization begins; never strip a planned path to quiet anything. A team that wants CI to demand code for every designed implementation sets `rules.conformance.requireCode: true`.
114
+ - **Declare planned code at design time**: give each implementation the `sourcePath` its code will live at (and a method its own `sourcePath` when its body lives elsewhere) — and each TYPE the `sourcePath` of the file that will declare it (with `symbol` when the code name differs): a type without one is never compared with the code's shape, which is `MISSING_TYPE_SOURCE_PATH` (a notice while its subsystem has no code, a warning once it has).
115
+ - **Plan type files per owner, not one file for all**: each implementer brief fences the files its component's specs ALONE name, so the type files decide what can be implemented in parallel. A type one component realizes (its `componentClass`, an aggregate's entity) or only its own contracts use goes in that component's own file or its own types file (`src/habits/types.ts`); value types several components share go in ONE subsystem types file that no component owns — every brief then lists it under shared, and the spawning session gives it one writer. Never put every type of the system in one `src/types.ts`: two Stores' entity types in one file put the same file in front of two agents.
116
+ - **`injectedParams` are NOT design-time**: leave them out of every `sdd_write_narrative` call. They record a parameter a framework imposes on code that exists (a request handle, a context), and the implementer declares them once its code takes one; constructor wiring guessed now (`db`, `deps`, `config`, `req, res`) is linkage nothing takes, which the gate reports. Code linkage — `sourcePath`, `symbol`, `exportedVia`, `simPath`, `injectedParams`, conformance tiers — is not part of the approval, so declaring or changing it never asks for a re-lock. A file not written yet is `SOURCE_FILE_PLANNED` (a notice, never a `--ci` failure) until the component's realization begins; never strip a planned path to quiet anything. A team that wants CI to demand code for every designed implementation sets `rules.conformance.requireCode: true`.
114
117
  - **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
118
  - **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
119
  - **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.
120
+ - **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
121
  - **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
122
  - **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
123
  - **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.
@@ -24,7 +24,7 @@ You are the **Delegation Orchestrator**. Your job is to hand scoped work to a fo
24
24
  - Pick the agent whose `ownedPaths`/domain matches the task. If no agent fits, stop and tell the user the topology has a gap.
25
25
  2. **Fetch the LIVE brief**:
26
26
  - Call `sdd_get_agent_brief(agentId)` (or read the `wairon-agent://<agentId>` resource).
27
- - The brief carries: `agentId`, `name`, `template`, `domainRoot?`, `ownedPaths`, `readPaths?`, `instructions`, `variantGuidance?`, `typeMapping?` (how the contracts' neutral types are spelled in the language the agent's code is written in, also folded into `instructions` under `## Types in <language>`; TypeScript's table is the one code conformance reads back, while Rust's and Python's are mapping-only tables the code analyzer does not check, and the table says so), `codeFence?` (where the agent may write code: exactly the files its specs alone name — its implementation, method, simPath and binding files and the files of the types it owns — planned ones included, never a folder glob, so parallel fences never overlap), `sharedPaths?` (files it may touch but does not own: files other components' specs name too, such as a shared type module, and the module setup on the way to its code — manifests, compiler settings, package roots — plus unnamed helpers beside it), an `## Externals used` section for a component reaching another project (each alias, the names it uses, the producer's transport and abi, the pinned snapshot `.wai/externals/<alias>.yaml`, which joins `readPaths` — code against the pin, never the producer's source — and the binding modules its implementations name), and — when the project opted into `execution.tier` — `profile` and `budget`.
27
+ - The brief carries: `agentId`, `name`, `template`, `domainRoot?`, `ownedPaths`, `readPaths?`, `instructions`, `variantGuidance?`, `typeMapping?` (how the contracts' neutral types are spelled in the language the agent's code is written in, also folded into `instructions` under `## Types in <language>`; TypeScript's table is the one code conformance reads back, while Rust's and Python's are mapping-only tables the code analyzer does not check, and the table says so), `codeFence?` (where the agent may write code: exactly the files its specs alone name — its implementation, method, simPath and binding files and the files of the types it owns — planned ones included, never a folder glob, so parallel fences never overlap), `sharedPaths?` (files it may touch but does not own: files other components' specs name too, such as a shared type module, and the module setup on the way to its code — manifests, compiler settings, package roots, a planned one included (a Portal's `mod.rs`, where its sibling adds only the line declaring its module) — plus unnamed helpers beside it), an `## Externals used` section for a component reaching another project (each alias, the names it uses, the producer's transport and abi where the pin records them, the pinned snapshot `.wai/externals/<alias>.yaml`, which joins `readPaths` — code against the pin, never the producer's source — or, for a member project, its L0 export table read live, with the names spelled as that table exports them; and the binding modules its implementations name), a `## Handler shape` section for a Portal (the contract's own parameters per verb, the handles named in `injectedParams`, the `router` linkage), a `## Code linkage you own` section (when to declare `injectedParams`, and the ones declared now, which may be design-time guesses to remove), and — when the project opted into `execution.tier` — `profile` and `budget`.
28
28
  - **Never reuse a brief across delegations or after a re-lock** — fetch fresh per delegation; the call is cheap and the brief is always current.
29
29
  3. **Spawn a GENERIC subagent from the brief**:
30
30
  - Prompt: `brief.instructions`, plus the concrete task description.
@@ -65,6 +65,11 @@ 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, what its handler shape section cannot know** — the brief's
69
+ `## Handler shape` states the rule; add the framework this project uses and the
70
+ handles it really hands each function, so the subagent declares exactly those in
71
+ `injectedParams` (and removes any guessed earlier). A subagent left to guess
72
+ writes `(req, res)` handlers, and the gate reads those as substitutions.
68
73
 
69
74
  One design decision never goes into a brief as an instruction: **an entry**. A
70
75
  subagent that meets an unreached Portal verb (`UNUSED_COMPONENT` /
@@ -184,6 +184,15 @@ 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
+ - **`injectedParams` are yours to set, when the code exists** (any component, not only a Portal): declare one only once your code takes a parameter its framework or wiring imposes beside the contract's own — with `sdd_write_narrative` (`injectedParams`) or `sdd_update_spec` (`{"injectedParams": ["ctx"]}`). A dependency the code holds as a field or receives in a constructor is not one. A list already on the implementation may be a guess written at design time, before any code existed (`db`, `deps`, `config`, `req, res`): keep the names your functions really take and remove the rest — `sdd_update_spec` with `{"injectedParams": [{"value": "db", "action": "delete"}]}`, or `[]` to clear it. It is code linkage, so neither change asks for a re-lock; the brief's **Code linkage you own** section lists what is declared now.
193
+ - `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.
194
+ - **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`.
195
+ - **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
196
  - **Strict Layer Isolation & No Persistence Shortcuts**:
188
197
  - 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
198
  - 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
@@ -28,6 +28,8 @@ re-run `wairon init` for an independent project).
28
28
  **What to commit.** All of `.wai/` — the specs, `project.yaml`, `lock.json`,
29
29
  `externals/` and `externals.lock.yaml`, vendored packs: the gate reads committed
30
30
  files only (a migration's scratch folder, `.wai/transactions/`, ignores itself).
31
+ The one exception is `.wai/.spec-write.lock`, the short-lived lock spec writers
32
+ take so two sessions never lose a write: add it to `.gitignore`.
31
33
  The generated `CLAUDE.md`/`GEMINI.md`, `.claude/` (or `.gemini/`) skills and
32
34
  `.mcp.json` hold no machine path: commit them and every clone's assistant starts
33
35
  with them, or ignore them and run `wairon generate` after a clone — either way
@@ -151,14 +153,19 @@ warnings as errors (notices are printed and counted, never fatal).
151
153
  declare the producer's names in the binding itself), or its component reaches
152
154
  neither a pinned external nor a member project that exports something. Both
153
155
  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**
156
+ `module.exports.X = function (…) {…}` or `= X`, each property of
157
+ `module.exports = { … }` — a shorthand `{ Client }` included —
158
+ `module.exports = class X { … }`, a TypeScript `export = X`, and JSDoc
159
+ `@typedef`/`@property`), a bare identifier through the module's own
160
+ declaration of it; a `_private` class or object member is not compared, and
161
+ 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
162
  project (declared under `members`, never pinned — named by its alias or its
157
163
  project id) is compared the same way with what the member's L0 exports now
158
164
  (its exported contracts and types, and the types they name), so a
159
165
  `rename-param` or `rename-field` in the member reaches its parent's binding
160
166
  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
167
+ never told to pin — one that exports nothing is told to export there. A
168
+ member in a `../` sibling checkout is a contained one here. A git
162
169
  member is pinned like an external (`wairon externals pin <alias>`) and
163
170
  compared with that pin. A file of another project imported directly from its
164
171
  source tree is not a binding: it is the producer's own code.
@@ -198,7 +205,8 @@ warnings as errors (notices are printed and counted, never fatal).
198
205
  `MISSING_SOURCE_FILE` (error) and a contract method naming no file is
199
206
  `METHOD_SOURCE_PATH_MISSING` (warning). `rules.conformance.requireCode: true`
200
207
  in `project.yaml` reports the two notices as errors, for a CI that must say
201
- every designed implementation has code.
208
+ every designed implementation has code. It is code-conformance configuration,
209
+ outside the approval identity: flipping it never stales a lock (see `lock-check`).
202
210
  - **Types are linked to code too.** A type spec without a `sourcePath` is
203
211
  `MISSING_TYPE_SOURCE_PATH` — a notice before its subsystem has code, a
204
212
  warning after. Plan a type's `sourcePath` when you plan the implementations'.
@@ -263,6 +271,79 @@ warnings as errors (notices are printed and counted, never fatal).
263
271
  that Portal is realized by the link, never reported `UNREALIZED_DEPENDENCY`:
264
272
  never add an import across a network boundary to satisfy a check.
265
273
 
274
+ #### Framework handlers, injected handles and routers
275
+
276
+ The parameter check pairs a verb's contract parameters with the function that
277
+ realizes it, in order. A framework handler is green without any design change:
278
+
279
+ - **Take the contract's own parameters, under its names.** `cancelOrder(req, res)`
280
+ realizing `cancelOrder(orderId, customerId)` is two substitutions
281
+ (`UNREALIZED_PARAM` + `UNDECLARED_PARAM`), however the handles are typed (a
282
+ hand-written `type Req = { params: … }` included) and wherever the contract's
283
+ types live (a member project's `shared::order_id` is read live): a parameter
284
+ named as a transport handle (req, res, ctx, request, response, next, reply) where
285
+ the contract declares an argument of its own is never a silent pairing. Neither
286
+ is a platform transport class (`IncomingMessage`, a fetch `Request`) or an
287
+ object sharing none of a record's fields where the contract declares that
288
+ record; an object holding every field under another name is a rename
289
+ (`PARAM_NAME_MISMATCH`), and one object carrying several of the contract's
290
+ parameters (`cancelOrder(input: { orderId, customerId })`) leaves each of them
291
+ unrealized.
292
+ - **A record parameter is typed as the record.** Where the contract declares a
293
+ record, a parameter typed `any`, `unknown`, `object` or
294
+ `Record<string, unknown>` (or not annotated at all, in TypeScript) names no field
295
+ to compare, and an object missing the record's fields or adding its own
296
+ (`{ stops; secret }`) is another shape: both are `PARAM_TYPE_MISMATCH`. The check
297
+ fails closed on them, as it does on an `any` receiver.
298
+ - **Declare what the framework hands you as wiring.** Name the handles in the
299
+ implementation's `injectedParams` (code linkage: `sdd_update_spec`, no
300
+ re-lock). A name matches with or without its leading `_`, at the start or the
301
+ end of the list — `handler(ctx, req, orderId, customerId, res)` with
302
+ `injectedParams: [ctx, req, res]`. An injection no handler takes is
303
+ `UNUSED_INJECTED_PARAM`, a notice: it hides nothing, so drop it when you see it.
304
+ - **Or read the parameters off the request.** Where the framework hands the
305
+ handler the request alone, inject it and read each contract parameter off it by
306
+ its own name (`req.params.orderId`, `req.query.limit`, `req.body.code`,
307
+ `const { orderId } = req.params`, `url.searchParams.get('limit')` on a
308
+ `new URL(req.url, base)`): that realizes the parameter through the handle. So
309
+ does a helper the request is passed to, written in the same file or in a module
310
+ no implementation claims (`const { orderId } = pathParams(req)`), and the one
311
+ record parameter is realized by reading the body whole (`req.body`). A
312
+ parameter never read off it stays `UNREALIZED_PARAM`.
313
+ - **`conformance: off`** on an implementation or a method is for code wairon is
314
+ not meant to compare with its contract (generated or vendored code). It switches
315
+ the realization checks off — method, parameters, async, whether narrated calls
316
+ are found — and never the doctrine: an unnarrated write
317
+ (`UNDECLARED_WRITE_CALL`), a call the analysis cannot follow, a Portal write
318
+ shortcut and route coverage are judged whatever it says. A dial turned off is
319
+ said: `REALIZATION_UNCHECKED` (a notice per implementation) and a
320
+ `Conformance off:` line in `wairon status`.
321
+
322
+ A Portal's `router` (code linkage) names the entry its own file exports
323
+ (`handleOrderRequest`), a central module's entry or table
324
+ (`src/routes.ts#ROUTES`), or a module alone (`src/routes.ts`, every route-bearing
325
+ export read). Several Portals may name one router — a module, or one table both
326
+ name (`router: routes`) — and a route is declared when any of them declares it.
327
+ Guards on `<request>.method` and `parts[i]` are read, and so are route tables — an
328
+ array of `{ method, path }` objects or an object keyed `VERB /path`
329
+ (template-literal keys over constants included), bound to a const or returned in
330
+ place, spreading other tables (`[...ingestRoutes, ...statsRoutes]`) from any
331
+ module. Route coverage is never silently off: a Portal whose contract binds HTTP
332
+ endpoints and whose implementation names no router gets a `ROUTER_UNDECLARED`
333
+ notice naming `router:`, and a router that cannot be read is
334
+ `UNREADABLE_ROUTER`. A table is read under the ONE prefix its router strips off the
335
+ path (`path.slice(BASE.length)`), so that prefix must be the Portal's `basePath`:
336
+ another one is `UNDECLARED_ROUTE` + `UNROUTED_ENDPOINT`, naming both.
337
+
338
+ Another project's code is reached through what it publishes — its member alias, a
339
+ pinned external — or through a binding module the implementation declares in
340
+ `bindings`. An import that resolves into its source under a name it does not
341
+ export is `CROSS_PROJECT_SOURCE_IMPORT` (warning); one into a module realizing its
342
+ Store is the persistence shortcut past its surface. A claimed call into a
343
+ component whose code is in another language (a Rust member behind a binding) is
344
+ not judged by the TypeScript call graph: it is said once per project and language
345
+ as `CALL_ACROSS_LANGUAGE` (notice), never as an unrealized call.
346
+
266
347
  #### Technologies and their packages
267
348
 
268
349
  `TECH_LEAKAGE_IN_CODE` compares each import's package with the packages of the
@@ -332,8 +413,16 @@ When any spec is still `draft` or `design`, the lock says how many of how many,
332
413
  per kind (subsystems, components, contracts, implementations, types), and that
333
414
  it approves them as they stand. Re-running it when nothing it records moved
334
415
  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.
416
+ unmodified; a change of approver, verdict or member pin writes a new record. A
417
+ new wairon release writes nothing by itself: when its built-in design rules are
418
+ the same as the ones the record was taken under (the record's `gateParts.release`
419
+ is unchanged), the approval simply still matches and the record — its release
420
+ stamp included — is left as it is; when they moved, the approval carries over
421
+ by re-validation and one `wairon lock` refreshes the stamp (see *Upgrading
422
+ wairon* under `lock-check`). Evidence recorded *beside* the approval (the code
423
+ results with the analyzer's version, the design counts, gate parts, a release
424
+ stamp that is missing, no version or newer than the running wairon) is refreshed
425
+ and named; the summary says when this run left the file untouched.
337
426
 
338
427
  **What it certifies is the design.** Only design findings can refuse a lock.
339
428
  Code-conformance findings (the code↔spec checks) are recorded **beside** the
@@ -419,11 +508,28 @@ whole design's, so it is refused as a first approval (run `wairon lock`), and
419
508
  refused whenever a spec outside the subsystem — or an input the identity covers
420
509
  besides the design (rule tuning, packs, network, a pin, a member's approval,
421
510
  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
511
+ exactly as it was, and the record is the one a full lock would write. A
512
+ member project's subsystem (`--subsystem payments_svc::payments` at a parent) is
513
+ refused: a member's subsystem is approved at the member's own root
514
+ (`wairon lock --subsystem payments` in its folder), then the parent locks to pin
515
+ its new approval. When
516
+ nothing changed, `lock` leaves the approval on record as it is — its `lockedAt`
517
+ and restamp trace — and `.wai/lock.json` untouched by that run; only evidence
425
518
  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.
519
+ that are missing or do not match the identity, a release stamp that is missing,
520
+ no version or newer than this wairon — replaced with this release) is refreshed,
521
+ and it says which. When a lock records the project's own inputs for the first
522
+ time (a first approval, or a record from before gate parts) or the rule tuning
523
+ or packs moved, it prints the inputs it records — projectType, each rule
524
+ severity override, the selected packs, a declared network, `composition` — so
525
+ the tuning you approve is shown, not only digested.
526
+
527
+ **In a family, lock bottom-up.** A parent's record pins each member's approval,
528
+ so lock every member that is not approved at its own root first, then the
529
+ parent: three projects cost three locks. A parent locked first records the
530
+ member as drifted, its `lock-check` fails until the member re-locks, and it
531
+ must lock again to pin the new approval — `lock` warns before it writes when a
532
+ direct member is not covered by its own approval.
427
533
  Another approver locking an unchanged design records their own approval. A shell
428
534
  with no terminal and no `--yes` writes nothing.
429
535
 
@@ -439,8 +545,10 @@ record counts too, so commit it before you rely on a local pass.
439
545
  | **`locked`** — the approval still covers this design | pass (0) | pass (0) |
440
546
  | **`stale`** — the design moved past its approval | **fail (1)** | **fail (1)** |
441
547
  | **`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)** |
548
+ | no `.wai/specs` in this directory at all, and no approval on record | pass, saying so (0) | **fail (1)** |
549
+ | no `.wai/specs` (or none left), while `.wai/lock.json` approves spec files | **fail (1)**: the tree was deleted | **fail (1)** |
550
+ | `.wai/project.yaml` missing while an approval is on record | **fail (1)**, naming the file | **fail (1)** |
551
+ | 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
552
  | a member project was never approved by anyone | pass, naming it as *not gated* (0) | **fail (1)** |
445
553
  | 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
554
 
@@ -488,8 +596,30 @@ them; later upgrades then carry over*, and `wairon lock` takes a real approval
488
596
  (never a restamp). The same holds for gate parts that do not break down the
489
597
  `stateId` beside them (they are ignored, with a notice; when the identity still
490
598
  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.
599
+ stamp that is no wairon version or is *newer* than the running wairon when the
600
+ identity no longer matches — a record *written by a newer wairon* is never
601
+ re-judged by an older one. When the identity still matches, the identity
602
+ decides: the approval holds, and every surface (`lock-check`, `status`,
603
+ `sdd_get_status`, `validate`) reports the stamp as a notice (`its release stamp
604
+ "banana" is no wairon version`, `… says wairon 9.9.9 took it, a newer release than
605
+ this one`); `wairon lock` replaces it with the running release. A matching
606
+ record that carries no gate parts at all gets the same notice `{}` gets, and
607
+ `lock` rewrites them. A `lock.json` that names one key twice (what a careless
608
+ conflict resolution leaves) cannot be read, like one with conflict markers.
609
+
610
+ **What the identity covers, and what it leaves out.** The project's own *design*
611
+ rule tuning — `projectType`, `sddRuleSeverity` overrides of design rules, the
612
+ other design settings under `rules` — its pack selection, network declaration
613
+ and `composition` are inputs: changing one stales the approval. A selected pack
614
+ that cannot load (not installed, not vendored) leaves the approval *owed* —
615
+ `lock-check` fails, naming the pack — because the doctrine it was judged under
616
+ cannot be read. **Code-conformance configuration is deliberately outside the
617
+ identity:** `rules.conformance` (`requireCode` included) and severity overrides
618
+ of rules that judge code govern how the *code* is held to the design, not what
619
+ the design is, so flipping `requireCode` changes what `validate --ci` fails on
620
+ and never stales an approval — exactly as adding a `sourcePath` never does. The
621
+ analyzer's doctrine is recorded beside the claim (`code.analyzer.doctrineDigest`)
622
+ instead.
493
623
 
494
624
  **It is optional by construction.** By default only a moved approval (or a
495
625
  member's unapproved changes) refuses, and neither can happen in a project that
@@ -1015,10 +1145,14 @@ content — `as:` only asserts it (a contradiction is `MEMBER_KIND_MISMATCH`):
1015
1145
  `partOf: { project: <parent id>, path: <parent root> }`.
1016
1146
  - A **project** is an independent boundary: its tree declares an id, holds an L0
1017
1147
  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.
1148
+ judged by its own gate and locked at its own root. A project contained in this
1149
+ project, or in a `../x` sibling checkout beside it, is followed by the family
1150
+ scan and composed **live**: its exports are read where it is stored, its own
1151
+ gate runs in the family run, and nothing is pinned (`wairon externals pin`
1152
+ says so for a member alias). A project fetched from git or a `hosted:` record
1153
+ is a **referenced** project: this project's gate judges it against its pin
1154
+ (`wairon externals pin`), and the family run opens it where it is stored and
1155
+ composes it per use. A hosted instance never reads a `../` source.
1022
1156
 
1023
1157
  Boundaries are earned: grow a system from subsystems, to parts when a piece
1024
1158
  needs its own folder or repository, to projects only when it needs its own team,
@@ -1097,7 +1231,7 @@ widen it (`EXPORT_WIDENS_AUDIENCE`).
1097
1231
  | `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
1232
  | `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
1233
  | `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 |
1234
+ | `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
1235
  | `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
1236
  | `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
1237
  | `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 +1351,14 @@ project, so it holds nothing machine-specific: it runs `wairon mcp serve` from t
1217
1351
  and the server attaches to the project it is started in. A `--global` registration is
1218
1352
  machine-wide and names the running CLI by its absolute path.
1219
1353
 
1220
- The local server offers 47 tools:
1354
+ The local server offers 49 tools:
1221
1355
 
1222
1356
  | Group | Tools |
1223
1357
  |-------|-------|
1224
1358
  | 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) |
1359
+ | 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
1360
  | 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` |
1361
+ | 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
1362
  | Members | `sdd_add_member`, `sdd_move_member` |
1229
1363
  | 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
1364
  | Externals | `sdd_add_external`, `sdd_update_external`, `sdd_remove_external`, `sdd_pin_externals`, `sdd_get_externals_status` |
@@ -1233,6 +1367,15 @@ The local server offers 47 tools:
1233
1367
  | Packs | `sdd_pack_impact` |
1234
1368
  | Delegation | `sdd_get_agent_brief` (the live brief for one agent; also served as the `wairon-agent://` resource) |
1235
1369
 
1370
+ Every tool description ends with its argument names (`Arguments: id, newId, dryRun?.`). Every write
1371
+ tool refuses a spec of another project — a member's spec read through this tree is written from the
1372
+ member's own root, and the refusal names its folder and the call to make there. `sdd_delete_spec` and
1373
+ a contract method removed through `sdd_update_spec` report `published` (the public names a consumer
1374
+ reaches what goes through) and `breaks` (the consumers in reach, and in the `search` folders, that use
1375
+ one), as the renames do; a removed contract method takes its implementation entries with it
1376
+ (`cascaded`) and is refused while another spec still calls it. A delta never changes a spec's id or
1377
+ subsystem: `sdd_rename_spec` and `sdd_move_spec` do, with every reference and published entry.
1378
+
1236
1379
  A hosted server adds its own `sdd_host_*` and `sdd_landscape_*` tools; see the
1237
1380
  [hosted server guide](https://github.com/SYW-Apps/Waffle-AIron/blob/main/docs/design/hosted-mcp-server.md).
1238
1381
 
@@ -1347,7 +1490,7 @@ return the impact of every pack they applied in their results.
1347
1490
  | `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
1491
  | `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
1492
  | `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 |
1493
+ | `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 — an optional one or a `T?` too, the body then optional or nullable, never wrapped under its name (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
1494
  | `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
1495
  | `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
1496
 
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.114",
4
4
  "description": "SYW Waffle AIron — CLI for managing AI coding agent topology across projects",
5
5
  "keywords": [
6
6
  "ai",