@wairon/cli 5.1.1-dev.105 → 5.1.1-dev.107
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -1
- package/dist/cli/index.js +13106 -8767
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +2478 -774
- package/dist/index.js +4721 -1476
- package/dist/index.js.map +1 -1
- package/dist/templates/skills/sdd-architect.md +16 -13
- package/dist/templates/skills/sdd-delegate.md +7 -1
- package/dist/templates/skills/sdd-implement.md +5 -1
- package/dist/webapp.html +53 -52
- package/docs/cli.md +184 -17
- package/docs/design-export.md +13 -8
- package/docs/network.md +104 -0
- package/docs/standards/architecture.md +101 -12
- package/package.json +1 -1
- package/schemas/{design-export-1.json → design-export-2.json} +44 -46
|
@@ -48,7 +48,8 @@ You must read, respect, and update the living quest log file: `.wai/phased_desig
|
|
|
48
48
|
- Do NOT design components or interfaces for the entire system all at once. Proceed **one subsystem at a time** to ensure focused, manageable design reviews.
|
|
49
49
|
- For the active subsystem:
|
|
50
50
|
1. Design and add all L2 Components (Portals, Orchestrators, Stores, etc.) using `sdd_add_component` (defaulting to `status: draft`).
|
|
51
|
-
The moment any need involves **storing, persisting, caching, or tracking state** — a config, a permission set, a session map, anything —
|
|
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
|
+
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.
|
|
52
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).
|
|
53
54
|
3. Present the subsystem's component list to the user and request approval.
|
|
54
55
|
4. Once approved, define the L3 Interfaces (`.interface.yaml`, via `sdd_define_interface`) for each component in this subsystem.
|
|
@@ -78,19 +79,18 @@ The full standard is the source of truth; this is the summary you design against
|
|
|
78
79
|
- **No wildcard block**: decompose instead. Held or derived state → Store/Index/Repository; behaviour over a value's own fields → a type method; external I/O → Adapter; logic → an Orchestrator with the narrowest `dependencyClass` that holds; one live thing → Actor. `Specialist` and `Gateway` are retired (`STEREOTYPE_RETIRED`): `wairon doctor --fix` retypes a Specialist as an Orchestrator, and a Gateway becomes the Portal it owned, with the `gateway` variant.
|
|
79
80
|
2. **Patterns** (named compositions; set `owns`): `Repository` — the data-access component for one aggregate: owns one Store + its Registry + Indexes + Queries + optional Adapter; consumers use the facade only, never the inner blocks. A `Query` lives only inside a Repository and depends only on its Store, a backend Adapter or pure logic. Reads across aggregates go through a read Orchestrator (or a read-model Repository whose Query runs the join), and an outbox is a sibling Repository. A pattern owns only building blocks, never another pattern — compose patterns at the subsystem (L1) level.
|
|
80
81
|
- **Variants** (`variant:` on a component): the built-in `arbiter`, `projector`, `composer` and `codec` (on Orchestrator) and `gateway` (on Portal: authenticates, authorizes, validates or rate-limits before it dispatches, with its inbound auth in `auth`), then any global and project variants, a later layer overriding by id. A variant carries guidance; what an Orchestrator may depend on is its own `dependencyClass`.
|
|
81
|
-
- **No Persistence Shortcuts**:
|
|
82
|
-
- **Held-state
|
|
83
|
-
1. `
|
|
84
|
-
2.
|
|
85
|
-
3.
|
|
86
|
-
- *
|
|
87
|
-
- *Never*: hold state as fields inside an Orchestrator because a Store link was refused. A refused link means "apply this recipe", not "inline the state" — state hidden inside a logic component is invisible to the spec and unrecoverable.
|
|
82
|
+
- **No Persistence Shortcuts**: held state always lives in a data component of its own — never as fields inside an `Orchestrator`, and never merged with logic into a "storage" or "helper" component. WHICH data shape is the smallest one that is sound for the state at hand:
|
|
83
|
+
- **Held-state guidance (apply the moment state appears — do not wait for a refused link), smallest first:**
|
|
84
|
+
1. **Genuinely simple state** — a settings record, a table read and written by key, a small set — is **one standalone `Store`**. This is the sanctioned shape, not a shortcut: its consumers are on the workflow layer (a workflow Orchestrator or an Actor; a Portal reaches it through that Orchestrator), and you acknowledge the `UNOWNED_STORE` warning with a `lint.allow` reason ("simple keyed state, no lookups beyond the key"). Do NOT add a Registry, Index or Repository around it to make it look complete.
|
|
85
|
+
2. **State with real lookup or index needs** — reads by something other than the key (by owner, by date range, by status), several read projections, or a write path that must keep invariants across them — is the **`Repository` pattern**: `<x>_store` (Store), `<x>_registry` (Registry — write path), one `<x>_index` per real lookup (Index — read path), and the `<x>_repository` facade that `owns` them; every consumer points at the facade. Add an Index only for a lookup a narrative actually performs; a Repository with one Index that only re-reads by key is the simple case dressed up — use step 1.
|
|
86
|
+
3. Never split Store/Registry/Index for state that step 1 covers, and never combine their roles into one component when step 2 applies.
|
|
87
|
+
- *Never*: hold state as fields inside an Orchestrator because a Store link was refused. A refused link means "give the state its component", not "inline the state" — state hidden inside a logic component is invisible to the spec and unrecoverable.
|
|
88
88
|
3. **owns vs dependsOn**: `owns` = a pattern's private member blocks (exactly one hop), or a Supervisor's supervision state (its own Stores and Registries). `dependsOn` = collaborators (other facades / standalone blocks). Never depend on a block privately owned by another pattern.
|
|
89
89
|
4. **Decoupling**: Registry (write) and Index (read) are independent — both work on the Store; the Registry never updates Indexes (Indexes share the Store's references and project structural changes). A Store is depended *upon*; it never depends on a Registry/Index. An Index depends on its Store, a backend Adapter or pure logic; only when one projection is itself worth re-presenting another way may a derived Index depend on another Index — owned by the same Repository, never in a cycle (`ARCHITECTURE_VIOLATION_INDEX_DEP` otherwise). Keep it the exception: it is not a way to chain lookups.
|
|
90
90
|
5. **Behaviour placement**: behaviour lives where it can be performed autonomously over its own state (`order.total()`, `dog.bark()`); behaviour needing an external actor lives on the *acting* component, taking the entity as an argument (a `Carrier` ships an order — not `order.ship()`). Prefer composition + interfaces over inheritance.
|
|
91
91
|
6. **Narrative coding (L5)**: each method reads top-to-bottom as named steps; one level of abstraction per function; a pattern facade's method is exactly one `call` step (pure 1:1 forwarding, no logic).
|
|
92
92
|
7. **Right-size**: L1, concurrency, and events are all optional. However, **layer boundaries and persistence structures are mandatory**. Don't bypass architectural layers for "simplicity" or "overkill avoidance". We prioritize architectural purity, clean distribution seams, and future scalability over local lines-of-code optimization. Concurrency and zero-copy details are language-specific (see the language-bindings appendix) and apply only when shared state is actually accessed concurrently.
|
|
93
|
-
8. **Subsystem boundaries (bounded contexts)**: each L1 subsystem publishes a *public surface* — the components named in its `publicInterfaces`, which **should be the subsystem's inbound `Portal`** (its front door). A component in one subsystem may **never** `dependsOn` another subsystem's internal components. Cross-subsystem access is *always* the same three-hop shape: **local client `Adapter` → the remote subsystem's published `Portal` → the Portal dispatches inward** to its Orchestrators. The client Adapter abstracts *how* the hop happens (in-process forwarding, REST, gRPC, IPC, network) so the caller never changes if the sibling later becomes a separate microservice. Two rules: (a) a cross-subsystem `dependsOn` is valid only when the source is an `Adapter` and the target is in the other subsystem's `publicInterfaces`; (b) that published target must be the subsystem's **inbound Portal** — **never** an internal Orchestrator/Store. Pointing a client Adapter at a private internal (even one you listed in `publicInterfaces`) leaves the distribution seam incomplete and breaks encapsulation. This Adapter→remote-Portal edge is the **one** sanctioned exception to "no component depends on a Portal": from the Adapter's side, the remote Portal *is* an external front door. (Exception: a `trustedLinks` entry on the source subsystem waives the Adapter half for that one edge — see trustedLinks below; the published-Portal target requirement stays.)
|
|
93
|
+
8. **Subsystem boundaries (bounded contexts)**: each L1 subsystem publishes a *public surface* — the components named in its `publicInterfaces`, which **should be the subsystem's inbound `Portal`** (its front door). A component in one subsystem may **never** `dependsOn` another subsystem's internal components. Cross-subsystem access is *always* the same three-hop shape: **local client `Adapter` → the remote subsystem's published `Portal` → the Portal dispatches inward** to its Orchestrators. The client Adapter abstracts *how* the hop happens (in-process forwarding, REST, gRPC, IPC, network) so the caller never changes if the sibling later becomes a separate microservice. Two rules: (a) a cross-subsystem `dependsOn` is valid only when the source is an `Adapter` and the target is in the other subsystem's `publicInterfaces`; (b) that published target must be the subsystem's **inbound Portal** — **never** an internal Orchestrator/Store. Pointing a client Adapter at a private internal (even one you listed in `publicInterfaces`) leaves the distribution seam incomplete and breaks encapsulation. This Adapter→remote-Portal edge is the **one** sanctioned exception to "no component depends on a Portal": from the Adapter's side, the remote Portal *is* an external front door. (Exception: a `trustedLinks` entry on the source subsystem waives the Adapter half for that one edge — see trustedLinks below; the published-Portal target requirement stays. Across projects, a library — another project's `InProcess` Portal — is the other exception: any component calls it directly, see *Libraries are called directly* below.)
|
|
94
94
|
- **Portal auth & public/internal separation**: a `Portal` carries its own `auth` (OpenAPI-shaped: `none | apiKey | bearer | basic | oauth2 | openIdConnect | custom`) and each Portal renders to its **own** named OpenAPI spec. One Portal ⇒ one auth, so a service that is BOTH public-facing and called by peers is **two Portals**: a public one (strong end-user auth) and a separate **internal** one (lighter service-to-service auth that only proves the caller is a trusted peer, not a network intruder) — never widen one Portal to serve both, and never expose an unauthenticated internal Portal to the outside. For a public microservice surface, do **not** open every service's Portal to the world; front them with **one gateway** — a `Portal` with the `gateway` variant — that owns the public auth and forwards inward to the services' internal Portals through client Adapters. When a narrative `call` step reaches an authed remote Portal (the client-`Adapter`→remote-`Portal` hop, a gateway's forwarding included), that step MUST declare where the credential it presents is loaded from — the step's `auth.from`. Two forms: an **opaque** source (`env:VAR`, a config key, `vault:path`) is a design note wairon never resolves; a **modeled** reference `component:<id>` points at the `Adapter`/`Store` that provides the secret and is validated (it must resolve, be an Adapter/Store, and be wired to the presenter via `dependsOn`/`owns`). The secret itself is never in the spec. Omitting `auth.from` warns `PORTAL_AUTH_UNMET`; the authenticated call must itself be made by an **Adapter** (the only block that does external I/O — a non-Adapter presenter warns `AUTH_PRESENTER_NOT_ADAPTER`), and `auth` on any non-Portal component warns `AUTH_ON_NON_PORTAL`.
|
|
95
95
|
|
|
96
96
|
## 📋 Spec Shapes (compact reference)
|
|
@@ -101,11 +101,14 @@ JSON for review. You never hand-author spec YAML. What to know beyond the tool
|
|
|
101
101
|
schemas:
|
|
102
102
|
|
|
103
103
|
- **Levels & files** (written by the tools): L0 `.wai/specs/.index.yaml` → L1 `<sub>/.index.yaml` → L2 `<sub>/<comp>/.index.yaml` (owned members nest one level deeper) → L3 `.interface.yaml` → L4+L5 `.implementation.yaml`. Types live under `types/` (system-level shared, or per subsystem).
|
|
104
|
-
- **Statuses**: `draft` → `design` → `complete` — authoring readiness. Completeness rules relax to warnings while draft/design. Approval is separate: `wairon lock` (human-run) validates the tree as if complete and records the approval in `.wai/lock.json`; it never rewrites a spec's `status`.
|
|
104
|
+
- **Statuses**: `draft` → `design` → `complete` — authoring readiness. Completeness rules relax to warnings while draft/design. Approval is separate: `wairon lock` (human-run) validates the tree as if complete and records the approval in `.wai/lock.json`; it never rewrites a spec's `status`. A status is readiness, never design: it is left out of the approval, so promoting a spec to `complete` after the lock never reopens it (and never needs a re-lock).
|
|
105
105
|
- **designDepth** (project `rules.designDepth`, per-subsystem override, or a pack profile's default): how deep the team COMMITS to designing — `components | interfaces | implementations | narratives` (default `narratives` = full depth). Expectation checks below the depth are gated (nothing deeper is demanded to exist); soundness of whatever IS authored always applies. Ask the user their intended depth at Stage 1 if unclear — do not assume a team that stops at L3 is "incomplete".
|
|
106
|
-
- **publicInterfaces (L1)**: each entry MUST name the `component` realizing it
|
|
106
|
+
- **publicInterfaces (L1)**: each entry MUST name the `component` realizing it. Its export kind is DERIVED from the backing Portal's `transport` — never author a `type`. Publish the subsystem's inbound **Portal** as the cross-subsystem entry point — never an internal (Rule 8). A contract consumers IMPLEMENT rather than call (a trait, a callback, a webhook) is exported with `role: implement` (typically the Adapter holding the port); the consumer's interface declares `implements: alias::name` and states every method with the same signature. Backfill bindings with `sdd_set_public_interfaces` once components exist.
|
|
107
107
|
- **Subsystem consumers**: an entry may list `consumers: [<subsystem>, …]` — the subsystems the surface is published to. Absent, any subsystem may depend on it; present, only those may, and a dependency from any other subsystem is `CROSS_SUBSYSTEM_UNLISTED_CONSUMER` (an error). Use it for an internal Portal meant for one or two peers, so the boundary says who may call it.
|
|
108
|
-
- **Portal
|
|
108
|
+
- **Portal transport and entries**: every Portal states its `transport` (HTTP, gRPC, GraphQL, MessageBus, CLI, NamedPipe, IPC, JSONRPC for JSON-RPC over stdio, InProcess for a library, Custom). Which process serves which Portal is implementation: there are no listener `mounts` (retired; `doctor --fix` migrates them). A Portal whose verbs callers OUTSIDE the design reach declares a Portal-level `invokedBy: { kind: entry, caller: "who calls it" }` (with a `scope` on a network transport); a Portal reached only by modelled callers declares none. Never add an entry to quiet a finding: an entry the author did not decide hides unreached verbs. An `UNUSED_COMPONENT` / `UNUSED_METHOD` on a Portal has two honest answers — model the caller (a `call` step along `dependsOn`, or `alias::portal.verb` across projects), or, when the callers truly are outside the design (browsers, a CLI user, an AI tool over stdio, the applications that link a library), declare the entry; a verb with neither is removed. An InProcess Portal binds no endpoints; its verbs are its contract methods.
|
|
109
|
+
- **Networks and gateways**: a project may declare `network: true` (or `{ description }`) in `.wai/project.yaml`; it and its members then form an isolated network. An entry's `scope` is relative — `outside` (the default) or `network` (sibling services inside the innermost network) — and only on a network transport. Inside a network only a Portal with the `gateway` variant may be entered from `outside`, and a modelled call crossing into a network must land on its gateway (`GATEWAY_BYPASSED`). A `network` entry is proven by the family run at the root (`ENTRY_UNPROVEN` when no modelled caller reaches it). A network verb exported beyond the project (`audience` wider than `project`) must be the outermost gateway (`EXPORT_BEYOND_NETWORK`). Declare no network until the deployment really isolates something: a project without one never sees these rules.
|
|
110
|
+
- **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
|
+
- **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.
|
|
109
112
|
- **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.
|
|
110
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). 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`.
|
|
111
114
|
- **Externals — the pin gates, live drift is visible**: declare a project this one consumes with `sdd_add_external` (alias; source `../sibling`, `hosted:<id>`, `<git url>` or `<git url>#<commit>`; optional `project`, `use`, `description`) instead of editing `.wai/project.yaml`. It refuses in one sentence what cannot be declared, takes the declaration back out when the producer contradicts it (another id, an unexported `use` name), and pins it by default. The owner's gate judges each external against its pin; `sdd_validate_tree` and `sdd_get_status` add ADVISORY live-drift findings (`EXTERNAL_LIVE_INCOMPATIBLE`, `EXTERNAL_DRIFTED`, `EXTERNAL_LIVE_UNCOMPARED`) that never make the tree invalid — adapt the uses, then the human re-pins. `wairon externals status` is the human's opt-in live gate.
|
|
@@ -117,7 +120,7 @@ schemas:
|
|
|
117
120
|
- **lint.allow (per-spec suppression)**: any L1–L4/type spec may carry `lint: { allow: [{ code, reason }] }` — silences that WARNING (or NOTICE) code on that spec only (wairon's `#[allow]`). Errors are never locally suppressible; unknown codes and allows that no longer match anything are flagged. Prefer fixing — an allow is for a documented false positive or a deliberate, reviewable exception.
|
|
118
121
|
- **Methods (L3)**: prefer structured `params: [{name, type}]` — authoritative for type checking; the signature text is derived from them. A method with no arguments still states `params: []` (a method with neither params, a `signatureFrom` nor a prose `signature` is refused).
|
|
119
122
|
- **Forwarding and shared shapes — `signatureFrom`**: a method that takes the same params and returns as another states ONE source instead of restating them: `signatureFrom: <component>.<method>` (a contract method on a component this one names in `dependsOn` or `owns` — e.g. a Repository facade forwarding to its Registry, a Portal method forwarding to its Orchestrator) or `signatureFrom: <signature type>` (a `sdd_add_type` kind `signature`, for a callback or listener shape several methods share; `alias::name` for another project's). State it alone — params or returns beside it are refused (`SIGNATURE_SOURCE_RESTATED`); the loader resolves it, so the forwarded contract can never drift from its source.
|
|
120
|
-
- **Types speak one neutral grammar** in every param, returns and field: the primitives `string`, `int`, `float`, `bool`, `bytes`, `date` (a calendar day), `datetime` (an instant), `duration`, `void`, `any`; the collections `list<T>`, `set<T>` and `map<K, V>` (K is `string`, `int` or an enum); `T?` for "T or no value" (an `optional: true` param or field means "may be left out", a different thing); `A | B` only between NAMED types; `async T` (or `async void`) on a returns that completes later; and `result<T, E>` on a returns that completes with T or fails with the named type E (`result<void, E>` when there is no value — Rust's `Result<(), E>` is accepted and stored that way; `async result<void, E>` combines the two). A named type is an entity, a value-object, an `enum` (`sdd_add_type` kind `enum` with ordered `values`) or a signature type; a value that needs its own meaning but is one primitive is a named scalar, a value-object with `holds: string` (or another primitive) in place of fields. Write `async void`, never `Promise<void>`; `list<Invoice>`, never `Invoice[]`. TypeScript spellings are still accepted and stored canonical — the answer lists each respelling — but `number` is refused ("int or float?"), and inline object shapes, inline function types, literal unions (`'a' | 'b'`) and unions mixing in a primitive are refused with their named replacement (a value-object or named scalar, a signature type, an enum). Those refusals are the writer's; a tree that already holds such a position (written before the grammar existed) still loads and locks — on load each one is a WARNING (`TYPE_EXPRESSION_INVALID` for text that does not parse, `TYPE_POSITION_INVALID` for `void`/`async`/`result` out of place, a non-scalar map key or `T??`, `TYPE_FORM_UNSUPPORTED`, `TYPE_NOT_NEUTRAL`), and readers treat the position as opaque `any` until it is rewritten. `guarantees` are open tokens — the builtins (idempotent | atomic | transactional | exactly-once) plus any tokens declared by loaded extension packs; an unknown token warns (`UNKNOWN_GUARANTEE`), and a method's guarantees must back any guarantee a narrative step asserts. Every Portal
|
|
123
|
+
- **Types speak one neutral grammar** in every param, returns and field: the primitives `string`, `int`, `float`, `bool`, `bytes`, `date` (a calendar day), `datetime` (an instant), `duration`, `void`, `any`; the collections `list<T>`, `set<T>` and `map<K, V>` (K is `string`, `int` or an enum); `T?` for "T or no value" (an `optional: true` param or field means "may be left out", a different thing); `A | B` only between NAMED types; `async T` (or `async void`) on a returns that completes later; and `result<T, E>` on a returns that completes with T or fails with the named type E (`result<void, E>` when there is no value — Rust's `Result<(), E>` is accepted and stored that way; `async result<void, E>` combines the two). A named type is an entity, a value-object, an `enum` (`sdd_add_type` kind `enum` with ordered `values`) or a signature type; a value that needs its own meaning but is one primitive is a named scalar, a value-object with `holds: string` (or another primitive) in place of fields. Write `async void`, never `Promise<void>`; `list<Invoice>`, never `Invoice[]`. TypeScript spellings are still accepted and stored canonical — the answer lists each respelling — but `number` is refused ("int or float?"), and inline object shapes, inline function types, literal unions (`'a' | 'b'`) and unions mixing in a primitive are refused with their named replacement (a value-object or named scalar, a signature type, an enum). Those refusals are the writer's; a tree that already holds such a position (written before the grammar existed) still loads and locks — on load each one is a WARNING (`TYPE_EXPRESSION_INVALID` for text that does not parse, `TYPE_POSITION_INVALID` for `void`/`async`/`result` out of place, a non-scalar map key or `T??`, `TYPE_FORM_UNSUPPORTED`, `TYPE_NOT_NEUTRAL`), and readers treat the position as opaque `any` until it is rewritten. `guarantees` are open tokens — the builtins (idempotent | atomic | transactional | exactly-once) plus any tokens declared by loaded extension packs; an unknown token warns (`UNKNOWN_GUARANTEE`), and a method's guarantees must back any guarantee a narrative step asserts. Every method of a Portal whose transport is not `InProcess` needs an `endpoint` — bind with `sdd_set_endpoints` after `sdd_define_interface`. A method the process's own runtime invokes (a composition root, a timer, a signal or shutdown hook, a framework callback) declares `invokedBy: { kind: runtime, caller }`; a Portal verb callers outside the design reach declares `invokedBy: { kind: entry, caller }` (or inherits the Portal's). Unused-detection seeds both and reachability propagates through their narratives (thin `caller` prose → `INVOKED_BY_UNDESCRIBED`; a runtime method the internal walk already reaches → `INVOKED_BY_REDUNDANT`). A caller inside the design is never an `invokedBy`: model its call. Prefer a `register` step when the wiring is internal.
|
|
121
124
|
- **Narratives (L5, inside L4)**: a FLAT numbered step list; order mimics the code lines. `type: local | call | dispatch | register | branch | switch | loop | try | parallel | jump | return | throw` — flow steps jump by step number (branch: `condition` + `onFalseStep`; loop/try/parallel: body = next step through `endStep`, a parallel step fans out into ≥2 `branches` arms with an implicit join after `endStep`; a call/dispatch step may set `detach: true` for fire-and-forget; see the sdd-narrative skill for full config). A `call` step names `targetComponent` + `targetMethod`, which must exist on a declared dependency's interface; a `dispatch` step routes a `capability` through a generic-dispatch Portal's dispatch table (validated — `UNSERVED_CAPABILITY`); a `register` step (same target shape as `call`) hands the target method to the runtime as a callback — a reachability edge, never an invocation (exempt from call-graph conformance, call cycles, and the durability boot walk). Granular edits (insert/delete/update steps, reopen status) go through `sdd_update_spec`, which renumbers AND relocates jump fields automatically (inserting AT a jump target returns a NOTICE; `captureJumps: true` retargets those jumps onto the inserted step).
|
|
122
125
|
- **Semantic wiring (dispatch / lifecycle / durability)**: a generic-dispatch Portal declares a machine-readable `dispatch` table (`capability → component.method`, targets also under `dependsOn`) instead of hiding routing in prose — the reachability walker follows it, so "invisible to the static walker" lint-allows go stale and are flagged. Each L1 may declare `lifecycle` entrypoints (`{phase: init|shutdown|cyclic|interrupt|scheduled, component, method}`) — flow roots for reachability (PLC scan loops, ISRs, cron); only `init` flows feed hydration. A `Store` declares `durability: ram-projection | durable | read-through | cache`; only `durable` (persisted RAM projection) requires the hydration round-trip — its L3 methods carry `effect: read | write | lifecycle` tags and its writes and lifecycle changes need a read-back reachable from a lifecycle `init` flow (`MISSING_HYDRATION`); `lifecycle` creates, destroys or (un)registers what exists without modifying domain fields, and calls only read and lifecycle methods (`LIFECYCLE_CALLS_WRITE`); `read-through` = persisted with no RAM copy (every read is the read-back), `ram-projection` = rebuilt not restored, `cache` = evictable and loss-safe. Cross-subsystem method params/returns must be typed — bare `any` (and what reads as it: `json`, `object`, `unknown`) on a published surface is flagged (`UNTYPED_SEAM`), and persistence claims that exist only in prose are flagged (`UNREALIZED_CLAIM`).
|
|
123
126
|
- **Narrative detail dial**: per method (or L4 spec-level) `detail: full | calls-only | intent`; omitted = stereotype default (Portal/Observer/Adapter → calls-only, Store/Index/Query/Registry → intent, logic components → full). `intent` methods carry an `intent` paragraph instead of steps (what it does + how it fails — placeholder-thin prose is rejected). Levels are floors, not ceilings.
|
|
@@ -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
|
|
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 — for a subsystem owner also the project's shared setup files: manifest, compiler settings, crate or package root), an `## Externals used` section for a component reaching another project (each alias, the names it uses, the producer's transport and abi, and the pinned snapshot `.wai/externals/<alias>.yaml`, which joins `readPaths` — code against the pin, never the producer's source), 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,12 @@ Point the subagent at that skill and spend the brief on what only you know:
|
|
|
65
65
|
* **The premise you are asking them to act on**, stated *as* a premise, so it can
|
|
66
66
|
be contradicted.
|
|
67
67
|
|
|
68
|
+
One design decision never goes into a brief as an instruction: **an entry**. A
|
|
69
|
+
subagent that meets an unreached Portal verb (`UNUSED_COMPONENT` /
|
|
70
|
+
`UNUSED_METHOD`) reports it; whether to model the caller, declare an entry for
|
|
71
|
+
real outside callers, or remove the verb is the architect's call, made in the
|
|
72
|
+
spec — never an entry invented in passing to make a gate green.
|
|
73
|
+
|
|
68
74
|
## Receiving the report
|
|
69
75
|
|
|
70
76
|
* **A measurement or a refusal is a delivery, not a failure.** "This lights up 362
|
|
@@ -51,7 +51,7 @@ You are the **Spec-to-Code Compiler**. Your job is to generate concrete source c
|
|
|
51
51
|
- Confirm the design is approved: `sdd_get_status` reports "Approved: …" and does not list any of this component's specs as changed since the approval. If there is no approval on record, or this component's specs changed after it, stop immediately and ask the human to review the design and run `wairon lock` (never run it yourself).
|
|
52
52
|
2. **Setup Workspace & Align with Standards**:
|
|
53
53
|
- Align with the inlined **Core Architecture & Coding Standards** (see below) to ensure naming, narrative coding, and stereotype conventions are respected. Do NOT read these standards from disk; they are already fully specified in your system context.
|
|
54
|
-
- Locate the target implementation source file (mapped by `sourcePath` in L4). A file the brief marks `(planned — create it)` does not exist yet: create it at exactly that path — the fence already holds it.
|
|
54
|
+
- Locate the target implementation source file (mapped by `sourcePath` in L4). A file the brief marks `(planned — create it)` does not exist yet: create it at exactly that path — the fence already holds it. A named file not written yet is only `SOURCE_FILE_PLANNED` (a notice), at method level as at implementation level; once any file a component names exists, a contract method naming no file is `METHOD_SOURCE_PATH_MISSING` and one its existing file does not hold is `UNREALIZED_METHOD`.
|
|
55
55
|
- **Code linkage is not approval.** `sourcePath`, `symbol`, `exportedVia`, `simPath`, `injectedParams` and conformance tiers say where the design is realized, not what it is: they are outside the approved digests. When the implementation names no `sourcePath` yet, or names the wrong file, set it with `sdd_update_spec` — it costs no re-lock and never turns the approval stale.
|
|
56
56
|
- **Externals.** A contract you consume from another project is judged against its pin. If `sdd_validate_tree` reports an ADVISORY `EXTERNAL_LIVE_INCOMPATIBLE` (a used member changed, was renamed — the new name is given — or is gone in the live producer), adapt the uses, then ask the human to re-pin (`wairon externals pin <alias>`). Advisory findings never fail the gate; they are not a reason to stop, nor to code against the live producer's new shape without adapting the spec first.
|
|
57
57
|
3. **Write Tests First (TDD)**:
|
|
@@ -180,6 +180,10 @@ All implementation work must strictly adhere to these rules:
|
|
|
180
180
|
- `Actor` (owns one live thing, such as a session, connection, timer or entity instance, and its runtime state; its methods are full flowcharts, and it changes that state only after a commit).
|
|
181
181
|
- `Adapter` (the only block doing external I/O).
|
|
182
182
|
- `Observer` (subscribes to events and forwards them).
|
|
183
|
+
- **Reach, transports and libraries**:
|
|
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
|
+
- 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
|
+
- 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.
|
|
183
187
|
- **Strict Layer Isolation & No Persistence Shortcuts**:
|
|
184
188
|
- 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).
|
|
185
189
|
- 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.
|