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

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 (67) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/MIGRATING-from-OpenSpec.md +4 -1
  3. package/README.md +11 -10
  4. package/ROADMAP.md +103 -17
  5. package/SCHEMA.md +86 -7
  6. package/dist/commands/archive/run.js +46 -2
  7. package/dist/commands/archive/run.js.map +1 -1
  8. package/dist/commands/seed/plan.js +1 -1
  9. package/dist/commands/seed/plan.js.map +1 -1
  10. package/dist/commands/subsystem/sync.js +111 -20
  11. package/dist/commands/subsystem/sync.js.map +1 -1
  12. package/dist/commands/subsystem/txn/txn.js +20 -1
  13. package/dist/commands/subsystem/txn/txn.js.map +1 -1
  14. package/dist/commands/subsystem/txn/views.js +23 -11
  15. package/dist/commands/subsystem/txn/views.js.map +1 -1
  16. package/dist/commands/validate/fleet/landscape.js +6 -0
  17. package/dist/commands/validate/fleet/landscape.js.map +1 -1
  18. package/dist/commands/validate/fleet/views/ids.js +6 -1
  19. package/dist/commands/validate/fleet/views/ids.js.map +1 -1
  20. package/dist/commands/validate/fleet/views/projects.js +90 -0
  21. package/dist/commands/validate/fleet/views/projects.js.map +1 -0
  22. package/dist/commands/validate/fleet/views/stale.js +15 -39
  23. package/dist/commands/validate/fleet/views/stale.js.map +1 -1
  24. package/dist/core/agent/agents-md/command-map.js +3 -1
  25. package/dist/core/agent/agents-md/command-map.js.map +1 -1
  26. package/dist/core/agent/agents-md/map/subsystem.js +18 -2
  27. package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
  28. package/dist/core/agent/agents-md.js +4 -0
  29. package/dist/core/agent/agents-md.js.map +1 -1
  30. package/dist/core/agent/workflows/adopt.js +13 -3
  31. package/dist/core/agent/workflows/adopt.js.map +1 -1
  32. package/dist/core/agent/workflows/check.js +1 -0
  33. package/dist/core/agent/workflows/check.js.map +1 -1
  34. package/dist/core/brief/checks.js +9 -0
  35. package/dist/core/brief/checks.js.map +1 -1
  36. package/dist/core/brief/targets.js +7 -4
  37. package/dist/core/brief/targets.js.map +1 -1
  38. package/dist/core/brief/unchecked.js +1 -1
  39. package/dist/core/brief/unchecked.js.map +1 -1
  40. package/dist/core/docs.js +11 -7
  41. package/dist/core/docs.js.map +1 -1
  42. package/dist/core/doctor/doctor.js +17 -6
  43. package/dist/core/doctor/doctor.js.map +1 -1
  44. package/dist/core/explain/families.js +1 -1
  45. package/dist/core/explain/families.js.map +1 -1
  46. package/dist/core/repo/paths.js +39 -1
  47. package/dist/core/repo/paths.js.map +1 -1
  48. package/dist/core/repo/tree/render/body.js +219 -0
  49. package/dist/core/repo/tree/render/body.js.map +1 -0
  50. package/dist/core/repo/tree/render/projects.js +124 -0
  51. package/dist/core/repo/tree/render/projects.js.map +1 -0
  52. package/dist/core/repo/tree/render/stale.js +26 -0
  53. package/dist/core/repo/tree/render/stale.js.map +1 -0
  54. package/dist/core/repo/tree/render/text.js +84 -0
  55. package/dist/core/repo/tree/render/text.js.map +1 -0
  56. package/dist/core/repo/tree/{views.js → render/views.js} +38 -34
  57. package/dist/core/repo/tree/render/views.js.map +1 -0
  58. package/examples/README.md +6 -1
  59. package/examples/docs/architecture/subsystems.likec4 +4 -0
  60. package/examples/docs/services/checkout-web/likec4.config.json +4 -0
  61. package/examples/docs/services/order-service/likec4.config.json +4 -0
  62. package/examples/docs/services/payment-service/likec4.config.json +4 -0
  63. package/examples/docs/services/payment-service/model.likec4 +4 -2
  64. package/examples/docs/services/platform/identity-service/likec4.config.json +4 -0
  65. package/examples/docs/services/platform/notification-service/likec4.config.json +4 -0
  66. package/package.json +1 -1
  67. package/dist/core/repo/tree/views.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -10,6 +10,130 @@ 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.5] - 2026-09-03
14
+
15
+ An adopted service's model renders from the docs root. Still a prerelease on the `alpha` line:
16
+ `npm i @ybotok/loam@alpha` installs it. Nothing below is breaking. What a repo with adopted services
17
+ NOTICES: one `service.likec4-config-missing` warning per service on its first `validate --all`
18
+ until one `loam subsystem sync`, and from that sync on `--project` for `likec4 validate` at the
19
+ docs root.
20
+
21
+ ### Added — an adopted service's model renders from the docs root
22
+
23
+ The root `likec4.config.json` excludes `services/**`, and has to; what that cost was never stated:
24
+ an adopted service's `model.likec4` — the artifact `loam adopt` spends the most effort on — was a
25
+ box on the fleet map with nothing renderable inside it in the renderer opened at the docs root, and
26
+ the only documented way to see its containers was one `npx likec4 start services/<id>` per service.
27
+ One problem report; one file per service closes it.
28
+
29
+ - `loam subsystem sync` now writes `services/<…>/<svc>/likec4.config.json` beside every service
30
+ `model.likec4` that has none, whenever the root `likec4.config.json` exists — **create-only**: a
31
+ file that exists is yours and is never compared, rewritten or removed. Measured at the pinned
32
+ likec4, a project file inside a service directory registers the model as a project of its own
33
+ regardless of the root's exclusion, so `npx likec4 start` at the docs root shows `fleet` plus one
34
+ project per service. Without the root file nothing is written and the text says `loam doctor`
35
+ prints it. `subsystem sync --json` carries an additive `projects` key, `{ root, created, current }`
36
+ — whether the root file exists, the repo-relative paths written this run (sorted by id), and how
37
+ many model-bearing services already had one. `action` still describes
38
+ `architecture/subsystems.likec4` alone.
39
+ - `loam validate --all` gains one warning per service, `service.likec4-config-missing`, on the
40
+ `landscape` target with the service's tree path as its `scope`: `model.likec4` exists and the
41
+ project file does not. Never an error, never gating, never in `--service`, `status` or `doctor`;
42
+ the fix is `loam subsystem sync`, never a hand-written file. `doctor.likec4-config-missing`'s fix
43
+ text now hands over to `sync` once the root file is written.
44
+ - The project name: the id with every `.` folded to `-` (LikeC4's name grammar refuses the byte),
45
+ prefixed `service-` when that would equal the root project's `fleet` or LikeC4's reserved
46
+ `default`; `title` is the id verbatim, and the title is what the renderer's picker shows. Not
47
+ injective — an id spelled billing.core beside one spelled billing-core, or `service-fleet` beside
48
+ `fleet` — and LikeC4 suffixes the later one `-1`; disclosed, never graded.
49
+
50
+ What a repo NOTICES on upgrade: one warning per adopted service on the first `validate --all` until
51
+ one `loam subsystem sync` — under `--strict`, CI is red until then; after that `sync`, N untracked
52
+ files (`sync` does not `git add` them) and `move-uncommitted` on the next `loam subsystem move` of
53
+ such a service until they are committed; from the first `sync` on, `likec4 validate` at the docs
54
+ root needs `--project <name>` (measured: with more than one project it refuses bare, while
55
+ `likec4 build` and `likec4 export` take every project and need no flag); and the renderer's project
56
+ list grows by one per adopted service, beside `fleet`. No command, flag, exit code or envelope key
57
+ changes; `projects` is additive.
58
+
59
+ ## [0.2.0-alpha.4] - 2026-09-03
60
+
61
+ The view title becomes the subsystem's path. `0.2.0-alpha.3` was tagged and **never published** — it
62
+ was withdrawn before its publish step for a reason unrelated to the code — so this is the release
63
+ that carries both it and the entry below, and `npm i @ybotok/loam@alpha` moves from `0.2.0-alpha.2`
64
+ straight to this one. A repo upgrading from either reports one `subsystem.views-stale` until one
65
+ `loam subsystem sync`.
66
+
67
+ ### Changed — a generated subsystem view is titled by its path
68
+
69
+ `0.2.0-alpha.3` gave each view the subsystem's own label. It now carries the whole path — every
70
+ marked ancestor's label and its own, joined with ` / `, so `services/platform/order-flow` reads
71
+ `Platform / Order flow`. Measured against the pinned likec4: the renderer reads that as a folder
72
+ path, so the view list mirrors the directory tree a human already navigates — one folder per
73
+ ancestor, the leaf's own label as the title — and two subsystems whose labels are equal are told
74
+ apart by their parents instead of appearing twice under one name. The box a view draws keeps the
75
+ subsystem's own label, unprefixed.
76
+
77
+ Because loam is now the one composing with `/`, it owns what a `/` inside an authored label would
78
+ do: measured, LikeC4 would show only the last segment and add a folder level the tree does not have,
79
+ so a slash in a marker's `title` becomes `∕` (U+2215) and the whole authored label stays visible as
80
+ one folder name. The alternative — a fixed `loam subsystems /` prefix — bought the same folder
81
+ without the churn and put loam's own name in a reader's UI; the tree's own words won.
82
+
83
+ Two costs, both real: the generated bytes move again, so a repo upgrading from `0.2.0-alpha.3`
84
+ reports one `subsystem.views-stale` until one `loam subsystem sync`; and renaming a PARENT's title
85
+ now rewrites the bytes of every view beneath it.
86
+
87
+ ## [0.2.0-alpha.3] - 2026-09-03
88
+
89
+ The generated subsystem views, made readable — and the defect that hunt found underneath them.
90
+ Still a prerelease on the `alpha` line: `npm i @ybotok/loam@alpha` installs it. Nothing below is
91
+ breaking, and one entry is a change every repo NOTICES: the generated file's bytes move, so the
92
+ first `loam validate --all` after upgrading asks for one `loam subsystem sync`.
93
+
94
+ ### Fixed — a `subsystem.views-stale` no command could clear
95
+
96
+ `loam validate --all` grades `architecture/subsystems.likec4` against the whole `architecture/`
97
+ project — the landscape merged with every `architecture/usecases/*.likec4` — while `loam subsystem
98
+ sync` and the write verbs rendered it from `landscape.likec4` alone. One use-case document declaring
99
+ an element bound to a filed service was therefore enough to make `sync` answer `current` while
100
+ `validate --all` exited 1 with `subsystem.views-stale`, whose message names `sync` as the repair.
101
+ Reproduced on `examples/docs`; the two now read the same documents. No code, flag or payload key
102
+ changes.
103
+
104
+ ### Changed — the generated subsystem views carry a label and a boundary
105
+
106
+ `architecture/subsystems.likec4` exists only to be rendered, and in the renderer its views were the
107
+ least readable objects in a docs repo: no title, so LikeC4 showed the hex-escaped id
108
+ (`subsystem_platform__order_2dflow`), and leaf includes only, so a fleet drawn as ordinary
109
+ grouped C4 rendered flat. Every view now carries `title` (the marker's, or the directory name) and
110
+ `description` when the marker authors one, and draws a boundary: the model's own containing element
111
+ when one holds exactly the subsystem's members, a `group '<title>'` mirroring the directory subtree
112
+ when the landscape nests nothing, and — when the members are nested but no element owns exactly them
113
+ — no box plus one comment line, `// model: no boundary — …`, which is loam's whole answer to a tree
114
+ and a map that disagree. Nothing grades that comment: placement still carries no policy.
115
+
116
+ Marker text is normalized to one line and escaped before it is written (measured: a raw `'` in a
117
+ title is two parse errors and blanks the fleet's whole map in the renderer; `Ops' } view evil {` is
118
+ zero errors and injects a view). [SCHEMA.md](SCHEMA.md) states the rules.
119
+
120
+ What a repo notices, and what it costs:
121
+
122
+ - the first `loam validate --all` after upgrading reports exactly one `subsystem.views-stale` error
123
+ (exit 1) until one `loam subsystem sync` runs. `sync` does not `git add`, so the repair leaves one
124
+ unstaged one-file diff to commit;
125
+ - the boundary depends on which OTHER services sit under an element, so an unrelated adoption or
126
+ archive can flip a view from boxed to flat and restale the file. `loam archive` now withholds its
127
+ "living spec + landscape are now complete + current" line, prints one warning naming
128
+ `loam subsystem sync`, and adds `subsystemViewsStale: true` to its `--json` payload when it left
129
+ the file behind;
130
+ - `subsystem.views-stale` and `subsystem.view-id-collision` now carry `locations`: the generated
131
+ file (or `architecture/` when it does not exist yet) and the claimant document respectively. Both
132
+ used to fall back to the landscape, sending an editor to a file with nothing to fix in it.
133
+
134
+ No command, flag, exit code, envelope key or stable code string changes; `subsystem.views-stale` is
135
+ not in the `IssueCode` union and carries no `gates` key, so `loam archive` remains unaffected by it.
136
+
13
137
  ## [0.2.0-alpha.2] - 2026-09-02
14
138
 
15
139
  The deployment axis, and the two defects the axis surfaced on its way in. Still a prerelease on the
@@ -312,7 +312,10 @@ services, loam frontmatter, Operations/Covers links and feature identity still r
312
312
  specs intentionally remain `status: draft`.
313
313
  - `Operations:` links to provider OpenAPI `operationId`s, and `Covers:` links where architecture
314
314
  requirements are created.
315
- - `architecture/landscape.likec4`, service `model.likec4`, and explicit fleet relationships.
315
+ - `architecture/landscape.likec4`, service `model.likec4`, and explicit fleet relationships — then
316
+ `loam subsystem sync`, which writes each model's `likec4.config.json` (the staged repo already
317
+ carries the root one) so the models render beside the map; `validate --all` warns
318
+ `service.likec4-config-missing` for any model still without one.
316
319
  - Service OpenAPI contracts and provider-before-consumer sequencing where inbound edges already name
317
320
  operations.
318
321
  - LikeC4 and OpenAPI deltas for the already mapped active feature ids (`FEAT-12`, `BUG-42`, and so
package/README.md CHANGED
@@ -5,13 +5,14 @@
5
5
  ![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)
6
6
  ![node: >=22.22.3](https://img.shields.io/badge/node-%3E%3D22.22.3-brightgreen.svg)
7
7
  [![CI](https://github.com/ybotok/loam/actions/workflows/ci.yml/badge.svg)](https://github.com/ybotok/loam/actions/workflows/ci.yml)
8
- [![npm](https://img.shields.io/npm/v/@ybotok/loam.svg)](https://www.npmjs.com/package/@ybotok/loam)
8
+ [![npm](https://img.shields.io/npm/v/@ybotok/loam/alpha.svg?label=npm%20alpha)](https://www.npmjs.com/package/@ybotok/loam)
9
9
 
10
- > **Pre-release: `0.2.0-alpha.2`**, the second release of the 0.2 line and the newest published
10
+ > **Pre-release: `0.2.0-alpha.5`**, the fourth 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
- > `alpha` npm dist-tag: `npm i @ybotok/loam@alpha`, or the exact version, always installs this
13
- > line. `latest` is moved onto a release by hand once it has published, so a plain
14
- > `npm i @ybotok/loam` may still resolve to `0.1.0-beta.3` until it is. See [Status](#status).
12
+ > `alpha` npm dist-tag — which is what the badge above reads: `npm i @ybotok/loam@alpha`, or the
13
+ > exact version, always installs this line. `latest` is moved onto a release by hand once it has
14
+ > published, so a plain `npm i @ybotok/loam` resolves to an earlier release until it is moved. See
15
+ > [Status](#status).
15
16
 
16
17
  `loam` keeps an authored C4 model, requirements, API and event contracts, permissions, domain
17
18
  vocabulary and verification evidence in explicit agreement as a system changes. The same lifecycle
@@ -486,7 +487,7 @@ always preserved byte for byte, with the entry printed for manual merge.
486
487
  maps, what is lost, what must be added.
487
488
  - [ROADMAP.md](ROADMAP.md) — the evidence-backed improvement plan, priorities, non-goals and exit
488
489
  criteria.
489
- - [CHANGELOG.md](CHANGELOG.md) — released beta.1–alpha.2 plus the changes on `main` under
490
+ - [CHANGELOG.md](CHANGELOG.md) — released beta.1–alpha.5 plus the changes on `main` under
490
491
  `[Unreleased]`.
491
492
  - [docs/BENCHMARKS.md](https://github.com/ybotok/loam/blob/main/docs/BENCHMARKS.md) — what a
492
493
  fleet-sized run costs, with the method, the machine and the committed runs behind every number.
@@ -520,14 +521,14 @@ component baselines, the `architecture/permissions.yaml` authorization vocabular
520
521
  authored-capability axes with the `Realizes:` join, use cases graded as `dynamic view`s, the
521
522
  `glossary/<term>.md` domain vocabulary checked through the links that cite it, and the
522
523
  `architecture/obligations.yaml` architectural obligations checked through `#obl-` tags and
523
- `Covers:`. Known limits, each with its owner in [ROADMAP.md](ROADMAP.md): the complete gate still
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 **157 test files** (counted 2026-09-02): the count is graded
524
+ `Covers:`. Known limits, each with its owner in [ROADMAP.md](ROADMAP.md): the scheduled CI
525
+ `stability` job has still to be observed green. Speculative `render`, health composition and UI
526
+ generation come later. Behind that status stand **159 test files** (counted 2026-09-02): the count is graded
526
527
  against a live readdir by `test/docs-facts.test.ts`, so this sentence fails the suite the moment it
527
528
  trails the tree.
528
529
 
529
530
  The package is published as [`@ybotok/loam`](https://www.npmjs.com/package/@ybotok/loam), currently
530
- `0.2.0-alpha.2`: the unscoped `loam` name is taken by an unrelated GDAL wrapper, so the package ships
531
+ `0.2.0-alpha.5`: the unscoped `loam` name is taken by an unrelated GDAL wrapper, so the package ships
531
532
  under the maintainer's own npm user scope. Releases are tag-driven and maintainer-only
532
533
  ([CONTRIBUTING.md](CONTRIBUTING.md)); the version this line names is pinned to `package.json` by a
533
534
  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-09-02._
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,27 +18,30 @@ 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 **424 TypeScript modules in 129 source packages**, with an acyclic package graph
21
+ The tree contains **429 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 **157 test files**. Those four counts are deliberately stated OUTSIDE the
25
+ and the suite stands at **159 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-09-02 at `0.2.0-alpha.2`, and dated because neither number has a cheap derivation:_
30
+ _Measured 2026-09-03 at `0.2.0-alpha.5`, and dated because neither number has a cheap derivation:_
31
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%
32
+ **3,434/3,434 tests**, with two `skipIf(asRoot)` cases the root gate container cannot run; the
33
+ coverage gate passing with **92.30% statements, 84.11% branches, 96.64% functions, and 94.24%
34
34
  lines** against its thresholds of 91 / 82 / 95 / 93; and `npm run release:check` plus
35
35
  `npm run test:package` green against the real tarball.
36
36
 
37
- One qualification remains, and it is observation rather than code: nothing has been pushed since the
38
- CI `stability` job and the tarball-reading package smoke landed, so repeatable CI executions — the
39
- stability job seen green, and the installed-package smoke run from a pushed commit — are still
40
- release evidence to collect, even though the gate-stress proof passed locally on its landing tree
41
- and the smoke passes locally against the real tarball. The runner class recorded on 2026-08-18 is
37
+ Most of that qualification is now closed by observation rather than by code. Measured 2026-09-03 on
38
+ pushed commits: `ci` at `1de7c97` green — `test` on Node 22.22.3 and 24, and the installed-package
39
+ `package` job on ubuntu (both Node lines) and windows — and the `release` workflow on tag
40
+ `v0.2.0-alpha.4` green end to end, `release-gate` (lint, typecheck, coverage, the packed candidate,
41
+ the smoke of that exact candidate, `release:check`) followed by `publish`, which put the tarball on
42
+ npm under the `alpha` dist-tag. What is left is one job: `stability` is SKIPPED on a push — it runs
43
+ on the weekly schedule and on dispatch — so it is still release evidence to collect, even though the
44
+ gate-stress proof passes locally. The runner class recorded on 2026-08-18 is
42
45
  still the reading to apply to a red of that shape: a restricted filesystem sandbox that denies `tsx`
43
46
  its local IPC socket fails only the
44
47
  [test/cli-entry.test.ts](https://github.com/ybotok/loam/blob/main/test/cli-entry.test.ts) cases with
@@ -103,12 +106,15 @@ has landed (see [Recently landed](#recently-landed)) — the authored business a
103
106
  axis, and as of 2026-09-02 the deployment axis, which is the first item this file ever wrote
104
107
  directly into this section rather than promoting from [Later](#later--promote-only-from-evidence).
105
108
 
106
- ### Release evidence, unchanged
109
+ ### Release evidence — one job left
107
110
 
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.
111
+ What is left is the one thing this file cannot close by writing code, and it stays where it is
112
+ rather than being promoted for the shape of it. The installed-package half closed on 2026-09-03:
113
+ the `package` job ran green on a pushed commit across three runner/Node combinations, and the whole
114
+ publication path ran green on tag `v0.2.0-alpha.4`. The CI `stability` job has still to be OBSERVED
115
+ green — it is skipped on a push and runs on the weekly schedule — so it is still release evidence to
116
+ collect, not an improvement to make. `workflow_dispatch` is the way to collect it deliberately
117
+ rather than waiting for Monday.
112
118
 
113
119
  An item returns to this section from [Later](#later--promote-only-from-evidence) when fleet evidence
114
120
  names its operator, repeated task, failure mode, frequency, current workaround and measurable
@@ -125,6 +131,72 @@ changes, exit criteria, and what each review surfaced — are in this file's his
125
131
  working tree and not committed, and the range goes in with the commit rather than being guessed at
126
132
  here.
127
133
 
134
+ #### The service model, renderable from the fleet root
135
+
136
+ **Landed 2026-09-03**, from one problem report. The root `likec4.config.json` that
137
+ `loam init --create` writes excludes `services/**` on purpose — every `model.likec4` declares its
138
+ own `specification` block, and a renderer merging the tree calls each one a duplicate — and the
139
+ cost of that exclusion had gone unstated for a release: the model `loam adopt` spends the most
140
+ effort on was a box with nothing inside it in the renderer opened at the docs root, and the only
141
+ documented path to its containers was one renderer invocation per service directory. The report's
142
+ hand-written workaround, one project file per service, is what this entry makes loam's.
143
+
144
+ **Measured** at the pinned likec4 before a line was written. A `services/<svc>/likec4.config.json`
145
+ is registered as a project of its own even though the root excludes the directory: the workspace
146
+ loads `fleet` plus one project per service with zero errors, the root project sees exactly the
147
+ files it saw before, and each service project carries an auto `index` view of the containers beside
148
+ the authored view. A nested project named `fleet` is silently registered as `fleet-1`. With more
149
+ than one project, `likec4 validate` at the root refuses with `Specify exact project` until given
150
+ `--project`. A nested project holding no `.likec4` file is silently absent from the project list. A
151
+ service model that does not parse breaks only its own project; siblings and `fleet` still compute.
152
+ And the project picker shows `title ?? id`, so the title is what a human reads.
153
+
154
+ **What landed.** `loam subsystem sync` writes the file — create-only, two keys, bytes a function of
155
+ the id alone — beside every service model that has none, once the root project file exists, and
156
+ reports them under an additive `projects` key. `loam validate --all` grades presence with one
157
+ warning per service, `service.likec4-config-missing`, whose fix is `sync`. No new command: the
158
+ [Later](#later--promote-only-from-evidence) Rendering row's evidence bar was met for the SCOPING
159
+ half of its trigger and answered by wiring — one file that tells the renderer where a project is —
160
+ not by a drawing verb; drawing stays LikeC4's, and the row says so now.
161
+
162
+ **Rejected.** A new command, because the report's problem was a missing file, not a missing verb,
163
+ and a verb would have put loam between a team and its renderer for good. The agent writing the file
164
+ from the adopt brief, because a file written by hand per service is the state the report found —
165
+ one of four adopted services had one — and loam already writes the root file and knows every
166
+ service directory. A generated-and-compared file with a `-stale` grade, because the bytes cannot go
167
+ stale and a compare would fight the `title` and styles LikeC4 lets a team add. Grading in `doctor`
168
+ alone, because the gap is per service and `doctor` reports on the repository, not on its services;
169
+ the docs repo's CI runs `validate --all`.
170
+
171
+ **Costs**, each where a user meets it. A repo with adopted services reports one warning per service
172
+ on its first `validate --all` after upgrading, until one `sync`; that `sync` leaves N untracked
173
+ files it does not `git add`; from the first service project on, `likec4 validate` at the docs root
174
+ needs `--project <name>` (`build` and `export` take every project without one — measured); the
175
+ renderer's project list grows by one per adopted service, and a fleet-map box is not a link into
176
+ that project (LikeC4 projects are separate models); and the project name is not injective (an id spelled billing.core beside one spelled
177
+ billing-core, `service-fleet` beside `fleet`), disclosed in SCHEMA rather than escaped away,
178
+ because the picker shows the id.
179
+
180
+ #### The generated subsystem views — a label, a boundary, and one reader of the map
181
+
182
+ **Landed 2026-09-03.** Three problem reports from a fleet running loam, one axis. The generated
183
+ `architecture/subsystems.likec4` exists only to be rendered and was the least readable object in a
184
+ docs repo: no `title`, so the renderer showed the hex-escaped view id, and leaf includes only, so a
185
+ fleet drawn as grouped C4 rendered flat. Each view now carries the marker's title (the directory
186
+ name when it has none) and description, and draws a boundary the LANDSCAPE decides: the model's own
187
+ containing element when one holds exactly the members, a `group` mirroring the directory subtree
188
+ when the map nests nothing, and one `// model: no boundary — …` comment when the two disagree.
189
+ That comment is the whole answer to the drift the third report asked about — disclosure in a
190
+ generated artifact, never a finding, because grading placement against the map is the policy the
191
+ tree is defined not to carry. The graded alternative is written down under
192
+ [Later](#later--promote-only-from-evidence) with the evidence bar it has to clear.
193
+
194
+ The hunt found what neither report did: `validate --all` graded that file against the whole
195
+ `architecture/` project while every writer rendered it from `landscape.likec4` alone, so a
196
+ use-case document binding one element could produce a `subsystem.views-stale` that
197
+ `loam subsystem sync` reported as `current` — reproduced on `examples/docs`, and fixed by pointing
198
+ both sides at the same read.
199
+
128
200
  #### The deployment axis — topology joined to requirements
129
201
 
130
202
  **Promoted 2026-09-02, and authored as a feature in loam's own docs repo** rather than as an item
@@ -345,10 +417,24 @@ item gets promoted — it went from here to `## Now` and from there to
345
417
  invocation, scoping, or artifact reproducibility is the recurring problem, and it must stay
346
418
  outside validation so view computation cannot slow or change the gate. This item is about
347
419
  **drawing**, and LikeC4 keeps that job. Reading a `dynamic view`'s declared steps is not rendering
348
- and is not this item — see docs/DESIGN.md rule 26.
420
+ and is not this item — see docs/DESIGN.md rule 26. The scoping half of that bar was met and
421
+ answered on 2026-09-03 by the per-service project file `loam subsystem sync` writes (see
422
+ [Recently landed](#recently-landed)) — wiring rather than a verb; drawing stays LikeC4's.
349
423
  - **UI generation:** begin with a disposable projection over the stable JSON contract only after CLI
350
424
  consumers demonstrate a repeated navigation problem. It must not introduce a second mutable state,
351
425
  hidden workflow state, or a required service.
426
+ - **A graded subsystem/model correspondence:** a fleet that draws its landscape as grouped C4 states
427
+ its grouping twice — once as directories under `services/`, once as the nesting of the elements —
428
+ and `loam subsystem move` changes the first while the second stays put. Since 2026-09-02 the
429
+ generated views file DISCLOSES that (the `// model: no boundary — …` line; see
430
+ [SCHEMA.md](SCHEMA.md)), and nothing grades it, because grading placement against the map is the
431
+ policy the tree is defined not to carry. The shape a graded version would take is known and
432
+ measured: an author's opt-in `metadata { subsystem '<name>' }` on the grouping element, read
433
+ through the existing metadata reader, the NAME rather than the path (names are unique in one flat
434
+ namespace and survive a move, which a path binding would not), with the check structurally silent
435
+ for every fleet that never writes one. Promote only on evidence from a second fleet, after living
436
+ with the disclosure — the first report classified itself `inconclusive`, and one fleet is not
437
+ enough to qualify an invariant that says placement is never part of any identity.
352
438
  - **Landscape scaling:** retain one landscape while conflicts are exceptional. If same-service
353
439
  conflicts become routine — the current trigger in [SCHEMA.md](SCHEMA.md) is weekly rather than
354
440
  monthly — evaluate service-owned model files plus a thin global cross-service map. Migration must
package/SCHEMA.md CHANGED
@@ -72,6 +72,7 @@ docs/
72
72
  glossary/<term>.md the system's domain vocabulary, one file per term; nesting allowed [authored, opt-in]
73
73
  services/<svc>/ at the root (unfiled), or inside subsystem directories at any depth
74
74
  model.likec4 boundary C4 (containers/components) [adopt]
75
+ likec4.config.json one LikeC4 project per service model, create-only [GENERATED: loam subsystem sync]
75
76
  spec.md living requirements (Requirement/Scenario) [adopt]
76
77
  arch.spec.md living ARCHITECTURE requirements (outbox, retries, alerts; Covers:) [authored]
77
78
  openapi.yaml API contract [adopt / authored]
@@ -164,14 +165,84 @@ check uses; a member nothing models is omitted, which `landscape.service-unmodel
164
165
  reports). The output is deterministic and line-oriented — subsystems sorted by path, members by id,
165
166
  one `include` per line, no timestamps, no absolute paths — so it is byte-reproducible on any machine
166
167
  and two concurrent moves into different groups merge in git without intervention.
168
+
169
+ Each view carries the **label a human reads**: `title`, and `description` when the marker authors
170
+ one. The title is the subsystem's **path** — every marked ancestor's label and its own, joined with
171
+ ` / `, so `services/platform/order-flow` reads `Platform / Order flow` — and each label is the
172
+ marker's `title`, falling back to the directory name (an unlabelled view is shown by its id, which
173
+ is hex-escaped for injectivity and reads as `subsystem_platform__order_2dflow`). LikeC4
174
+ reads that path as folders: the view list mirrors the directory tree, showing one folder per
175
+ ancestor and the leaf's own label as the title. A `/` **inside** a label becomes `∕` (U+2215), so an
176
+ authored slash can neither hide half of what somebody wrote nor invent a folder level the tree does
177
+ not have. Both strings are somebody's prose written into syntax, so both are normalized to one line
178
+ and escaped. Titles are not unique — only ids are — and renaming a parent's title rewrites every
179
+ view beneath it.
180
+
181
+ The **boundary** each view draws is a fact about the landscape, never a setting:
182
+
183
+ 1. an element that holds *exactly* the subsystem's members and nothing else is included beside the
184
+ leaves, so the renderer nests them in the box the author drew, under the author's title;
185
+ 2. a landscape that nests nothing puts the services in a `group '<title>'` of loam's own, mirroring
186
+ the directory subtree, so a parent subsystem shows its children as boxes rather than one pile;
187
+ 3. members that ARE nested with no element owning exactly them get no box and one comment line —
188
+ `// model: no boundary — <id> also holds services outside this subsystem.` — because a synthetic
189
+ box would look identical whether or not the tree and the map agree. That comment is the whole of
190
+ what loam says about the two disagreeing: nothing grades it, nothing fails on it, and it lands in
191
+ the diff of the commit that caused it. **The tree and any `group` nesting in the landscape stay
192
+ independent**; `loam subsystem move` never rewrites the map, and no finding compares them.
193
+
194
+ Because the boundary depends on which *other* services sit under an element, an unrelated adoption
195
+ or archive can flip a view from boxed to flat — a new reason to see `subsystem.views-stale`, and why
196
+ `loam archive` withholds its "complete + current" line and names `loam subsystem sync` when it left
197
+ the file behind.
198
+
167
199
  `loam subsystem sync` is the one regenerator (it also *removes* the file when the tree has no
168
200
  subsystems), every `loam subsystem` write verb regenerates it inside the same transaction, and
169
- `validate --all` grades staleness by **byte comparison** — exactly one error,
201
+ `validate --all` grades staleness by **content comparison** (line endings normalized; a CRLF
202
+ checkout changes no fact) — exactly one error,
170
203
  `subsystem.views-stale`, on exactly that file; nothing in loam ever parses it: a views-only document
171
204
  does not parse standalone (its `include` lines reference elements the landscape defines), and it
172
205
  holds **static** views, whose contents loam never reads in any document. Only the LikeC4 renderer,
173
206
  which merges the whole `architecture/` project, resolves its includes.
174
207
 
208
+ The tree's **second renderer file** is per service — `services/<…>/<svc>/likec4.config.json`,
209
+ which `loam subsystem sync` also writes, beside every `model.likec4` that has none. The root
210
+ project is scoped to `architecture/` and excludes `services/**`, and must be, since each model
211
+ declares its own `specification` block; what that exclusion cost went unstated for a release: an
212
+ adopted service was a box on the fleet map with nothing renderable inside it. Measured at the
213
+ pinned likec4, a project file INSIDE a service directory is registered as a project of its own
214
+ regardless of that exclusion, so one `npx likec4 start` at the docs root loads `fleet` plus one
215
+ project per service, each carrying LikeC4's own `index` view of the containers beside the authored
216
+ one, and a model that does not parse breaks only its own project. The file is **create-only** —
217
+ never compared, never rewritten, never removed — under the root config's ownership rule, not the
218
+ views file's: there is no `-stale` grade because nothing can make it stale. Its bytes are a
219
+ function of the id alone (two keys, `name` and `title`), so placement never changes them and a
220
+ `subsystem move` carries the file with no write of its own — it rides inside the rename the move's
221
+ transaction already journals (in a git-backed repo, commit the files `sync` wrote first: `move`
222
+ refuses over untracked paths, `move-uncommitted`); a compare would only fight the keys LikeC4 lets a
223
+ team add (`title`, styles). `name` is the id with every
224
+ `.` folded to `-` (LikeC4's name grammar refuses the byte; loam's id grammar admits it), prefixed
225
+ `service-` when the result would equal the root project's `fleet` (a nested project of that name
226
+ is silently registered as `fleet-1`) or LikeC4's reserved `default`; `title` is the id verbatim,
227
+ and the title is what the renderer's project picker shows. The name is **not injective**, and
228
+ that is disclosed rather than escaped away: an id spelled billing.core beside a service literally
229
+ named billing-core, or `service-fleet` beside `fleet`, share a name and LikeC4 suffixes the later
230
+ one `-1`; no loam
231
+ finding grades it, because the picker still reads the id. Order is a gate — the root file first
232
+ (`loam init --create` writes it, `loam doctor` prints it), then `sync`: without the root file
233
+ nothing is written and the text output says so. No model, no file: a spec-only service is owed
234
+ nothing and gets its file on the first `sync` after its model lands. No legal id, no file either: a
235
+ directory whose name breaks the service-id grammar (`service.id-invalid`) would get a `name` the
236
+ renderer silently drops, and a create-only file the renderer ignores would silence the grade for
237
+ good, so `sync` skips it and the rename that clears `service.id-invalid` earns the file. A file left behind by a
238
+ deleted model is the team's and harmless (measured: a nested project holding no `.likec4` file is
239
+ absent from the project list). `validate --all` grades presence only — one warning per service,
240
+ `service.likec4-config-missing`, on the fleet `landscape` target, never an error, never in
241
+ `--service`, `status` or `doctor`; the fix is `loam subsystem sync`, never a hand-written file. A
242
+ feature's `delta.likec4` is deliberately out of scope: the directory is transient and
243
+ `loam archive` moves it where a project file would still be registered, so every shipped feature
244
+ would keep a dead project in the list forever.
245
+
175
246
  A move is **one journaled transaction over N directory renames plus the regenerated views file**
176
247
  (`loam subsystem move <name>... --into <sub|.>`; `rename` is a one-rename move; whole subtrees move
177
248
  as one rename). It refuses only when git reports uncommitted changes under a directory being moved
@@ -2263,9 +2334,17 @@ through the forge, not through loam features:
2263
2334
  **parsed** model and never computes a view — it reads what a view **declares** (a `dynamic view`'s
2264
2335
  tags and ordered steps, already materialized by the parse loam pays for) and never what a view
2265
2336
  **shows**. It renders nothing, and view *computation* is what is superlinear in edges. (If you
2266
- want diagrams, add a `views` block and render it with LikeC4's own tooling — `npx likec4 start` in
2267
- the docs repo for the fleet map, `npx likec4 start services/<id>` for one service model — and
2268
- scope the views, because an `include *` over a fleet-sized landscape takes minutes. `loam init`
2337
+ want diagrams, add a `views` block and render it with LikeC4's own tooling — `npx likec4 start`
2338
+ at the docs root, which renders the fleet map AND every adopted service's model, each as the
2339
+ project its own `services/<svc>/likec4.config.json` declares (written by `loam subsystem sync`;
2340
+ the Layout section has the rule), and `npx likec4 start features/<FEAT>` for a delta, which is
2341
+ transient and gets no project — and scope the views, because an `include *` over a fleet-sized
2342
+ landscape takes minutes. Two costs arrive with the first service project. Measured at the
2343
+ pinned likec4: with more than one project, `likec4 validate` at the docs root refuses without
2344
+ `--project <name>`, so a CI step that ran it bare changes (`likec4 build` and `likec4 export` take
2345
+ every project and need no flag). And the renderer's project list grows by one per adopted service
2346
+ beside `fleet` — LikeC4 projects are separate models, so a box on the fleet map is not a link into
2347
+ that service's project. `loam init`
2269
2348
  scaffolds the landscape with no `views` block for that reason: loam should not plant a diagram it
2270
2349
  never draws **— and never asks for one: a fleet that draws nothing owes no views block.** It also
2271
2350
  scaffolds a `likec4.config.json` that scopes the root LikeC4 project to `architecture/`: loam
@@ -2524,9 +2603,9 @@ component-surface pins), `verify`,
2524
2603
  transactional `archive` / `unarchive`, `vouch`, and the OpenSpec audit/migration on-ramp are
2525
2604
  implemented, each with a `--json` contract.
2526
2605
 
2527
- Known and unclosed, so nobody has to discover them: the complete gate still needs repeatable CI and
2528
- installed-package evidence observed from an actual push, even though behavior, coverage and the
2529
- local stress runner pass. Both contract axes now merge a components-only delta: a feature
2606
+ Known and unclosed, so nobody has to discover them: the scheduled CI `stability` job has still to be
2607
+ observed green, even though behavior, coverage, the installed-package smoke and the local stress
2608
+ runner all pass. Both contract axes now merge a components-only delta: a feature
2530
2609
  asyncapi.yaml declaring no channel/operation/message slot (`components.schemas`-only, say) merges
2531
2610
  the component surfaces it declares, pinned by the root `x-loam-baselines` record, and they enter the
2532
2611
  ref sweep like anything else the merge wrote — so a `$ref` whose target the delta itself defines
@@ -15,6 +15,10 @@ import { message, quietPruneEmptyParents, quietRm, rollbackError, rollbackStaged
15
15
  import { clearCommitIntent, writeCommitIntent } from "../../core/staging/recovery/intent.js";
16
16
  import { SNAPSHOT_DIR, SnapshotClobberError, snapshotDir, writeSnapshot, } from "../../core/staging/snapshot.js";
17
17
  import { enumeratedServices } from "../../core/repo/service-target.js";
18
+ import { listFleetTree } from "../../core/repo/repo.js";
19
+ import { subsystemViewsPath } from "../../core/repo/paths.js";
20
+ import { viewsState } from "../../core/repo/tree/render/stale.js";
21
+ import { loadArchitecture } from "../../core/c4/project/architecture.js";
18
22
  import { FleetContext } from "../../core/fleet-context.js";
19
23
  import { gate } from "./plan/gate.js";
20
24
  import {} from "./plan/refusal.js";
@@ -227,8 +231,19 @@ export async function archiveLocked(config, featureId, opts) {
227
231
  : "";
228
232
  throw new ArchiveFailure(failures.length > 0 ? "rollback-incomplete" : "merge-failed", wrapped.message + kept);
229
233
  }
234
+ // This merge just changed the map, and the generated views file is a
235
+ // function of the tree AND the map: a delta whose element lands inside a
236
+ // group that exactly covered a subsystem takes that subsystem's boundary
237
+ // line away, and a delta binding an element to a filed service adds an
238
+ // include. Archive does not rewrite the file — `loam subsystem sync` owns it
239
+ // outright, and folding a second generated artifact into this transaction
240
+ // would mean archive computing the post-merge tree AND the post-merge map,
241
+ // which is how the two writers disagree — but it must not report a repo as
242
+ // current when the next `validate --all` will call it stale. Graded through
243
+ // validate's own function, so the two cannot answer differently.
244
+ const staleViews = await subsystemViewsStale(config.docsDir);
230
245
  if (json) {
231
- emitJson(payload(true));
246
+ emitJson({ ...payload(true), ...(staleViews ? { subsystemViewsStale: true } : {}) });
232
247
  return;
233
248
  }
234
249
  console.log(`\n archived: features/${dirName} → features/archive/${dirName}`);
@@ -239,14 +254,43 @@ export async function archiveLocked(config, featureId, opts) {
239
254
  // caller told it to merge past. Printing it over either is how a red fleet
240
255
  // gets reported as a finished one.
241
256
  const incomplete = planWarns.filter((w) => w.code === "service.no-model");
242
- if (incomplete.length === 0 && overridden.length === 0) {
257
+ if (incomplete.length === 0 && overridden.length === 0 && !staleViews) {
243
258
  console.log(" living spec + landscape are now complete + current.");
244
259
  return;
245
260
  }
261
+ if (staleViews) {
262
+ console.log(" ⚠ architecture/subsystems.likec4 no longer matches the tree and this map — run `loam subsystem sync`.");
263
+ }
246
264
  for (const w of incomplete)
247
265
  console.log(` ⚠ ${w.message}`);
248
266
  if (overridden.length > 0) {
249
267
  console.log(` ⚠ merged past ${overridden.length} gating issue(s) with --approve — the living docs carry them now; \`loam validate --all\` says what they cost.`);
250
268
  }
251
269
  }
270
+ /**
271
+ * Would the next `validate --all` report `subsystem.views-stale`?
272
+ *
273
+ * Asked through validate's OWN function rather than a second compare, so the
274
+ * warning archive prints and the error validate raises can never disagree —
275
+ * including about the gate validate holds: a map that does not parse is
276
+ * `landscape.invalid`'s business, and grading a generated file against a map
277
+ * nobody can read would cascade a second complaint behind the first. An
278
+ * unreadable map therefore answers "no claim", exactly as validate does.
279
+ */
280
+ async function subsystemViewsStale(docsDir) {
281
+ try {
282
+ const doc = await loadArchitecture(docsDir);
283
+ if (doc.errors.length > 0)
284
+ return false;
285
+ const state = await viewsState(subsystemViewsPath(docsDir), await listFleetTree(docsDir), doc.elements);
286
+ return !state.agrees;
287
+ }
288
+ catch {
289
+ // Tolerant for the reason the snapshot resolver above is: a docs repo with
290
+ // no `services/` at all still archives a landscape-only feature, and this
291
+ // question is an ADVISORY printed after a commit that already succeeded.
292
+ // Nothing here may turn a landed archive into a non-zero exit.
293
+ return false;
294
+ }
295
+ }
252
296
  //# sourceMappingURL=run.js.map