@wairon/cli 5.1.1-dev.105 → 5.1.1-dev.106
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 +8513 -6068
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +2023 -741
- package/dist/index.js +2913 -1271
- package/dist/index.js.map +1 -1
- package/dist/templates/skills/sdd-architect.md +7 -4
- package/dist/templates/skills/sdd-delegate.md +6 -0
- package/dist/templates/skills/sdd-implement.md +5 -1
- package/dist/webapp.html +53 -52
- package/docs/cli.md +58 -4
- package/docs/design-export.md +13 -8
- package/docs/network.md +99 -0
- package/docs/standards/architecture.md +101 -12
- package/package.json +1 -1
- package/schemas/{design-export-1.json → design-export-2.json} +38 -46
|
@@ -90,7 +90,7 @@ The full standard is the source of truth; this is the summary you design against
|
|
|
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)
|
|
@@ -103,9 +103,12 @@ schemas:
|
|
|
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
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`.
|
|
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.
|
|
@@ -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.
|