@ybotok/loam 0.2.0-alpha.6 → 0.2.0-alpha.7

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 (105) hide show
  1. package/CHANGELOG.md +162 -0
  2. package/README.md +4 -4
  3. package/ROADMAP.md +27 -6
  4. package/SCHEMA.md +152 -32
  5. package/dist/commands/archive/run.js +49 -22
  6. package/dist/commands/archive/run.js.map +1 -1
  7. package/dist/commands/seed/commit.js +34 -1
  8. package/dist/commands/seed/commit.js.map +1 -1
  9. package/dist/commands/seed/plan.js +64 -13
  10. package/dist/commands/seed/plan.js.map +1 -1
  11. package/dist/commands/subsystem/edit.js +4 -2
  12. package/dist/commands/subsystem/edit.js.map +1 -1
  13. package/dist/commands/subsystem/move.js +55 -17
  14. package/dist/commands/subsystem/move.js.map +1 -1
  15. package/dist/commands/subsystem/sync.js +142 -45
  16. package/dist/commands/subsystem/sync.js.map +1 -1
  17. package/dist/commands/subsystem/txn/report.js +55 -5
  18. package/dist/commands/subsystem/txn/report.js.map +1 -1
  19. package/dist/commands/subsystem/txn/txn.js +118 -21
  20. package/dist/commands/subsystem/txn/txn.js.map +1 -1
  21. package/dist/commands/subsystem/txn/views.js +51 -8
  22. package/dist/commands/subsystem/txn/views.js.map +1 -1
  23. package/dist/commands/validate/checks/health-deps.js +9 -6
  24. package/dist/commands/validate/checks/health-deps.js.map +1 -1
  25. package/dist/commands/validate/fleet/census.js +26 -3
  26. package/dist/commands/validate/fleet/census.js.map +1 -1
  27. package/dist/commands/validate/fleet/landscape.js +12 -9
  28. package/dist/commands/validate/fleet/landscape.js.map +1 -1
  29. package/dist/commands/validate/fleet/map/attest.js +4 -5
  30. package/dist/commands/validate/fleet/map/attest.js.map +1 -1
  31. package/dist/commands/validate/fleet/map/bindings.js +15 -2
  32. package/dist/commands/validate/fleet/map/bindings.js.map +1 -1
  33. package/dist/commands/validate/fleet/map/consumers.js +43 -122
  34. package/dist/commands/validate/fleet/map/consumers.js.map +1 -1
  35. package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -1
  36. package/dist/commands/validate/fleet/views/fleet-project.js +1 -1
  37. package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -1
  38. package/dist/commands/validate/fleet/views/ids.js +33 -7
  39. package/dist/commands/validate/fleet/views/ids.js.map +1 -1
  40. package/dist/commands/validate/fleet/views/root-documents.js +9 -7
  41. package/dist/commands/validate/fleet/views/root-documents.js.map +1 -1
  42. package/dist/commands/validate/fleet/views/stale.js +31 -14
  43. package/dist/commands/validate/fleet/views/stale.js.map +1 -1
  44. package/dist/core/agent/agents-md/command-map.js +1 -1
  45. package/dist/core/agent/agents-md/command-map.js.map +1 -1
  46. package/dist/core/agent/agents-md/map/subsystem.js +27 -14
  47. package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
  48. package/dist/core/agent/agents-md/refusals.js +5 -3
  49. package/dist/core/agent/agents-md/refusals.js.map +1 -1
  50. package/dist/core/agent/agents-md.js +16 -7
  51. package/dist/core/agent/agents-md.js.map +1 -1
  52. package/dist/core/agent/workflows/check.js +5 -6
  53. package/dist/core/agent/workflows/check.js.map +1 -1
  54. package/dist/core/agent/workflows/reference/spine.js +12 -6
  55. package/dist/core/agent/workflows/reference/spine.js.map +1 -1
  56. package/dist/core/brief/brief.js +25 -8
  57. package/dist/core/brief/brief.js.map +1 -1
  58. package/dist/core/brief/targets.js +64 -0
  59. package/dist/core/brief/targets.js.map +1 -1
  60. package/dist/core/brief/unchecked.js +1 -1
  61. package/dist/core/brief/unchecked.js.map +1 -1
  62. package/dist/core/c4/project/architecture.js +31 -9
  63. package/dist/core/c4/project/architecture.js.map +1 -1
  64. package/dist/core/c4/project/documents.js +5 -4
  65. package/dist/core/c4/project/documents.js.map +1 -1
  66. package/dist/core/c4/project/load.js +8 -4
  67. package/dist/core/c4/project/load.js.map +1 -1
  68. package/dist/core/c4/project/staged.js +7 -3
  69. package/dist/core/c4/project/staged.js.map +1 -1
  70. package/dist/core/deployment/delta.js +12 -10
  71. package/dist/core/deployment/delta.js.map +1 -1
  72. package/dist/core/gate/checks.js +5 -1
  73. package/dist/core/gate/checks.js.map +1 -1
  74. package/dist/core/gate/partners.js +60 -17
  75. package/dist/core/gate/partners.js.map +1 -1
  76. package/dist/core/repo/paths.js +34 -29
  77. package/dist/core/repo/paths.js.map +1 -1
  78. package/dist/core/repo/tree/render/body.js +45 -6
  79. package/dist/core/repo/tree/render/body.js.map +1 -1
  80. package/dist/core/repo/tree/render/service/header.js +125 -0
  81. package/dist/core/repo/tree/render/service/header.js.map +1 -0
  82. package/dist/core/repo/tree/render/service/peers.js +28 -0
  83. package/dist/core/repo/tree/render/service/peers.js.map +1 -0
  84. package/dist/core/repo/tree/render/service/view-id.js +67 -0
  85. package/dist/core/repo/tree/render/service/view-id.js.map +1 -0
  86. package/dist/core/repo/tree/render/service/views.js +78 -0
  87. package/dist/core/repo/tree/render/service/views.js.map +1 -0
  88. package/dist/core/repo/tree/render/stale.js +19 -2
  89. package/dist/core/repo/tree/render/stale.js.map +1 -1
  90. package/dist/core/repo/tree/render/views.js +102 -33
  91. package/dist/core/repo/tree/render/views.js.map +1 -1
  92. package/dist/core/verify/checklist.js +42 -10
  93. package/dist/core/verify/checklist.js.map +1 -1
  94. package/dist/core/vocabulary/issue.js.map +1 -1
  95. package/dist/core/vocabulary/maturity.js +41 -1
  96. package/dist/core/vocabulary/maturity.js.map +1 -1
  97. package/examples/README.md +7 -3
  98. package/examples/docs/architecture/landscape.likec4 +13 -7
  99. package/examples/docs/architecture/services.likec4 +70 -0
  100. package/examples/docs/architecture/subsystems.likec4 +3 -0
  101. package/examples/docs/services/order-service/model.likec4 +10 -8
  102. package/examples/docs/services/payment-service/model.likec4 +6 -4
  103. package/examples/docs/services/platform/identity-service/model.likec4 +9 -7
  104. package/examples/docs/services/platform/notification-service/model.likec4 +4 -5
  105. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -10,6 +10,168 @@ 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.7] - 2026-09-07
14
+
15
+ Infrastructure belongs on the fleet map, and the render is navigable at fleet scale. Still a
16
+ prerelease on the `alpha` line: `npm i @ybotok/loam@alpha` installs it. One code is REMOVED —
17
+ `landscape.datastore-private`, the first code this project has ever retired — and no flag, path or
18
+ payload key is. What a repo NOTICES on its first `validate --all` after upgrading: a
19
+ `subsystem.views-stale` error against the new `architecture/services.likec4`, in every fleet whose
20
+ map draws a service, cleared by one `loam subsystem sync`; a second one against
21
+ `architecture/subsystems.likec4` wherever subsystems exist, because those views gained boundary
22
+ edges and an `of` subject; and silence where a datastore drawn beside the services used to be told
23
+ to move.
24
+
25
+ **Upgrading: one `loam subsystem sync` per docs repo, once.** `loam validate --all` reports
26
+ `subsystem.views-stale` (error) against the new `architecture/services.likec4` in EVERY existing
27
+ docs repo whose fleet map draws a service — flat fleets with no subsystems included, because the
28
+ file is new everywhere and an absent generated file is a state of its own. This is not a
29
+ subsystems-only upgrade: a fleet that has never run `loam subsystem new` gets the finding too. A
30
+ repo that DOES have subsystems gets a second finding on the same run, against
31
+ `architecture/subsystems.likec4`, because the subsystem views gained boundary edges and an `of`
32
+ subject and every existing copy therefore re-renders differently. One `loam subsystem sync` writes
33
+ both files and clears both findings. `loam archive` names the same repair rather than claiming
34
+ "complete + current". Nothing else in a repository has to change, and no document a team wrote is
35
+ touched — and a document a team wrote that is already standing at one of the two generated paths is
36
+ refused by name rather than overwritten (below).
37
+
38
+ ### Removed — `landscape.datastore-private`, because where a store is drawn is the team's choice
39
+
40
+ - **`landscape.datastore-private` (warn) is retired.** It fired on a `database` element drawn at
41
+ fleet level with exactly ONE consumer, and its whole message prescribed deleting the peer and
42
+ writing the store inside its owner. The identical store nested was silent, so the check graded the
43
+ DRAWING and not the architecture — a restriction rather than a fact about the fleet. A store one
44
+ service owns may now be nested under that service's element or drawn beside the services, and loam
45
+ grades neither. The code is not in the frozen `ErrorCode` or `IssueCode` unions, so removing it
46
+ breaks no contract; `loam explain landscape.datastore-private` no longer answers, and the row is
47
+ gone from the agent-facing check table.
48
+ - **`landscape.datastore-shared` (warn) is unchanged**, both arms. Two or more services on one store
49
+ is a fact about coupling, not a restriction, and it fires at two consumers for both placements
50
+ alike. What survives from the retired code's message is the one clause that was a fact about
51
+ LikeC4 rather than a preference, and SCHEMA's "Private stores" now leads with it: a second
52
+ consumer's edge resolves only against a declaration the MAP holds, because a per-service project
53
+ is `architecture/` plus one model.
54
+ - **Infrastructure is not a service.** A new `INFRA_KINDS` set in `core/vocabulary/maturity.ts` —
55
+ `database`, `queue`, `topic`, `cache`, `bucket`, compared case-insensitively — takes such an
56
+ element out of four surfaces at once: the drawn-service census behind
57
+ `landscape.service-undocumented`, the `loam gate` partner set (an infra endpoint is dropped from
58
+ the `partners` payload, exactly as an actor already was), the `loam verify` checklist (a
59
+ fleet-level store no longer mints a `service.exists` claim no repository can answer), and the
60
+ scorecard's element denominator, whose `c4` row now counts services rather than services plus
61
+ their stores. The hook is the kind NAME, with the same limitation `ACTOR_KINDS` lives with: a
62
+ fleet spelling its kind `postgres` is not covered, and a tag can be added later as a second
63
+ spelling. A store BOUND to a directory with `metadata { service }`, or titled like one, stays an
64
+ adopted service on every one of the four surfaces — the census predicate wins over the kind.
65
+ `landscape.service-undocumented`'s message gained a THIRD remedy beside "bind it" and "tag it
66
+ `#external`", spelled from the same set: neither of the old two was true of a store the fleet
67
+ owns, and that message is the only place an author learns which kind spellings loam reads.
68
+ - `c4.uncovered` was left alone, and the measurement is the reason: a delta drawing
69
+ `splitLedger = database 'split-ledger'` earns it, and adding `Covers: splitLedger` to the
70
+ feature's `arch.spec.md` delta silences it exactly, with no `covers.unknown`. The remedy the
71
+ message names is true, so this is not a residual restriction — and the code fires only on elements
72
+ a feature DELTA adds, never from `validate --all` over a living tree.
73
+ - The acceptance criterion, run for real: a `database` drawn at fleet level, owned by the fleet, with
74
+ one consumer, no `#external` tag and no binding, now earns ZERO findings from `loam validate --all`
75
+ and from `loam gate`.
76
+
77
+ ### Added — `architecture/services.likec4`: one generated overview view per service
78
+
79
+ - **New generated path `architecture/services.likec4`**, written by `loam subsystem sync` beside
80
+ `architecture/subsystems.likec4`, removed by it when the fleet map draws no service, and owned
81
+ outright by loam: views only, byte-reproducible, one view per service the map resolves, sorted by
82
+ service directory id. Each view is declared `of` its service's element, which is what makes the
83
+ fleet map, every subsystem view and every other service view click-through into it with no
84
+ authored link — measured at likec4 1.59.2. Peers are NAMED one by one rather than wildcarded,
85
+ because on a map that groups its services the wildcard form collapses every peer to its group box
86
+ and yields no drill-down at all (measured on a 120-service fleet: 0 of 10 nodes navigable). Median
87
+ at 58 services in 11 groups: 10 nodes, 8 edges per view, about 7 ms of layout each.
88
+ - **Titles are folders.** A per-service view is titled `<subsystem path> / <service directory id>`,
89
+ built from the same path helper the subsystem views use, so both generated files fold into one
90
+ browser folder tree mirroring `services/`. LikeC4 has no view-folder syntax — `views { group … }`
91
+ is a parse error at the pin — and splitting the title on `/` is the whole mechanism.
92
+ - **An authored view of the same element keeps precedence.** Two views `of` one element are legal;
93
+ the first document alphabetically wins the default navigation target and both stay reachable,
94
+ because the subject's own node cycles to the other. `architecture/landscape.likec4` sorts before
95
+ `architecture/services.likec4`. loam does not detect the authored view and never will:
96
+ docs/DESIGN.md rule 26 forbids reading what a view SHOWS.
97
+ - **Subsystem views gained two things** in the same generator: `include <member> -> *, * -> <member>`
98
+ per member, so a subsystem view answers "what does this group touch" and not only "who is in it",
99
+ and `view subsystem_<name> of <ancestor>` when the map holds an element owning exactly that
100
+ subsystem's members. Measured at 58/11: ~14 nodes, ~38 edges, 12 of 14 nodes navigable. Every
101
+ existing `architecture/subsystems.likec4` therefore re-renders differently and is stale once.
102
+ - **`subsystem.views-stale` now grades two files**, one finding per file, each naming its own path in
103
+ `subject`, `locations[0]` and the message. No new code was minted: both files are written by one
104
+ command in one commit and repaired by one run, which is what makes one code honest.
105
+ - **`subsystem.view-id-collision` reserves `service_` as well as `subsystem_`.** A `service_` id is
106
+ reserved only for a service the fleet map actually DRAWS, so an unmodelled `services/<id>/`
107
+ reserves nothing; the message names which generated file and which prefix.
108
+ - `deployment.doc-reserved` refuses a feature deployment document named `services.likec4` for the
109
+ same reason it already refused `subsystems.likec4`: both names are generated, overwritten
110
+ wholesale, and parsed by nothing.
111
+ - **No writer overwrites a generated views file it did not write, and refusing is now an exit 1.**
112
+ Both generated paths are filenames loam RESERVES inside `architecture/`, a directory the team
113
+ owns, and every writer of them rewrites the file whole — so a repository that already kept a
114
+ hand-authored diagram at one of those paths would have lost it, silently, on the first run after
115
+ the release that reserved the name. Reproduced before the guard existed: with a hand-authored
116
+ `architecture/services.likec4` in the tree, `loam subsystem new platform` printed
117
+ `architecture/services.likec4 updated` and destroyed it, first line and all.
118
+ - **What makes a file loam's is its FIRST LINE**, `// generated by loam`, with a leading byte-order
119
+ mark stripped before the test — a Windows editor adds one without changing a fact, and without the
120
+ strip loam would call its own output authored and refuse to regenerate it for good. A hand EDIT of
121
+ a generated file keeps that line, so it is still loam's to repair and one `loam subsystem sync`
122
+ puts it back: that has always been what `subsystem.views-stale` says. Only a file that never
123
+ carried the mark is somebody else's. **A 0-byte file at either path counts as somebody else's** —
124
+ it carries no mark — so an empty `architecture/subsystems.likec4` or
125
+ `architecture/services.likec4` is no longer filled in; delete it and re-run.
126
+ - **`loam subsystem sync` declines the one file and finishes the rest.** `--json` reports the new
127
+ `action` value **`refused`** — on `action` for `architecture/subsystems.likec4`, on
128
+ `serviceViews.action` for `architecture/services.likec4`, one predicate behind both — while the
129
+ other generated file, the per-service project files and the root `exclude` are still done and
130
+ still reported, and the **exit code is 1**. That is `loam validate`'s shape rather than a refusal
131
+ envelope, and the non-zero exit is the point: `loam validate --all` goes on reporting
132
+ `subsystem.views-stale` for that path with `loam subsystem sync` named as the repair, so a run in
133
+ a hook or a CI step must not report success over a file it can never fix.
134
+ - **`loam subsystem new` / `rm` / `move` / `rename` and `loam seed` refuse WHOLE**, before a byte is
135
+ staged: `error.code` `already-exists`, exit 1, nothing written and — for `move` and `rename` —
136
+ nothing renamed. No new code was minted, because that is what `already-exists` has always meant on
137
+ these verbs; `loam seed` can answer it for the first time. They refuse whole rather than per-file
138
+ because their entire contract is that the generated files land in the SAME commit as the change,
139
+ so completing the structural half would manufacture the very `subsystem.views-stale` state that
140
+ commit window exists never to leave behind — irreversibly, for a `move` that also stages renames.
141
+ - **The refusal sentence is one sentence, verbatim from all five writers.** It names the mark loam
142
+ looked for, says that not one byte changed, and gives the way out: rename the file to a name loam
143
+ does not reserve — every other `.likec4` under `architecture/` renders in the same project, so the
144
+ diagram is not lost — or delete it, then re-run.
145
+ - **Payload keys, all additive.** `loam subsystem sync --json` keeps `path` and `action` meaning the
146
+ subsystem file and adds `serviceViews: { path, action }`; `loam subsystem new` / `move` / `rm` /
147
+ `rename --json` keep `views` and add `serviceViews`; `loam archive --json` keeps
148
+ `subsystemViewsStale` and adds `staleViews: string[]`, the generated files it left behind. Nothing
149
+ existing changed meaning.
150
+ - The generated-file SET is now one table (`GENERATED_ARCHITECTURE_FILES`), read by the path
151
+ builders, the name reservation, both project loads that must EXCLUDE these files (the fleet map's
152
+ and `c4.fleet-project-invalid`'s), archive's staging and `loam seed`'s. Four readers that must
153
+ never disagree about which files are generated, and a name in one but not another failed silently
154
+ both ways while there was only one.
155
+
156
+ ### Added — the intra-service use-case slot, published rather than graded
157
+
158
+ - **`loam adopt` briefs `services/<…>/<id>/usecases/<name>.likec4` as an OPTIONAL target**, with the
159
+ tag grammar and a worked example: a `dynamic view` tagged `#req-<Requirement-ID>` as the first
160
+ thing in the view body, the tag declared once (in the landscape's `specification` for an extending
161
+ model, in the model's own for a standalone one), every hop backed by a relationship the model
162
+ declares, and the view id namespaced by the service because an extending model's views share the
163
+ root project's flat id namespace. Optional-and-absent prints lowercase `missing` and is not a
164
+ finding — the existing mechanism for an optional artifact, not a new one.
165
+ - **The generated AGENTS.md names the slot and its title convention**,
166
+ `'<subsystem path> / <service id> / <use case>'`, so an authored use-case view lands in the same
167
+ browser folder as its service's generated overview.
168
+ - **No new finding code, and no check on absence.** docs/DESIGN.md rule 26 — presence is never owed,
169
+ and no check may grade a view's absence — is enforced by `scripts/arch-check.mjs`, so there is no
170
+ service.usecases-missing and there never will be. The grader (`c4.valid`'s
171
+ ` · K tagged flow(s) graded`) has existed since 2026-09-03; all that was missing was anyone being
172
+ told. A usecase.flow-untagged check, which would grade a view that EXISTS, is recorded on
173
+ [ROADMAP.md](ROADMAP.md) with its promotion bar rather than built.
174
+
13
175
  ## [0.2.0-alpha.6] - 2026-09-04
14
176
 
15
177
  A service model may extend the fleet map, and the nine problem reports one fleet filed against the
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/alpha.svg?label=npm%20alpha)](https://www.npmjs.com/package/@ybotok/loam)
9
9
 
10
- > **Pre-release: `0.2.0-alpha.6`**, the fifth published release of the 0.2 line and the newest published
10
+ > **Pre-release: `0.2.0-alpha.7`**, the sixth 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 — which is what the badge above reads: `npm i @ybotok/loam@alpha`, or the
13
13
  > exact version, always installs this line. `latest` is moved onto a release by hand once it has
@@ -493,7 +493,7 @@ always preserved byte for byte, with the entry printed for manual merge.
493
493
  maps, what is lost, what must be added.
494
494
  - [ROADMAP.md](ROADMAP.md) — the evidence-backed improvement plan, priorities, non-goals and exit
495
495
  criteria.
496
- - [CHANGELOG.md](CHANGELOG.md) — released beta.1–alpha.6 plus the changes on `main` under
496
+ - [CHANGELOG.md](CHANGELOG.md) — released beta.1–alpha.7 plus the changes on `main` under
497
497
  `[Unreleased]`.
498
498
  - [docs/BENCHMARKS.md](https://github.com/ybotok/loam/blob/main/docs/BENCHMARKS.md) — what a
499
499
  fleet-sized run costs, with the method, the machine and the committed runs behind every number.
@@ -529,12 +529,12 @@ authored-capability axes with the `Realizes:` join, use cases graded as `dynamic
529
529
  `architecture/obligations.yaml` architectural obligations checked through `#obl-` tags and
530
530
  `Covers:`. Known limits, each with its owner in [ROADMAP.md](ROADMAP.md): the scheduled CI
531
531
  `stability` job has still to be observed green. Speculative `render`, health composition and UI
532
- generation come later. Behind that status stand **173 test files** (counted 2026-09-04): the count is graded
532
+ generation come later. Behind that status stand **174 test files** (counted 2026-09-07): the count is graded
533
533
  against a live readdir by `test/docs-facts.test.ts`, so this sentence fails the suite the moment it
534
534
  trails the tree.
535
535
 
536
536
  The package is published as [`@ybotok/loam`](https://www.npmjs.com/package/@ybotok/loam), currently
537
- `0.2.0-alpha.6`: the unscoped `loam` name is taken by an unrelated GDAL wrapper, so the package ships
537
+ `0.2.0-alpha.7`: the unscoped `loam` name is taken by an unrelated GDAL wrapper, so the package ships
538
538
  under the maintainer's own npm user scope. Releases are tag-driven and maintainer-only
539
539
  ([CONTRIBUTING.md](CONTRIBUTING.md)); the version this line names is pinned to `package.json` by a
540
540
  drift test, so it cannot silently trail a release.
package/ROADMAP.md CHANGED
@@ -18,19 +18,19 @@ 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 **464 TypeScript modules in 143 source packages**, with an acyclic package graph
21
+ The tree contains **468 TypeScript modules in 144 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 **173 test files**. Those four counts are deliberately stated OUTSIDE the
25
+ and the suite stands at **174 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-04 at `0.2.0-alpha.6`, and dated because neither number has a cheap derivation:_
30
+ _Measured 2026-09-07 at `0.2.0-alpha.7`, 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,772/3,772 tests**, with two `skipIf(asRoot)` cases the root gate container cannot run; the
33
- coverage gate passing with **92.78% statements, 84.65% branches, 97.08% functions, and 94.63%
32
+ **3,802/3,802 tests**, with two `skipIf(asRoot)` cases the root gate container cannot run; the
33
+ coverage gate passing with **92.88% statements, 84.83% branches, 97.14% functions, and 94.70%
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
 
@@ -193,7 +193,9 @@ merged map before writing either. Second, the consumer census read `architecture
193
193
  this shape a private store's consuming edges — which live in the models now — counted for nothing:
194
194
  `landscape.datastore-private` went silent or named the wrong service, and a nested store two
195
195
  services reach was graded nowhere. It reads every extending model's attested calls now, and
196
- `landscape.datastore-shared` fires for a nested store. Third, the wiring guard this entry landed
196
+ `landscape.datastore-shared` fires for a nested store. (That census still stands; the code named
197
+ here does not — `landscape.datastore-private` was retired on 2026-09-07, because where a store is
198
+ drawn is the team's choice. See the CHANGELOG entry.) Third, the wiring guard this entry landed
197
199
  failed open on every root `exclude` spelling except `services/<tree>/**`, `loam subsystem sync`
198
200
  never removed the stray per-service project file it told repositories to delete (and that file
199
201
  CLAIMS the model, rather than holding nothing as the design note said), and
@@ -675,6 +677,25 @@ item gets promoted — it went from here to `## Now` and from there to
675
677
  change as the `arch.spec.md` requirement it answers. Until then a service-local flow is written
676
678
  directly into the service directory in the same pull request, where git produces the conflict;
677
679
  building the feature route ahead of that evidence is the invented need the non-goals forbid.
680
+ - **A subsystem-scoped use-case slot.** A flow whose hops all sit inside one subsystem's subtree
681
+ looks like it wants a home between the two that exist — `architecture/usecases/` is the fleet's
682
+ slot, `services/<…>/<svc>/usecases/` is one service's. Deliberately NOT built (2026-09-07), and
683
+ the reason is that the capability is already there: a subsystem-scoped flow IS a fleet use case
684
+ whose hops happen to share a subtree, `architecture/usecases/<name>.likec4` already grades it, and
685
+ its view already folds into the same title-path folders as the generated subsystem view above it.
686
+ A second slot would duplicate the grader, the tag vocabulary and the archive route for no
687
+ capability that does not exist today. Promote only on evidence that a fleet's subsystem flows are
688
+ being lost among its fleet flows in the renderer's view list — which is a naming problem before it
689
+ is a slot problem, and one the title convention answers first.
690
+ - **A flow-untagged check** (it would be spelled usecase.flow-untagged): a `.likec4` beside a
691
+ service's model that mentions a `req-` prefix while no view in that project carries the tag — an
692
+ author who reached for the intra-service use-case axis and missed, whose flow renders and is
693
+ graded by nothing. It grades a view that EXISTS rather than an absence, so docs/DESIGN.md rule 26
694
+ permits it, unlike the service.usecases-missing nobody may ever build. Not built (2026-09-07) for
695
+ a mechanical reason worth stating: `core/brief/checks.ts` sits at 399 of 400 lines inside a
696
+ five-file-capped package, so a new non-ok code forces a split-module commit before any behaviour
697
+ can land, and that split should be chosen for a seam rather than for a code. Promote when a second
698
+ fleet reports a flow that rendered and was graded by nothing.
678
699
  - **Landscape scaling:** retain one landscape while conflicts are exceptional, and half of what
679
700
  this row once asked for arrived on 2026-09-04. Per-service internals now live in
680
701
  `services/<svc>/model.likec4` by default: `loam archive` routes a delta's fqn-spelled interior
package/SCHEMA.md CHANGED
@@ -62,6 +62,7 @@ docs/
62
62
  architecture/
63
63
  landscape.likec4 global C4 (system landscape)
64
64
  subsystems.likec4 one view per subsystem, views only [GENERATED: loam subsystem sync]
65
+ services.likec4 one overview view per service, views only [GENERATED: loam subsystem sync]
65
66
  permissions.yaml system-wide authorization vocabulary [authored, opt-in]
66
67
  capabilities.yaml system-wide capability vocabulary [authored, opt-in]
67
68
  obligations.yaml system-wide architectural obligations [authored, opt-in]
@@ -135,7 +136,7 @@ model.likec4 stays THE model:
135
136
  `c4.valid`, `Covers:` and every other model check read it alone, and a sibling that does not parse
136
137
  is one `usecase.flow-invalid` while the model keeps grading. Files loam does not read: a `.likec4`
137
138
  in a subsystem directory, in a feature directory outside `delta.likec4`/`usecases/`/`deployment/`,
138
- the generated `subsystems.likec4`, a sibling the service's own `likec4.config.json` `exclude`
139
+ the generated `subsystems.likec4` and `services.likec4`, a sibling the service's own `likec4.config.json` `exclude`
139
140
  covers (it is in no project the renderer loads either), and every UNTAGGED `dynamic view`
140
141
  anywhere — a diagram, never a use case.
141
142
 
@@ -153,8 +154,8 @@ a map the next reader's `loam validate --all` fails on.
153
154
  top-level `specification { }` block containing an `element` declaration:
154
155
 
155
156
  - an **extending model** declares no element kind. It is parsed INSIDE the fleet's `architecture/`
156
- project — the landscape plus every other `architecture/**/*.likec4` except the generated
157
- `subsystems.likec4` — and says what is inside the service with `extend <fqn> { … }` on the element
157
+ project — the landscape plus every other `architecture/**/*.likec4` except the generated views
158
+ files (`subsystems.likec4`, `services.likec4`) — and says what is inside the service with `extend <fqn> { … }` on the element
158
159
  the map already binds to the directory. `<fqn>` is the FULLY-QUALIFIED id: `extend orderService`
159
160
  resolves to nothing when the element is `marketplace.orderService`. No `specification` is needed,
160
161
  no partner is re-declared, and `model {}` or no model at all are both fine. `extend` accepts
@@ -181,7 +182,7 @@ exactly why its copies of the map's elements can drift.
181
182
  |---|---|---|
182
183
  | `c4.declaration-diverged` (warn, service) | standalone | an element this model and the fleet map (`architecture/`) both declare, whose `kind`, `title`, tag SET or `metadata { service }` binding differ. The two are joined by id, then by the `metadata { service }` binding both wrote, then by the map element whose id ends with the model's as a dotted tail (`paymentService` ↔ `marketplace.paymentService`) — a tail two map elements answer to joins nothing, and a tail that is a BARE last segment (`db`, `api`) is a key only for a FLEET-LEVEL map element: one no ancestor of which stands for a service, so a top-level element or a box the map draws inside a plain grouping element, never an element drawn inside a service's own. `description` is deliberately not compared — it is prose, nothing grades it, and the other four each change what a view shows or what loam resolves. When the tag sets are all that differ and copying the map's declaration would not clear them — because the difference comes off `specification { element <kind> { … } }` on one side or the other — the message says so and names the document whose specification declares them, rather than asking for a copy. One finding per divergent element; silent while the map does not parse |
183
184
  | `c4.element-unowned` (warn, service) | extending | the model declares an element OUTSIDE the element that resolves to this service. Three remedies, first first: a store or component this service owns goes INSIDE the `extend <fqn> { }` block, where its id becomes `<fqn>.<name>`; a system the service reaches belongs in the map, declared once; another service's internals belong in that service's model |
184
- | `c4.fleet-project-invalid` (warn, landscape) | extending | `--all` only, and not while `landscape.excluded` holds — this load reads what the renderer reads and so has no floor under the map, and one `exclude` entry covering the map turns every extending model's `extend` into an unresolved reference (measured on `examples/docs`: 161 warnings from one line). loam loads the fleet project the way the renderer does — a walk of the docs root, minus everything the root `exclude` covers, minus the generated `subsystems.likec4`, minus every STANDALONE service directory (`service.model-unexcluded` owns that state), minus `features/` (a delta carries its own `specification` and the directory is transient) — and reports each error NOT already present in the map alone or in any per-service project the same run made. A `.likec4` in a SUBSYSTEM directory, above every service, is in that set. This is the class no per-service grade can see: two services declaring the same tag locally, two models adding the same child id, a broken untagged sibling nobody opted in. All of them are renderer-fatal. `locations[0]` is the file the error is in and the message tail names the failure it actually is |
185
+ | `c4.fleet-project-invalid` (warn, landscape) | extending | `--all` only, and not while `landscape.excluded` holds — this load reads what the renderer reads and so has no floor under the map, and one `exclude` entry covering the map turns every extending model's `extend` into an unresolved reference (measured on `examples/docs`: 161 warnings from one line). loam loads the fleet project the way the renderer does — a walk of the docs root, minus everything the root `exclude` covers, minus the generated views files (`subsystems.likec4`, `services.likec4`), minus every STANDALONE service directory (`service.model-unexcluded` owns that state), minus `features/` (a delta carries its own `specification` and the directory is transient) — and reports each error NOT already present in the map alone or in any per-service project the same run made. A `.likec4` in a SUBSYSTEM directory, above every service, is in that set. This is the class no per-service grade can see: two services declaring the same tag locally, two models adding the same child id, a broken untagged sibling nobody opted in. All of them are renderer-fatal. `locations[0]` is the file the error is in and the message tail names the failure it actually is |
185
186
  | `service.likec4-config-stray` (warn, landscape) | extending | a `likec4.config.json` beside an extending model. The renderer registers it as a project of its own rooted at that directory, so the docs root stops being a one-project workspace (`likec4 validate .` then refuses without `--project`), and wherever that nested project claims the model it parses alone, out of the root project where the map's kinds are: the model does not resolve and `export json --project fleet` loses the service's containers (measured, likec4 1.59.2 — which of the two happens depends on the workspace; on `examples/docs` the fleet export was byte-identical with and without the file, on a seeded fleet the containers went). Run `loam subsystem sync`: it deletes the file. Deleting it by hand works too. loam never writes one beside an extending model |
186
187
  | `service.model-excluded` (warn, landscape) | extending | the root `likec4.config.json`'s `exclude` covers this model's directory, so the renderer never loads it and the service is a box with nothing inside it. An entry is read the way the renderer reads it (measured, likec4 1.59.2): a bare entry is a raw path PREFIX starting on a `/` boundary, so `services`, `services/`, `services/*`, `services/**`, `services/**/*`, a wildcard segment in the middle and the bare `services/pay` all cover `services/payment-service` — while the glob form `services/pay/**` wants the literal separator and covers nothing there. An entry shaped like a FILE rather than a directory (`services/**/model.likec4`, `**/model.likec4`) is reported too, with a different repair: `loam subsystem sync` maintains only the `services/` directory entries, so that shape has to be narrowed or deleted by hand. The finding quotes the entry verbatim, because the repair is to edit that line; `loam subsystem sync` is the repair for the directory shape |
187
188
  | `service.model-unexcluded` (warn, landscape) | standalone | the mirror, and the one that costs most: the root `exclude` does NOT cover a standalone model's directory, so the renderer merges it into the root project and reports every kind and element it declares as a duplicate — blanking the whole project, not just this service. An entry already covering the directory in any of the spellings above satisfies it, and `sync` appends no second glob for one. Same repair |
@@ -218,24 +219,34 @@ So a hop naming `<service-fqn>.<container>` resolves in neither the preview nor
218
219
  leaves, and `usecase.flow-invalid` refuses — where the un-routed preview resolved it, passed the
219
220
  gate `--approve` cannot override, and landed a flow the next `loam validate --all` failed on.
220
221
 
221
- **Private stores.** A store one service owns is nested UNDER that service's element — written in
222
- the service's `extend <fqn> { store = database … }` block, or inside the service's element on the
223
- map when the map draws that service's containers. The `extend` placement needs an EXTENDING model:
224
- a model that declares its own `specification` is parsed alone, so `extend <fqn>` resolves nothing
225
- there, and for such a service the map is the only placement (or migrate the model — see "Two
226
- shapes of a service model" above). `landscape.datastore-private` names whichever of the two the
227
- consumer's model shape allows. Drawn beside the services instead it reads as a
228
- system in its own right (`landscape.datastore-private`). Nesting closes no door: loam counts a
222
+ **Private stores.** **Where a store is drawn is the team's choice.** A store one service owns may be
223
+ nested UNDER that service's element — written in the service's `extend <fqn> { store = database … }`
224
+ block, or inside the service's element on the map when the map draws that service's containers — or
225
+ drawn beside the services as an element of its own. Both are first-class and loam grades neither as
226
+ wrong. Until 2026-09-07 the second earned `landscape.datastore-private` at one consumer, a warning
227
+ whose whole message prescribed deleting the peer while the identical store nested was silent: it
228
+ graded the drawing rather than the architecture, and it is retired. Infrastructure is not a service
229
+ either — an element whose kind is `database`, `queue`, `topic`, `cache` or `bucket` is out of the
230
+ service census, so an owned store drawn at fleet level is not `landscape.service-undocumented`, is
231
+ not a `loam gate` partner, and mints no `service.exists` verification claim. The `extend` placement
232
+ needs an EXTENDING model: a model that declares its own `specification` is parsed alone, so
233
+ `extend <fqn>` resolves nothing there, and for such a service the map is the only nesting site (or
234
+ migrate the model — see "Two shapes of a service model" above).
235
+
236
+ What the choice DOES decide is one thing, and it is a fact about LikeC4 rather than a preference: **a
237
+ second consumer's edge resolves only against a declaration the MAP holds**, because a per-service
238
+ project is `architecture/` plus one model. Draw a container-level edge to `<owner-fqn>.<store>` in
239
+ another service's model while the store lives in the owner's `extend` block and that model is
240
+ `c4.invalid` ("Could not resolve reference to Referenceable named '<store>'"). So a store a second
241
+ consumer will name has to be on the map — nested inside the owner's element there, or beside the
242
+ services. That was always the load-bearing half; the prescription around it was not.
243
+
244
+ Nesting closes no door in the other direction: loam counts a
229
245
  consumer a service's EXTENDING model attests exactly as it counts one the map draws, so a nested
230
246
  store two services reach is `landscape.datastore-shared` — the store's owner plus every service
231
247
  whose model draws an outbound call at it or below it. The second consumer draws that call as a
232
248
  container-level edge to `<owner-fqn>.<store>` in its OWN model, and the service-level twin on the
233
- map as usual. Which of the two placements to choose is decided by that edge: a per-service project
234
- is `architecture/` plus one model, so the edge resolves only when the MAP declares the store —
235
- written in the owner's `extend` block, the same edge is `c4.invalid` on the consumer ("Could not
236
- resolve reference to Referenceable named '<store>'"). Nest it on the map when a second consumer is
237
- foreseeable, and in the model when it is not. The choice is cheap in ONE direction, and the
238
- direction is the whole of the rule. The two placements give the store the same id,
249
+ map as usual. The two placements give the store the same id,
239
250
  `<owner-fqn>.<store>`, so moving the declaration OUT of the owner's `extend` block into the owner's
240
251
  element on the map rewrites no edge and no `Covers:` line — measured on `examples/docs`, where
241
252
  moving `db` out of order-service's `extend` block into `orderService` on the map left the model at
@@ -246,7 +257,10 @@ order-service's `extend` block is `c4.invalid` on checkout-web's model ("Could n
246
257
  reference to Referenceable named 'db'"), or `landscape.invalid` plus one `spine.landscape-invalid`
247
258
  per service when that second edge is drawn on the map. `landscape.datastore-shared` needs a second
248
259
  consumer to fire, so it always prints in the state where map→extend breaks, and its message says
249
- so. The consumer census is fleet-scope: it reads
260
+ so. It is the one datastore grade left, it fires at two consumers for both placements alike, and it
261
+ states a coupling rather than prescribing a move: shared means the same DATA, and if two services
262
+ only share a host or a cluster the honest model is one private store each. The consumer census is
263
+ fleet-scope: it reads
250
264
  EXTENDING models only, because their ids are the map's fully-qualified ids and the join is exact,
251
265
  while a standalone model's ids are its own file's and are never matched to a landscape element.
252
266
 
@@ -433,11 +447,12 @@ the file behind.
433
447
  `loam subsystem sync` is the one regenerator (it also *removes* the file when the tree has no
434
448
  subsystems), every `loam subsystem` write verb regenerates it inside the same transaction, and
435
449
  `validate --all` grades staleness by **content comparison** (line endings normalized; a CRLF
436
- checkout changes no fact) — exactly one error,
437
- `subsystem.views-stale`, on exactly that file; nothing in loam ever parses it: a views-only document
438
- does not parse standalone (its `include` lines reference elements the landscape defines), and it
439
- holds **static** views, whose contents loam never reads in any document. Only the LikeC4 renderer,
440
- which merges the whole `architecture/` project, resolves its includes.
450
+ checkout changes no fact) — one error,
451
+ `subsystem.views-stale`, per stale generated file, each naming its own path (there are two of them;
452
+ `architecture/services.likec4` is below); nothing in loam ever parses either: a views-only document
453
+ does not parse standalone (its `include` lines reference elements the landscape defines), and both
454
+ hold **static** views, whose contents loam never reads in any document. Only the LikeC4 renderer,
455
+ which merges the whole `architecture/` project, resolves their includes.
441
456
 
442
457
  `sync` refuses to rewrite this file while the `architecture/` project cannot be read. The render
443
458
  needs the map's elements and its global-style census, and a tolerant "no elements" answer wrote a
@@ -452,7 +467,111 @@ surface. With no file yet, and inside a tree-changing verb's commit window (`new
452
467
  `rename`, whose contract is that the file lands with the change), the tolerant shape is written as
453
468
  before and the same note is printed.
454
469
 
455
- The tree's **second renderer file** is per service — `services/<…>/<svc>/likec4.config.json`,
470
+ ### `architecture/services.likec4` — one overview view per service
471
+
472
+ The **second generated views file**, written by the same `loam subsystem sync`, in the same commit,
473
+ removed by it when the map draws no service, and graded by the same `subsystem.views-stale` on its
474
+ own path. Views only, one per service the fleet map draws, sorted by service directory id:
475
+
476
+ ```
477
+ view service_payment_2dservice of marketplace.paymentService {
478
+ title 'Payments / payment-service'
479
+ global style services
480
+ include *
481
+ include kafka.orderEvents
482
+ include marketplace.checkoutWeb
483
+ include stripe
484
+ exclude * -> *
485
+ include -> marketplace.paymentService
486
+ include marketplace.paymentService ->
487
+ include marketplace.paymentService.* -> marketplace.paymentService.*
488
+ }
489
+ ```
490
+
491
+ **Why the peers are named one by one, measured at the 1.59.2 pin on a 120-service and a 58-service
492
+ fleet.** The obvious recipe — `include <fqn> -> *` and `include * -> <fqn>` — is wrong at fleet
493
+ scale and looks perfect on a small flat fixture, which is how it would have shipped. LikeC4 draws a
494
+ counterpart at the highest ancestor that is not also an ancestor of the subject, so on a map that
495
+ GROUPS its services (which is exactly what a 100-service fleet needs at fleet level) every peer
496
+ collapses to its group box: the naive view of one service in the 120-service fleet drew six group
497
+ boxes and **0 of its 10 nodes had drill-down**. loam already reads elements and relationships, so it
498
+ names each peer itself — the far endpoint of every edge touching the service or anything inside it,
499
+ spelled as the map declares it, so an edge at a topic names the topic and not the broker, which is
500
+ the truer picture and the one the map already draws. `exclude * -> *` then drops the peer-to-peer
501
+ cross traffic (23 edges down to 7 on the same fixture), and the three lines after it put back
502
+ exactly the subject's own: the UNARY `include -> <fqn>` / `include <fqn> ->`, never the binary
503
+ wildcard, which re-introduces the parent group boxes as extra nodes (16 instead of 10). `include
504
+ <fqn>.* -> <fqn>.*` is the interior wiring, and it is legal on a childless element — it simply
505
+ matches nothing — which is what makes one line work for both model shapes: an extending model's
506
+ containers are in the root project and appear, a standalone model's are not and the line is silent.
507
+
508
+ Measured at 58 services in 11 groups: a median of **10 nodes and 8 edges** per generated view, every
509
+ peer that is a service carrying drill-down into its own generated view, and `npx likec4 export json`
510
+ — which layouts every view — costing about **7 ms per generated view** (1.3 s with 120 of them
511
+ against 451 ms without). `likec4 start` layouts on demand, so a reader pays for the one view they
512
+ open, and the generated views are all small BY CONSTRUCTION; the expensive view is the hand-authored
513
+ fleet one, and it is unaffected.
514
+
515
+ **Drill-down is the `of`, and it costs one keyword.** `view <id> of <element-fqn>` names the view's
516
+ subject, and LikeC4 then computes `navigateTo` on that element's node in **every other view that
517
+ draws it** — no authored links anywhere. So one generated view per service makes the fleet map,
518
+ every subsystem view and every other service view click-through into the service. The subsystem
519
+ views take the same keyword when the map holds an element owning exactly that subsystem's members,
520
+ which closes the mesh — `fleet → payments → payments/service-22 → identity/service-45 → identity`,
521
+ every hop generated, none authored.
522
+
523
+ **Two views of one element are legal, and the FIRST DOCUMENT ALPHABETICALLY wins** the default
524
+ navigation target — measured by renaming the files, LikeC4's load order being alphabetical by path.
525
+ Both stay reachable: inside a view of the subject, the subject's own node navigates to the OTHER
526
+ view of the same element, so the renderer cycles between siblings and nothing is orphaned.
527
+ `architecture/landscape.likec4` sorts before `architecture/services.likec4`, so a team's authored
528
+ `view of <element>` written in the map keeps precedence over the generated one, and neither is lost.
529
+ loam does not detect this and never will: **docs/DESIGN.md rule 26 forbids loam reading what a view
530
+ SHOWS**, in any document, so it may not look at an authored `view of` to decide whether to emit. It
531
+ emits for every service the map draws, and the alphabet settles the rest.
532
+
533
+ **The first line is the deed, and every writer checks it.** Both generated views files open with a
534
+ `// generated by loam` mark, and that mark is what makes the path loam's rather than the team's.
535
+ These are the only two filenames loam RESERVES inside `architecture/`, a directory that is otherwise
536
+ entirely authored, so a repository that already had a file at one of those paths must not lose it to
537
+ an upgrade — the archive's promise that authored bytes stay authored holds here too. Every writer of
538
+ these paths reads the first line first: `loam subsystem sync` reports that one file `refused`,
539
+ writes nothing for it, still does the rest of its work, and exits 1; `loam subsystem new`, `rm`,
540
+ `move` and `rename` and `loam seed` refuse the WHOLE operation with `already-exists`, writing,
541
+ renaming and staging nothing, because their commit window exists precisely to land the generated
542
+ file in the same commit as the change to the tree — completing half of that would manufacture the
543
+ `subsystem.views-stale` state the window exists never to leave behind. The consequence to know: a
544
+ file with no mark is never regenerated, and an EMPTY file carries no mark either, so a truncated one
545
+ must be deleted by hand before sync will write it again. loam's own staging is atomic and cannot
546
+ produce one.
547
+
548
+ **Titles are folders, because there is nothing else.** LikeC4 has **no view-folder syntax** —
549
+ `views { group 'X' { … } }` is a parse error at the pinned version and the AST carries no view-group
550
+ type at all. What it has is the title: a view title is split on `/` into browser folders and only
551
+ the last segment is shown. So a per-service view is titled `<subsystem path> / <service directory
552
+ id>`, built from the same path helper the subsystem views use, and the two generated files fold into
553
+ ONE folder tree mirroring `services/`. An unfiled service is its own id at the root. The service
554
+ **directory id** is used rather than the map element's title: it is the name every loam command
555
+ addresses the service by, it cannot drift from the map, and a directory id cannot contain `/`. A
556
+ `dynamic view` you author under `services/<…>/<id>/usecases/` lands in the same folder when you
557
+ title it `'<subsystem path> / <service id> / <use case>'` — a convention loam states and never
558
+ grades.
559
+
560
+ **Which services appear.** Exactly those the element→service join resolves, the same join the
561
+ subsystem views use. A service the map does not draw is omitted — its absence is already
562
+ `landscape.service-unmodelled`, and rendering only what the committed map resolves is what keeps the
563
+ output a function of committed bytes. **View ids** are `service_` plus the same hex escaping the
564
+ subsystem view ids use, so the two generated files cannot collide with each other, and an authored
565
+ view claiming one is `subsystem.view-id-collision` — which names the file and the reserved prefix. A
566
+ `service_` id is reserved only for a service the map actually draws. **Style**: `global style
567
+ services`, presence-detected off the parsed map's declared style ids exactly as `subsystems` is, and
568
+ written only when a group of that name is declared — a reference to an undeclared group is a parse
569
+ error that blanks the whole project.
570
+
571
+ ### The per-service LikeC4 project file
572
+
573
+ A different artifact from the two generated views files above, and the tree's other renderer
574
+ concern: `services/<…>/<svc>/likec4.config.json`,
456
575
  which `loam subsystem sync` also writes, beside every **standalone** `model.likec4` that has none.
457
576
  An extending model needs no such file and never gets one: it belongs to the root project, which is
458
577
  where it parses, and a project file beside it is `service.likec4-config-stray` — which `sync`
@@ -975,7 +1094,7 @@ There is no manifest of services: the directories under `services/` **are** the
975
1094
  touched/untouched, never a set difference over the attested calls, because loam cannot tell a
976
1095
  call the map forgot from one the fleet deliberately does not draw at its own altitude. "Edge in
977
1096
  the map" means an edge in the `architecture/` PROJECT — the landscape plus every other `.likec4`
978
- under it, minus the generated `subsystems.likec4` — and `loam adopt` reads the same set. An
1097
+ under it, minus the generated views files (`subsystems.likec4`, `services.likec4`) — and `loam adopt` reads the same set. An
979
1098
  `#external` element is never the subject, whatever directory it is bound to.
980
1099
 
981
1100
  **`architecture/landscape.likec4` is required, not optional.** Its absence is a finding, never a
@@ -1440,9 +1559,9 @@ the CI escalation.
1440
1559
  match, with a did-you-mean hint. Deliberately the service model and never the landscape read on
1441
1560
  its own: the
1442
1561
  landscape carries only what crosses the service boundary, the model carries everything the service
1443
- touches (a private store nested inside the service's own element — `landscape.datastore-private`
1444
- is the check pushing it there, and `landscape.datastore-shared` is what a second consumer of that
1445
- nested store now earns), and that is the set the on-call file must agree with. For an extending model
1562
+ touches (a private store nested inside the service's own element — one of the two placements
1563
+ "Private stores" above leaves to the team, and `landscape.datastore-shared` is what a second
1564
+ consumer of that nested store earns), and that is the set the on-call file must agree with. For an extending model
1446
1565
  the set is its own SLICE: the elements it adds, plus the partners its own edges point at and their
1447
1566
  ancestors — so a dependency named `kafka` resolves through an edge that names `kafka.orderEvents`,
1448
1567
  and a peer service resolves through the `metadata { service }` binding on the map's element the
@@ -2947,9 +3066,10 @@ arrived. Per-service internals now live in `services/<svc>/model.likec4` **by de
2947
3066
  where a second team adopting a service writes, so it adds nothing to this file. It is a default and
2948
3067
  not a prohibition: the map MAY hold a service's interior, and sometimes must. A store a second
2949
3068
  service will reach has to be declared where that consumer's project can see it — a per-service
2950
- project is `architecture/` plus one model — so it is nested inside the owner's element ON THE MAP,
2951
- which is exactly what "Private stores" above prescribes and what `landscape.datastore-private` and
2952
- `landscape.datastore-shared` offer as a placement. A delta spelling a parent in the legacy
3069
+ project is `architecture/` plus one model — so it is drawn ON THE MAP, nested inside the owner's
3070
+ element or beside the services, whichever the team prefers. Which of the two is nobody's business
3071
+ but the team's: "Private stores" above leaves the placement open and loam grades neither, so nothing
3072
+ here says a store must move. A delta spelling a parent in the legacy
2953
3073
  title-join lands its container on the map too. What an extending model owns is what it DECLARES,
2954
3074
  not everything below the service's element. `loam archive` routes rather than forbids — a delta's
2955
3075
  additions nested under a service whose model extends the map, with the whole parent chain spelled