@ybotok/loam 0.2.0-alpha.1 → 0.2.0-alpha.2

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.
Files changed (65) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/README.md +6 -6
  3. package/ROADMAP.md +123 -18
  4. package/dist/commands/archive/plan/gate.js +18 -12
  5. package/dist/commands/archive/plan/gate.js.map +1 -1
  6. package/dist/commands/archive/plan/landscape.js +19 -0
  7. package/dist/commands/archive/plan/landscape.js.map +1 -1
  8. package/dist/commands/archive/run.js +2 -1
  9. package/dist/commands/archive/run.js.map +1 -1
  10. package/dist/commands/validate/arch-coverage.js +11 -1
  11. package/dist/commands/validate/arch-coverage.js.map +1 -1
  12. package/dist/commands/validate/fleet/obligations.js +43 -9
  13. package/dist/commands/validate/fleet/obligations.js.map +1 -1
  14. package/dist/commands/validate/fleet/usecases/steps.js +3 -3
  15. package/dist/commands/validate/fleet/usecases/steps.js.map +1 -1
  16. package/dist/commands/validate/service/service.js +1 -0
  17. package/dist/commands/validate/service/service.js.map +1 -1
  18. package/dist/commands/validate/service/specs.js +33 -1
  19. package/dist/commands/validate/service/specs.js.map +1 -1
  20. package/dist/core/agent/workflows/check.js +3 -0
  21. package/dist/core/agent/workflows/check.js.map +1 -1
  22. package/dist/core/brief/unchecked.js +1 -0
  23. package/dist/core/brief/unchecked.js.map +1 -1
  24. package/dist/core/c4/arch.js +72 -180
  25. package/dist/core/c4/arch.js.map +1 -1
  26. package/dist/core/c4/likec4.js +2 -0
  27. package/dist/core/c4/likec4.js.map +1 -1
  28. package/dist/core/c4/parsed/deployment.js +158 -0
  29. package/dist/core/c4/parsed/deployment.js.map +1 -0
  30. package/dist/core/c4/project/load.js +4 -0
  31. package/dist/core/c4/project/load.js.map +1 -1
  32. package/dist/core/c4/project/staged.js +220 -0
  33. package/dist/core/c4/project/staged.js.map +1 -0
  34. package/dist/core/c4/resolve/service.js +46 -0
  35. package/dist/core/c4/resolve/service.js.map +1 -1
  36. package/dist/core/c4/resolve/steps.js +144 -0
  37. package/dist/core/c4/resolve/steps.js.map +1 -0
  38. package/dist/core/c4/splice/delta-blocks.js +15 -2
  39. package/dist/core/c4/splice/delta-blocks.js.map +1 -1
  40. package/dist/core/c4/workspace.js +2 -0
  41. package/dist/core/c4/workspace.js.map +1 -1
  42. package/dist/core/coherence/coherence.js +36 -0
  43. package/dist/core/coherence/coherence.js.map +1 -1
  44. package/dist/core/deployment/delta.js +188 -0
  45. package/dist/core/deployment/delta.js.map +1 -0
  46. package/dist/core/deployment/objects.js +145 -0
  47. package/dist/core/deployment/objects.js.map +1 -0
  48. package/dist/core/pack/living.js +19 -0
  49. package/dist/core/pack/living.js.map +1 -1
  50. package/dist/core/repo/authored/paths.js +18 -0
  51. package/dist/core/repo/authored/paths.js.map +1 -1
  52. package/dist/core/repo/paths.js +43 -2
  53. package/dist/core/repo/paths.js.map +1 -1
  54. package/dist/core/status/feature/artifacts.js +29 -4
  55. package/dist/core/status/feature/artifacts.js.map +1 -1
  56. package/dist/core/status/fleet/fleet.js +11 -1
  57. package/dist/core/status/fleet/fleet.js.map +1 -1
  58. package/dist/core/usecases/delta/overlay.js +28 -191
  59. package/dist/core/usecases/delta/overlay.js.map +1 -1
  60. package/dist/core/usecases/fleet.js.map +1 -1
  61. package/dist/core/usecases/operations.js +2 -2
  62. package/dist/core/usecases/operations.js.map +1 -1
  63. package/dist/core/vocabulary/issue.js +3 -0
  64. package/dist/core/vocabulary/issue.js.map +1 -1
  65. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -10,6 +10,93 @@ case for a change — the alternative that was rejected, the defect it came from
10
10
  generalises — lives where it is maintained: [SCHEMA.md](SCHEMA.md) for the rule,
11
11
  [ROADMAP.md](ROADMAP.md) for the priority and its exit criteria, and the commit that landed it._
12
12
 
13
+ ## [0.2.0-alpha.2] - 2026-09-02
14
+
15
+ The deployment axis, and the two defects the axis surfaced on its way in. Still a prerelease on the
16
+ `alpha` line: `npm i @ybotok/loam@alpha` installs it. Nothing below is breaking, but two entries are
17
+ changes a repo can NOTICE — a fleet that had already placed a `#obl-` tag on a `deploymentNode`
18
+ starts seeing findings it was never shown, and `loam status` stops asking an architecture-only
19
+ feature for a business spec.
20
+
21
+ ### Added — the deployment axis: topology joined to requirements
22
+
23
+ A LikeC4 `deployment { }` block has always been legal in a docs repo and was completely unread. The
24
+ parser resolved every `instanceOf`, and after that no requirement could name a node, no `#obl-` tag
25
+ on a datacenter was graded, and the context pack an agent implements from did not mention topology
26
+ at all. Five things change, and a fleet that draws no topology is unaffected by every one of them —
27
+ the axis opts in on the block's existence, the way the obligation vocabulary opts in on its file.
28
+
29
+ - **`Covers:` gains a fourth entry form.** `node:<id>` names a deployment node or an instance, and
30
+ `node:a -> node:b` names an edge between them; both match literally. This is the form that lets an
31
+ architecture requirement about replication name the two clusters it is written about, and a
32
+ topology change that renames one now reports `covers.unknown` with the real ids offered — spelled
33
+ with the prefix, because that is the line the author has to type. Both sides of an edge carry the
34
+ prefix or neither; a bare id stays an element entry.
35
+ - **An obligation on a deployment object is graded.** `obligation.unknown`, `obligation.unapplied`
36
+ and `obligation.uncovered` now walk the topology as well as the logical model. This is a
37
+ behaviour change a repo can notice: a `#obl-` tag on a `deploymentNode` used to be invisible to
38
+ every check, so a fleet that had placed one starts seeing findings it was never shown. A
39
+ datacenter is owned by no service, so `obligation.uncovered` drops `subject` there and names the
40
+ requirement's home in general terms rather than picking a team.
41
+ - **A feature can bring topology.** `features/<FEAT>/deployment/<name>.likec4` is a create-only
42
+ document `loam archive` copies into `architecture/` and `loam unarchive` takes back — the third
43
+ whole-file slot, beside `usecases/` and `glossary/`. Say `extend` in it to add to a region the
44
+ living map already declares; two features may extend one region from documents of their own and
45
+ both archive. Two new codes, both errors and neither overridable: **`deployment.doc-exists`** when
46
+ the feature's document names a file the living tree already holds, and **`deployment.doc-invalid`**
47
+ when it cannot be read against the map the feature's own merge would leave behind. The second is
48
+ what makes the ordinary case legal — a document instancing a service only this feature's
49
+ `delta.likec4` adds resolves, because the corpus inside a feature window is feature ∪ living — and
50
+ refuses the case where the merge does not land what the document names. `delta.likec4` still
51
+ refuses a `deployment { }` block and now names this slot instead of the living landscape.
52
+ A third, **`deployment.doc-reserved`**, refuses a name loam owns inside `architecture/` —
53
+ `subsystems.likec4`, `landscape.likec4`, or a path under `usecases/` — whether or not the target
54
+ exists, because two of the three fail silently when it does not: nothing parses the generated
55
+ subsystem views and `loam subsystem sync` overwrites them wholesale.
56
+ - **`loam context` carries the topology.** `--json` gains `living.deployment` — the service's
57
+ instances, and the deployment edges touching them from either end.
58
+ - **One more statement in the adoption brief's unchecked list**, now sixteen: nothing here knows
59
+ whether the second cluster exists, is reachable, or holds the data a requirement claims, and no
60
+ check counts datacenters — how many a service needs is a decision the fleet writes down as a
61
+ requirement, never one loam evaluates.
62
+
63
+ ### Fixed — the never-overridable archive refusal names what actually refused
64
+
65
+ It said `N element binding(s) name an illegal service id` for every code in that register, and had
66
+ done since the use-case slot put a second one there — about features that bind nothing. The headline
67
+ now counts the refusals and the closing line names their codes; each issue keeps its own sentence.
68
+ The `--json` envelope is unchanged: consumers branch on `issues[].code` and `overridable`, which is
69
+ what that split exists for.
70
+
71
+ ### Fixed — `loam status` no longer invents work for an architecture-only feature
72
+
73
+ A feature whose only requirement delta is `specs/<svc>/arch.spec.md` reported two things that were
74
+ not true, and both are corrected. The `spec` row read `missing` and `next.author-spec` told the
75
+ author to write business requirements the change does not have — while `loam validate --feature`
76
+ accepted the same tree at exit 0 and `coherence`'s `service.no-requirement-delta` stayed silent,
77
+ because every other reader counts the service DIRECTORY and not which corpus is in it. And the
78
+ `verification` row read `blocked` with `blockedBy: ["delta", "spec", "openapi", "asyncapi"]`,
79
+ telling the reader there was nothing to derive a checklist from — while `featureChecklist` reads
80
+ `arch.spec.md` and `loam verify` on that feature returns a real checklist.
81
+
82
+ The fleet form carried the same defect independently, and it broke the rule stated one line below
83
+ it in its own source: `loam status` with no feature reported `missing <svc>/spec` and staged the
84
+ feature `missing`, over a feature `loam status <FEAT>` called `ready`. Both forms now accept either
85
+ corpus as the service's requirement delta, and `<svc>/spec` is named only when neither exists.
86
+
87
+ Three payload-visible consequences, all in `loam status --json`: `artifacts[]`'s `spec` row is
88
+ `required: false` when the service's `arch.spec.md` delta exists (it was unconditionally `true`),
89
+ `arch-spec` now counts as a verification feeder, so `blockedBy` gained `"arch-spec"` in the
90
+ one case that is still genuinely blocked — a feature with no requirement delta of either kind —
91
+ and `features[]`'s `missing` and `stage` no longer report an architecture-only feature as
92
+ incomplete. No finding code, flag or exit code changes. This is the change to read if a script
93
+ branched on `artifacts[].required` or on `features[].missing`.
94
+
95
+ The case is the one `arch.spec.md` exists for: an outbox, a retry policy, an alert — obligations no
96
+ business scenario was ever going to carry, so the feature that adds one has no business requirement
97
+ to write. Found by authoring loam's own architecture change through loam's own lifecycle, which is
98
+ the trigger ROADMAP's self-hosting section records for that axis.
99
+
13
100
  ## [0.2.0-alpha.1] - 2026-09-01
14
101
 
15
102
  The first release of the 0.2 line, and the whole accumulated body since `0.1.0-beta.3` — 116
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  [![CI](https://github.com/ybotok/loam/actions/workflows/ci.yml/badge.svg)](https://github.com/ybotok/loam/actions/workflows/ci.yml)
8
8
  [![npm](https://img.shields.io/npm/v/@ybotok/loam.svg)](https://www.npmjs.com/package/@ybotok/loam)
9
9
 
10
- > **Pre-release: `0.2.0-alpha.1`**, the first release of the 0.2 line and the newest published
10
+ > **Pre-release: `0.2.0-alpha.2`**, the second release of the 0.2 line and the newest published
11
11
  > version of [`@ybotok/loam`](https://www.npmjs.com/package/@ybotok/loam). It publishes under the
12
12
  > `alpha` npm dist-tag: `npm i @ybotok/loam@alpha`, or the exact version, always installs this
13
13
  > line. `latest` is moved onto a release by hand once it has published, so a plain
@@ -108,7 +108,7 @@ both halves are written down: the question in the brief or the checklist, the an
108
108
 
109
109
  Most of this category will tell you what it checks. loam ships the other list too, in the binary:
110
110
  **[`src/core/brief/unchecked.ts`](https://github.com/ybotok/loam/blob/main/src/core/brief/unchecked.ts)
111
- is fifteen statements of what no check will ever tell you**, printed into the adoption brief beside
111
+ is sixteen statements of what no check will ever tell you**, printed into the adoption brief beside
112
112
  the checks that do run. It says, in loam's own words, that nothing here knows whether the model is
113
113
  the architecture the code actually has, whether a requirement is *true*, whether an `operationId` is
114
114
  the one the code serves, or whether `sources` names the files anybody read. And it says the one that
@@ -358,7 +358,7 @@ cannot hold.
358
358
  | `loam new <FEAT>` | `--title <text>` `--touches <id>` `--new-service <id>` `--capability <id>` | Scaffold a feature: intent, C4 delta, a requirement delta per service named by `--touches`/`--new-service`, and a capability delta per promise named by `--capability` (all repeatable) |
359
359
  | `loam show <service\|FEAT>` | `--type <kind>` | Everything loam knows about one service or feature. A feature's JSON includes a semantic `review` pack: intent, exact architecture objects, API/event slices, dependencies, artifacts, verification, blockers and next actions. `--type` forces an ambiguous reading |
360
360
  | `loam subsystem <verb> [names...]` | `--into <name>` `--under <name>` `--title <text>` `--description <text>` `--owner <name>` | Manage the grouping tree under `services/` — a navigable tree no identity depends on. `new`/`rm`, `move … --into`, `rename`, `list`, `history`, `sync` |
361
- | `loam adopt` | `--service <id>` `--subsystem <name>` `--targets` | Brief an agent to write this service's baseline into the docs repo as `draft`: the target paths, the grammar of each, what the landscape already says, and the checks that follow. Writes nothing. `--targets` narrows it to what varies by service — the walk, the 37 checks and the fifteen statements of what nothing checks are the same bytes everywhere, so a `full` field names the one run that carries them: 42,873 bytes to 17,805 |
361
+ | `loam adopt` | `--service <id>` `--subsystem <name>` `--targets` | Brief an agent to write this service's baseline into the docs repo as `draft`: the target paths, the grammar of each, what the landscape already says, and the checks that follow. Writes nothing. `--targets` narrows it to what varies by service — the walk, the 37 checks and the sixteen statements of what nothing checks are the same bytes everywhere, so a `full` field names the one run that carries them: 42,873 bytes to 17,805 |
362
362
  | `loam delta <FEAT>` | `--service <id>` | Project a feature onto one service: the intent, its requirement deltas with every body and Given/When/Then line verbatim, the endpoints it adds or retires, and the edges around it. Doubles as a coding-agent task |
363
363
  | `loam gherkin [<FEAT>]` | `--service <id>` `--dry-run` | Emit spec scenarios as digest-stamped Gherkin `.feature` files into the service repo's `<gherkinDir>/loam/` |
364
364
  | `loam rebase [<FEAT>]` | `--service <id>` `--dry-run` `--living` | Pin the feature to the living versions it was written against, on the requirement axis and the contract axis. `--living` takes no feature: it pins the living corpus's `Realizes:` entries to the capability requirements they name, which is what makes `capability.realizes-stale` able to fire later |
@@ -486,7 +486,7 @@ always preserved byte for byte, with the entry printed for manual merge.
486
486
  maps, what is lost, what must be added.
487
487
  - [ROADMAP.md](ROADMAP.md) — the evidence-backed improvement plan, priorities, non-goals and exit
488
488
  criteria.
489
- - [CHANGELOG.md](CHANGELOG.md) — released beta.1–alpha.1 plus the changes on `main` under
489
+ - [CHANGELOG.md](CHANGELOG.md) — released beta.1–alpha.2 plus the changes on `main` under
490
490
  `[Unreleased]`.
491
491
  - [docs/BENCHMARKS.md](https://github.com/ybotok/loam/blob/main/docs/BENCHMARKS.md) — what a
492
492
  fleet-sized run costs, with the method, the machine and the committed runs behind every number.
@@ -522,12 +522,12 @@ authored-capability axes with the `Realizes:` join, use cases graded as `dynamic
522
522
  `architecture/obligations.yaml` architectural obligations checked through `#obl-` tags and
523
523
  `Covers:`. Known limits, each with its owner in [ROADMAP.md](ROADMAP.md): the complete gate still
524
524
  needs repeatable CI and installed-package evidence observed from an actual push. Speculative
525
- `render`, health composition and UI generation come later. Behind that status stand **156 test files** (counted 2026-09-01): the count is graded
525
+ `render`, health composition and UI generation come later. Behind that status stand **157 test files** (counted 2026-09-02): the count is graded
526
526
  against a live readdir by `test/docs-facts.test.ts`, so this sentence fails the suite the moment it
527
527
  trails the tree.
528
528
 
529
529
  The package is published as [`@ybotok/loam`](https://www.npmjs.com/package/@ybotok/loam), currently
530
- `0.2.0-alpha.1`: the unscoped `loam` name is taken by an unrelated GDAL wrapper, so the package ships
530
+ `0.2.0-alpha.2`: the unscoped `loam` name is taken by an unrelated GDAL wrapper, so the package ships
531
531
  under the maintainer's own npm user scope. Releases are tag-driven and maintainer-only
532
532
  ([CONTRIBUTING.md](CONTRIBUTING.md)); the version this line names is pinned to `package.json` by a
533
533
  drift test, so it cannot silently trail a release.
package/ROADMAP.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Roadmap
2
2
 
3
- _Assessed 2026-08-28._
3
+ _Assessed 2026-09-02._
4
4
 
5
5
  This is loam's canonical improvement sequence. It records priorities and exit criteria, not delivery
6
6
  dates. An item is complete only when its exit criteria are supported by repeatable evidence; moving
@@ -18,20 +18,21 @@ acceptance tests, and implementation evidence remain ordinary files in repositor
18
18
  state inside a service. The CLI is small enough to audit, and every writer — not only archive — now
19
19
  commits through a locked, journaled transaction that a crash cannot leave half-applied.
20
20
 
21
- The tree contains **419 TypeScript modules in 128 source packages**, with an acyclic package graph
21
+ The tree contains **424 TypeScript modules in 129 source packages**, with an acyclic package graph
22
22
  checked by
23
23
  [scripts/package-graph.mjs](https://github.com/ybotok/loam/blob/main/scripts/package-graph.mjs). The
24
24
  CLI exposes **29 commands** from [src/cli.ts](https://github.com/ybotok/loam/blob/main/src/cli.ts),
25
- and the suite stands at **156 test files**. Those four counts are deliberately stated OUTSIDE the
25
+ and the suite stands at **157 test files**. Those four counts are deliberately stated OUTSIDE the
26
26
  dated snapshot below: each derives from the tree in one readdir, so
27
27
  [test/docs-facts.test.ts](https://github.com/ybotok/loam/blob/main/test/docs-facts.test.ts) grades
28
28
  them live and this paragraph cannot quietly trail the code the way its predecessor did.
29
29
 
30
- _Measured 2026-08-31 on `ab4c856` plus the changes on top of it, and dated because neither number
31
- has a cheap derivation:_ lint, typecheck, `npm run arch:check` and `npm run meta:check` green;
32
- `npm test` passing **3,358/3,358 tests**, with two `skipIf(asRoot)` cases the root gate container
33
- cannot run; and the coverage gate passing with **92.20% statements, 83.97% branches, 96.75%
34
- functions, and 94.13% lines** against its thresholds of 91 / 82 / 95 / 93.
30
+ _Measured 2026-09-02 at `0.2.0-alpha.2`, and dated because neither number has a cheap derivation:_
31
+ lint, typecheck, `npm run arch:check` and `npm run meta:check` green; `npm test` passing
32
+ **3,399/3,399 tests**, with two `skipIf(asRoot)` cases the root gate container cannot run; the
33
+ coverage gate passing with **92.25% statements, 84.04% branches, 96.62% functions, and 94.18%
34
+ lines** against its thresholds of 91 / 82 / 95 / 93; and `npm run release:check` plus
35
+ `npm run test:package` green against the real tarball.
35
36
 
36
37
  One qualification remains, and it is observation rather than code: nothing has been pushed since the
37
38
  CI `stability` job and the tarball-reading package smoke landed, so repeatable CI executions — the
@@ -98,14 +99,16 @@ The strongest foundations to preserve are:
98
99
  ## Now
99
100
 
100
101
  Nothing is queued behind code. Every integrity, enforcement and lifecycle item that preceded this
101
- has landed (see [Recently landed](#recently-landed)) — the authored business axis and, with the three
102
- entries at the top of that section, the use-case axis too. The axis is now writable through the
103
- change lifecycle, and the gate and the report agree about what it contains.
102
+ has landed (see [Recently landed](#recently-landed)) — the authored business axis, the use-case
103
+ axis, and as of 2026-09-02 the deployment axis, which is the first item this file ever wrote
104
+ directly into this section rather than promoting from [Later](#later--promote-only-from-evidence).
104
105
 
105
- What is left is the one thing this file cannot close by writing code, and it stays where it is
106
- rather than being promoted here for the shape of it: the CI `stability` job and the installed-package
107
- smoke have still to be OBSERVED green from a pushed commit. That is release evidence to collect, not
108
- an improvement to make.
106
+ ### Release evidence, unchanged
107
+
108
+ What is left is the one thing this file cannot close by writing code, and it stays
109
+ where it is rather than being promoted for the shape of it: the CI `stability` job and the
110
+ installed-package smoke have still to be OBSERVED green from a pushed commit. That is release
111
+ evidence to collect, not an improvement to make.
109
112
 
110
113
  An item returns to this section from [Later](#later--promote-only-from-evidence) when fleet evidence
111
114
  names its operator, repeated task, failure mode, frequency, current workaround and measurable
@@ -122,6 +125,105 @@ changes, exit criteria, and what each review surfaced — are in this file's his
122
125
  working tree and not committed, and the range goes in with the commit rather than being guessed at
123
126
  here.
124
127
 
128
+ #### The deployment axis — topology joined to requirements
129
+
130
+ **Promoted 2026-09-02, and authored as a feature in loam's own docs repo** rather than as an item
131
+ text here: `meta/docs/features/archive/FEAT-1-the-deployment-axis/`, whose `intent.md` carries the
132
+ four measurements and whose `specs/loam/arch.spec.md` carries the five rules the change establishes,
133
+ each with scenarios. This entry is the decision and the gate; that feature is the specification, and
134
+ the two must not be allowed to become two accounts of the same change.
135
+
136
+ **Landed 2026-09-02, complete.** Every step is in, and step 4 was corrected rather than built
137
+ (below) — the item specified two finding codes that the implementation showed it should not mint.
138
+ FEAT-1 archived once every edge it claimed existed —
139
+ and it archived having been CORRECTED against the tree twice, which is the argument for the route
140
+ rather than a footnote to it: the delta predicted a `core/kernel/` dependency that turned out to be
141
+ type-only (a brand costs an annotation, not an import) and missed `core/coherence/` entirely.
142
+ `npm run meta:check` was red for the whole of the build and went green with the archive, which is
143
+ the axis working rather than a gap in it.
144
+
145
+ The whole change is reviewable as five commits on `feat/deployment-axis`, and the one thing to read
146
+ first is what the implementation taught the plan: two of the codes this item specified were not
147
+ minted, and the reasons are in step 4.
148
+
149
+ **What is wrong today.** A LikeC4 `deployment { }` block is legal in a docs repo and completely
150
+ unread. The parser resolves every `instanceOf` in it — a container renamed out from under a
151
+ deployment node fails the gate as `landscape.invalid` — and after that nothing: no requirement can
152
+ name a node, no report counts one, and the context pack an agent implements from does not mention
153
+ topology at all. Two of the four measurements taken on a copy of `examples/docs` are the reason this
154
+ is an item rather than a wish. A `#obl-` tag on a `deploymentNode` is invisible to
155
+ `validate/fleet/obligations.ts`, which walks the logical model only — the same undeclared tag raises
156
+ `obligation.unknown` on a container and nothing on a datacenter, which is fail-open and a defect on
157
+ its own terms. And `parsedModel().deployment` already returns nodes with their tags, instances
158
+ carrying the id of the logical element each one instances, and relationships carrying metadata, so
159
+ the read costs no second parse, no new file format and no change to the frozen CLI surface.
160
+
161
+ **Required changes**, in the order the dependency graph forces:
162
+
163
+ 1. Walk the deployment model in every `#obl-` grade. Fail-open becomes fail-closed; no new code.
164
+ 2. A fourth `Covers:` entry form, `node:<id>`, with the same prefix accepted on either side of the
165
+ edge form. No new code.
166
+ 3. The parse adapter in `src/core/c4/parsed/`, and the deployment records added to
167
+ `test/likec4-model-parity.test.ts` — a two-stage substitution nothing measures is an assumption.
168
+ `src/core/c4/` holds exactly five files, so this is a sub-package and not a sixth module.
169
+ 4. **No new finding code, and this is a correction the implementation earned.** The item planned
170
+ two. The first, for a tagged node or edge no living arch requirement covers, turned out to be
171
+ `obligation.uncovered` asked of a second model: once step 1 walks the topology, the existing code
172
+ answers it, and a caller acts identically either way — write a `Covers:` line. A second code for
173
+ one question is exactly what the add-a-code checklist refuses. The second, for a container no
174
+ deployment instances, is declined outright: it asserts that everything modelled ought to be
175
+ deployed somewhere, which is a COMPLETENESS claim, and completeness is the one thing
176
+ `src/core/brief/unchecked.ts` says loam never checks. It is the same category error as the
177
+ at-least-two-datacenters rule below, one axis over.
178
+ 5. `features/<FEAT>/deployment/<name>.likec4`, create-only and copied whole — `core/usecases/delta/`
179
+ one axis over. It works because `extend` was measured to resolve across documents of one project,
180
+ so no text splice is needed; `delta-blocks.ts` goes on refusing a `deployment { }` block inside
181
+ `delta.likec4` and its message names the new slot. The collision is on the FILE, never on what is
182
+ inside it: two features may both extend one living region from documents of their own and both
183
+ archive, which is the shape the fleet-wide change actually takes.
184
+
185
+ **Graded against the merge preview**, which shipped one commit behind the slot and is the
186
+ second code, `deployment.doc-invalid` (error, never overridable). This axis needs the post-merge
187
+ corpus more literally than the use-case one: a feature that stands a NEW service up in a cluster
188
+ declares it in `delta.likec4` and instances it in the topology, so the element the document names
189
+ exists only after the merge. The staging that makes that readable moved OUT of
190
+ `core/usecases/delta/overlay.ts` into `core/c4/project/staged.ts` rather than being copied — the
191
+ two axes would have disagreed first about the merge preview, which is the one thing both
192
+ refusals rest on. The extraction dropped `core/usecases/` → `core/kernel/`, which is a one-line
193
+ edge and was edited into the model directly.
194
+ 6. The surfaces: the topology in the context pack (`context --json`'s `living.deployment`), the code
195
+ table `test/codes-drift.test.ts` requires, and one more statement in
196
+ `src/core/brief/unchecked.ts`. `loam explain` needed nothing — it parses the /loam-check fix
197
+ tables at runtime, so the rows added there ARE the entries. The scorecard row the item listed was
198
+ dropped: the fleet scorecard aggregates at the bound `service`, and a topology row there would
199
+ count deployment nodes against a denominator no service owns.
200
+
201
+ **Exit criteria.** Every step above green under the full gate; `covers.unknown` demonstrated firing
202
+ on a covered node that a topology change renamed, which is the acceptance criterion the trigger
203
+ names; the parity suite extended and shown to fail against an un-extended adapter; and
204
+ `test/self-model.test.ts` re-pinned once FEAT-1 archives.
205
+
206
+ **Out of scope, and each refusal is load-bearing.** No reading of Terraform, Helm or cluster state —
207
+ that is the extractor the non-goals forbid. No check that a service is deployed in at least two
208
+ datacenters: it would need `criticality` promoted from prose to a read field, and it would turn
209
+ `architecture/obligations.yaml` into the policy engine that file says in its own words it is not. A
210
+ multi-datacenter rule is an authored requirement whose `Covers:` line names the nodes; loam grades
211
+ the join and never the architecture. And no `environment` concept above LikeC4, because
212
+ `deploymentNode` is that mechanism and the fleet names its own kinds.
213
+
214
+ **The evidence, and its one gap stated plainly.** Operator: a team standing up a standby cluster in
215
+ a second datacenter. Repeated task: keeping RTO/RPO requirements attached to a topology that keeps
216
+ changing. Failure mode: a requirement about replication staying green after the node it described
217
+ was renamed or removed. Frequency: every topology change, which is the cadence a migration runs at.
218
+ Current workaround: a `Covers:` line that resolves to nothing, or no written requirement at all.
219
+ Acceptance criterion: a topology change that orphans a covered requirement is convicted without a
220
+ human noticing it first. **That is one operator, not two.** The maintainer judged the trigger
221
+ sufficient on the same basis recorded for landscape scaling and the authored business axis, and the
222
+ [Later](#later--promote-only-from-evidence) exit criteria are amended below to say so rather than
223
+ leaving the two paragraphs to contradict each other. Steps 1 and 2 do not rest on that judgement at
224
+ all: the first is a fail-open defect and the second is a grammar entry, and neither is a new axis.
225
+
226
+
125
227
  - **A feature can bring a use case.** `features/<FEAT>/usecases/<name>.likec4` — a views-only
126
228
  document `loam archive` copies into `architecture/usecases/` and `loam unarchive` takes back,
127
229
  create-only, refused by `usecase.flow-exists` when the living tree already holds that file. All
@@ -264,8 +366,11 @@ item gets promoted — it went from here to `## Now` and from there to
264
366
 
265
367
  Exit criteria for promoting a Later item:
266
368
 
267
- - Evidence comes from at least two independent operators or fleets, except landscape scaling and the
268
- authored business axis, whose recorded triggers are sufficient.
369
+ - Evidence comes from at least two independent operators or fleets, except landscape scaling, the
370
+ authored business axis and — added 2026-09-02 — the deployment axis in [Now](#now), whose recorded
371
+ triggers are sufficient. Three exceptions is where this criterion starts describing the rule
372
+ rather than the exception: the next single-operator promotion has to say why the criterion should
373
+ survive, or retire it.
269
374
  - An ADR compares the existing workflow, external tooling, and the smallest loam-owned change.
270
375
  - A prototype is measured against a predeclared acceptance criterion and preserves every non-goal
271
376
  below.
@@ -276,7 +381,7 @@ Exit criteria for promoting a Later item:
276
381
 
277
382
  Placed in a section of its own, and the placement is the argument. This is not a `## Now` item, and
278
383
  nothing here may be mistaken for one. It is not a "Later" candidate either: promotion
279
- from that section requires evidence from at least two independent operators or fleets (bar the two
384
+ from that section requires evidence from at least two independent operators or fleets (bar the three
280
385
  whose recorded triggers were judged sufficient when they were written), and loam's own repository is
281
386
  one operator by construction, so a Later entry could only ever be a permanent exception to the rule
282
387
  it sits under. Nor is its value speculative — the trigger below is a defect this repository has
@@ -142,19 +142,23 @@ export async function gate(config, featureId, opts, say) {
142
142
  // the rest. The register is issue.ts's `approveOverrides` — the same data
143
143
  // the `overridable` key in the envelope is resolved from — never a code
144
144
  // string spelled here, because two spellings of "what the flag moves" would
145
- // drift. Today the register holds one code: a `metadata { service }` binding
146
- // the id grammar refuses, anywhere in a tagged element's spliced block. The
147
- // landscape merge would splice that name into the living map verbatim, and
148
- // the new-service scan would probe `services/<binding>/` with it — a '../'
149
- // collapses the probe out of services/ altogether — so the refusal is a
150
- // mechanical fact about the path the name becomes, not a judgment about the
151
- // feature. Same doctrine as the illegal-specs-name refusal above; the dry
152
- // run is gated the same way, and the --json envelope stays not-coherent
153
- // with every issue attached, exactly like the overridable branch below, so
154
- // a consumer branches on the code and not on which refusal path fired.
145
+ // drift. What the register HOLDS has grown: it began as one code (a
146
+ // `metadata { service }` binding the id grammar refuses, where the merge
147
+ // would splice the name into the living map verbatim and probe
148
+ // `services/<binding>/` with it) and now holds the create-only collisions of
149
+ // every whole-file axis besides. They share the doctrine and nothing else —
150
+ // each is a mechanical fact about what the merge would write, never a
151
+ // judgment about the feature — which is why the message below counts them
152
+ // and lets each issue say its own sentence.
153
+ //
154
+ // IT USED TO NAME BINDINGS, and had been wrong for every other code in the
155
+ // register since the use-case slot landed: a feature whose flow collided
156
+ // with a living one was refused with "1 element binding(s) name an illegal
157
+ // service id", about a feature that binds nothing. A refusal headline that
158
+ // describes the wrong breach sends its reader to the wrong file first.
155
159
  const nonOverridable = issues.filter((i) => !approveOverrides(i));
156
160
  if (nonOverridable.length > 0) {
157
- const msg = `archive ${id} — BLOCKED: ${nonOverridable.length} element binding(s) name an illegal service id; --approve does not override this`;
161
+ const msg = `archive ${id} — BLOCKED: ${nonOverridable.length} issue(s) --approve does not override; each names what the merge would write`;
158
162
  if (json) {
159
163
  refuseJson("not-coherent", msg, issues, `features/${dirName}`);
160
164
  return null;
@@ -166,7 +170,9 @@ export async function gate(config, featureId, opts, say) {
166
170
  // line says which of the marks the flag cannot move.
167
171
  for (const i of issues)
168
172
  console.error(` ${SEVERITY_MARK[i.severity]} ${i.message}`);
169
- console.error(`\nOf these, --approve does not override the ${nonOverridable.length} illegal binding(s) — the merge would write a name loam can never resolve into services/, mechanically. Fix the binding(s); the other issues gate as usual.`);
173
+ console.error(`\nOf these, --approve does not override ${nonOverridable.length}: ${[...new Set(nonOverridable.map((i) => i.code))].sort().join(", ")}. ` +
174
+ "Each is a mechanical fact about what the merge would write — a name loam cannot resolve, or a file it would replace wholesale — not a judgement the flag can move. " +
175
+ "Fix those; the other issues gate as usual.");
170
176
  sayExplain("not-coherent");
171
177
  process.exitCode = 1;
172
178
  return null;
@@ -1 +1 @@
1
- {"version":3,"file":"gate.js","sourceRoot":"","sources":["../../../../src/commands/archive/plan/gate.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Every refusal that happens before a byte is planned.\n *\n * They are gathered here because they share one doctrine that is easy to lose:\n * `--approve` overrides judgments about the FEATURE, and nothing else. A service\n * directory whose name the id grammar refuses, a living document holding git\n * conflict markers, a requirement outside `## Requirements` — those are\n * mechanical facts about paths and files, so `--approve` does not reach them,\n * and the dry run is gated too, since a plan built on one describes a merge loam\n * must never perform.\n *\n * `null` means the refusal has already been printed and the exit code set.\n */\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { fail, repoPath, sayExplain } from \"../../../core/envelope/json.js\";\nimport { approveOverrides, gatesArchive, type Issue } from \"../../../core/vocabulary/issue.js\";\nimport { recoverInterruptedCommit } from \"../../../core/staging/recovery/recover.js\";\nimport { readUtf8 } from \"../../../core/staging/writes.js\";\nimport { loadFile } from \"../../../core/c4/likec4.js\";\nimport { archiveDir as archiveRoot, featurePaths, featureSpecPaths, SPEC_AXES } from \"../../../core/repo/paths.js\";\nimport { livingCapabilityPaths } from \"../../../core/repo/authored/paths.js\";\nimport { featureCapabilityDeltas } from \"../../../core/capabilities/delta/tree.js\";\nimport { locateServicePaths } from \"../../../core/repo/service-target.js\";\nimport { featureSpecServices, missingFeatureMessage, resolveFeature } from \"../../../core/repo/repo.js\";\nimport { featureCoherence } from \"../../../core/coherence/coherence.js\";\nimport {\n invalidSpecServiceFindings,\n livingMergeConflicts,\n unknownDeltaServices,\n} from \"../../../core/coherence/living.js\";\nimport { SEVERITY_MARK } from \"../../../core/vocabulary/report.js\";\nimport { isRequirementsHeading, parseRequirements } from \"../../../core/document/parse.js\";\nimport { refuseFindings, refuseJson, sayRecovery, type ArchiveOptions } from \"./refusal.js\";\nimport { type Gated } from \"./state.js\";\nimport type { DocsDir } from \"../../../core/kernel/ids/dirs.js\";\nimport type { FleetContext } from \"../../../core/fleet-context.js\";\n\nexport async function gate(\n config: { docsDir: DocsDir; fleet?: FleetContext },\n featureId: string,\n opts: ArchiveOptions,\n say: (line?: string) => void,\n): Promise<Gated | null> {\n const json = opts.json === true;\n const dryRun = opts.dryRun === true;\n // the living docs half-written, and every other command in loam would have\n // planned against them as if they were the truth. Under the same lock, so the\n // repair cannot race a writer. It refuses when the half-merged files have been\n // edited since — that is a human's call, not a merge's.\n const recovered = await recoverInterruptedCommit(config.docsDir);\n if (recovered !== null && !json) sayRecovery(recovered);\n\n const feature = await resolveFeature(config.docsDir, featureId, \"exclude\");\n if (!feature) {\n fail(json, \"unknown-target\", await missingFeatureMessage(config.docsDir, featureId));\n return null;\n }\n // The raw argument's last appearance: from here on the feature answers only to\n // its canonical id — tags, coherence, and the self-exclusion scans all match\n // on `id`, and `archive FEAT-5-slug` must plan exactly like `archive FEAT-5`.\n const { id, dirName, dir: featureDir } = feature;\n\n // Parse delta.likec4 ONCE for the whole run: the coherence gate and the\n // architecture merge below read the same file, and loading it spins up a\n // fresh Langium workspace each time. Nothing writes it in between.\n const deltaLikec4 = featurePaths(featureDir).delta;\n const deltaDoc = existsSync(deltaLikec4) ? await loadFile(deltaLikec4) : undefined;\n\n const deltaServices = await featureSpecServices(featureDir);\n // The business corpus's delta tree, walked once HERE and carried on `Gated`:\n // the conflict-marker scan, the strayed-requirement scan and the merge itself\n // all ask which capability documents this feature rewrites, and three walks\n // are three chances to answer differently. `featureCoherence` below walks it\n // again on its own — it is called without `config.fleet`, the same\n // no-context path `core/coherence/declared.ts` already documents paying for\n // the arch delta — so this is not the run's only walk, only the one every\n // phase downstream of the gate shares.\n const capabilityDeltas = (\n config.fleet === undefined\n ? await featureCapabilityDeltas(featureDir)\n : await config.fleet.featureCapabilityDeltas(featureDir)\n ).docs;\n\n // A `specs/<svc>/` whose NAME is not a legal service id, refused before\n // anything joins that name into a path: the conflict scan below probes\n // `services/<svc>/<axis>` with it, and the requirements merge would\n // materialise `services/<svc>/` from it — a directory `service.id-invalid`\n // then calls an error on the next `validate --all`, and one no loam command\n // can address or re-create. The set comes from coherence/living.ts's\n // `invalidSpecServiceFindings` — the same function validate reads — so the\n // two gates cannot drift.\n //\n // --approve does not apply, exactly as with the conflict-marker refusal\n // below: --approve overrides judgments about the FEATURE, and a name the id\n // grammar refuses is a mechanical fact about the path it would become. The\n // dry run is gated too: a plan built on that name describes a merge loam\n // must never perform.\n const illegalServices = await invalidSpecServiceFindings(featureDir);\n if (illegalServices.length > 0) {\n const msg = `archive ${id} — BLOCKED: ${illegalServices.length} per-service delta(s) named by an illegal service id; --approve does not override this`;\n if (json) {\n refuseFindings(\"not-coherent\", msg, illegalServices, `features/${dirName}`);\n return null;\n }\n console.error(`${msg}:`);\n for (const f of illegalServices) console.error(` ✗ ${f.message}`);\n console.error(`\\n--approve does not override this — the directory the merge would create is one loam can never address, mechanically.`);\n // Every arm in this file refuses by hand — it prints a headline, a list and\n // a closing sentence rather than the one message `fail()` takes — so the\n // code and its lookup have to be asked for explicitly. `sayExplain` is\n // called rather than the format being copied six times, and the code passed\n // is the SAME one the `--json` branch a few lines up emits: text and\n // envelope must name one refusal, or a reader who switches modes to find\n // the code is handed a different answer than the one they saw.\n sayExplain(\"not-coherent\");\n process.exitCode = 1;\n return null;\n }\n\n // Git conflict markers in a LIVING document this merge would rewrite, before\n // anything else is asked of it. Both sides of somebody's merge are in the\n // file, so nothing it says is anyone's text — and the requirements rewrite\n // deletes whichever marker lines fall inside the section it owns, turning a\n // conflict anyone can see into a file nobody can tell is wrong. Checked ahead\n // of the coherence gate for `inspectLandscape`'s reason: a conflicted\n // document parses, so every verdict downstream of it is an opinion about half\n // of two people's work.\n //\n // --approve does not apply, exactly as with the strayed-requirement refusal\n // below: --approve overrides judgments about the FEATURE, and this is a fact\n // about the living document.\n const conflicted = await livingMergeConflicts(config.docsDir, deltaServices, capabilityDeltas.map((d) => d.id));\n if (conflicted.length > 0) {\n const msg = `archive ${id} — BLOCKED: ${conflicted.length} living document(s) still hold git conflict markers`;\n if (json) {\n refuseFindings(\"merge-failed\", msg, conflicted, `features/${dirName}`);\n return null;\n }\n console.error(`${msg}:`);\n for (const f of conflicted) console.error(` ✗ ${f.message}`);\n console.error(`\\n--approve does not override this — the loss is mechanical, not a judgment call.`);\n sayExplain(\"merge-failed\");\n process.exitCode = 1;\n return null;\n }\n\n // Gate: GATING issues block the archive. Severity and gating are two\n // different questions (issue.ts): errors gate because the merge would write\n // something wrong, and the rare warning marked `gates` blocks too — the\n // document is legal but the merge would drop authored content. Advisory\n // warnings are printed (and carried into --json) but never block, matching\n // validate's own doctrine (report.ts: \"warnings never gate\"). --approve\n // overrides the gating issues ONLY, and must say exactly which ones it is\n // walking past. A dry run is gated too: a plan for a merge that would be\n // refused describes nothing that will happen.\n const issues = await featureCoherence({ docsDir: config.docsDir, featureDir, featureId: id, preloadedDelta: deltaDoc });\n // The issues --approve cannot move, carved out before the gate below reads\n // the rest. The register is issue.ts's `approveOverrides` — the same data\n // the `overridable` key in the envelope is resolved from — never a code\n // string spelled here, because two spellings of \"what the flag moves\" would\n // drift. Today the register holds one code: a `metadata { service }` binding\n // the id grammar refuses, anywhere in a tagged element's spliced block. The\n // landscape merge would splice that name into the living map verbatim, and\n // the new-service scan would probe `services/<binding>/` with it — a '../'\n // collapses the probe out of services/ altogether — so the refusal is a\n // mechanical fact about the path the name becomes, not a judgment about the\n // feature. Same doctrine as the illegal-specs-name refusal above; the dry\n // run is gated the same way, and the --json envelope stays not-coherent\n // with every issue attached, exactly like the overridable branch below, so\n // a consumer branches on the code and not on which refusal path fired.\n const nonOverridable = issues.filter((i) => !approveOverrides(i));\n if (nonOverridable.length > 0) {\n const msg = `archive ${id} — BLOCKED: ${nonOverridable.length} element binding(s) name an illegal service id; --approve does not override this`;\n if (json) {\n refuseJson(\"not-coherent\", msg, issues, `features/${dirName}`);\n return null;\n }\n console.error(`${msg}:`);\n // The whole list, not just the carve-out: the reader is about to fix the\n // feature, and the issues --approve COULD have moved are work they still\n // owe — hiding them here made the second refusal a surprise. The closing\n // line says which of the marks the flag cannot move.\n for (const i of issues) console.error(` ${SEVERITY_MARK[i.severity]} ${i.message}`);\n console.error(\n `\\nOf these, --approve does not override the ${nonOverridable.length} illegal binding(s) — the merge would write a name loam can never resolve into services/, mechanically. Fix the binding(s); the other issues gate as usual.`,\n );\n sayExplain(\"not-coherent\");\n process.exitCode = 1;\n return null;\n }\n const gating = issues.filter(gatesArchive);\n const advisory = issues.filter((i) => !gatesArchive(i));\n if (gating.length > 0 && !opts.approve) {\n const msg = `archive ${id} — BLOCKED: not coherent (${gating.length} gating issue(s), ${advisory.length} advisory warning(s))`;\n if (json) {\n refuseJson(\"not-coherent\", msg, issues, `features/${dirName}`);\n return null;\n }\n console.error(`${msg}:`);\n for (const i of issues) console.error(` ${SEVERITY_MARK[i.severity]} ${i.message}`);\n console.error(`\\nFix the gating issues (advisory warnings never block), or re-run with --approve to override them (may corrupt the living docs).`);\n sayExplain(\"not-coherent\");\n process.exitCode = 1;\n return null;\n }\n if (gating.length > 0) {\n say(`⚠ archiving despite ${gating.length} gating issue(s) (--approve):`);\n for (const i of gating) say(` ${SEVERITY_MARK[i.severity]} ${i.message}`);\n say();\n }\n if (advisory.length > 0) {\n say(`⚠ ${advisory.length} warning(s) (non-blocking):`);\n for (const i of advisory) say(` ⚠ ${i.message}`);\n say();\n }\n\n // A `specs/<svc>/` addressed to a service that exists nowhere. `validate`\n // graded this and archive did not, which is the half that costs something:\n // `specs/<svc>/` is what the merge MATERIALISES `services/<svc>/` from, so a\n // one-character slip in `--touches` archived at exit 0 and left the fleet a\n // living service nobody meant to adopt. The set comes from coherence/living.ts's\n // `unknownDeltaServices` — the same function validate reads — so the two\n // gates cannot drift into disagreeing about which ids are real.\n //\n // Gated exactly like a coherence error, --approve included: which service you\n // meant is a judgment about the FEATURE. A feature that genuinely introduces\n // one has a better escape hatch than --approve anyway — tag its element in\n // delta.likec4, which is also what makes the fleet map true afterwards.\n const unknownServices = await unknownDeltaServices(config.docsDir, featureDir, id, { preloadedDelta: deltaDoc });\n if (unknownServices.length > 0 && !opts.approve) {\n const msg = `archive ${id} — BLOCKED: ${unknownServices.length} per-service delta(s) address a service that does not exist`;\n if (json) {\n refuseFindings(\"not-coherent\", msg, unknownServices, `features/${dirName}`);\n return null;\n }\n console.error(`${msg}:`);\n for (const f of unknownServices) console.error(` ✗ ${f.message}`);\n console.error(`\\nFix the id, or introduce the service in this feature's delta.likec4 — or re-run with --approve to create it anyway.`);\n sayExplain(\"not-coherent\");\n process.exitCode = 1;\n return null;\n }\n if (unknownServices.length > 0) {\n say(`⚠ archiving despite ${unknownServices.length} unknown service(s) (--approve) — this merge creates them:`);\n for (const f of unknownServices) say(` ✗ ${f.message}`);\n say();\n }\n\n // A LIVING requirement outside `## Requirements` is a merge loam cannot do\n // correctly: the rewrite replaces only the requirements RUN inside that\n // section (rewriteRequirementsRun) while parseRequirements collects from\n // every section — so the strayed requirement keeps its authored copy in the\n // preserved prose AND lands again in the rewritten run, and the next\n // archive's MODIFIED replaces only one of them. Refused rather than\n // repaired: excising blocks from prose programmatically would be archive\n // editing text it does not understand. --approve does not apply — it\n // overrides judgments about the FEATURE, and this is neither.\n const strayed: Issue[] = [];\n const scanStrayed = async (livingPath: string, subject: string): Promise<void> => {\n if (!existsSync(livingPath)) return;\n for (const r of parseRequirements(await readUtf8(livingPath))) {\n // The ONE definition of the heading (spec.ts): the guard and the rewrite\n // boundary match the same way, so they cannot disagree about \"outside\".\n if (r.section !== undefined && isRequirementsHeading(r.section)) continue;\n const where = r.section === undefined ? \"above every heading\" : `under '${r.section}'`;\n strayed.push({\n severity: \"error\",\n code: \"living.requirement-outside-requirements\",\n subject,\n message: `${subject}: living requirement '${r.name}' sits ${where} in ${repoPath(config.docsDir, livingPath)} — the merge rewrites only '## Requirements', so it would land in the file twice. Re-home it under '## Requirements' first, then re-run.`,\n });\n }\n };\n for (const svc of deltaServices) {\n for (const axis of SPEC_AXES) {\n if (!existsSync(featureSpecPaths(featureDir, svc)[axis.key])) continue;\n // The shared context, because this sits in a deltaServices × SPEC_AXES\n // loop: without it every iteration re-walks and re-reads the fleet.\n await scanStrayed((await locateServicePaths(config.docsDir, svc, config.fleet))[axis.key], svc);\n }\n }\n // The business corpus, for the identical mechanical reason: a capability\n // document is narrative ABOVE `## Requirements`, `rewriteRequirementsRun`\n // rewrites only the run inside that heading, and a requirement filed under\n // `## Notes` would keep its authored copy in the preserved prose AND land\n // again in the rewritten run.\n for (const doc of capabilityDeltas) {\n await scanStrayed(livingCapabilityPaths(config.docsDir, doc.id).spec, doc.id);\n }\n if (strayed.length > 0) {\n const msg = `archive ${id} — BLOCKED: ${strayed.length} living requirement(s) outside '## Requirements'`;\n if (json) {\n refuseJson(\"living-outside-requirements\", msg, strayed, `features/${dirName}`);\n return null;\n }\n console.error(`${msg}:`);\n for (const i of strayed) console.error(` ✗ ${i.message}`);\n console.error(`\\n--approve does not override this — the duplication is mechanical, not a judgment call.`);\n sayExplain(\"living-outside-requirements\");\n process.exitCode = 1;\n return null;\n }\n\n // Pre-flight: the archive destination must be free, or the final move would fail\n // after the living docs were already rewritten.\n const archiveDir = archiveRoot(config.docsDir);\n const archiveDest = join(archiveDir, dirName);\n if (existsSync(archiveDest)) {\n fail(json, \"archive-exists\", `archive ${id} — BLOCKED: features/archive/${dirName} already exists. Remove or rename it, then re-run.`);\n return null;\n }\n\n say(`archive ${id}${dryRun ? \" (dry run)\" : \"\"}\\n`);\n\n return {\n id,\n dirName,\n featureDir,\n deltaDoc,\n deltaServices,\n capabilityDeltas,\n gating,\n advisory,\n archiveDir,\n archiveDest,\n recovered,\n issues,\n };\n}\n"]}
1
+ {"version":3,"file":"gate.js","sourceRoot":"","sources":["../../../../src/commands/archive/plan/gate.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Every refusal that happens before a byte is planned.\n *\n * They are gathered here because they share one doctrine that is easy to lose:\n * `--approve` overrides judgments about the FEATURE, and nothing else. A service\n * directory whose name the id grammar refuses, a living document holding git\n * conflict markers, a requirement outside `## Requirements` — those are\n * mechanical facts about paths and files, so `--approve` does not reach them,\n * and the dry run is gated too, since a plan built on one describes a merge loam\n * must never perform.\n *\n * `null` means the refusal has already been printed and the exit code set.\n */\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { fail, repoPath, sayExplain } from \"../../../core/envelope/json.js\";\nimport { approveOverrides, gatesArchive, type Issue } from \"../../../core/vocabulary/issue.js\";\nimport { recoverInterruptedCommit } from \"../../../core/staging/recovery/recover.js\";\nimport { readUtf8 } from \"../../../core/staging/writes.js\";\nimport { loadFile } from \"../../../core/c4/likec4.js\";\nimport { archiveDir as archiveRoot, featurePaths, featureSpecPaths, SPEC_AXES } from \"../../../core/repo/paths.js\";\nimport { livingCapabilityPaths } from \"../../../core/repo/authored/paths.js\";\nimport { featureCapabilityDeltas } from \"../../../core/capabilities/delta/tree.js\";\nimport { locateServicePaths } from \"../../../core/repo/service-target.js\";\nimport { featureSpecServices, missingFeatureMessage, resolveFeature } from \"../../../core/repo/repo.js\";\nimport { featureCoherence } from \"../../../core/coherence/coherence.js\";\nimport {\n invalidSpecServiceFindings,\n livingMergeConflicts,\n unknownDeltaServices,\n} from \"../../../core/coherence/living.js\";\nimport { SEVERITY_MARK } from \"../../../core/vocabulary/report.js\";\nimport { isRequirementsHeading, parseRequirements } from \"../../../core/document/parse.js\";\nimport { refuseFindings, refuseJson, sayRecovery, type ArchiveOptions } from \"./refusal.js\";\nimport { type Gated } from \"./state.js\";\nimport type { DocsDir } from \"../../../core/kernel/ids/dirs.js\";\nimport type { FleetContext } from \"../../../core/fleet-context.js\";\n\nexport async function gate(\n config: { docsDir: DocsDir; fleet?: FleetContext },\n featureId: string,\n opts: ArchiveOptions,\n say: (line?: string) => void,\n): Promise<Gated | null> {\n const json = opts.json === true;\n const dryRun = opts.dryRun === true;\n // the living docs half-written, and every other command in loam would have\n // planned against them as if they were the truth. Under the same lock, so the\n // repair cannot race a writer. It refuses when the half-merged files have been\n // edited since — that is a human's call, not a merge's.\n const recovered = await recoverInterruptedCommit(config.docsDir);\n if (recovered !== null && !json) sayRecovery(recovered);\n\n const feature = await resolveFeature(config.docsDir, featureId, \"exclude\");\n if (!feature) {\n fail(json, \"unknown-target\", await missingFeatureMessage(config.docsDir, featureId));\n return null;\n }\n // The raw argument's last appearance: from here on the feature answers only to\n // its canonical id — tags, coherence, and the self-exclusion scans all match\n // on `id`, and `archive FEAT-5-slug` must plan exactly like `archive FEAT-5`.\n const { id, dirName, dir: featureDir } = feature;\n\n // Parse delta.likec4 ONCE for the whole run: the coherence gate and the\n // architecture merge below read the same file, and loading it spins up a\n // fresh Langium workspace each time. Nothing writes it in between.\n const deltaLikec4 = featurePaths(featureDir).delta;\n const deltaDoc = existsSync(deltaLikec4) ? await loadFile(deltaLikec4) : undefined;\n\n const deltaServices = await featureSpecServices(featureDir);\n // The business corpus's delta tree, walked once HERE and carried on `Gated`:\n // the conflict-marker scan, the strayed-requirement scan and the merge itself\n // all ask which capability documents this feature rewrites, and three walks\n // are three chances to answer differently. `featureCoherence` below walks it\n // again on its own — it is called without `config.fleet`, the same\n // no-context path `core/coherence/declared.ts` already documents paying for\n // the arch delta — so this is not the run's only walk, only the one every\n // phase downstream of the gate shares.\n const capabilityDeltas = (\n config.fleet === undefined\n ? await featureCapabilityDeltas(featureDir)\n : await config.fleet.featureCapabilityDeltas(featureDir)\n ).docs;\n\n // A `specs/<svc>/` whose NAME is not a legal service id, refused before\n // anything joins that name into a path: the conflict scan below probes\n // `services/<svc>/<axis>` with it, and the requirements merge would\n // materialise `services/<svc>/` from it — a directory `service.id-invalid`\n // then calls an error on the next `validate --all`, and one no loam command\n // can address or re-create. The set comes from coherence/living.ts's\n // `invalidSpecServiceFindings` — the same function validate reads — so the\n // two gates cannot drift.\n //\n // --approve does not apply, exactly as with the conflict-marker refusal\n // below: --approve overrides judgments about the FEATURE, and a name the id\n // grammar refuses is a mechanical fact about the path it would become. The\n // dry run is gated too: a plan built on that name describes a merge loam\n // must never perform.\n const illegalServices = await invalidSpecServiceFindings(featureDir);\n if (illegalServices.length > 0) {\n const msg = `archive ${id} — BLOCKED: ${illegalServices.length} per-service delta(s) named by an illegal service id; --approve does not override this`;\n if (json) {\n refuseFindings(\"not-coherent\", msg, illegalServices, `features/${dirName}`);\n return null;\n }\n console.error(`${msg}:`);\n for (const f of illegalServices) console.error(` ✗ ${f.message}`);\n console.error(`\\n--approve does not override this — the directory the merge would create is one loam can never address, mechanically.`);\n // Every arm in this file refuses by hand — it prints a headline, a list and\n // a closing sentence rather than the one message `fail()` takes — so the\n // code and its lookup have to be asked for explicitly. `sayExplain` is\n // called rather than the format being copied six times, and the code passed\n // is the SAME one the `--json` branch a few lines up emits: text and\n // envelope must name one refusal, or a reader who switches modes to find\n // the code is handed a different answer than the one they saw.\n sayExplain(\"not-coherent\");\n process.exitCode = 1;\n return null;\n }\n\n // Git conflict markers in a LIVING document this merge would rewrite, before\n // anything else is asked of it. Both sides of somebody's merge are in the\n // file, so nothing it says is anyone's text — and the requirements rewrite\n // deletes whichever marker lines fall inside the section it owns, turning a\n // conflict anyone can see into a file nobody can tell is wrong. Checked ahead\n // of the coherence gate for `inspectLandscape`'s reason: a conflicted\n // document parses, so every verdict downstream of it is an opinion about half\n // of two people's work.\n //\n // --approve does not apply, exactly as with the strayed-requirement refusal\n // below: --approve overrides judgments about the FEATURE, and this is a fact\n // about the living document.\n const conflicted = await livingMergeConflicts(config.docsDir, deltaServices, capabilityDeltas.map((d) => d.id));\n if (conflicted.length > 0) {\n const msg = `archive ${id} — BLOCKED: ${conflicted.length} living document(s) still hold git conflict markers`;\n if (json) {\n refuseFindings(\"merge-failed\", msg, conflicted, `features/${dirName}`);\n return null;\n }\n console.error(`${msg}:`);\n for (const f of conflicted) console.error(` ✗ ${f.message}`);\n console.error(`\\n--approve does not override this — the loss is mechanical, not a judgment call.`);\n sayExplain(\"merge-failed\");\n process.exitCode = 1;\n return null;\n }\n\n // Gate: GATING issues block the archive. Severity and gating are two\n // different questions (issue.ts): errors gate because the merge would write\n // something wrong, and the rare warning marked `gates` blocks too — the\n // document is legal but the merge would drop authored content. Advisory\n // warnings are printed (and carried into --json) but never block, matching\n // validate's own doctrine (report.ts: \"warnings never gate\"). --approve\n // overrides the gating issues ONLY, and must say exactly which ones it is\n // walking past. A dry run is gated too: a plan for a merge that would be\n // refused describes nothing that will happen.\n const issues = await featureCoherence({ docsDir: config.docsDir, featureDir, featureId: id, preloadedDelta: deltaDoc });\n // The issues --approve cannot move, carved out before the gate below reads\n // the rest. The register is issue.ts's `approveOverrides` — the same data\n // the `overridable` key in the envelope is resolved from — never a code\n // string spelled here, because two spellings of \"what the flag moves\" would\n // drift. What the register HOLDS has grown: it began as one code (a\n // `metadata { service }` binding the id grammar refuses, where the merge\n // would splice the name into the living map verbatim and probe\n // `services/<binding>/` with it) and now holds the create-only collisions of\n // every whole-file axis besides. They share the doctrine and nothing else —\n // each is a mechanical fact about what the merge would write, never a\n // judgment about the feature — which is why the message below counts them\n // and lets each issue say its own sentence.\n //\n // IT USED TO NAME BINDINGS, and had been wrong for every other code in the\n // register since the use-case slot landed: a feature whose flow collided\n // with a living one was refused with \"1 element binding(s) name an illegal\n // service id\", about a feature that binds nothing. A refusal headline that\n // describes the wrong breach sends its reader to the wrong file first.\n const nonOverridable = issues.filter((i) => !approveOverrides(i));\n if (nonOverridable.length > 0) {\n const msg = `archive ${id} — BLOCKED: ${nonOverridable.length} issue(s) --approve does not override; each names what the merge would write`;\n if (json) {\n refuseJson(\"not-coherent\", msg, issues, `features/${dirName}`);\n return null;\n }\n console.error(`${msg}:`);\n // The whole list, not just the carve-out: the reader is about to fix the\n // feature, and the issues --approve COULD have moved are work they still\n // owe — hiding them here made the second refusal a surprise. The closing\n // line says which of the marks the flag cannot move.\n for (const i of issues) console.error(` ${SEVERITY_MARK[i.severity]} ${i.message}`);\n console.error(\n `\\nOf these, --approve does not override ${nonOverridable.length}: ${[...new Set(nonOverridable.map((i) => i.code))].sort().join(\", \")}. ` +\n \"Each is a mechanical fact about what the merge would write — a name loam cannot resolve, or a file it would replace wholesale — not a judgement the flag can move. \" +\n \"Fix those; the other issues gate as usual.\",\n );\n sayExplain(\"not-coherent\");\n process.exitCode = 1;\n return null;\n }\n const gating = issues.filter(gatesArchive);\n const advisory = issues.filter((i) => !gatesArchive(i));\n if (gating.length > 0 && !opts.approve) {\n const msg = `archive ${id} — BLOCKED: not coherent (${gating.length} gating issue(s), ${advisory.length} advisory warning(s))`;\n if (json) {\n refuseJson(\"not-coherent\", msg, issues, `features/${dirName}`);\n return null;\n }\n console.error(`${msg}:`);\n for (const i of issues) console.error(` ${SEVERITY_MARK[i.severity]} ${i.message}`);\n console.error(`\\nFix the gating issues (advisory warnings never block), or re-run with --approve to override them (may corrupt the living docs).`);\n sayExplain(\"not-coherent\");\n process.exitCode = 1;\n return null;\n }\n if (gating.length > 0) {\n say(`⚠ archiving despite ${gating.length} gating issue(s) (--approve):`);\n for (const i of gating) say(` ${SEVERITY_MARK[i.severity]} ${i.message}`);\n say();\n }\n if (advisory.length > 0) {\n say(`⚠ ${advisory.length} warning(s) (non-blocking):`);\n for (const i of advisory) say(` ⚠ ${i.message}`);\n say();\n }\n\n // A `specs/<svc>/` addressed to a service that exists nowhere. `validate`\n // graded this and archive did not, which is the half that costs something:\n // `specs/<svc>/` is what the merge MATERIALISES `services/<svc>/` from, so a\n // one-character slip in `--touches` archived at exit 0 and left the fleet a\n // living service nobody meant to adopt. The set comes from coherence/living.ts's\n // `unknownDeltaServices` — the same function validate reads — so the two\n // gates cannot drift into disagreeing about which ids are real.\n //\n // Gated exactly like a coherence error, --approve included: which service you\n // meant is a judgment about the FEATURE. A feature that genuinely introduces\n // one has a better escape hatch than --approve anyway — tag its element in\n // delta.likec4, which is also what makes the fleet map true afterwards.\n const unknownServices = await unknownDeltaServices(config.docsDir, featureDir, id, { preloadedDelta: deltaDoc });\n if (unknownServices.length > 0 && !opts.approve) {\n const msg = `archive ${id} — BLOCKED: ${unknownServices.length} per-service delta(s) address a service that does not exist`;\n if (json) {\n refuseFindings(\"not-coherent\", msg, unknownServices, `features/${dirName}`);\n return null;\n }\n console.error(`${msg}:`);\n for (const f of unknownServices) console.error(` ✗ ${f.message}`);\n console.error(`\\nFix the id, or introduce the service in this feature's delta.likec4 — or re-run with --approve to create it anyway.`);\n sayExplain(\"not-coherent\");\n process.exitCode = 1;\n return null;\n }\n if (unknownServices.length > 0) {\n say(`⚠ archiving despite ${unknownServices.length} unknown service(s) (--approve) — this merge creates them:`);\n for (const f of unknownServices) say(` ✗ ${f.message}`);\n say();\n }\n\n // A LIVING requirement outside `## Requirements` is a merge loam cannot do\n // correctly: the rewrite replaces only the requirements RUN inside that\n // section (rewriteRequirementsRun) while parseRequirements collects from\n // every section — so the strayed requirement keeps its authored copy in the\n // preserved prose AND lands again in the rewritten run, and the next\n // archive's MODIFIED replaces only one of them. Refused rather than\n // repaired: excising blocks from prose programmatically would be archive\n // editing text it does not understand. --approve does not apply — it\n // overrides judgments about the FEATURE, and this is neither.\n const strayed: Issue[] = [];\n const scanStrayed = async (livingPath: string, subject: string): Promise<void> => {\n if (!existsSync(livingPath)) return;\n for (const r of parseRequirements(await readUtf8(livingPath))) {\n // The ONE definition of the heading (spec.ts): the guard and the rewrite\n // boundary match the same way, so they cannot disagree about \"outside\".\n if (r.section !== undefined && isRequirementsHeading(r.section)) continue;\n const where = r.section === undefined ? \"above every heading\" : `under '${r.section}'`;\n strayed.push({\n severity: \"error\",\n code: \"living.requirement-outside-requirements\",\n subject,\n message: `${subject}: living requirement '${r.name}' sits ${where} in ${repoPath(config.docsDir, livingPath)} — the merge rewrites only '## Requirements', so it would land in the file twice. Re-home it under '## Requirements' first, then re-run.`,\n });\n }\n };\n for (const svc of deltaServices) {\n for (const axis of SPEC_AXES) {\n if (!existsSync(featureSpecPaths(featureDir, svc)[axis.key])) continue;\n // The shared context, because this sits in a deltaServices × SPEC_AXES\n // loop: without it every iteration re-walks and re-reads the fleet.\n await scanStrayed((await locateServicePaths(config.docsDir, svc, config.fleet))[axis.key], svc);\n }\n }\n // The business corpus, for the identical mechanical reason: a capability\n // document is narrative ABOVE `## Requirements`, `rewriteRequirementsRun`\n // rewrites only the run inside that heading, and a requirement filed under\n // `## Notes` would keep its authored copy in the preserved prose AND land\n // again in the rewritten run.\n for (const doc of capabilityDeltas) {\n await scanStrayed(livingCapabilityPaths(config.docsDir, doc.id).spec, doc.id);\n }\n if (strayed.length > 0) {\n const msg = `archive ${id} — BLOCKED: ${strayed.length} living requirement(s) outside '## Requirements'`;\n if (json) {\n refuseJson(\"living-outside-requirements\", msg, strayed, `features/${dirName}`);\n return null;\n }\n console.error(`${msg}:`);\n for (const i of strayed) console.error(` ✗ ${i.message}`);\n console.error(`\\n--approve does not override this — the duplication is mechanical, not a judgment call.`);\n sayExplain(\"living-outside-requirements\");\n process.exitCode = 1;\n return null;\n }\n\n // Pre-flight: the archive destination must be free, or the final move would fail\n // after the living docs were already rewritten.\n const archiveDir = archiveRoot(config.docsDir);\n const archiveDest = join(archiveDir, dirName);\n if (existsSync(archiveDest)) {\n fail(json, \"archive-exists\", `archive ${id} — BLOCKED: features/archive/${dirName} already exists. Remove or rename it, then re-run.`);\n return null;\n }\n\n say(`archive ${id}${dryRun ? \" (dry run)\" : \"\"}\\n`);\n\n return {\n id,\n dirName,\n featureDir,\n deltaDoc,\n deltaServices,\n capabilityDeltas,\n gating,\n advisory,\n archiveDir,\n archiveDest,\n recovered,\n issues,\n };\n}\n"]}
@@ -27,6 +27,7 @@ import {} from "./state.js";
27
27
  import { ArchiveFailure } from "./refusal.js";
28
28
  import { featurePaths } from "../../../core/repo/paths.js";
29
29
  import { featureFlows, livingFlowPath, USECASE_SUBDIR } from "../../../core/usecases/delta/flows.js";
30
+ import { featureDeployments, livingDeploymentPath } from "../../../core/deployment/delta.js";
30
31
  export async function planLandscape(config, gated, plan, say) {
31
32
  const { id, featureDir, deltaDoc, deltaServices } = gated;
32
33
  const deltaLikec4 = featurePaths(featureDir).delta;
@@ -158,4 +159,22 @@ export async function planFlows(config, gated, plan, say) {
158
159
  say(` use case: ${flow.rel} — created architecture/${USECASE_SUBDIR}/${flow.rel}`);
159
160
  }
160
161
  }
162
+ /**
163
+ * The same copy for the third whole-file axis: the topology a feature brings.
164
+ *
165
+ * Beside `planFlows` rather than folded into it, because the two write to
166
+ * different places — a flow into `architecture/usecases/`, a topology straight
167
+ * into `architecture/` — and one function taking a subdirectory argument would
168
+ * make that difference a parameter instead of a decision each axis states.
169
+ * `unarchive` needs nothing here either: a create is undone by deleting.
170
+ */
171
+ export async function planDeployments(config, gated, plan, say) {
172
+ const docs = await featureDeployments(gated.featureDir);
173
+ if (docs.length === 0)
174
+ return;
175
+ for (const doc of docs) {
176
+ plan.writes.push(planWrite(livingDeploymentPath(config.docsDir, doc.rel), await readUtf8(doc.path)));
177
+ say(` deployment: ${doc.rel} — created architecture/${doc.rel}`);
178
+ }
179
+ }
161
180
  //# sourceMappingURL=landscape.js.map