@wairon/cli 5.1.1-dev.107 → 5.1.1-dev.109
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/CONTRIBUTING.md +193 -0
- package/README.md +16 -9
- package/dist/cli/index.js +4702 -2453
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +4626 -852
- package/dist/index.js +3740 -1685
- package/dist/index.js.map +1 -1
- package/dist/templates/skills/sdd-architect.md +1 -1
- package/docs/architecture.md +1 -1
- package/docs/cli.md +174 -27
- package/docs/design/approval-baseline.md +257 -0
- package/docs/design/connecting-agent-entrypoint.md +167 -0
- package/docs/design/execution-budgets.md +199 -0
- package/docs/design/pack-scoping.md +581 -0
- package/docs/design/supervisor-doctrine.md +110 -0
- package/docs/extending-wairon.md +5 -4
- package/docs/network.md +9 -4
- package/docs/roadmap.md +2 -2
- package/package.json +9 -3
- package/schemas/design-export-2.json +15 -1
|
@@ -110,7 +110,7 @@ schemas:
|
|
|
110
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
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.
|
|
112
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.
|
|
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`.
|
|
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
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.
|
|
115
115
|
- **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
116
|
- **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.
|
package/docs/architecture.md
CHANGED
|
@@ -71,7 +71,7 @@ to the artifacts the host AI tool consumes:
|
|
|
71
71
|
(`runWithProjectRoot`) — reusing the core, validation, and MCP layers unchanged.
|
|
72
72
|
- **Everything is file-based** under `.wai/` — no database, and no daemon for the
|
|
73
73
|
core workflow. `wairon serve` is an **opt-in** hosting daemon (see the
|
|
74
|
-
[hosted server guide](design/hosted-mcp-server.md)) that serves the same
|
|
74
|
+
[hosted server guide](https://github.com/SYW-Apps/Waffle-AIron/blob/main/docs/design/hosted-mcp-server.md)) that serves the same
|
|
75
75
|
file-based tools over HTTP.
|
|
76
76
|
|
|
77
77
|
---
|
package/docs/cli.md
CHANGED
|
@@ -25,6 +25,16 @@ nothing and printing the exact `wairon member add <alias> <path> --project` to
|
|
|
25
25
|
run from the parent (or: make the folder its own repository with `git init` and
|
|
26
26
|
re-run `wairon init` for an independent project).
|
|
27
27
|
|
|
28
|
+
**What to commit.** All of `.wai/` — the specs, `project.yaml`, `lock.json`,
|
|
29
|
+
`externals/` and `externals.lock.yaml`, vendored packs: the gate reads committed
|
|
30
|
+
files only (a migration's scratch folder, `.wai/transactions/`, ignores itself).
|
|
31
|
+
The generated `CLAUDE.md`/`GEMINI.md`, `.claude/` (or `.gemini/`) skills and
|
|
32
|
+
`.mcp.json` hold no machine path: commit them and every clone's assistant starts
|
|
33
|
+
with them, or ignore them and run `wairon generate` after a clone — either way
|
|
34
|
+
`wairon generate` / `wairon doctor --fix` refreshes them.
|
|
35
|
+
wairon writes no `.gitignore` — your toolchain's own entries (`node_modules/`,
|
|
36
|
+
`dist/`, …) are all it needs.
|
|
37
|
+
|
|
28
38
|
#### Which project a command acts on
|
|
29
39
|
Every command, the MCP server (`wairon mcp serve`) and `wairon dev` bind the same
|
|
30
40
|
project: from the folder you are in, the nearest folder whose `.wai/specs` holds
|
|
@@ -36,6 +46,15 @@ repository the walk is unbounded. Each command that acts on a project prints
|
|
|
36
46
|
`project <id> at <root>` on stderr first, so `wairon export` and every `--json`
|
|
37
47
|
output keep a pure stdout.
|
|
38
48
|
|
|
49
|
+
#### When a command refuses
|
|
50
|
+
A refusal is one `✖` line and a non-zero exit — never a Node stack trace, whatever
|
|
51
|
+
the input. A `.wai/project.yaml` that does not parse as YAML (most often a key
|
|
52
|
+
written twice, e.g. a second `rules:` block appended by hand) stops every command
|
|
53
|
+
the same way, naming the file, the line and the key —
|
|
54
|
+
`✖ .wai/project.yaml:33: duplicated mapping key "rules" — …` — and `wairon doctor`
|
|
55
|
+
diagnoses it too; no command runs on defaults instead. To report a bug, set
|
|
56
|
+
`WAIRON_DEBUG=1` and the stack is printed after the line.
|
|
57
|
+
|
|
39
58
|
### `wairon status [--subsystem <id>] [--no-recursive] [--all]`
|
|
40
59
|
Print the SDD spec tree as a hierarchy. Its percentages measure **authoring
|
|
41
60
|
progress** — 80% once a component's component, contract and implementation specs
|
|
@@ -74,8 +93,12 @@ warnings as errors (notices are printed and counted, never fatal).
|
|
|
74
93
|
**family root** (the project that declares the members). A member validated on
|
|
75
94
|
its own judges its own gate with its `network` entries counted as declared, and
|
|
76
95
|
says so in one line, so a green member run is never read as a proven one.
|
|
77
|
-
**In CI, run `wairon validate --ci` at the family root
|
|
78
|
-
|
|
96
|
+
**In CI, run `wairon validate --ci` at the family root**, after installing
|
|
97
|
+
the project's dependencies (`npm ci` or your package manager's equivalent —
|
|
98
|
+
the gate reads TypeScript/JavaScript code with the project's own TypeScript
|
|
99
|
+
when it is 5 or 6, and with the copy wairon ships otherwise;
|
|
100
|
+
see [Using it in GitHub Actions](#using-it-in-github-actions)); a member in
|
|
101
|
+
its own repository adds the root's run to its pipeline or relies on the root
|
|
79
102
|
repository's.
|
|
80
103
|
- **Externals, compared live (advisory: the pin gates).** The gate judges each
|
|
81
104
|
external against its pin. Then every external the run did not compose is
|
|
@@ -97,6 +120,9 @@ warnings as errors (notices are printed and counted, never fatal).
|
|
|
97
120
|
`METHOD_SOURCE_PATH_MISSING` (warning). `rules.conformance.requireCode: true`
|
|
98
121
|
in `project.yaml` reports the two notices as errors, for a CI that must say
|
|
99
122
|
every designed implementation has code.
|
|
123
|
+
- **Types are linked to code too.** A type spec without a `sourcePath` is
|
|
124
|
+
`MISSING_TYPE_SOURCE_PATH` — a notice before its subsystem has code, a
|
|
125
|
+
warning after. Plan a type's `sourcePath` when you plan the implementations'.
|
|
100
126
|
- **What the closing line says.** Plain `validate` passes with warnings and
|
|
101
127
|
says so — `Passed with N warning(s)` (`--ci` fails on them); `All checks
|
|
102
128
|
passed` means none. A tree holding no design yet (no L0, or an L0 with nothing
|
|
@@ -110,16 +136,58 @@ warnings as errors (notices are printed and counted, never fatal).
|
|
|
110
136
|
collaborator is collaboration whatever the import's form: when its type comes
|
|
111
137
|
from a component the caller never declared, even through `import type`, it is
|
|
112
138
|
`UNDECLARED_DEPENDENCY`; a Portal calling a write- or lifecycle-effect method
|
|
113
|
-
of a Repository, Index, Store or Registry
|
|
139
|
+
of a Repository, Index, Store or Registry is
|
|
114
140
|
`PORTAL_WRITE_SHORTCUT_IN_CODE` (error, the code twin of
|
|
115
|
-
`PORTAL_WRITE_SHORTCUT`)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
141
|
+
`PORTAL_WRITE_SHORTCUT`). A Portal's calls are resolved by the TypeScript
|
|
142
|
+
type checker, so how the receiver is written does not matter: the call lands
|
|
143
|
+
on the method the checker resolves it to, and a call through an interface or
|
|
144
|
+
type — a port declared in the Portal's own contracts file included — lands on
|
|
145
|
+
every class of the project that realizes it (the ones that say `implements`
|
|
146
|
+
when any does, else every class the checker finds assignable). A call the
|
|
147
|
+
checker cannot resolve because its receiver is typed `any` or `unknown`, cast
|
|
148
|
+
to either, or typed nothing, under the name of a data component's write, is
|
|
149
|
+
`PORTAL_CALL_UNRESOLVED` (a warning, so `--ci` fails): give the receiver its
|
|
150
|
+
type. An Orchestrator (or any component) whose code calls a write- or
|
|
151
|
+
lifecycle-effect method of another component that its narrative never
|
|
152
|
+
claims is `UNDECLARED_WRITE_CALL`. And a file importing a technology's package
|
|
153
|
+
outside the components that bind it is `TECH_LEAKAGE_IN_CODE`. A technology's
|
|
154
|
+
packages are its declared tokens (its name, or its `matches`) plus the common
|
|
155
|
+
packages wairon knows for it — see [Technologies and their
|
|
156
|
+
packages](#technologies-and-their-packages). An Adapter's call step to a verb
|
|
157
|
+
of a Portal that declares an out-of-process transport (HTTP, gRPC, a
|
|
158
|
+
database, a bus, a CLI…) is the link the design models: it is never resolved
|
|
159
|
+
to the remote Portal's file, so it never reads as `CALL_ORIGIN_UNRESOLVED`;
|
|
160
|
+
the call to the Adapter stays checked. Likewise the Adapter's `dependsOn` to
|
|
161
|
+
that Portal is realized by the link, never reported `UNREALIZED_DEPENDENCY`:
|
|
162
|
+
never add an import across a network boundary to satisfy a check.
|
|
163
|
+
|
|
164
|
+
#### Technologies and their packages
|
|
165
|
+
|
|
166
|
+
`TECH_LEAKAGE_IN_CODE` compares each import's package with the packages of the
|
|
167
|
+
technologies an implementation binds. A design writes the technology the way
|
|
168
|
+
people say it (`postgres`) while the code imports its driver (`pg`), so wairon
|
|
169
|
+
knows the common packages of the common technologies by default:
|
|
170
|
+
|
|
171
|
+
| Technology (any of these names) | Packages |
|
|
172
|
+
| --- | --- |
|
|
173
|
+
| `postgres`, `postgresql`, `pg` | `pg`, `postgres`, `pg-promise`, `@neondatabase/serverless`, `@vercel/postgres` |
|
|
174
|
+
| `mysql`, `mariadb` | `mysql2`, `mysql`, `mariadb` |
|
|
175
|
+
| `redis`, `valkey` | `redis`, `ioredis`, `@redis/client` |
|
|
176
|
+
| `mongodb`, `mongo` | `mongodb`, `mongoose` |
|
|
177
|
+
| `sqlite`, `sqlite3` | `better-sqlite3`, `sqlite3`, `sqlite` |
|
|
178
|
+
| `kafka` | `kafkajs`, `node-rdkafka` |
|
|
179
|
+
| `rabbitmq`, `amqp` | `amqplib`, `amqp-connection-manager` |
|
|
180
|
+
|
|
181
|
+
An HTTP client (`fetch`, `axios`, `got`, `undici`) is never a technology — it is
|
|
182
|
+
how an Adapter reaches anything — so no entry names one. To extend the list,
|
|
183
|
+
write the technology as `{ name, matches }` (each `matches` token is a package
|
|
184
|
+
too), or have a pack contribute `technologyPackages: { <technology>: [<package>,
|
|
185
|
+
…] }`.
|
|
186
|
+
|
|
187
|
+
A package of a technology that **no** implementation binds is reported too:
|
|
188
|
+
the code uses a technology the design never placed, so every importer is
|
|
189
|
+
outside its (missing) home. Bind it on the data-layer component that owns the
|
|
190
|
+
vendor call (`technologies: [redis]`), and that component becomes its home.
|
|
123
191
|
|
|
124
192
|
### `wairon generate [--target <name>] [--domain <id>] [--domains <ids>] [--root] [--family] [--no-prune] [--global] [--dry-run]`
|
|
125
193
|
Reconcile the generated guides, skills and context, and — only when the project
|
|
@@ -162,7 +230,13 @@ approval lives in `.wai/lock.json`. Implementation gates on that approval —
|
|
|
162
230
|
**What it certifies is the design.** Only design findings can refuse a lock.
|
|
163
231
|
Code-conformance findings (the code↔spec checks) are recorded **beside** the
|
|
164
232
|
claim in the record's `code` block, with the analyzer that produced them, and
|
|
165
|
-
printed as `code: N error(s), … recorded beside the claim
|
|
233
|
+
printed as `code: N error(s), … recorded beside the claim`, with the analysis
|
|
234
|
+
**grade** — the weakest any analyzed file got: `exact` (read with the project's
|
|
235
|
+
TypeScript compiler API: declarations, imports and calls resolved), `pattern`
|
|
236
|
+
(read through the language's declaration and import patterns — recognized, not
|
|
237
|
+
resolved), `generic` (word-boundary identifiers only, the floor for any
|
|
238
|
+
language), or `none` (no file analyzed yet: no code, or none the specs name).
|
|
239
|
+
CI enforces them:
|
|
166
240
|
`wairon validate --ci` still fails on a code error. A design can be approved
|
|
167
241
|
before its code exists.
|
|
168
242
|
|
|
@@ -299,9 +373,17 @@ merging is the thing that was reviewed.
|
|
|
299
373
|
|
|
300
374
|
#### Using it in GitHub Actions
|
|
301
375
|
|
|
302
|
-
This repository publishes it as a **reusable workflow
|
|
303
|
-
|
|
304
|
-
|
|
376
|
+
This repository publishes it as a **reusable workflow**. Pin it — and the CLI it
|
|
377
|
+
installs — to **the version you lock with**: the one `wairon --version` prints
|
|
378
|
+
where you run `wairon lock`. Every published version has a git tag `v<version>`
|
|
379
|
+
and the same version on npm: a release `X.Y.Z` is `@vX.Y.Z` with
|
|
380
|
+
`wairon-version: 'X.Y.Z'`; a dev build `X.Y.Z-dev.N` is `@vX.Y.Z-dev.N` with
|
|
381
|
+
`wairon-version: 'X.Y.Z-dev.N'` (the npm `dev` dist-tag names the newest dev
|
|
382
|
+
build, but it moves — pin the exact version). Check that the version you pin is
|
|
383
|
+
on npm before you commit the job: `npm view @wairon/cli@<version> version`. The
|
|
384
|
+
workflow and the `lock-check` command first ship with 6.0.0 and its dev builds;
|
|
385
|
+
an older version has neither. Below, `<version>` stands for yours. Add one job
|
|
386
|
+
to your own workflow:
|
|
305
387
|
|
|
306
388
|
```yaml
|
|
307
389
|
# .github/workflows/ci.yml in YOUR repository
|
|
@@ -309,9 +391,9 @@ on: [pull_request]
|
|
|
309
391
|
|
|
310
392
|
jobs:
|
|
311
393
|
approved-design:
|
|
312
|
-
uses: SYW-Apps/Waffle-AIron/.github/workflows/lock-check.yml@
|
|
394
|
+
uses: SYW-Apps/Waffle-AIron/.github/workflows/lock-check.yml@v<version>
|
|
313
395
|
with:
|
|
314
|
-
wairon-version: '
|
|
396
|
+
wairon-version: '<version>'
|
|
315
397
|
strict: true # a deleted .wai/lock.json fails instead of switching the gate off
|
|
316
398
|
```
|
|
317
399
|
|
|
@@ -319,12 +401,13 @@ With inputs (all optional):
|
|
|
319
401
|
|
|
320
402
|
```yaml
|
|
321
403
|
approved-design:
|
|
322
|
-
uses: SYW-Apps/Waffle-AIron/.github/workflows/lock-check.yml@
|
|
404
|
+
uses: SYW-Apps/Waffle-AIron/.github/workflows/lock-check.yml@v<version>
|
|
323
405
|
with:
|
|
324
406
|
working-directory: packages/api # where the .wai/ tree lives (default: .)
|
|
325
|
-
wairon-version: '
|
|
407
|
+
wairon-version: '<version>' # the version you lock with, or an npm dist-tag (default: latest)
|
|
326
408
|
strict: true # fail when nothing was approved — use it once you have locked (default: false)
|
|
327
409
|
validate: true # also run `wairon validate --ci` there (default: true)
|
|
410
|
+
install: auto # the project's dependencies before validate: auto | none | <command> (default: auto)
|
|
328
411
|
node-version: '20' # (default: '20')
|
|
329
412
|
runs-on: ubuntu-latest # (default: ubuntu-latest)
|
|
330
413
|
```
|
|
@@ -343,6 +426,47 @@ directory (`validate: false` turns it off). Point `working-directory` at the
|
|
|
343
426
|
**family root** — the project that declares the members — so that run is the
|
|
344
427
|
family run, which judges the network proofs no member's own gate can.
|
|
345
428
|
|
|
429
|
+
**The project's dependencies come first.** The conformance gate reads
|
|
430
|
+
TypeScript/JavaScript code with the project's own TypeScript compiler when it is
|
|
431
|
+
TypeScript 5 or 6, and with the copy wairon ships otherwise — a fresh checkout,
|
|
432
|
+
a project without TypeScript, or one on TypeScript 7 (whose native compiler has
|
|
433
|
+
no JavaScript API) is still read at exact grade, and `CONFORMANCE_DEGRADED` is
|
|
434
|
+
left for a file the compiler could not read at all. Installing the project's
|
|
435
|
+
dependencies keeps the analysis on the project's own compiler. So before
|
|
436
|
+
`validate --ci` the job
|
|
437
|
+
installs them with the project's own package manager (`install: auto`, the
|
|
438
|
+
default): nothing without a `package.json`; otherwise the nearest lockfile from
|
|
439
|
+
`working-directory` up to the repository root decides — `package-lock.json` →
|
|
440
|
+
`npm ci`, `pnpm-lock.yaml` → `pnpm install --frozen-lockfile`, `yarn.lock` →
|
|
441
|
+
`yarn install` (`--immutable`, or `--frozen-lockfile` for Yarn 1). `install:
|
|
442
|
+
none` skips it; any other value is run as the install command. The approval
|
|
443
|
+
check alone (`validate: false`) installs nothing.
|
|
444
|
+
|
|
445
|
+
#### Without the reusable workflow
|
|
446
|
+
|
|
447
|
+
The same gate as plain steps — for another CI system, or a job of your own. For
|
|
448
|
+
a TypeScript/JavaScript project with code, **install the project's dependencies
|
|
449
|
+
before `wairon validate --ci`** (`npm ci`, `pnpm install --frozen-lockfile` or
|
|
450
|
+
`yarn install --immutable`, whichever your lockfile is for):
|
|
451
|
+
|
|
452
|
+
```yaml
|
|
453
|
+
# .github/workflows/design.yml in YOUR repository
|
|
454
|
+
on: [pull_request]
|
|
455
|
+
|
|
456
|
+
jobs:
|
|
457
|
+
design:
|
|
458
|
+
runs-on: ubuntu-latest
|
|
459
|
+
steps:
|
|
460
|
+
- uses: actions/checkout@v4
|
|
461
|
+
- uses: actions/setup-node@v4
|
|
462
|
+
with:
|
|
463
|
+
node-version: '20'
|
|
464
|
+
- run: npm install --global @wairon/cli@<version> # the version you lock with (wairon --version)
|
|
465
|
+
- run: wairon lock-check --strict
|
|
466
|
+
- run: npm ci # the project's dependencies: the gate reads your code with your TypeScript (5 or 6)
|
|
467
|
+
- run: wairon validate --ci
|
|
468
|
+
```
|
|
469
|
+
|
|
346
470
|
**A failing job does not block a merge on its own.** A workflow can only fail;
|
|
347
471
|
making a failing job stop a merge is a branch-protection / ruleset setting on
|
|
348
472
|
your repository ("Require status checks to pass" → add this job). No workflow
|
|
@@ -597,6 +721,20 @@ field that does not exist, a name that is not an identifier, and a name another
|
|
|
597
721
|
field of the type holds or retired. The MCP twin is `sdd_rename_field`. As with
|
|
598
722
|
any design edit, the next `wairon lock` approves it.
|
|
599
723
|
|
|
724
|
+
### `wairon method rename-param <component> <method> <param> <new-name>`
|
|
725
|
+
Renames a parameter of a contract method. The parameter moves on every
|
|
726
|
+
interface of the component that declares the method, keeping its place, type,
|
|
727
|
+
description and optionality; its old name joins the parameter's rename trace
|
|
728
|
+
(`previousNames`), which `wairon export` shows as `formerly`, so a generator or
|
|
729
|
+
consumer reads a rename, not a signature change. Each signature is re-derived
|
|
730
|
+
from the params (a method whose `signatureFrom` names this one follows by
|
|
731
|
+
itself), and an HTTP endpoint path placeholder `{old}` on the method's own
|
|
732
|
+
binding is respelled — the URL a caller sends is unchanged. Prose is never
|
|
733
|
+
rewritten. It refuses, writing nothing, a component, method or parameter that
|
|
734
|
+
does not exist (or a parameter the method takes from a `signatureFrom`), a name
|
|
735
|
+
that is not an identifier, and a name another parameter of the method holds or
|
|
736
|
+
retired. The MCP twin is `sdd_rename_param`. The next `wairon lock` approves it.
|
|
737
|
+
|
|
600
738
|
### `wairon network flows|policy|diagram|check|why|declare|undeclare`
|
|
601
739
|
Networking derived from the design: the reach the validator already models
|
|
602
740
|
(modelled calls, Portal entries and their scopes, declared networks and their
|
|
@@ -767,7 +905,13 @@ included); applying it stages every change with a backup under each project's
|
|
|
767
905
|
`.wai/transactions/<id>/` (never committed) and swaps them in, restoring every
|
|
768
906
|
backup on any failure. A crash mid-swap leaves a journal: `wairon status` and
|
|
769
907
|
`wairon validate` show it as a notice, and `wairon doctor --fix` rolls it back.
|
|
770
|
-
No verb ever locks — each names the projects to re-lock. A
|
|
908
|
+
No verb ever locks — each names the projects to re-lock. A promote (or `externalize --as project`) re-roots every
|
|
909
|
+
source path that moves with the new project: a path inside the new project's folder becomes
|
|
910
|
+
member-relative, and one outside it (source code you have not moved yet) becomes a member-relative
|
|
911
|
+
*planned* path — `SOURCE_FILE_PLANNED`, never a path that escapes the root — so the new project
|
|
912
|
+
validates on day one and you move the code when you are ready. A topic finding
|
|
913
|
+
(`UNCONSUMED_TOPIC`, `UNSOURCED_SUBSCRIPTION`) is sited at its topic: an allow names it
|
|
914
|
+
with `at: <topic>`. A member stored in a
|
|
771
915
|
`../` sibling checkout on the same volume joins the same transaction: its files
|
|
772
916
|
change all-or-nothing with the family's, the coordinator's journal names every
|
|
773
917
|
owner (so `wairon doctor --fix` recovers from either root), and the plan says
|
|
@@ -841,21 +985,23 @@ project, so it holds nothing machine-specific: it runs `wairon mcp serve` from t
|
|
|
841
985
|
and the server attaches to the project it is started in. A `--global` registration is
|
|
842
986
|
machine-wide and names the running CLI by its absolute path.
|
|
843
987
|
|
|
844
|
-
The local server offers
|
|
988
|
+
The local server offers 47 tools:
|
|
845
989
|
|
|
846
990
|
| Group | Tools |
|
|
847
991
|
|-------|-------|
|
|
848
992
|
| Topology (read) | `listAgents`, `getAgent`, `listDomains`, `validateTopology`, `getProjectConfig` |
|
|
849
993
|
| Authoring | `sdd_initialize_system`, `sdd_add_subsystem`, `sdd_set_public_interfaces`, `sdd_add_component`, `sdd_define_interface`, `sdd_set_endpoints`, `sdd_write_narrative`, `sdd_add_type`, `sdd_update_spec`, `sdd_delete_spec` |
|
|
850
994
|
| Reading and checking | `sdd_get_spec`, `sdd_get_status`, `sdd_validate_tree` |
|
|
851
|
-
| Renames and moves (in this tree) | `sdd_rename_component`, `sdd_rename_method`, `sdd_rename_type`, `sdd_move_methods` |
|
|
995
|
+
| Renames and moves (in this tree) | `sdd_rename_component`, `sdd_rename_method`, `sdd_rename_param`, `sdd_rename_type`, `sdd_rename_field`, `sdd_move_methods` |
|
|
852
996
|
| Members and family migrations (each takes `dryRun`) | `sdd_add_member`, `sdd_move_member`, `sdd_externalize_subsystem`, `sdd_promote_member`, `sdd_demote_member`, `sdd_internalize_member`, `sdd_attach_member`, `sdd_detach_member`, `sdd_adopt_member`, `sdd_rename_project`, `sdd_rename_member_alias` |
|
|
853
997
|
| Externals | `sdd_add_external`, `sdd_update_external`, `sdd_remove_external`, `sdd_pin_externals`, `sdd_get_externals_status` |
|
|
998
|
+
| Public surface (the producer's side) | `sdd_list_consumers`, `sdd_surface_diff` |
|
|
999
|
+
| Network | `sdd_set_network`, `sdd_get_network_flows`, `sdd_explain_flow` |
|
|
854
1000
|
| Packs | `sdd_pack_impact` |
|
|
855
1001
|
| Delegation | `sdd_get_agent_brief` (the live brief for one agent; also served as the `wairon-agent://` resource) |
|
|
856
1002
|
|
|
857
1003
|
A hosted server adds its own `sdd_host_*` and `sdd_landscape_*` tools; see the
|
|
858
|
-
[hosted server guide](design/hosted-mcp-server.md).
|
|
1004
|
+
[hosted server guide](https://github.com/SYW-Apps/Waffle-AIron/blob/main/docs/design/hosted-mcp-server.md).
|
|
859
1005
|
|
|
860
1006
|
---
|
|
861
1007
|
|
|
@@ -865,7 +1011,7 @@ A hosted server adds its own `sdd_host_*` and `sdd_landscape_*` tools; see the
|
|
|
865
1011
|
projects behind one endpoint — a public **data plane** (the `sdd_*` tools over
|
|
866
1012
|
streamable HTTP, per-project API-key scoped) and an admin **control plane**
|
|
867
1013
|
(project & key lifecycle, state-scoped lock). See the
|
|
868
|
-
[hosted server guide](design/hosted-mcp-server.md) for the architecture, Docker
|
|
1014
|
+
[hosted server guide](https://github.com/SYW-Apps/Waffle-AIron/blob/main/docs/design/hosted-mcp-server.md) for the architecture, Docker
|
|
869
1015
|
self-hosting, auth, and sizing.
|
|
870
1016
|
|
|
871
1017
|
### `wairon serve [--host <h>] [--port <p>] [--admin-host <h>] [--admin-port <p>] [--data-dir <path>] [--no-auth]`
|
|
@@ -964,18 +1110,19 @@ return the impact of every pack they applied in their results.
|
|
|
964
1110
|
| `wairon externals add <alias> [<source>] [--project <id>] [--ref <ref>] [--dir <dir>] [--use a,b\|'*'] [--description <text>] [--no-pin] [--dry-run] [--json]` | Declare one external in `.wai/project.yaml`. The source is the location grammar members use: `../sibling`, `hosted:<id>`, `<git url>` or `<git url>#<commit>` (the commit is the ref the pin follows, fixed there); omit it when the family provides the producer. Text that is no location (braces, quotes, whitespace) is refused. The declaration is checked against the producer it reaches **before** anything is written — `--dry-run` included: one answering to another id, or not exporting a `use` name to this project, is refused naming the id, the closest exported names, or the narrower audience the name is exported to (see Export audiences). Pins by default; a producer that cannot be read leaves it declared and unpinned, saying why, and exits 2 (nothing was pinned). A refusal is one sentence naming the accepted form, and exits 1 |
|
|
965
1111
|
| `wairon externals use <alias> [--add a,b\|'*'] [--remove c] [--dry-run] [--json]` | Change one declared external's `use` imports, so the specs may name the producer's public names bare (e.g. so an `implements` of the producer's trait spells its types as the producer does — though `alias::name` and the bare imported name compare as one type either way). An added name the producer does not export to this project is refused. The pin is untouched. Exits 1 on a refusal |
|
|
966
1112
|
| `wairon externals remove <alias> [--dry-run] [--json]` | Remove one external: its declaration and its pin (`.wai/externals/<alias>.yaml` and its lock entry) together. An orphaned pin — one whose declaration was deleted by hand, which `externals list` shows — is removed too. Exits 1 when the alias is neither declared nor pinned |
|
|
967
|
-
| `wairon externals consumers [--json]` | From a producer's root: the
|
|
1113
|
+
| `wairon externals consumers [--search <dirs...>] [--json]` | From a producer's root: the projects that consume it — each with the alias and section it declares it under and the public names its specs use — so a producer sees who breaks before it changes its surface. Without `--search`, the family read from the highest root in reach. `--search ..` (any folders) also scans those folders and the project roots directly under them for projects declaring this one as an external by a path that resolves here, or by git — a sibling checkout is found this way. A hosted consumer, or one on another machine, declares its dependency on its own side and is not visible here (`sdd_list_consumers` is the same answer for an assistant) |
|
|
968
1114
|
| `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) |
|
|
969
1115
|
| `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 |
|
|
970
1116
|
| `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 |
|
|
971
|
-
| `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 document per portal), import one, or list them |
|
|
1117
|
+
| `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 — a `{name}` segment of an endpoint path is a parameter `in: path`, the other params the query of a GET/DELETE or the JSON body of a POST/PUT/PATCH), import one, or list them. An unknown `--portal` is refused naming the portals the surface renders |
|
|
1118
|
+
| `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. 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`). `sdd_surface_diff` is the same answer for an assistant |
|
|
972
1119
|
| `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) |
|
|
973
1120
|
|
|
974
1121
|
## Tooling
|
|
975
1122
|
|
|
976
1123
|
| Command | Description |
|
|
977
1124
|
|---------|-------------|
|
|
978
|
-
| `wairon dev [--port <port>] [--open]` | A local single-project dev server: the wairon web UI over the current project, no login or tenancy (loopback only) |
|
|
1125
|
+
| `wairon dev [--port <port>] [--open]` | A local single-project dev server: the wairon web UI over the current project, no login or tenancy (loopback only). It serves the same project over MCP (streamable HTTP) at `POST http://127.0.0.1:<port>/mcp?project=local` — the current project is registered as `local`; without `?project=local` the endpoint answers 403. For an AI tool on the same machine the stdio server (`wairon mcp serve`, what `wairon mcp install` registers) remains the usual route |
|
|
979
1126
|
| `wairon update [--check] [--channel <name>]` | Check/install the latest release; switch channel |
|
|
980
1127
|
| `wairon aliases list` | Show command aliases (`wai`) and their status |
|
|
981
1128
|
| `wairon aliases enable <name>` / `disable <name>` | Create / remove an alias |
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# Approval baselines — what `lock` approves, and why it stopped writing
|
|
2
|
+
|
|
3
|
+
> Status: **implemented**. Written against wairon 5.1.x. The "problem" section
|
|
4
|
+
> describes the behaviour as it was BEFORE this work, not as it is now.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## The problem
|
|
9
|
+
|
|
10
|
+
`wairon lock` had grown two jobs fused into one. The first is the one it was
|
|
11
|
+
designed for: **a human reviews the design and approves it**. The second was
|
|
12
|
+
bookkeeping — and the bookkeeping is what people actually experienced.
|
|
13
|
+
|
|
14
|
+
Three symptoms, all the same cause:
|
|
15
|
+
|
|
16
|
+
**It flooded the working tree.** Approving ratcheted every spec's `status` from
|
|
17
|
+
`draft` to `complete` on disk. On this project's own tree that is **786 files**
|
|
18
|
+
rewritten for a decision that changed no design. A lock scoped to one subsystem
|
|
19
|
+
still rewrote everything it could reach, and the specs a human had actually
|
|
20
|
+
edited were buried under files whose content had not changed.
|
|
21
|
+
|
|
22
|
+
**It reported a verdict nobody could act on.** `wairon status` printed:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
Lock: STALE — the specs or the governing doctrine changed since 2026-08-12,
|
|
26
|
+
so this lock no longer holds. Re-run `wairon lock`.
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
…on a tree validating **0 errors / 0 warnings**. The banner named nothing to
|
|
30
|
+
look at, offered no way to approve part of the change, and asked for work that
|
|
31
|
+
produced no new information. It could only ever say "something moved", because
|
|
32
|
+
a `StateId` is a hash: it answers *did anything change*, never *what*.
|
|
33
|
+
|
|
34
|
+
**It fought the maintainer's own workflow.** `.wai/phased_design.md` records the
|
|
35
|
+
blanket draft→complete freeze being reverted by hand **four times** (lines 18,
|
|
36
|
+
27, 34, 41), once annotated *"product gap: lock needs phase awareness"*.
|
|
37
|
+
|
|
38
|
+
And a second gate had grown on top of the first — see
|
|
39
|
+
[the removed `promote` step](https://github.com/SYW-Apps/Waffle-AIron/blob/main/docs/design/hosted-mcp-server.md#4-state-scoped-lock).
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## The model
|
|
44
|
+
|
|
45
|
+
One addition retires five concepts: **record what was approved per spec, not one
|
|
46
|
+
hash of the whole tree.**
|
|
47
|
+
|
|
48
|
+
A review needs two things — the current tree and the tree as it stood when
|
|
49
|
+
someone approved it. wairon had the first and only a whole-tree fingerprint of
|
|
50
|
+
the second. With **one content digest per spec file**, the question changes from
|
|
51
|
+
*"did anything move?"* to *"what moved?"*, which is the only form a human can
|
|
52
|
+
review.
|
|
53
|
+
|
|
54
|
+
`src/core/approval.ts` derives it; `.wai/lock.json` carries it. Three properties
|
|
55
|
+
are load-bearing.
|
|
56
|
+
|
|
57
|
+
### It is committed, and it is one file
|
|
58
|
+
|
|
59
|
+
The approval rides in the lock record, which was always committed. That is what
|
|
60
|
+
lets a **teammate, a fresh clone and CI** see the same approval the approver saw
|
|
61
|
+
— an earlier revision of this design kept it in `~/.wairon/baselines/`, where
|
|
62
|
+
nobody else could read it, so every other checkout judged an approved tree as
|
|
63
|
+
unapproved.
|
|
64
|
+
|
|
65
|
+
Digests rather than content is what makes that affordable: for this project's
|
|
66
|
+
786 specs, **~90 KB instead of ~2 MB**. And no consumer ever needed the content
|
|
67
|
+
— `diffAgainstApproval` and `settledSpecPaths` only ever ask whether a spec
|
|
68
|
+
still matches. For the content itself there is already git: the record is
|
|
69
|
+
committed, so `git diff <lock commit> -- .wai/specs` is the real diff, and
|
|
70
|
+
better.
|
|
71
|
+
|
|
72
|
+
Keys are written **sorted**, so re-approving a one-spec change produces a
|
|
73
|
+
**two-line diff** — the timestamp and that spec's digest — in a 96 KB file.
|
|
74
|
+
Measured on this repo. That is a good signal in a PR rather than noise, and it
|
|
75
|
+
is categorically different from the ratchet this replaced, which rewrote
|
|
76
|
+
hundreds of *spec* files for a decision that changed no design. A test asserts
|
|
77
|
+
the spec tree is byte-identical after `lock`.
|
|
78
|
+
|
|
79
|
+
Digests **normalize line endings** before hashing. This is not tidiness: git
|
|
80
|
+
rewrites line endings on checkout (`core.autocrlf`), so an approval taken on
|
|
81
|
+
Windows would otherwise report every spec as drifted on a Linux CI runner. The
|
|
82
|
+
defect was unreachable while the record was machine-local — sharing it is what
|
|
83
|
+
made it live.
|
|
84
|
+
|
|
85
|
+
### It is per project root, and members are pinned by their subjects
|
|
86
|
+
|
|
87
|
+
Every `.wai` owns its own lock record, including each member project. A
|
|
88
|
+
parent's approval never freezes a member's in-flight work, a member cloned on
|
|
89
|
+
its own carries its approval with it, and a parent lock writes nothing below
|
|
90
|
+
itself.
|
|
91
|
+
|
|
92
|
+
What crosses between them is a **composition subject**: the `stateId` in the
|
|
93
|
+
member's own lock record, read and never recomputed. Each direct member's
|
|
94
|
+
subject is an input of the parent's gate identity, and the parent's record
|
|
95
|
+
(format 2) lists it under `members` with the member's state as the parent saw
|
|
96
|
+
it — `approved` (its lock matches its tree), `drifted` (it has a lock, its tree
|
|
97
|
+
changed) or `never` (no lock). Approval is transitive by hashing: a member's
|
|
98
|
+
subject already includes its own members' subjects, so nothing recurses into a
|
|
99
|
+
member's files. A member editing its own specs does not appear in the parent's
|
|
100
|
+
diff or move the parent's identity — the decisions are separate — until the
|
|
101
|
+
member re-locks; then its subject moves and the parent's approval reads stale.
|
|
102
|
+
`wairon status` prints the pin tree (each member's state from its own root,
|
|
103
|
+
and whether the parent's pin `matches`, has `moved`, or is `unpinned`).
|
|
104
|
+
|
|
105
|
+
A parent may require its members' approvals with
|
|
106
|
+
`composition.requireApprovedMembers: true`: its lock then refuses while a
|
|
107
|
+
direct member is drifted or never approved, naming each. By default it proceeds
|
|
108
|
+
and records their states.
|
|
109
|
+
|
|
110
|
+
This replaced a stage-1 `children` map that copied each child's `StateId` into
|
|
111
|
+
the parent's record without ever re-verifying it; `children` is still read for
|
|
112
|
+
one release and never written.
|
|
113
|
+
|
|
114
|
+
### Settledness is derived, not stored
|
|
115
|
+
|
|
116
|
+
This is what replaced the on-disk `status` ratchet, and it needed care, because
|
|
117
|
+
the ratchet was doing real work. The MCP authoring tools **always** write
|
|
118
|
+
`status: 'draft'` (`src/mcp/server.ts:825, 1027, 1125, 1331`); draft specs get
|
|
119
|
+
`COMPLETENESS_RULES` findings downgraded from error to warning; and `lock` was
|
|
120
|
+
the only thing that ever promoted. Delete it naively and the gate goes
|
|
121
|
+
**permanently soft** — which is exactly how an `UNEXPECTED_IMPLEMENTATION_METHOD`
|
|
122
|
+
came to surface days late in real use.
|
|
123
|
+
|
|
124
|
+
The record already knows which specs were approved and which have moved, so a
|
|
125
|
+
spec that is *approved and unchanged* is presented to the rules as `complete` —
|
|
126
|
+
in memory, restored in `finally`, the same mechanism `treatAllAsComplete`
|
|
127
|
+
already used (`src/core/validation.ts`). Nothing is written.
|
|
128
|
+
|
|
129
|
+
Two things get strictly better than the ratchet:
|
|
130
|
+
|
|
131
|
+
- **It is bidirectional.** The on-disk version was one-way by design
|
|
132
|
+
(`allowStatusDemotion` exists to stop a re-add reopening a locked spec), so an
|
|
133
|
+
edited spec stayed marked complete and in-flux work kept being judged at full
|
|
134
|
+
strictness with no way back short of a manual demotion. A spec that drifts
|
|
135
|
+
after approval now returns to draft context by itself.
|
|
136
|
+
- **It cannot drift.** Derived state has no second copy to fall out of sync.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## What `lock` does now
|
|
141
|
+
|
|
142
|
+
1. Capture the gate identity (own specs, design doctrine, declared inputs,
|
|
143
|
+
`composition`, each direct member's subject).
|
|
144
|
+
2. Validate as complete (full strictness, no draft relaxation, mutates nothing).
|
|
145
|
+
Only the **design** half may refuse; code-conformance results are recorded
|
|
146
|
+
beside the claim (`code`, with the analyzer that produced them) and enforced
|
|
147
|
+
by `validate --ci`, not by the lock.
|
|
148
|
+
3. Report what moved since the last approval, the code results, and each direct
|
|
149
|
+
member's state; refuse under `composition.requireApprovedMembers` if a member
|
|
150
|
+
is not approved.
|
|
151
|
+
4. Ask.
|
|
152
|
+
5. Confirm the gate identity is still the captured one (refuse, writing nothing,
|
|
153
|
+
if an input moved while the lock ran).
|
|
154
|
+
6. Record the approval — a digest per own spec, `members`, `code` — on
|
|
155
|
+
`.wai/lock.json` (format 2), together with who approved and how that identity
|
|
156
|
+
was established.
|
|
157
|
+
7. Regenerate this project's derived topology (`generate --family` walks the
|
|
158
|
+
members; a lock never does).
|
|
159
|
+
|
|
160
|
+
A hosted lock runs the same steps. A hosted lock *request* also records the
|
|
161
|
+
gate identity it was made about; if the tree moved after the request, the
|
|
162
|
+
approved execution refuses, writes nothing, and cancels the request with the
|
|
163
|
+
reason.
|
|
164
|
+
|
|
165
|
+
A scoped approval (`--subsystem x`) approves **only what it covers**; everything
|
|
166
|
+
outside keeps the approval it already had. Whole-tree capture under a scoped
|
|
167
|
+
flag would silently mark the rest of the tree reviewed, which is a worse bug
|
|
168
|
+
than the one being removed.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Who approved
|
|
173
|
+
|
|
174
|
+
`lockedBy` is `{ id, name?, source }`, and the `source` is the point. A `hosted`
|
|
175
|
+
identity was **authenticated** by the instance that issued the caller's
|
|
176
|
+
credential. `git` and `os` are self-declared, read from the machine's own
|
|
177
|
+
config. Recording which keeps the record honest instead of leaving a bare name
|
|
178
|
+
to imply more than it can.
|
|
179
|
+
|
|
180
|
+
Locally the preference is the **git author identity**, because it is the one
|
|
181
|
+
identity the repository already attributes work to — a reviewer can match it
|
|
182
|
+
against the author of the commit that carries the lock, and that matching is the
|
|
183
|
+
whole value. The fallback is `user@hostname`, which is what Terraform records as
|
|
184
|
+
a lock's holder and what git itself synthesizes when no identity is configured; a
|
|
185
|
+
bare OS username is strictly worse, because in CI it is `runner` and identifies
|
|
186
|
+
nobody. Deliberately not a MAC address or other hardware id: modern systems
|
|
187
|
+
randomize MACs, they differ per interface and VPN, and collecting one is device
|
|
188
|
+
fingerprinting for a field that is not authentication anyway.
|
|
189
|
+
|
|
190
|
+
Hosted is the one surface where wairon genuinely authenticated the approver, and
|
|
191
|
+
it was **throwing that away** — `executeApprovedLock` wrote the constant
|
|
192
|
+
`admin:master` while its caller held a resolved principal. It now records the
|
|
193
|
+
subject. On the approval path the **decider** is recorded, not the requester:
|
|
194
|
+
the requester did not have the authority, and the decision is what conferred it.
|
|
195
|
+
The requester is not lost — the `ApprovalRequest` and the audit event carry them.
|
|
196
|
+
|
|
197
|
+
None of this is proof. The trust anchor is the commit that introduces
|
|
198
|
+
`lock.json` — signed commits, a protected branch, a reviewed PR. `lockedBy` is a
|
|
199
|
+
convenience copy that earns its place only where git does not follow: a
|
|
200
|
+
`.waitree` archive, a `remote push` into a hosted instance, a tarball.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Reversals worth recording
|
|
205
|
+
|
|
206
|
+
**Keeping the approval outside the repo was wrong, and only sharing it showed
|
|
207
|
+
why.** The first revision put it in `~/.wairon/baselines/` on the reasoning that
|
|
208
|
+
"approving must not dirty the repo". That reasoning conflated two very different
|
|
209
|
+
writes: rewriting 786 *spec* files, and one lockfile changing the way lockfiles
|
|
210
|
+
do. The cost was that no teammate, clone or CI run could see the approval at
|
|
211
|
+
all. Moving it into the committed record — as digests, so it fits — keeps the
|
|
212
|
+
property that mattered and drops the one that did not.
|
|
213
|
+
|
|
214
|
+
**Two defects were only reachable once the record was shared.** Hashing raw
|
|
215
|
+
bytes meant a Windows approval reported all 786 specs as drifted on a Linux
|
|
216
|
+
checkout; digests now normalize line endings, and a test rewrites the whole
|
|
217
|
+
fixture tree to CRLF to hold that. And the hosted lock **published before
|
|
218
|
+
writing the record**, so the commit shipping the specs did not contain the
|
|
219
|
+
approval certifying them — harmless when the record was four bookkeeping fields,
|
|
220
|
+
not when it *is* the approval. Neither was observable while the approval sat on
|
|
221
|
+
one machine.
|
|
222
|
+
|
|
223
|
+
**The signature change was caught by a test, not by the compiler.** Adding an
|
|
224
|
+
`approver` parameter to `executeApprovedLock` silently re-bound an existing
|
|
225
|
+
three-argument call to the new slot. `tsconfig.json` excludes `tests/`, so
|
|
226
|
+
nothing typechecks the suite — vitest transpiles without checking. The test
|
|
227
|
+
failed loudly and the fix was obvious, but the compiler should have said so
|
|
228
|
+
first.
|
|
229
|
+
|
|
230
|
+
**`--subsystem` nearly became a silent whole-tree approval.** Removing the
|
|
231
|
+
ratchet left nothing scoping the capture. Two existing lock tests failed and
|
|
232
|
+
named it. Scoped capture was added rather than discovered later.
|
|
233
|
+
|
|
234
|
+
**The first e2e assertion was written before it could pass.** *"Approving writes
|
|
235
|
+
nothing into the project"* is the property the design exists for, so it was
|
|
236
|
+
asserted first; it failed on the status ratchet, was narrowed to what was true
|
|
237
|
+
that day with a comment saying where it stopped, and tightened to byte-identity
|
|
238
|
+
once the ratchet was gone. A test that encodes a temporary truth is worse than
|
|
239
|
+
one that admits its scope.
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Deliberately out of scope
|
|
244
|
+
|
|
245
|
+
**A local wairon account.** The CLI has no identity of its own: `remote attach`
|
|
246
|
+
stores a bearer token, but resolving it to a person would need a `whoami`
|
|
247
|
+
endpoint and a network call, and `lock` is deliberately an offline command. The
|
|
248
|
+
git identity is the honest local answer until that changes.
|
|
249
|
+
|
|
250
|
+
**Storing the approved content.** Digests cannot show what changed *inside* a
|
|
251
|
+
spec. Git already can, against the lock's own commit, so keeping a second copy
|
|
252
|
+
of the tree to duplicate it is not worth its weight.
|
|
253
|
+
|
|
254
|
+
**Separation of duties.** `promote` was the hook for "two people must sign off".
|
|
255
|
+
It never worked, and it was removed rather than kept as a placeholder. If it
|
|
256
|
+
returns, it belongs on the approval — where *"approved by X at StateId S"* is a
|
|
257
|
+
reviewable, committed fact — not as a status string nothing reads.
|