@wairon/cli 5.1.1-dev.102 → 5.1.1-dev.103

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.
@@ -0,0 +1,308 @@
1
+ # The design export (`wairon-design`)
2
+
3
+ The design export is one JSON document holding a project's whole design, resolved. It is meant
4
+ for tools that work from a design: code generators, translators to other modelling languages,
5
+ documentation sites and design diff tools. Such a tool reads the export instead of walking
6
+ `.wai/specs`.
7
+
8
+ - **Every element has a key, and every reference is a key.** A consumer never resolves a
9
+ name itself.
10
+ - **Signatures are inlined.** A method that takes its signature from a source carries that
11
+ source's params and returns.
12
+ - **Types are canonical.** Every type position is canonical text plus a parsed expression.
13
+ - **Narratives are flat.** Each method body is a numbered step list, its targets resolved to
14
+ keys.
15
+
16
+ The export is a projection. It holds no information the spec tree does not, and it is never
17
+ read back into a tree.
18
+
19
+ ## Getting one
20
+
21
+ | Where | How |
22
+ |---|---|
23
+ | CLI | `wairon export` prints the JSON to stdout, and nothing else. `wairon export --out design.json` writes the file and reports the path and the approval state. A project with member projects names them (on stderr without `--out`, when it holds no design of its own): they are dependencies, exported from their own roots. See [cli.md](cli.md#wairon-export---out-file). |
24
+ | Library | `const { exportDesign } = require('@wairon/cli')` (or `import` from an ES module), then `exportDesign(outPath?, approval?)`, which returns the document and writes it when `outPath` is given. The package ships TypeScript declarations for the library entry (`dist/index.d.ts`, the `types` field), so `DesignExport` and `exportDesign` are typed. |
25
+ | JSON Schema | `schemas/design-export-1.json`, shipped in the package (`require.resolve('@wairon/cli/schemas/design-export-1.json')`). It is a JSON Schema draft-07 document (it declares `$schema`) and carries no `$id`: reference it by its path in the package. It is generated from the zod schema in `src/models/design-export.ts` at build time, and a test fails if the two drift. |
26
+
27
+ The CLI decides the approval verdict first, the same way `wairon lock-check` does (not strict),
28
+ and stamps it. It also exports a tree that is not approved, stamped with its state.
29
+
30
+ ## Compatibility promise
31
+
32
+ - **`format`** is always `"wairon-design"`.
33
+ - **`formatVersion`** is `MAJOR.MINOR` and is currently `1.0`.
34
+ - **A minor version only adds.** It adds new fields, or new members of a closed set (a
35
+ `role`, an approval state). A consumer must **ignore fields it does not know**. A 1.x
36
+ document validates against `design-export-1.json`.
37
+ - **A major version breaks.** It removes or renames a field, or changes what one means. Every
38
+ major version is named in the CHANGELOG, and wairon emits one major version at a time.
39
+ - **A design change is never a format change.** Two exports of different trees in the same
40
+ format differ only in content.
41
+ - **`generator`** is the wairon version that wrote the document. It is for diagnostics only,
42
+ and consumers must not branch on it.
43
+
44
+ ## Determinism
45
+
46
+ The same tree with the same approval verdict produces **byte-identical** JSON, whatever order the
47
+ spec files sit in on disk.
48
+
49
+ - **Object keys** are sorted (ordinal).
50
+ - **Element lists** are sorted by key: `subsystems`, `components`, `interfaces`,
51
+ `implementations`, `types`, `dependencies` and each export table.
52
+ - **Inner lists keep their declared order**, because that order is part of the design: params,
53
+ fields, enum values, narrative steps, lifecycle roots and `formerly`.
54
+ - **The document has no timestamp.** Equal `source.stateId`s with equal approvals mean equal
55
+ documents, so a consumer can cache on `stateId`.
56
+ - **`source.stateId` is a content id, not the lock's gate identity.** The two are different
57
+ digests by design, and never compare equal:
58
+ - `source.stateId` (`sha256:<digest>`) hashes the parsed spec tree the export was read
59
+ from, every spec the scan loads, timestamps left out and specs in id order. Snapshots and
60
+ archives carry the same id. It changes exactly when the specs change.
61
+ - The lock's `stateId` in `.wai/lock.json` (`sha256+content+doctrine+inputs+members:<digest>`)
62
+ is the gate identity. It hashes this project's own specs together with the governing
63
+ doctrine, the declared inputs, `composition` and each direct member's composition subject,
64
+ so it also moves when a rule, an input or a member's approval moves.
65
+
66
+ To relate an export to an approval, read `source.approval`: it is `locked` exactly when
67
+ that lock record covers this tree.
68
+
69
+ ## Keys
70
+
71
+ | Element | Key |
72
+ |---|---|
73
+ | Subsystem, component, interface, implementation | its id |
74
+ | Type | `subsystem::id` when a subsystem owns it, the bare id when it is system-level |
75
+ | Contract method | `<interface key>.<name>` |
76
+ | Type method | `<type key>.<name>` |
77
+ | Field, param, enum value | its name, within its owner |
78
+ | Narrative step | its `stepNumber`, within its method |
79
+ | Anything in another project | `alias::publicName`: the alias this project declares the project under, and the public name that project's L0 exports it as |
80
+
81
+ A type's key `subsystem::id` uses `::` as a **key separator** inside this document: its first
82
+ segment is one of this document's own `subsystems` keys. A cross-project key's first segment is
83
+ an alias from `dependencies` instead, so the two never collide. Signature text (a method's
84
+ `signature` string) keeps the spec's own spelling, which writes a subsystem-owned type as
85
+ `subsystem.id`; read the structured `params` and `returns`, not the signature text, when you
86
+ need keys.
87
+
88
+ A reference into another project is always written the way a consumer can resolve it: through
89
+ the producer's **public** export table. It is never written with the loader's internal key, so
90
+ the reference stays valid when the producer renames something behind an `as`. A reference that
91
+ binds no exported name stays as written; the validator reports it.
92
+
93
+ ## The document
94
+
95
+ ### Top level (`design_export`)
96
+
97
+ | Field | Meaning |
98
+ |---|---|
99
+ | `format`, `formatVersion`, `generator` | See [Compatibility promise](#compatibility-promise). |
100
+ | `source` | Which tree the document was projected from, and the approval verdict over it. |
101
+ | `project` | The L0 of the exported project. |
102
+ | `dependencies` | The other projects this project depends on. They are listed, never inlined. |
103
+ | `subsystems`, `components`, `interfaces`, `implementations`, `types` | The design itself, one list per level. |
104
+
105
+ ### `source` (`design_source`)
106
+
107
+ | Field | Meaning |
108
+ |---|---|
109
+ | `projectId` | The project's effective id (its system name when it declares none). |
110
+ | `stateId` | `sha256:<digest>`, the tree's content identity. It is the cache key. |
111
+ | `approval` | `locked`, `stale`, `unlocked` or `unjudged`. |
112
+ | `approved` | `true` exactly when `approval` is `locked`. |
113
+
114
+ **What `approved` means.** It is the same verdict `wairon lock-check` and `wairon status` give:
115
+
116
+ - The committed lock record is resolved against the gate identity. That identity is the parsed
117
+ own tree, its governing doctrine, its declared inputs, `composition`, and the approvals of its
118
+ direct members.
119
+ - It is never a weaker per-spec digest comparison.
120
+ - `stale` means a lock exists but the tree moved since. `unlocked` means no lock was ever
121
+ written.
122
+ - `unjudged` means an in-process caller passed no verdict. It claims nothing either way.
123
+
124
+ Validation findings are not part of the document. Run `wairon validate` for those.
125
+
126
+ ### `project` (`design_project`)
127
+
128
+ `name`, `vision`, `boundaries` and `requirements` (as text), `targetLanguage`, and `exports`:
129
+ the resolved L0 export table, as `design_export_entry` rows.
130
+
131
+ ### Export entries (`design_export_entry`)
132
+
133
+ | Field | Meaning |
134
+ |---|---|
135
+ | `publicName` | The name consumers use. |
136
+ | `targetKind` | `component`, `interface` or `type`. |
137
+ | `target` | The canonical target's key. Every re-export has already been followed to that target. |
138
+ | `audience`, `version`, `stability` | As declared. |
139
+
140
+ ### `dependencies` (`design_dependency`)
141
+
142
+ | Field | Meaning |
143
+ |---|---|
144
+ | `alias` | The alias the project uses for the dependency. It is the prefix of every `alias::publicName` that points into it. |
145
+ | `projectId` | The dependency's project id. |
146
+ | `role` | `member` for a project this one contains (an in-tree subdirectory member, a sibling or git member, a legacy mount). `external` for a declared external. A part is not a dependency: its subsystems are this project's own. |
147
+ | `digest` | The content digest pinned in `.wai/externals.lock.yaml`, when the dependency is pinned. It is absent when it is not pinned, or when the lock cannot be read. |
148
+ | `uses` | The public names this project's references reach in the dependency, sorted. |
149
+
150
+ ### `subsystems` (`design_subsystem`)
151
+
152
+ | Field | Meaning |
153
+ |---|---|
154
+ | `key`, `name`, `description`, `status`, `profile`, `targetLanguage` | As declared. |
155
+ | `lifecycle` | The lifecycle roots: `{ phase, component, method }`. These are what the runtime invokes. |
156
+ | `exports` | The resolved L1 export table. |
157
+ | `trustedLinks` | The subsystem keys this subsystem holds a trusted link to. |
158
+ | `ext` | Pack extension data. |
159
+
160
+ ### `components` (`design_component`)
161
+
162
+ | Field | Meaning |
163
+ |---|---|
164
+ | `key`, `name`, `description`, `status`, `subsystem` | As declared. |
165
+ | `stereotype`, `variant`, `dependencyClass`, `durability`, `portalType` | As declared. |
166
+ | `owns`, `dependsOn` | Keys. |
167
+ | `emits`, `subscribesTo` | The topics the component publishes to and consumes from. |
168
+ | `auth`, `basePath`, `dispatch` | A Portal's authentication, base path and dispatch table. Dispatch targets are keys. |
169
+ | `mounts` | A listener's mounts. Each portal is a key. |
170
+ | `patterns`, `externalLinks` | As declared. |
171
+ | `formerly` | The rename trace. See [Renames](#renames-formerly). |
172
+ | `ext` | Pack extension data. |
173
+
174
+ ### `interfaces` (`design_interface`) and methods (`design_method`)
175
+
176
+ An interface carries `key`, `component`, `name`, `description`, `status`, `methods`, `formerly`
177
+ and `ext`. Each method carries:
178
+
179
+ | Field | Meaning |
180
+ |---|---|
181
+ | `key`, `name`, `description` | As declared. |
182
+ | `params`, `returns` | Type refs. A method whose signature comes from a source has its source's params and returns inlined. |
183
+ | `signature` | The derived signature text, for display only. |
184
+ | `signatureType` | The key of the signature type the method takes its signature from, when it names one. A method source is inlined and never named here. |
185
+ | `effect`, `guarantees`, `invokedBy` | As declared. |
186
+ | `endpoint` | The endpoint as declared: `transport` plus that transport's address fields. |
187
+ | `formerly`, `ext` | The method's rename trace (its former `<interface>.<name>` keys), and pack data. |
188
+
189
+ A type's pure methods use the same shape.
190
+
191
+ ### Params, fields and type refs
192
+
193
+ - **`design_param`**: `name`, `type`, `optional` and `description`. `optional` means the
194
+ param may be left out. "May be none" is part of the type, written as `T?`.
195
+ - **`design_field`**: the same fields, plus `key` (`primary`, `unique` or `foreign`) and
196
+ `references`, which is a type key or `<type key>.<field>`.
197
+ - **`design_type_ref`**:
198
+ - `text` is the canonical text in the neutral type grammar.
199
+ - `expression` is the parsed form. It is a tree of `{ form, name?, args }`, where `form` is
200
+ `primitive`, `named`, `list`, `set`, `map`, `optional`, `union`, `async` or `applied`.
201
+ - The `name` of every named member is the **key** of the type it names.
202
+ - When the grammar cannot read a position, `expression` is absent. The text is then opaque:
203
+ the export never invents a shape.
204
+
205
+ ### `implementations` (`design_implementation`) and bodies (`design_method_body`)
206
+
207
+ An implementation carries:
208
+
209
+ - `key`, `contract`, `component`, `name`, `description` and `status`;
210
+ - `technologies`, by name;
211
+ - `sourcePath`, `methods`, `formerly` and `ext`.
212
+
213
+ Each method body carries:
214
+
215
+ | Field | Meaning |
216
+ |---|---|
217
+ | `method` | The contract method's key. |
218
+ | `detail` | `full`, `calls-only` or `intent`, after the stereotype default is applied. |
219
+ | `intent` | The prose that stands in for steps, at `intent` detail. |
220
+ | `calls` | The declared calls, as method keys. |
221
+ | `narrative` | The flat step list as stored. A `call`, `register` or `dispatch` step's target, and a credential source, are keys. Flow steps jump by `stepNumber`. |
222
+
223
+ ### `types` (`design_type`)
224
+
225
+ A type carries:
226
+
227
+ - `key`, `kind`, `name`, `description` and `status`;
228
+ - `fields` and its pure `methods`;
229
+ - a signature type's `params` and `returns`;
230
+ - an enum's `values`, in declared order;
231
+ - a named scalar's `holds`;
232
+ - `invariants`;
233
+ - `componentClass`, a key;
234
+ - `database`, `table` and `linkedEntity`;
235
+ - `formerly` and `ext`.
236
+
237
+ ## What is left out, and why
238
+
239
+ | Left out | Why |
240
+ |---|---|
241
+ | Lint allows, conformance tiers and finding declarations | They are gate configuration, not design. |
242
+ | `symbol` and `exportedVia` | They bind to this repository's code. |
243
+ | `simPath` | It names a test harness. |
244
+ | `createdAt` and `updatedAt` | They would break determinism. |
245
+ | A member project's specs | They are listed as a dependency. Export the member itself for its design. |
246
+
247
+ Pack `ext` data is passed through untouched, under each pack's own key. It is the sanctioned
248
+ extension channel.
249
+
250
+ ## Executable or library: the consumer decides
251
+
252
+ The export has no `kind` that says "program" or "library". Whether a design ships as a
253
+ service, a CLI, a library or a plugin is a packaging decision. The same architecture can be
254
+ packaged in each of those ways, so wairon leaves the decision to the consumer, as it does with
255
+ deployment. The export carries the facts a consumer needs to decide for its own target:
256
+
257
+ - **Lifecycle roots** (`subsystems[].lifecycle`): what the runtime invokes, with the phase
258
+ (`init`, `shutdown`, `cyclic`, `interrupt` or `scheduled`).
259
+ - **Endpoints and transports** (`interfaces[].methods[].endpoint`, `components[].portalType`):
260
+ calls that arrive from outside the process. A `Custom` endpoint can be an in-process binding.
261
+ - **Listener mounts** (`components[].mounts`): which listener serves which portal.
262
+ - **The export table** (`project.exports`): what other projects link against.
263
+
264
+ For example, one generator might emit a `main` that runs the `init` roots and serves the HTTP
265
+ portals. Another might emit a library that publishes `project.exports`. Both are correct readings
266
+ of the same export.
267
+
268
+ ## Renames: `formerly`
269
+
270
+ Components, interfaces, implementations and types carry `formerly`: every key they held before,
271
+ oldest first. Contract methods carry their former `<interface>.<name>` keys. The rename tools
272
+ write these traces:
273
+
274
+ | Tool | What it records |
275
+ |---|---|
276
+ | `sdd_rename_component` | The component, and its `i<id>` and `<id>_impl` |
277
+ | `sdd_rename_method` | The method |
278
+ | `sdd_move_methods` | Each method that moved |
279
+ | `sdd_rename_type` | The type |
280
+
281
+ **Retired names cannot be reused.**
282
+
283
+ - A rename tool or a create that would give a new spec an id a trace holds is refused
284
+ (`id-retired`).
285
+ - So is a new method under a name its own contract retired (`name-retired`).
286
+ - A hand edit that reuses one is reported as `RENAME_TRACE_CONFLICT`.
287
+ - Unsetting the trace releases the name. A consumer then sees a delete plus an add.
288
+
289
+ **The consumer's algorithm.** This is how a generator keeps its user's wired code across
290
+ renames:
291
+
292
+ 1. Keep the keys you generated from.
293
+ 2. For a key that is now missing, first resolve its **owner** (subsystem, component, interface
294
+ or type) through `formerly`. Then resolve the **member** by name, or through the member's own
295
+ `formerly`.
296
+ 3. A match is a rename. Anything left unmatched is a deletion. A key present now and absent
297
+ before is an addition.
298
+ 4. Traces accumulate, so this works across chains of renames, and across exports you skipped.
299
+
300
+ **A published name survives an internal rename.** When a renamed component or type backed an
301
+ export entry whose public name came from its id, the rename writes `as: <old name>` on that
302
+ entry. Consumers' pins and `alias::publicName` references keep resolving. A renamed method that
303
+ is published is reported, but not prevented: no mainstream export mechanism aliases a single
304
+ member.
305
+
306
+ **What escapes the trace.** Moving elements between projects (`wairon subsystem externalize`,
307
+ `wairon member internalize`) is a delete in one project and an add in another. No
308
+ single-project trace follows such a move. Renames made before traces existed left none behind.