@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.
- package/CHANGELOG.md +162 -0
- package/README.md +4 -4
- package/ROADMAP.md +27 -6
- package/SCHEMA.md +152 -32
- package/dist/commands/archive/run.js +49 -22
- package/dist/commands/archive/run.js.map +1 -1
- package/dist/commands/seed/commit.js +34 -1
- package/dist/commands/seed/commit.js.map +1 -1
- package/dist/commands/seed/plan.js +64 -13
- package/dist/commands/seed/plan.js.map +1 -1
- package/dist/commands/subsystem/edit.js +4 -2
- package/dist/commands/subsystem/edit.js.map +1 -1
- package/dist/commands/subsystem/move.js +55 -17
- package/dist/commands/subsystem/move.js.map +1 -1
- package/dist/commands/subsystem/sync.js +142 -45
- package/dist/commands/subsystem/sync.js.map +1 -1
- package/dist/commands/subsystem/txn/report.js +55 -5
- package/dist/commands/subsystem/txn/report.js.map +1 -1
- package/dist/commands/subsystem/txn/txn.js +118 -21
- package/dist/commands/subsystem/txn/txn.js.map +1 -1
- package/dist/commands/subsystem/txn/views.js +51 -8
- package/dist/commands/subsystem/txn/views.js.map +1 -1
- package/dist/commands/validate/checks/health-deps.js +9 -6
- package/dist/commands/validate/checks/health-deps.js.map +1 -1
- package/dist/commands/validate/fleet/census.js +26 -3
- package/dist/commands/validate/fleet/census.js.map +1 -1
- package/dist/commands/validate/fleet/landscape.js +12 -9
- package/dist/commands/validate/fleet/landscape.js.map +1 -1
- package/dist/commands/validate/fleet/map/attest.js +4 -5
- package/dist/commands/validate/fleet/map/attest.js.map +1 -1
- package/dist/commands/validate/fleet/map/bindings.js +15 -2
- package/dist/commands/validate/fleet/map/bindings.js.map +1 -1
- package/dist/commands/validate/fleet/map/consumers.js +43 -122
- package/dist/commands/validate/fleet/map/consumers.js.map +1 -1
- package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -1
- package/dist/commands/validate/fleet/views/fleet-project.js +1 -1
- package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -1
- package/dist/commands/validate/fleet/views/ids.js +33 -7
- package/dist/commands/validate/fleet/views/ids.js.map +1 -1
- package/dist/commands/validate/fleet/views/root-documents.js +9 -7
- package/dist/commands/validate/fleet/views/root-documents.js.map +1 -1
- package/dist/commands/validate/fleet/views/stale.js +31 -14
- package/dist/commands/validate/fleet/views/stale.js.map +1 -1
- package/dist/core/agent/agents-md/command-map.js +1 -1
- package/dist/core/agent/agents-md/command-map.js.map +1 -1
- package/dist/core/agent/agents-md/map/subsystem.js +27 -14
- package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
- package/dist/core/agent/agents-md/refusals.js +5 -3
- package/dist/core/agent/agents-md/refusals.js.map +1 -1
- package/dist/core/agent/agents-md.js +16 -7
- package/dist/core/agent/agents-md.js.map +1 -1
- package/dist/core/agent/workflows/check.js +5 -6
- package/dist/core/agent/workflows/check.js.map +1 -1
- package/dist/core/agent/workflows/reference/spine.js +12 -6
- package/dist/core/agent/workflows/reference/spine.js.map +1 -1
- package/dist/core/brief/brief.js +25 -8
- package/dist/core/brief/brief.js.map +1 -1
- package/dist/core/brief/targets.js +64 -0
- package/dist/core/brief/targets.js.map +1 -1
- package/dist/core/brief/unchecked.js +1 -1
- package/dist/core/brief/unchecked.js.map +1 -1
- package/dist/core/c4/project/architecture.js +31 -9
- package/dist/core/c4/project/architecture.js.map +1 -1
- package/dist/core/c4/project/documents.js +5 -4
- package/dist/core/c4/project/documents.js.map +1 -1
- package/dist/core/c4/project/load.js +8 -4
- package/dist/core/c4/project/load.js.map +1 -1
- package/dist/core/c4/project/staged.js +7 -3
- package/dist/core/c4/project/staged.js.map +1 -1
- package/dist/core/deployment/delta.js +12 -10
- package/dist/core/deployment/delta.js.map +1 -1
- package/dist/core/gate/checks.js +5 -1
- package/dist/core/gate/checks.js.map +1 -1
- package/dist/core/gate/partners.js +60 -17
- package/dist/core/gate/partners.js.map +1 -1
- package/dist/core/repo/paths.js +34 -29
- package/dist/core/repo/paths.js.map +1 -1
- package/dist/core/repo/tree/render/body.js +45 -6
- package/dist/core/repo/tree/render/body.js.map +1 -1
- package/dist/core/repo/tree/render/service/header.js +125 -0
- package/dist/core/repo/tree/render/service/header.js.map +1 -0
- package/dist/core/repo/tree/render/service/peers.js +28 -0
- package/dist/core/repo/tree/render/service/peers.js.map +1 -0
- package/dist/core/repo/tree/render/service/view-id.js +67 -0
- package/dist/core/repo/tree/render/service/view-id.js.map +1 -0
- package/dist/core/repo/tree/render/service/views.js +78 -0
- package/dist/core/repo/tree/render/service/views.js.map +1 -0
- package/dist/core/repo/tree/render/stale.js +19 -2
- package/dist/core/repo/tree/render/stale.js.map +1 -1
- package/dist/core/repo/tree/render/views.js +102 -33
- package/dist/core/repo/tree/render/views.js.map +1 -1
- package/dist/core/verify/checklist.js +42 -10
- package/dist/core/verify/checklist.js.map +1 -1
- package/dist/core/vocabulary/issue.js.map +1 -1
- package/dist/core/vocabulary/maturity.js +41 -1
- package/dist/core/vocabulary/maturity.js.map +1 -1
- package/examples/README.md +7 -3
- package/examples/docs/architecture/landscape.likec4 +13 -7
- package/examples/docs/architecture/services.likec4 +70 -0
- package/examples/docs/architecture/subsystems.likec4 +3 -0
- package/examples/docs/services/order-service/model.likec4 +10 -8
- package/examples/docs/services/payment-service/model.likec4 +6 -4
- package/examples/docs/services/platform/identity-service/model.likec4 +9 -7
- package/examples/docs/services/platform/notification-service/model.likec4 +4 -5
- 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
|
[](https://github.com/ybotok/loam/actions/workflows/ci.yml)
|
|
8
8
|
[](https://www.npmjs.com/package/@ybotok/loam)
|
|
9
9
|
|
|
10
|
-
> **Pre-release: `0.2.0-alpha.
|
|
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.
|
|
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 **
|
|
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.
|
|
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 **
|
|
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 **
|
|
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-
|
|
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,
|
|
33
|
-
coverage gate passing with **92.
|
|
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.
|
|
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.**
|
|
222
|
-
the service's `extend <fqn> { store = database … }`
|
|
223
|
-
map when the map draws that service's containers
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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.
|
|
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.
|
|
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) —
|
|
437
|
-
`subsystem.views-stale`,
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
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 —
|
|
1444
|
-
|
|
1445
|
-
nested store
|
|
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
|
|
2951
|
-
|
|
2952
|
-
|
|
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
|