@wairon/cli 5.1.1-dev.113 → 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,7 +111,9 @@ 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.
@@ -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,13 +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, 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
+ * **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.
75
73
 
76
74
  One design decision never goes into a brief as an instruction: **an entry**. A
77
75
  subagent that meets an unreached Portal verb (`UNUSED_COMPONENT` /
@@ -189,6 +189,7 @@ All implementation work must strictly adhere to these rules:
189
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
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
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.
192
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.
193
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`.
194
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`.
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
@@ -276,38 +278,60 @@ realizes it, in order. A framework handler is green without any design change:
276
278
 
277
279
  - **Take the contract's own parameters, under its names.** `cancelOrder(req, res)`
278
280
  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`).
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.
285
298
  - **Declare what the framework hands you as wiring.** Name the handles in the
286
299
  implementation's `injectedParams` (code linkage: `sdd_update_spec`, no
287
300
  re-lock). A name matches with or without its leading `_`, at the start or the
288
301
  end of the list — `handler(ctx, req, orderId, customerId, res)` with
289
302
  `injectedParams: [ctx, req, res]`. An injection no handler takes is
290
- `UNUSED_INJECTED_PARAM`.
303
+ `UNUSED_INJECTED_PARAM`, a notice: it hides nothing, so drop it when you see it.
291
304
  - **Or read the parameters off the request.** Where the framework hands the
292
305
  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`.
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`.
296
313
  - **`conformance: off`** on an implementation or a method is for code wairon is
297
314
  not meant to compare with its contract (generated or vendored code). It switches
298
315
  the realization checks off — method, parameters, async, whether narrated calls
299
316
  are found — and never the doctrine: an unnarrated write
300
317
  (`UNDECLARED_WRITE_CALL`), a call the analysis cannot follow, a Portal write
301
- shortcut and route coverage are judged whatever it says.
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`.
302
321
 
303
322
  A Portal's `router` (code linkage) names the entry its own file exports
304
323
  (`handleOrderRequest`), a central module's entry or table
305
324
  (`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
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
311
335
  path (`path.slice(BASE.length)`), so that prefix must be the Portal's `basePath`:
312
336
  another one is `UNDECLARED_ROUTE` + `UNROUTED_ENDPOINT`, naming both.
313
337
 
@@ -1466,7 +1490,7 @@ return the impact of every pack they applied in their results.
1466
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) |
1467
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 |
1468
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 |
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 |
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 |
1470
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 |
1471
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) |
1472
1496
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wairon/cli",
3
- "version": "5.1.1-dev.113",
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",