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

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 (92) hide show
  1. package/CHANGELOG.md +165 -0
  2. package/README.md +6 -6
  3. package/ROADMAP.md +155 -18
  4. package/SCHEMA.md +33 -1
  5. package/dist/commands/archive/plan/gate.js +18 -12
  6. package/dist/commands/archive/plan/gate.js.map +1 -1
  7. package/dist/commands/archive/plan/landscape.js +19 -0
  8. package/dist/commands/archive/plan/landscape.js.map +1 -1
  9. package/dist/commands/archive/run.js +48 -3
  10. package/dist/commands/archive/run.js.map +1 -1
  11. package/dist/commands/seed/plan.js +1 -1
  12. package/dist/commands/seed/plan.js.map +1 -1
  13. package/dist/commands/subsystem/sync.js +1 -1
  14. package/dist/commands/subsystem/sync.js.map +1 -1
  15. package/dist/commands/subsystem/txn/txn.js +1 -1
  16. package/dist/commands/subsystem/txn/txn.js.map +1 -1
  17. package/dist/commands/subsystem/txn/views.js +23 -11
  18. package/dist/commands/subsystem/txn/views.js.map +1 -1
  19. package/dist/commands/validate/arch-coverage.js +11 -1
  20. package/dist/commands/validate/arch-coverage.js.map +1 -1
  21. package/dist/commands/validate/fleet/obligations.js +43 -9
  22. package/dist/commands/validate/fleet/obligations.js.map +1 -1
  23. package/dist/commands/validate/fleet/usecases/steps.js +3 -3
  24. package/dist/commands/validate/fleet/usecases/steps.js.map +1 -1
  25. package/dist/commands/validate/fleet/views/ids.js +6 -1
  26. package/dist/commands/validate/fleet/views/ids.js.map +1 -1
  27. package/dist/commands/validate/fleet/views/stale.js +15 -39
  28. package/dist/commands/validate/fleet/views/stale.js.map +1 -1
  29. package/dist/commands/validate/service/service.js +1 -0
  30. package/dist/commands/validate/service/service.js.map +1 -1
  31. package/dist/commands/validate/service/specs.js +33 -1
  32. package/dist/commands/validate/service/specs.js.map +1 -1
  33. package/dist/core/agent/agents-md/map/subsystem.js +8 -1
  34. package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
  35. package/dist/core/agent/agents-md.js +4 -0
  36. package/dist/core/agent/agents-md.js.map +1 -1
  37. package/dist/core/agent/workflows/check.js +3 -0
  38. package/dist/core/agent/workflows/check.js.map +1 -1
  39. package/dist/core/brief/unchecked.js +1 -0
  40. package/dist/core/brief/unchecked.js.map +1 -1
  41. package/dist/core/c4/arch.js +72 -180
  42. package/dist/core/c4/arch.js.map +1 -1
  43. package/dist/core/c4/likec4.js +2 -0
  44. package/dist/core/c4/likec4.js.map +1 -1
  45. package/dist/core/c4/parsed/deployment.js +158 -0
  46. package/dist/core/c4/parsed/deployment.js.map +1 -0
  47. package/dist/core/c4/project/load.js +4 -0
  48. package/dist/core/c4/project/load.js.map +1 -1
  49. package/dist/core/c4/project/staged.js +220 -0
  50. package/dist/core/c4/project/staged.js.map +1 -0
  51. package/dist/core/c4/resolve/service.js +46 -0
  52. package/dist/core/c4/resolve/service.js.map +1 -1
  53. package/dist/core/c4/resolve/steps.js +144 -0
  54. package/dist/core/c4/resolve/steps.js.map +1 -0
  55. package/dist/core/c4/splice/delta-blocks.js +15 -2
  56. package/dist/core/c4/splice/delta-blocks.js.map +1 -1
  57. package/dist/core/c4/workspace.js +2 -0
  58. package/dist/core/c4/workspace.js.map +1 -1
  59. package/dist/core/coherence/coherence.js +36 -0
  60. package/dist/core/coherence/coherence.js.map +1 -1
  61. package/dist/core/deployment/delta.js +188 -0
  62. package/dist/core/deployment/delta.js.map +1 -0
  63. package/dist/core/deployment/objects.js +145 -0
  64. package/dist/core/deployment/objects.js.map +1 -0
  65. package/dist/core/pack/living.js +19 -0
  66. package/dist/core/pack/living.js.map +1 -1
  67. package/dist/core/repo/authored/paths.js +18 -0
  68. package/dist/core/repo/authored/paths.js.map +1 -1
  69. package/dist/core/repo/paths.js +44 -3
  70. package/dist/core/repo/paths.js.map +1 -1
  71. package/dist/core/repo/tree/render/body.js +219 -0
  72. package/dist/core/repo/tree/render/body.js.map +1 -0
  73. package/dist/core/repo/tree/render/stale.js +26 -0
  74. package/dist/core/repo/tree/render/stale.js.map +1 -0
  75. package/dist/core/repo/tree/render/text.js +84 -0
  76. package/dist/core/repo/tree/render/text.js.map +1 -0
  77. package/dist/core/repo/tree/{views.js → render/views.js} +38 -34
  78. package/dist/core/repo/tree/render/views.js.map +1 -0
  79. package/dist/core/status/feature/artifacts.js +29 -4
  80. package/dist/core/status/feature/artifacts.js.map +1 -1
  81. package/dist/core/status/fleet/fleet.js +11 -1
  82. package/dist/core/status/fleet/fleet.js.map +1 -1
  83. package/dist/core/usecases/delta/overlay.js +28 -191
  84. package/dist/core/usecases/delta/overlay.js.map +1 -1
  85. package/dist/core/usecases/fleet.js.map +1 -1
  86. package/dist/core/usecases/operations.js +2 -2
  87. package/dist/core/usecases/operations.js.map +1 -1
  88. package/dist/core/vocabulary/issue.js +3 -0
  89. package/dist/core/vocabulary/issue.js.map +1 -1
  90. package/examples/docs/architecture/subsystems.likec4 +4 -0
  91. package/package.json +1 -1
  92. package/dist/core/repo/tree/views.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -10,6 +10,171 @@ 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.4] - 2026-09-03
14
+
15
+ The view title becomes the subsystem's path. `0.2.0-alpha.3` was tagged and **never published** — it
16
+ was withdrawn before its publish step for a reason unrelated to the code — so this is the release
17
+ that carries both it and the entry below, and `npm i @ybotok/loam@alpha` moves from `0.2.0-alpha.2`
18
+ straight to this one. A repo upgrading from either reports one `subsystem.views-stale` until one
19
+ `loam subsystem sync`.
20
+
21
+ ### Changed — a generated subsystem view is titled by its path
22
+
23
+ `0.2.0-alpha.3` gave each view the subsystem's own label. It now carries the whole path — every
24
+ marked ancestor's label and its own, joined with ` / `, so `services/platform/order-flow` reads
25
+ `Platform / Order flow`. Measured against the pinned likec4: the renderer reads that as a folder
26
+ path, so the view list mirrors the directory tree a human already navigates — one folder per
27
+ ancestor, the leaf's own label as the title — and two subsystems whose labels are equal are told
28
+ apart by their parents instead of appearing twice under one name. The box a view draws keeps the
29
+ subsystem's own label, unprefixed.
30
+
31
+ Because loam is now the one composing with `/`, it owns what a `/` inside an authored label would
32
+ do: measured, LikeC4 would show only the last segment and add a folder level the tree does not have,
33
+ so a slash in a marker's `title` becomes `∕` (U+2215) and the whole authored label stays visible as
34
+ one folder name. The alternative — a fixed `loam subsystems /` prefix — bought the same folder
35
+ without the churn and put loam's own name in a reader's UI; the tree's own words won.
36
+
37
+ Two costs, both real: the generated bytes move again, so a repo upgrading from `0.2.0-alpha.3`
38
+ reports one `subsystem.views-stale` until one `loam subsystem sync`; and renaming a PARENT's title
39
+ now rewrites the bytes of every view beneath it.
40
+
41
+ ## [0.2.0-alpha.3] - 2026-09-03
42
+
43
+ The generated subsystem views, made readable — and the defect that hunt found underneath them.
44
+ Still a prerelease on the `alpha` line: `npm i @ybotok/loam@alpha` installs it. Nothing below is
45
+ breaking, and one entry is a change every repo NOTICES: the generated file's bytes move, so the
46
+ first `loam validate --all` after upgrading asks for one `loam subsystem sync`.
47
+
48
+ ### Fixed — a `subsystem.views-stale` no command could clear
49
+
50
+ `loam validate --all` grades `architecture/subsystems.likec4` against the whole `architecture/`
51
+ project — the landscape merged with every `architecture/usecases/*.likec4` — while `loam subsystem
52
+ sync` and the write verbs rendered it from `landscape.likec4` alone. One use-case document declaring
53
+ an element bound to a filed service was therefore enough to make `sync` answer `current` while
54
+ `validate --all` exited 1 with `subsystem.views-stale`, whose message names `sync` as the repair.
55
+ Reproduced on `examples/docs`; the two now read the same documents. No code, flag or payload key
56
+ changes.
57
+
58
+ ### Changed — the generated subsystem views carry a label and a boundary
59
+
60
+ `architecture/subsystems.likec4` exists only to be rendered, and in the renderer its views were the
61
+ least readable objects in a docs repo: no title, so LikeC4 showed the hex-escaped id
62
+ (`subsystem_platform__order_2dflow`), and leaf includes only, so a fleet drawn as ordinary
63
+ grouped C4 rendered flat. Every view now carries `title` (the marker's, or the directory name) and
64
+ `description` when the marker authors one, and draws a boundary: the model's own containing element
65
+ when one holds exactly the subsystem's members, a `group '<title>'` mirroring the directory subtree
66
+ when the landscape nests nothing, and — when the members are nested but no element owns exactly them
67
+ — no box plus one comment line, `// model: no boundary — …`, which is loam's whole answer to a tree
68
+ and a map that disagree. Nothing grades that comment: placement still carries no policy.
69
+
70
+ Marker text is normalized to one line and escaped before it is written (measured: a raw `'` in a
71
+ title is two parse errors and blanks the fleet's whole map in the renderer; `Ops' } view evil {` is
72
+ zero errors and injects a view). [SCHEMA.md](SCHEMA.md) states the rules.
73
+
74
+ What a repo notices, and what it costs:
75
+
76
+ - the first `loam validate --all` after upgrading reports exactly one `subsystem.views-stale` error
77
+ (exit 1) until one `loam subsystem sync` runs. `sync` does not `git add`, so the repair leaves one
78
+ unstaged one-file diff to commit;
79
+ - the boundary depends on which OTHER services sit under an element, so an unrelated adoption or
80
+ archive can flip a view from boxed to flat and restale the file. `loam archive` now withholds its
81
+ "living spec + landscape are now complete + current" line, prints one warning naming
82
+ `loam subsystem sync`, and adds `subsystemViewsStale: true` to its `--json` payload when it left
83
+ the file behind;
84
+ - `subsystem.views-stale` and `subsystem.view-id-collision` now carry `locations`: the generated
85
+ file (or `architecture/` when it does not exist yet) and the claimant document respectively. Both
86
+ used to fall back to the landscape, sending an editor to a file with nothing to fix in it.
87
+
88
+ No command, flag, exit code, envelope key or stable code string changes; `subsystem.views-stale` is
89
+ not in the `IssueCode` union and carries no `gates` key, so `loam archive` remains unaffected by it.
90
+
91
+ ## [0.2.0-alpha.2] - 2026-09-02
92
+
93
+ The deployment axis, and the two defects the axis surfaced on its way in. Still a prerelease on the
94
+ `alpha` line: `npm i @ybotok/loam@alpha` installs it. Nothing below is breaking, but two entries are
95
+ changes a repo can NOTICE — a fleet that had already placed a `#obl-` tag on a `deploymentNode`
96
+ starts seeing findings it was never shown, and `loam status` stops asking an architecture-only
97
+ feature for a business spec.
98
+
99
+ ### Added — the deployment axis: topology joined to requirements
100
+
101
+ A LikeC4 `deployment { }` block has always been legal in a docs repo and was completely unread. The
102
+ parser resolved every `instanceOf`, and after that no requirement could name a node, no `#obl-` tag
103
+ on a datacenter was graded, and the context pack an agent implements from did not mention topology
104
+ at all. Five things change, and a fleet that draws no topology is unaffected by every one of them —
105
+ the axis opts in on the block's existence, the way the obligation vocabulary opts in on its file.
106
+
107
+ - **`Covers:` gains a fourth entry form.** `node:<id>` names a deployment node or an instance, and
108
+ `node:a -> node:b` names an edge between them; both match literally. This is the form that lets an
109
+ architecture requirement about replication name the two clusters it is written about, and a
110
+ topology change that renames one now reports `covers.unknown` with the real ids offered — spelled
111
+ with the prefix, because that is the line the author has to type. Both sides of an edge carry the
112
+ prefix or neither; a bare id stays an element entry.
113
+ - **An obligation on a deployment object is graded.** `obligation.unknown`, `obligation.unapplied`
114
+ and `obligation.uncovered` now walk the topology as well as the logical model. This is a
115
+ behaviour change a repo can notice: a `#obl-` tag on a `deploymentNode` used to be invisible to
116
+ every check, so a fleet that had placed one starts seeing findings it was never shown. A
117
+ datacenter is owned by no service, so `obligation.uncovered` drops `subject` there and names the
118
+ requirement's home in general terms rather than picking a team.
119
+ - **A feature can bring topology.** `features/<FEAT>/deployment/<name>.likec4` is a create-only
120
+ document `loam archive` copies into `architecture/` and `loam unarchive` takes back — the third
121
+ whole-file slot, beside `usecases/` and `glossary/`. Say `extend` in it to add to a region the
122
+ living map already declares; two features may extend one region from documents of their own and
123
+ both archive. Two new codes, both errors and neither overridable: **`deployment.doc-exists`** when
124
+ the feature's document names a file the living tree already holds, and **`deployment.doc-invalid`**
125
+ when it cannot be read against the map the feature's own merge would leave behind. The second is
126
+ what makes the ordinary case legal — a document instancing a service only this feature's
127
+ `delta.likec4` adds resolves, because the corpus inside a feature window is feature ∪ living — and
128
+ refuses the case where the merge does not land what the document names. `delta.likec4` still
129
+ refuses a `deployment { }` block and now names this slot instead of the living landscape.
130
+ A third, **`deployment.doc-reserved`**, refuses a name loam owns inside `architecture/` —
131
+ `subsystems.likec4`, `landscape.likec4`, or a path under `usecases/` — whether or not the target
132
+ exists, because two of the three fail silently when it does not: nothing parses the generated
133
+ subsystem views and `loam subsystem sync` overwrites them wholesale.
134
+ - **`loam context` carries the topology.** `--json` gains `living.deployment` — the service's
135
+ instances, and the deployment edges touching them from either end.
136
+ - **One more statement in the adoption brief's unchecked list**, now sixteen: nothing here knows
137
+ whether the second cluster exists, is reachable, or holds the data a requirement claims, and no
138
+ check counts datacenters — how many a service needs is a decision the fleet writes down as a
139
+ requirement, never one loam evaluates.
140
+
141
+ ### Fixed — the never-overridable archive refusal names what actually refused
142
+
143
+ It said `N element binding(s) name an illegal service id` for every code in that register, and had
144
+ done since the use-case slot put a second one there — about features that bind nothing. The headline
145
+ now counts the refusals and the closing line names their codes; each issue keeps its own sentence.
146
+ The `--json` envelope is unchanged: consumers branch on `issues[].code` and `overridable`, which is
147
+ what that split exists for.
148
+
149
+ ### Fixed — `loam status` no longer invents work for an architecture-only feature
150
+
151
+ A feature whose only requirement delta is `specs/<svc>/arch.spec.md` reported two things that were
152
+ not true, and both are corrected. The `spec` row read `missing` and `next.author-spec` told the
153
+ author to write business requirements the change does not have — while `loam validate --feature`
154
+ accepted the same tree at exit 0 and `coherence`'s `service.no-requirement-delta` stayed silent,
155
+ because every other reader counts the service DIRECTORY and not which corpus is in it. And the
156
+ `verification` row read `blocked` with `blockedBy: ["delta", "spec", "openapi", "asyncapi"]`,
157
+ telling the reader there was nothing to derive a checklist from — while `featureChecklist` reads
158
+ `arch.spec.md` and `loam verify` on that feature returns a real checklist.
159
+
160
+ The fleet form carried the same defect independently, and it broke the rule stated one line below
161
+ it in its own source: `loam status` with no feature reported `missing <svc>/spec` and staged the
162
+ feature `missing`, over a feature `loam status <FEAT>` called `ready`. Both forms now accept either
163
+ corpus as the service's requirement delta, and `<svc>/spec` is named only when neither exists.
164
+
165
+ Three payload-visible consequences, all in `loam status --json`: `artifacts[]`'s `spec` row is
166
+ `required: false` when the service's `arch.spec.md` delta exists (it was unconditionally `true`),
167
+ `arch-spec` now counts as a verification feeder, so `blockedBy` gained `"arch-spec"` in the
168
+ one case that is still genuinely blocked — a feature with no requirement delta of either kind —
169
+ and `features[]`'s `missing` and `stage` no longer report an architecture-only feature as
170
+ incomplete. No finding code, flag or exit code changes. This is the change to read if a script
171
+ branched on `artifacts[].required` or on `features[].missing`.
172
+
173
+ The case is the one `arch.spec.md` exists for: an outbox, a retry policy, an alert — obligations no
174
+ business scenario was ever going to carry, so the feature that adds one has no business requirement
175
+ to write. Found by authoring loam's own architecture change through loam's own lifecycle, which is
176
+ the trigger ROADMAP's self-hosting section records for that axis.
177
+
13
178
  ## [0.2.0-alpha.1] - 2026-09-01
14
179
 
15
180
  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.4`**, the third published 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.4 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.4`: 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-03._
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 **427 TypeScript modules in 130 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-03 at `0.2.0-alpha.4`, 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,410/3,410 tests**, with two `skipIf(asRoot)` cases the root gate container cannot run; the
33
+ coverage gate passing with **92.26% statements, 84.06% branches, 96.62% functions, and 94.20%
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,125 @@ 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 generated subsystem views — a label, a boundary, and one reader of the map
129
+
130
+ **Landed 2026-09-03.** Three problem reports from a fleet running loam, one axis. The generated
131
+ `architecture/subsystems.likec4` exists only to be rendered and was the least readable object in a
132
+ docs repo: no `title`, so the renderer showed the hex-escaped view id, and leaf includes only, so a
133
+ fleet drawn as grouped C4 rendered flat. Each view now carries the marker's title (the directory
134
+ name when it has none) and description, and draws a boundary the LANDSCAPE decides: the model's own
135
+ containing element when one holds exactly the members, a `group` mirroring the directory subtree
136
+ when the map nests nothing, and one `// model: no boundary — …` comment when the two disagree.
137
+ That comment is the whole answer to the drift the third report asked about — disclosure in a
138
+ generated artifact, never a finding, because grading placement against the map is the policy the
139
+ tree is defined not to carry. The graded alternative is written down under
140
+ [Later](#later--promote-only-from-evidence) with the evidence bar it has to clear.
141
+
142
+ The hunt found what neither report did: `validate --all` graded that file against the whole
143
+ `architecture/` project while every writer rendered it from `landscape.likec4` alone, so a
144
+ use-case document binding one element could produce a `subsystem.views-stale` that
145
+ `loam subsystem sync` reported as `current` — reproduced on `examples/docs`, and fixed by pointing
146
+ both sides at the same read.
147
+
148
+ #### The deployment axis — topology joined to requirements
149
+
150
+ **Promoted 2026-09-02, and authored as a feature in loam's own docs repo** rather than as an item
151
+ text here: `meta/docs/features/archive/FEAT-1-the-deployment-axis/`, whose `intent.md` carries the
152
+ four measurements and whose `specs/loam/arch.spec.md` carries the five rules the change establishes,
153
+ each with scenarios. This entry is the decision and the gate; that feature is the specification, and
154
+ the two must not be allowed to become two accounts of the same change.
155
+
156
+ **Landed 2026-09-02, complete.** Every step is in, and step 4 was corrected rather than built
157
+ (below) — the item specified two finding codes that the implementation showed it should not mint.
158
+ FEAT-1 archived once every edge it claimed existed —
159
+ and it archived having been CORRECTED against the tree twice, which is the argument for the route
160
+ rather than a footnote to it: the delta predicted a `core/kernel/` dependency that turned out to be
161
+ type-only (a brand costs an annotation, not an import) and missed `core/coherence/` entirely.
162
+ `npm run meta:check` was red for the whole of the build and went green with the archive, which is
163
+ the axis working rather than a gap in it.
164
+
165
+ The whole change is reviewable as five commits on `feat/deployment-axis`, and the one thing to read
166
+ first is what the implementation taught the plan: two of the codes this item specified were not
167
+ minted, and the reasons are in step 4.
168
+
169
+ **What is wrong today.** A LikeC4 `deployment { }` block is legal in a docs repo and completely
170
+ unread. The parser resolves every `instanceOf` in it — a container renamed out from under a
171
+ deployment node fails the gate as `landscape.invalid` — and after that nothing: no requirement can
172
+ name a node, no report counts one, and the context pack an agent implements from does not mention
173
+ topology at all. Two of the four measurements taken on a copy of `examples/docs` are the reason this
174
+ is an item rather than a wish. A `#obl-` tag on a `deploymentNode` is invisible to
175
+ `validate/fleet/obligations.ts`, which walks the logical model only — the same undeclared tag raises
176
+ `obligation.unknown` on a container and nothing on a datacenter, which is fail-open and a defect on
177
+ its own terms. And `parsedModel().deployment` already returns nodes with their tags, instances
178
+ carrying the id of the logical element each one instances, and relationships carrying metadata, so
179
+ the read costs no second parse, no new file format and no change to the frozen CLI surface.
180
+
181
+ **Required changes**, in the order the dependency graph forces:
182
+
183
+ 1. Walk the deployment model in every `#obl-` grade. Fail-open becomes fail-closed; no new code.
184
+ 2. A fourth `Covers:` entry form, `node:<id>`, with the same prefix accepted on either side of the
185
+ edge form. No new code.
186
+ 3. The parse adapter in `src/core/c4/parsed/`, and the deployment records added to
187
+ `test/likec4-model-parity.test.ts` — a two-stage substitution nothing measures is an assumption.
188
+ `src/core/c4/` holds exactly five files, so this is a sub-package and not a sixth module.
189
+ 4. **No new finding code, and this is a correction the implementation earned.** The item planned
190
+ two. The first, for a tagged node or edge no living arch requirement covers, turned out to be
191
+ `obligation.uncovered` asked of a second model: once step 1 walks the topology, the existing code
192
+ answers it, and a caller acts identically either way — write a `Covers:` line. A second code for
193
+ one question is exactly what the add-a-code checklist refuses. The second, for a container no
194
+ deployment instances, is declined outright: it asserts that everything modelled ought to be
195
+ deployed somewhere, which is a COMPLETENESS claim, and completeness is the one thing
196
+ `src/core/brief/unchecked.ts` says loam never checks. It is the same category error as the
197
+ at-least-two-datacenters rule below, one axis over.
198
+ 5. `features/<FEAT>/deployment/<name>.likec4`, create-only and copied whole — `core/usecases/delta/`
199
+ one axis over. It works because `extend` was measured to resolve across documents of one project,
200
+ so no text splice is needed; `delta-blocks.ts` goes on refusing a `deployment { }` block inside
201
+ `delta.likec4` and its message names the new slot. The collision is on the FILE, never on what is
202
+ inside it: two features may both extend one living region from documents of their own and both
203
+ archive, which is the shape the fleet-wide change actually takes.
204
+
205
+ **Graded against the merge preview**, which shipped one commit behind the slot and is the
206
+ second code, `deployment.doc-invalid` (error, never overridable). This axis needs the post-merge
207
+ corpus more literally than the use-case one: a feature that stands a NEW service up in a cluster
208
+ declares it in `delta.likec4` and instances it in the topology, so the element the document names
209
+ exists only after the merge. The staging that makes that readable moved OUT of
210
+ `core/usecases/delta/overlay.ts` into `core/c4/project/staged.ts` rather than being copied — the
211
+ two axes would have disagreed first about the merge preview, which is the one thing both
212
+ refusals rest on. The extraction dropped `core/usecases/` → `core/kernel/`, which is a one-line
213
+ edge and was edited into the model directly.
214
+ 6. The surfaces: the topology in the context pack (`context --json`'s `living.deployment`), the code
215
+ table `test/codes-drift.test.ts` requires, and one more statement in
216
+ `src/core/brief/unchecked.ts`. `loam explain` needed nothing — it parses the /loam-check fix
217
+ tables at runtime, so the rows added there ARE the entries. The scorecard row the item listed was
218
+ dropped: the fleet scorecard aggregates at the bound `service`, and a topology row there would
219
+ count deployment nodes against a denominator no service owns.
220
+
221
+ **Exit criteria.** Every step above green under the full gate; `covers.unknown` demonstrated firing
222
+ on a covered node that a topology change renamed, which is the acceptance criterion the trigger
223
+ names; the parity suite extended and shown to fail against an un-extended adapter; and
224
+ `test/self-model.test.ts` re-pinned once FEAT-1 archives.
225
+
226
+ **Out of scope, and each refusal is load-bearing.** No reading of Terraform, Helm or cluster state —
227
+ that is the extractor the non-goals forbid. No check that a service is deployed in at least two
228
+ datacenters: it would need `criticality` promoted from prose to a read field, and it would turn
229
+ `architecture/obligations.yaml` into the policy engine that file says in its own words it is not. A
230
+ multi-datacenter rule is an authored requirement whose `Covers:` line names the nodes; loam grades
231
+ the join and never the architecture. And no `environment` concept above LikeC4, because
232
+ `deploymentNode` is that mechanism and the fleet names its own kinds.
233
+
234
+ **The evidence, and its one gap stated plainly.** Operator: a team standing up a standby cluster in
235
+ a second datacenter. Repeated task: keeping RTO/RPO requirements attached to a topology that keeps
236
+ changing. Failure mode: a requirement about replication staying green after the node it described
237
+ was renamed or removed. Frequency: every topology change, which is the cadence a migration runs at.
238
+ Current workaround: a `Covers:` line that resolves to nothing, or no written requirement at all.
239
+ Acceptance criterion: a topology change that orphans a covered requirement is convicted without a
240
+ human noticing it first. **That is one operator, not two.** The maintainer judged the trigger
241
+ sufficient on the same basis recorded for landscape scaling and the authored business axis, and the
242
+ [Later](#later--promote-only-from-evidence) exit criteria are amended below to say so rather than
243
+ leaving the two paragraphs to contradict each other. Steps 1 and 2 do not rest on that judgement at
244
+ all: the first is a fail-open defect and the second is a grammar entry, and neither is a new axis.
245
+
246
+
125
247
  - **A feature can bring a use case.** `features/<FEAT>/usecases/<name>.likec4` — a views-only
126
248
  document `loam archive` copies into `architecture/usecases/` and `loam unarchive` takes back,
127
249
  create-only, refused by `usecase.flow-exists` when the living tree already holds that file. All
@@ -247,6 +369,18 @@ item gets promoted — it went from here to `## Now` and from there to
247
369
  - **UI generation:** begin with a disposable projection over the stable JSON contract only after CLI
248
370
  consumers demonstrate a repeated navigation problem. It must not introduce a second mutable state,
249
371
  hidden workflow state, or a required service.
372
+ - **A graded subsystem/model correspondence:** a fleet that draws its landscape as grouped C4 states
373
+ its grouping twice — once as directories under `services/`, once as the nesting of the elements —
374
+ and `loam subsystem move` changes the first while the second stays put. Since 2026-09-02 the
375
+ generated views file DISCLOSES that (the `// model: no boundary — …` line; see
376
+ [SCHEMA.md](SCHEMA.md)), and nothing grades it, because grading placement against the map is the
377
+ policy the tree is defined not to carry. The shape a graded version would take is known and
378
+ measured: an author's opt-in `metadata { subsystem '<name>' }` on the grouping element, read
379
+ through the existing metadata reader, the NAME rather than the path (names are unique in one flat
380
+ namespace and survive a move, which a path binding would not), with the check structurally silent
381
+ for every fleet that never writes one. Promote only on evidence from a second fleet, after living
382
+ with the disclosure — the first report classified itself `inconclusive`, and one fleet is not
383
+ enough to qualify an invariant that says placement is never part of any identity.
250
384
  - **Landscape scaling:** retain one landscape while conflicts are exceptional. If same-service
251
385
  conflicts become routine — the current trigger in [SCHEMA.md](SCHEMA.md) is weekly rather than
252
386
  monthly — evaluate service-owned model files plus a thin global cross-service map. Migration must
@@ -264,8 +398,11 @@ item gets promoted — it went from here to `## Now` and from there to
264
398
 
265
399
  Exit criteria for promoting a Later item:
266
400
 
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.
401
+ - Evidence comes from at least two independent operators or fleets, except landscape scaling, the
402
+ authored business axis and — added 2026-09-02 — the deployment axis in [Now](#now), whose recorded
403
+ triggers are sufficient. Three exceptions is where this criterion starts describing the rule
404
+ rather than the exception: the next single-operator promotion has to say why the criterion should
405
+ survive, or retire it.
269
406
  - An ADR compares the existing workflow, external tooling, and the smallest loam-owned change.
270
407
  - A prototype is measured against a predeclared acceptance criterion and preserves every non-goal
271
408
  below.
@@ -276,7 +413,7 @@ Exit criteria for promoting a Later item:
276
413
 
277
414
  Placed in a section of its own, and the placement is the argument. This is not a `## Now` item, and
278
415
  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
416
+ from that section requires evidence from at least two independent operators or fleets (bar the three
280
417
  whose recorded triggers were judged sufficient when they were written), and loam's own repository is
281
418
  one operator by construction, so a Later entry could only ever be a permanent exception to the rule
282
419
  it sits under. Nor is its value speculative — the trigger below is a defect this repository has
package/SCHEMA.md CHANGED
@@ -164,9 +164,41 @@ check uses; a member nothing models is omitted, which `landscape.service-unmodel
164
164
  reports). The output is deterministic and line-oriented — subsystems sorted by path, members by id,
165
165
  one `include` per line, no timestamps, no absolute paths — so it is byte-reproducible on any machine
166
166
  and two concurrent moves into different groups merge in git without intervention.
167
+
168
+ Each view carries the **label a human reads**: `title`, and `description` when the marker authors
169
+ one. The title is the subsystem's **path** — every marked ancestor's label and its own, joined with
170
+ ` / `, so `services/platform/order-flow` reads `Platform / Order flow` — and each label is the
171
+ marker's `title`, falling back to the directory name (an unlabelled view is shown by its id, which
172
+ is hex-escaped for injectivity and reads as `subsystem_platform__order_2dflow`). LikeC4
173
+ reads that path as folders: the view list mirrors the directory tree, showing one folder per
174
+ ancestor and the leaf's own label as the title. A `/` **inside** a label becomes `∕` (U+2215), so an
175
+ authored slash can neither hide half of what somebody wrote nor invent a folder level the tree does
176
+ not have. Both strings are somebody's prose written into syntax, so both are normalized to one line
177
+ and escaped. Titles are not unique — only ids are — and renaming a parent's title rewrites every
178
+ view beneath it.
179
+
180
+ The **boundary** each view draws is a fact about the landscape, never a setting:
181
+
182
+ 1. an element that holds *exactly* the subsystem's members and nothing else is included beside the
183
+ leaves, so the renderer nests them in the box the author drew, under the author's title;
184
+ 2. a landscape that nests nothing puts the services in a `group '<title>'` of loam's own, mirroring
185
+ the directory subtree, so a parent subsystem shows its children as boxes rather than one pile;
186
+ 3. members that ARE nested with no element owning exactly them get no box and one comment line —
187
+ `// model: no boundary — <id> also holds services outside this subsystem.` — because a synthetic
188
+ box would look identical whether or not the tree and the map agree. That comment is the whole of
189
+ what loam says about the two disagreeing: nothing grades it, nothing fails on it, and it lands in
190
+ the diff of the commit that caused it. **The tree and any `group` nesting in the landscape stay
191
+ independent**; `loam subsystem move` never rewrites the map, and no finding compares them.
192
+
193
+ Because the boundary depends on which *other* services sit under an element, an unrelated adoption
194
+ or archive can flip a view from boxed to flat — a new reason to see `subsystem.views-stale`, and why
195
+ `loam archive` withholds its "complete + current" line and names `loam subsystem sync` when it left
196
+ the file behind.
197
+
167
198
  `loam subsystem sync` is the one regenerator (it also *removes* the file when the tree has no
168
199
  subsystems), every `loam subsystem` write verb regenerates it inside the same transaction, and
169
- `validate --all` grades staleness by **byte comparison** — exactly one error,
200
+ `validate --all` grades staleness by **content comparison** (line endings normalized; a CRLF
201
+ checkout changes no fact) — exactly one error,
170
202
  `subsystem.views-stale`, on exactly that file; nothing in loam ever parses it: a views-only document
171
203
  does not parse standalone (its `include` lines reference elements the landscape defines), and it
172
204
  holds **static** views, whose contents loam never reads in any document. Only the LikeC4 renderer,
@@ -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;