@ybotok/loam 0.2.0-alpha.4 → 0.2.0-alpha.6
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 +947 -0
- package/MIGRATING-from-OpenSpec.md +7 -1
- package/QUICKSTART.md +8 -3
- package/README.md +20 -13
- package/ROADMAP.md +329 -22
- package/SCHEMA.md +472 -24
- package/WORKFLOW.md +23 -10
- package/dist/commands/adopt/render.js +47 -8
- package/dist/commands/adopt/render.js.map +1 -1
- package/dist/commands/archive/plan/landscape.js +45 -1
- package/dist/commands/archive/plan/landscape.js.map +1 -1
- package/dist/commands/archive/plan/model/extending.js +85 -0
- package/dist/commands/archive/plan/model/extending.js.map +1 -0
- package/dist/commands/archive/run.js +5 -2
- package/dist/commands/archive/run.js.map +1 -1
- package/dist/commands/context/print.js +11 -1
- package/dist/commands/context/print.js.map +1 -1
- package/dist/commands/doctor.js +46 -0
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/explore.js +12 -2
- package/dist/commands/explore.js.map +1 -1
- package/dist/commands/list/views.js +5 -1
- package/dist/commands/list/views.js.map +1 -1
- package/dist/commands/seed/plan.js +97 -3
- package/dist/commands/seed/plan.js.map +1 -1
- package/dist/commands/seed/seed.js +7 -2
- package/dist/commands/seed/seed.js.map +1 -1
- package/dist/commands/show/feature.js +2 -1
- package/dist/commands/show/feature.js.map +1 -1
- package/dist/commands/show/marks.js +3 -5
- package/dist/commands/show/marks.js.map +1 -1
- package/dist/commands/show/service.js +39 -18
- package/dist/commands/show/service.js.map +1 -1
- package/dist/commands/subsystem/sync.js +184 -39
- package/dist/commands/subsystem/sync.js.map +1 -1
- package/dist/commands/subsystem/txn/exclude.js +132 -0
- package/dist/commands/subsystem/txn/exclude.js.map +1 -0
- package/dist/commands/subsystem/txn/report.js +124 -0
- package/dist/commands/subsystem/txn/report.js.map +1 -0
- package/dist/commands/subsystem/txn/txn.js +29 -3
- package/dist/commands/subsystem/txn/txn.js.map +1 -1
- package/dist/commands/subsystem/txn/views.js +113 -16
- package/dist/commands/subsystem/txn/views.js.map +1 -1
- package/dist/commands/validate/arch-coverage.js +16 -9
- package/dist/commands/validate/arch-coverage.js.map +1 -1
- package/dist/commands/validate/checks/fleet-shape.js +30 -77
- package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
- package/dist/commands/validate/checks/vocabulary.js +4 -19
- package/dist/commands/validate/checks/vocabulary.js.map +1 -1
- package/dist/commands/validate/feature.js +1 -1
- package/dist/commands/validate/feature.js.map +1 -1
- package/dist/commands/validate/fleet/census.js +11 -10
- package/dist/commands/validate/fleet/census.js.map +1 -1
- package/dist/commands/validate/fleet/landscape.js +128 -114
- package/dist/commands/validate/fleet/landscape.js.map +1 -1
- package/dist/commands/validate/fleet/load.js +66 -0
- package/dist/commands/validate/fleet/load.js.map +1 -1
- package/dist/commands/validate/fleet/map/attest.js +103 -0
- package/dist/commands/validate/fleet/map/attest.js.map +1 -0
- package/dist/commands/validate/fleet/map/bindings.js +96 -0
- package/dist/commands/validate/fleet/map/bindings.js.map +1 -0
- package/dist/commands/validate/fleet/map/consumers.js +298 -0
- package/dist/commands/validate/fleet/map/consumers.js.map +1 -0
- package/dist/commands/validate/fleet/map/isolation.js +116 -0
- package/dist/commands/validate/fleet/map/isolation.js.map +1 -0
- package/dist/commands/validate/fleet/map/usecases.js +56 -0
- package/dist/commands/validate/fleet/map/usecases.js.map +1 -0
- package/dist/commands/validate/fleet/usecases/steps.js +49 -17
- package/dist/commands/validate/fleet/usecases/steps.js.map +1 -1
- package/dist/commands/validate/fleet/views/fleet-project.js +217 -0
- package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -0
- package/dist/commands/validate/fleet/views/ids.js +13 -9
- package/dist/commands/validate/fleet/views/ids.js.map +1 -1
- package/dist/commands/validate/fleet/views/projects.js +278 -0
- package/dist/commands/validate/fleet/views/projects.js.map +1 -0
- package/dist/commands/validate/fleet/views/root-documents.js +97 -0
- package/dist/commands/validate/fleet/views/root-documents.js.map +1 -0
- package/dist/commands/validate/fleet/views/stale.js +2 -2
- package/dist/commands/validate/fleet/views/stale.js.map +1 -1
- package/dist/commands/validate/service/model/diverged.js +226 -0
- package/dist/commands/validate/service/model/diverged.js.map +1 -0
- package/dist/commands/validate/service/model/grade.js +163 -0
- package/dist/commands/validate/service/model/grade.js.map +1 -0
- package/dist/commands/validate/service/service.js +110 -34
- package/dist/commands/validate/service/service.js.map +1 -1
- package/dist/commands/validate/service/specs.js +39 -5
- package/dist/commands/validate/service/specs.js.map +1 -1
- package/dist/commands/validate/service/spine.js +41 -3
- package/dist/commands/validate/service/spine.js.map +1 -1
- package/dist/commands/validate/service/usecases/tags.js +113 -0
- package/dist/commands/validate/service/usecases/tags.js.map +1 -0
- package/dist/commands/validate/service/usecases/usecases.js +209 -0
- package/dist/commands/validate/service/usecases/usecases.js.map +1 -0
- package/dist/commands/validate/validate.js +10 -24
- package/dist/commands/validate/validate.js.map +1 -1
- package/dist/core/agent/agents-md/artifacts.js +1 -0
- package/dist/core/agent/agents-md/artifacts.js.map +1 -1
- package/dist/core/agent/agents-md/command-map.js +18 -2
- package/dist/core/agent/agents-md/command-map.js.map +1 -1
- package/dist/core/agent/agents-md/cycle.js +24 -9
- package/dist/core/agent/agents-md/cycle.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/context.js +4 -3
- package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -1
- package/dist/core/agent/agents-md/map/subsystem.js +51 -2
- package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
- package/dist/core/agent/agents-md/map/usecases.js +45 -3
- package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
- package/dist/core/agent/agents-md.js +14 -4
- package/dist/core/agent/agents-md.js.map +1 -1
- package/dist/core/agent/support/report.js +39 -5
- package/dist/core/agent/support/report.js.map +1 -1
- package/dist/core/agent/workflows/adopt.js +125 -6
- package/dist/core/agent/workflows/adopt.js.map +1 -1
- package/dist/core/agent/workflows/check.js +35 -19
- package/dist/core/agent/workflows/check.js.map +1 -1
- package/dist/core/agent/workflows/closing.js +3 -1
- package/dist/core/agent/workflows/closing.js.map +1 -1
- package/dist/core/brief/brief.js +63 -11
- package/dist/core/brief/brief.js.map +1 -1
- package/dist/core/brief/checks.js +138 -3
- package/dist/core/brief/checks.js.map +1 -1
- package/dist/core/brief/landscape.js +212 -129
- package/dist/core/brief/landscape.js.map +1 -1
- package/dist/core/brief/map/extend.js +81 -0
- package/dist/core/brief/map/extend.js.map +1 -0
- package/dist/core/brief/map/owed.js +284 -0
- package/dist/core/brief/map/owed.js.map +1 -0
- package/dist/core/brief/targets.js +30 -26
- package/dist/core/brief/targets.js.map +1 -1
- package/dist/core/brief/unchecked.js +12 -2
- package/dist/core/brief/unchecked.js.map +1 -1
- package/dist/core/c4/likec4.js +23 -1
- package/dist/core/c4/likec4.js.map +1 -1
- package/dist/core/c4/parsed/deployment.js +5 -3
- package/dist/core/c4/parsed/deployment.js.map +1 -1
- package/dist/core/c4/parsed/view-ids.js +39 -1
- package/dist/core/c4/parsed/view-ids.js.map +1 -1
- package/dist/core/c4/prefetch/fleet.js +142 -0
- package/dist/core/c4/prefetch/fleet.js.map +1 -0
- package/dist/core/c4/project/architecture.js +52 -4
- package/dist/core/c4/project/architecture.js.map +1 -1
- package/dist/core/c4/project/batch.js +140 -0
- package/dist/core/c4/project/batch.js.map +1 -0
- package/dist/core/c4/project/documents.js +9 -4
- package/dist/core/c4/project/documents.js.map +1 -1
- package/dist/core/c4/project/load.js +77 -38
- package/dist/core/c4/project/load.js.map +1 -1
- package/dist/core/c4/project/staged.js +46 -12
- package/dist/core/c4/project/staged.js.map +1 -1
- package/dist/core/c4/resolve/attested.js +0 -0
- package/dist/core/c4/resolve/attested.js.map +1 -0
- package/dist/core/c4/resolve/service.js +8 -17
- package/dist/core/c4/resolve/service.js.map +1 -1
- package/dist/core/c4/resolve/steps.js +18 -0
- package/dist/core/c4/resolve/steps.js.map +1 -1
- package/dist/core/c4/root-project/exclude.js +352 -0
- package/dist/core/c4/root-project/exclude.js.map +1 -0
- package/dist/core/c4/service-model/documents.js +47 -0
- package/dist/core/c4/service-model/documents.js.map +1 -0
- package/dist/core/c4/service-model/fleet/extending.js +82 -0
- package/dist/core/c4/service-model/fleet/extending.js.map +1 -0
- package/dist/core/c4/service-model/load.js +165 -0
- package/dist/core/c4/service-model/load.js.map +1 -0
- package/dist/core/c4/service-model/renderer.js +128 -0
- package/dist/core/c4/service-model/renderer.js.map +1 -0
- package/dist/core/c4/service-model/shape.js +126 -0
- package/dist/core/c4/service-model/shape.js.map +1 -0
- package/dist/core/c4/service-model/slice.js +188 -0
- package/dist/core/c4/service-model/slice.js.map +1 -0
- package/dist/core/c4/source-scan.js +26 -0
- package/dist/core/c4/source-scan.js.map +1 -1
- package/dist/core/c4/splice/authored-source.js +60 -13
- package/dist/core/c4/splice/authored-source.js.map +1 -1
- package/dist/core/c4/splice/contract.js.map +1 -1
- package/dist/core/c4/splice/identity/declared.js +87 -0
- package/dist/core/c4/splice/identity/declared.js.map +1 -0
- package/dist/core/c4/splice/identity/edges.js +17 -0
- package/dist/core/c4/splice/identity/edges.js.map +1 -1
- package/dist/core/c4/splice/identity/existing.js +80 -0
- package/dist/core/c4/splice/identity/existing.js.map +1 -0
- package/dist/core/c4/splice/identity/interior.js +80 -0
- package/dist/core/c4/splice/identity/interior.js.map +1 -0
- package/dist/core/c4/splice/identity/owner.js +50 -0
- package/dist/core/c4/splice/identity/owner.js.map +1 -0
- package/dist/core/c4/splice/landscape-merge.js +154 -100
- package/dist/core/c4/splice/landscape-merge.js.map +1 -1
- package/dist/core/c4/splice/model/merge.js +140 -0
- package/dist/core/c4/splice/model/merge.js.map +1 -0
- package/dist/core/c4/splice/placement.js +14 -4
- package/dist/core/c4/splice/placement.js.map +1 -1
- package/dist/core/c4/workspace.js +2 -1
- package/dist/core/c4/workspace.js.map +1 -1
- package/dist/core/coherence/coherence.js +7 -0
- package/dist/core/coherence/coherence.js.map +1 -1
- package/dist/core/delta/delta.js +9 -0
- package/dist/core/delta/delta.js.map +1 -1
- package/dist/core/deployment/delta.js +4 -0
- package/dist/core/deployment/delta.js.map +1 -1
- package/dist/core/diff/base-state.js +21 -5
- package/dist/core/diff/base-state.js.map +1 -1
- package/dist/core/diff/victims.js +10 -1
- package/dist/core/diff/victims.js.map +1 -1
- package/dist/core/docs.js +31 -22
- package/dist/core/docs.js.map +1 -1
- package/dist/core/doctor/config.js +7 -3
- package/dist/core/doctor/config.js.map +1 -1
- package/dist/core/doctor/doctor.js +48 -9
- package/dist/core/doctor/doctor.js.map +1 -1
- package/dist/core/doctor/report.js.map +1 -1
- package/dist/core/doctor/reports/scan.js +242 -0
- package/dist/core/doctor/reports/scan.js.map +1 -0
- package/dist/core/explain/families.js +1 -1
- package/dist/core/explain/families.js.map +1 -1
- package/dist/core/explain/refusals.js +1 -1
- package/dist/core/explain/refusals.js.map +1 -1
- package/dist/core/explore/explore.js +44 -9
- package/dist/core/explore/explore.js.map +1 -1
- package/dist/core/fleet-context.js +47 -0
- package/dist/core/fleet-context.js.map +1 -1
- package/dist/core/gate/partners.js +5 -9
- package/dist/core/gate/partners.js.map +1 -1
- package/dist/core/kernel/ids/fqn/ancestors.js +65 -0
- package/dist/core/kernel/ids/fqn/ancestors.js.map +1 -0
- package/dist/core/pack/living.js +47 -13
- package/dist/core/pack/living.js.map +1 -1
- package/dist/core/pack/pack.js +9 -6
- package/dist/core/pack/pack.js.map +1 -1
- package/dist/core/projection/arch-slice.js +2 -2
- package/dist/core/projection/arch-slice.js.map +1 -1
- package/dist/core/repo/paths.js +40 -0
- package/dist/core/repo/paths.js.map +1 -1
- package/dist/core/repo/tree/render/projects.js +182 -0
- package/dist/core/repo/tree/render/projects.js.map +1 -0
- package/dist/core/repo/tree/render/stale.js +2 -3
- package/dist/core/repo/tree/render/stale.js.map +1 -1
- package/dist/core/repo/tree/render/views.js +51 -3
- package/dist/core/repo/tree/render/views.js.map +1 -1
- package/dist/core/repo/tree/walk.js +45 -2
- package/dist/core/repo/tree/walk.js.map +1 -1
- package/dist/core/scaffold/landscape.js +219 -9
- package/dist/core/scaffold/landscape.js.map +1 -1
- package/dist/core/staging/writes.js.map +1 -1
- package/dist/core/usecases/delta/claims.js +1 -0
- package/dist/core/usecases/delta/claims.js.map +1 -1
- package/dist/core/usecases/delta/overlay.js +4 -0
- package/dist/core/usecases/delta/overlay.js.map +1 -1
- package/dist/core/usecases/fleet.js +18 -6
- package/dist/core/usecases/fleet.js.map +1 -1
- package/dist/core/usecases/operations.js +7 -0
- package/dist/core/usecases/operations.js.map +1 -1
- package/dist/core/usecases/service/exclude.js +119 -0
- package/dist/core/usecases/service/exclude.js.map +1 -0
- package/dist/core/usecases/service/flows.js +161 -0
- package/dist/core/usecases/service/flows.js.map +1 -0
- package/dist/core/vocabulary/maturity.js +10 -0
- package/dist/core/vocabulary/maturity.js.map +1 -1
- package/examples/README.md +23 -0
- package/examples/docs/architecture/landscape.likec4 +41 -0
- package/examples/docs/architecture/usecases/order-notification.likec4 +5 -2
- package/examples/docs/likec4.config.json +0 -1
- package/examples/docs/services/checkout-web/model.likec4 +39 -56
- package/examples/docs/services/order-service/arch.spec.md +13 -6
- package/examples/docs/services/order-service/model.likec4 +53 -67
- package/examples/docs/services/payment-service/arch.spec.md +7 -4
- package/examples/docs/services/payment-service/model.likec4 +32 -33
- package/examples/docs/services/platform/identity-service/arch.spec.md +7 -4
- package/examples/docs/services/platform/identity-service/model.likec4 +29 -33
- package/examples/docs/services/platform/notification-service/health.yaml +8 -3
- package/examples/docs/services/platform/notification-service/model.likec4 +39 -59
- package/package.json +1 -1
package/SCHEMA.md
CHANGED
|
@@ -71,11 +71,13 @@ docs/
|
|
|
71
71
|
spec.md narrative, then `## Requirements` — `Requirement-ID:` REQUIRED
|
|
72
72
|
glossary/<term>.md the system's domain vocabulary, one file per term; nesting allowed [authored, opt-in]
|
|
73
73
|
services/<svc>/ at the root (unfiled), or inside subsystem directories at any depth
|
|
74
|
-
model.likec4 boundary C4 (containers/components) [adopt]
|
|
74
|
+
model.likec4 boundary C4 (containers/components); extends the map, or stands alone [adopt]
|
|
75
|
+
likec4.config.json one LikeC4 project per STANDALONE service model, create-only [GENERATED: loam subsystem sync]
|
|
75
76
|
spec.md living requirements (Requirement/Scenario) [adopt]
|
|
76
77
|
arch.spec.md living ARCHITECTURE requirements (outbox, retries, alerts; Covers:) [authored]
|
|
77
78
|
openapi.yaml API contract [adopt / authored]
|
|
78
79
|
asyncapi.yaml async contract (AsyncAPI 3) [adopt / authored]
|
|
80
|
+
usecases/<name>.likec4 optional flows INSIDE this boundary — dynamic views over its containers, graded when tagged #req- (any .likec4 beside model.likec4 is read the same way) [authored, opt-in]
|
|
79
81
|
adrs/NNNN-*.md MADR decisions [adopt-seed / authored]
|
|
80
82
|
runbook.md operational runbook [adopt-draft / authored]
|
|
81
83
|
health.yaml SLI/SLO, checks, critical deps [authored]
|
|
@@ -105,6 +107,38 @@ tagged with the capability it realizes. A flow that spans services does not belo
|
|
|
105
107
|
service's directory, and the view's steps join to the landscape's own relationships, which is what
|
|
106
108
|
makes the flow checkable rather than merely drawn.
|
|
107
109
|
|
|
110
|
+
A flow that stays INSIDE one service — the hop sequence behind an arch.spec.md requirement whose
|
|
111
|
+
`Covers:` names containers — cannot live under `architecture/`: loam parses that directory as its
|
|
112
|
+
OWN document set — the map plus its siblings, and nothing under `services/` — so a step naming a
|
|
113
|
+
container does not resolve there, and one such file takes the whole fleet map down
|
|
114
|
+
(`landscape.invalid` plus one `spine.landscape-invalid` per service). That holds however the models
|
|
115
|
+
are shaped: the renderer's root project may also hold every extending model and draw such a step
|
|
116
|
+
perfectly, and loam still grades the map alone, because the map is the document every cross-service
|
|
117
|
+
check reads. It lives beside
|
|
118
|
+
the model instead: every `.likec4` under `services/<…>/<svc>/` other than model.likec4, plus any
|
|
119
|
+
tagged `dynamic view` inside model.likec4 itself, is that service's own LikeC4 project — for an
|
|
120
|
+
extending model, the map plus those documents; for a standalone one, the set
|
|
121
|
+
`services/<svc>/likec4.config.json` registers for the renderer — and `loam validate --service <id>`
|
|
122
|
+
(and `--all`, per service) reads exactly that set, whether or not the project file exists.
|
|
123
|
+
`usecases/<name>.likec4` is the convention; `views.likec4` reads the same. Tag the view
|
|
124
|
+
`#req-<Requirement-ID>` of a requirement in that service's spec.md or arch.spec.md. The tag has to
|
|
125
|
+
be DECLARED somewhere the project can see: for a standalone model that is model.likec4's own
|
|
126
|
+
`specification` — the one block that project has; for an extending model it is
|
|
127
|
+
`architecture/landscape.likec4`'s block, or a tags-only `specification { tag req-… }` beside the
|
|
128
|
+
`extend` (legal, and it lands the tag in the project's specification). Declaring it in BOTH is a
|
|
129
|
+
duplicate, and the code depends on which document carries the second declaration — the finding
|
|
130
|
+
lands where the file is, not at fleet altitude: `c4.invalid` (an ERROR, and the model's grading is
|
|
131
|
+
suspended behind it) when the tags-only block is in model.likec4, `usecase.flow-invalid` when it is
|
|
132
|
+
in a sibling. `c4.fleet-project-invalid` is the fleet-scope class one step out: TWO SERVICES each
|
|
133
|
+
declaring the same tag locally, where no per-service project sees the collision.
|
|
134
|
+
model.likec4 stays THE model:
|
|
135
|
+
`c4.valid`, `Covers:` and every other model check read it alone, and a sibling that does not parse
|
|
136
|
+
is one `usecase.flow-invalid` while the model keeps grading. Files loam does not read: a `.likec4`
|
|
137
|
+
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
|
+
covers (it is in no project the renderer loads either), and every UNTAGGED `dynamic view`
|
|
140
|
+
anywhere — a diagram, never a use case.
|
|
141
|
+
|
|
108
142
|
A feature **brings** one the way it brings every other axis: `features/<FEAT>/usecases/<name>.likec4`
|
|
109
143
|
is a views-only document that `loam archive` copies into `architecture/usecases/` and
|
|
110
144
|
`loam unarchive` takes back. Create-only, and graded before the copy against the map the same merge
|
|
@@ -112,6 +146,176 @@ would leave behind — so a flow may draw a hop into a service the feature's own
|
|
|
112
146
|
and a hop into an element the merge does NOT land is refused at plan time rather than archived into
|
|
113
147
|
a map the next reader's `loam validate --all` fails on.
|
|
114
148
|
|
|
149
|
+
### Two shapes of a service model
|
|
150
|
+
|
|
151
|
+
`services/<…>/<svc>/model.likec4` comes in two shapes, and **the file's own grammar decides which**
|
|
152
|
+
— never a config key, never a flag. loam scans the bytes (comments and strings masked) for a
|
|
153
|
+
top-level `specification { }` block containing an `element` declaration:
|
|
154
|
+
|
|
155
|
+
- 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
|
|
158
|
+
the map already binds to the directory. `<fqn>` is the FULLY-QUALIFIED id: `extend orderService`
|
|
159
|
+
resolves to nothing when the element is `marketplace.orderService`. No `specification` is needed,
|
|
160
|
+
no partner is re-declared, and `model {}` or no model at all are both fine. `extend` accepts
|
|
161
|
+
`#tags` and a `metadata { }` block on the element it opens (merged into it — the map's `service`
|
|
162
|
+
binding survives) and REFUSES `description`, which belongs on the map's declaration. This is the
|
|
163
|
+
shape `loam adopt` briefs, and the shape `loam init --create` scaffolds a repository for.
|
|
164
|
+
- a **standalone model** declares at least one `element <kind>` inside a `specification { }` block.
|
|
165
|
+
It is parsed ALONE, exactly as every model was before this shape existed; it may re-declare the
|
|
166
|
+
partners it names; and it renders only as a LikeC4 project of its own. Legal forever — a service
|
|
167
|
+
whose docs are reviewed from its own repository, a model adopted before the fleet had a map — and
|
|
168
|
+
not what a new model is written as.
|
|
169
|
+
|
|
170
|
+
**Who declares kinds and tags.** In the extending shape, `architecture/landscape.likec4` is the one
|
|
171
|
+
`specification` block: a kind a model needs and the map does not declare (`element database`,
|
|
172
|
+
`element queue`) is added THERE, once, and a missing one is a single parse error on the model. A
|
|
173
|
+
tags-only `specification { tag req-CANCEL }` beside an `extend` is legal and lands the tag in the
|
|
174
|
+
project's specification — but a tag the map already declares, declared again there, is a duplicate
|
|
175
|
+
blamed on both files. In the standalone shape the model declares everything it uses, which is
|
|
176
|
+
exactly why its copies of the map's elements can drift.
|
|
177
|
+
|
|
178
|
+
**Seven grades come with the pair**, and one existing grade is narrowed:
|
|
179
|
+
|
|
180
|
+
| code | shape | what it says |
|
|
181
|
+
|---|---|---|
|
|
182
|
+
| `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
|
+
| `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
|
+
| `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
|
+
| `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
|
+
| `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 |
|
|
188
|
+
| `service.likec4-config-missing` (warn, landscape) | standalone **only** | a standalone model with no project file beside it. It renders nowhere from the docs root until `loam subsystem sync` writes one. An extending model never earns this |
|
|
189
|
+
| `landscape.excluded` (warn, landscape) | either | `--all` only. The root `likec4.config.json`'s `exclude` covers `architecture/landscape.likec4` itself — an `architecture/*.likec4` written for a palette is the spelling that does it. The renderer then loads NO fleet map: it draws nothing, and every extending model in the root project resolves against nothing. loam is unaffected — the architecture loader keeps the map whatever the list says — which is exactly why the state was invisible. `c4.fleet-project-invalid` is suspended behind it, on the cascade rule `permissions.invalid` states: that check reads the renderer's project, so the same entry would come back once per unresolved reference instead of once as the cause, and the message says the fleet project is not graded until the entry goes. The finding quotes the entry verbatim, and `loam subsystem sync` is NOT the repair: it maintains only the `services/` entries and leaves every other line as the team wrote it |
|
|
190
|
+
|
|
191
|
+
`c4.invalid` and `c4.valid` are unchanged codes with adjusted scope: for an extending model every
|
|
192
|
+
error the per-service project raises is THIS model's, including one the parser blames on the
|
|
193
|
+
landscape, because the map parsed clean on its own; and `c4.valid`'s message reads
|
|
194
|
+
`extends the fleet map — N elements · M relationships`, followed by ` · K tagged flow(s) graded`
|
|
195
|
+
when the service's own project declared any (the one surface a healthy intra-service flow appears
|
|
196
|
+
on). When the map itself does not parse, an
|
|
197
|
+
extending model cannot be read at all: neither code is emitted, and `spine.landscape-invalid` says
|
|
198
|
+
so with the clause ` — and model.likec4 extends it, so the model cannot be read either`.
|
|
199
|
+
|
|
200
|
+
Every other model-dependent check reads the same slice it always read — the service's OWN elements
|
|
201
|
+
and relationships. For an extending model that is what the project holds minus what the map alone
|
|
202
|
+
holds, plus the **partners** its own edges point at and their ancestors, so `health.yaml`'s
|
|
203
|
+
`kafka` still resolves when the only edge names `kafka.orderEvents`. Nothing reads the map's edges
|
|
204
|
+
as the model's.
|
|
205
|
+
|
|
206
|
+
**What loam grades is still the `architecture/` project alone.** The renderer's root project is
|
|
207
|
+
WIDER than the fleet map: it also holds every extending model. loam's fleet checks — the spine, the
|
|
208
|
+
bindings, the fleet-shape advisories, and the steps of every `architecture/usecases/` flow — read
|
|
209
|
+
the landscape plus its `architecture/` siblings and nothing else, whatever shape the models take.
|
|
210
|
+
So a cross-service flow names SERVICE elements: a hop naming a container an extending model
|
|
211
|
+
declares renders perfectly and is `landscape.invalid` for loam. That is deliberate, and the reason
|
|
212
|
+
is blast radius — a service's model is graded on its own merits, and one broken model must never
|
|
213
|
+
blank the fleet's own map. A container-level flow has its own home beside the model that declares
|
|
214
|
+
the container. The same holds for a feature's `features/<FEAT>/usecases/` flow, and the gate says
|
|
215
|
+
so before the merge rather than after it: the preview stages no service model, and it ROUTES a
|
|
216
|
+
nested addition into the model that owns that service's interior exactly as `loam archive` would.
|
|
217
|
+
So a hop naming `<service-fqn>.<container>` resolves in neither the preview nor the tree the archive
|
|
218
|
+
leaves, and `usecase.flow-invalid` refuses — where the un-routed preview resolved it, passed the
|
|
219
|
+
gate `--approve` cannot override, and landed a flow the next `loam validate --all` failed on.
|
|
220
|
+
|
|
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
|
|
229
|
+
consumer a service's EXTENDING model attests exactly as it counts one the map draws, so a nested
|
|
230
|
+
store two services reach is `landscape.datastore-shared` — the store's owner plus every service
|
|
231
|
+
whose model draws an outbound call at it or below it. The second consumer draws that call as a
|
|
232
|
+
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,
|
|
239
|
+
`<owner-fqn>.<store>`, so moving the declaration OUT of the owner's `extend` block into the owner's
|
|
240
|
+
element on the map rewrites no edge and no `Covers:` line — measured on `examples/docs`, where
|
|
241
|
+
moving `db` out of order-service's `extend` block into `orderService` on the map left the model at
|
|
242
|
+
12 elements · 8 relationships, left `Covers: marketplace.orderService.db` in its `arch.spec.md`
|
|
243
|
+
resolving, and left checkout-web's container-level edge to it resolving. The move BACK is free only
|
|
244
|
+
while no other service names the store: with checkout-web's edge drawn, putting `db` back into
|
|
245
|
+
order-service's `extend` block is `c4.invalid` on checkout-web's model ("Could not resolve
|
|
246
|
+
reference to Referenceable named 'db'"), or `landscape.invalid` plus one `spine.landscape-invalid`
|
|
247
|
+
per service when that second edge is drawn on the map. `landscape.datastore-shared` needs a second
|
|
248
|
+
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
|
|
250
|
+
EXTENDING models only, because their ids are the map's fully-qualified ids and the join is exact,
|
|
251
|
+
while a standalone model's ids are its own file's and are never matched to a landscape element.
|
|
252
|
+
|
|
253
|
+
**Renderer consequences.** The root project holds the map **and** every extending model, so one
|
|
254
|
+
`npx likec4 start` at the docs root draws the fleet and the inside of every adopted service, and
|
|
255
|
+
the project picker holds one entry. A standalone model is the exception, and it costs two files:
|
|
256
|
+
ONE entry in the root `likec4.config.json`'s `exclude` covering its directory (its kinds would be
|
|
257
|
+
duplicates of the map's inside one project), plus its own `likec4.config.json`, which is what makes
|
|
258
|
+
it a project of its own.
|
|
259
|
+
`loam subsystem sync` maintains both halves — it writes the per-service file for standalone models
|
|
260
|
+
only, and rewrites the root `exclude` so that it holds `services/<tree>/**` for exactly the models
|
|
261
|
+
that stand alone. The rewrite removes the wholesale `services/**` and every `services/<tree>/**`
|
|
262
|
+
naming a directory that IS a service, keeps every other entry in the order the team wrote it (a
|
|
263
|
+
`services/legacy/**` for a directory that is no service survives untouched), and appends the
|
|
264
|
+
standalone entries sorted. It runs when at least one extending model exists, or when a standalone
|
|
265
|
+
model's directory is not covered — so a repository whose models all stand alone behind `services/**`
|
|
266
|
+
is never touched, and a fresh scaffold that gains its first standalone model earns the entry on the
|
|
267
|
+
next sync. `--json` reports it as `projects.exclude`, with what was added and removed; the text view
|
|
268
|
+
prints one line when it rewrote. An entry covers a directory when it names that directory —
|
|
269
|
+
`<dir>`, `<dir>/`, `<dir>/*`, `<dir>/**` and `<dir>/**/*` all do, and a wildcard segment in the
|
|
270
|
+
middle (`services/*/legacy`) matches one level — or when it is a raw path prefix of it from a `/`
|
|
271
|
+
boundary, which is what the renderer measures: `services/pay` covers `services/payment-service`,
|
|
272
|
+
while `services/pay/**` covers nothing there. An entry is normalised the way the renderer
|
|
273
|
+
normalises it before anything is matched: a leading `./`, an inner `/./` and an inner `//` are
|
|
274
|
+
resolved away, a TRAILING `/` is not (bare `architecture` also hides `architecture-old/`,
|
|
275
|
+
`architecture/` does not), `.//` leaves a literal `.` directory and hides nothing, and a BACKSLASH
|
|
276
|
+
is a literal character to the matcher, never a separator — `services\order-service\**` hides
|
|
277
|
+
nothing at all, so loam neither grades on it nor rewrites it. Measured at the 1.59.2 pin against
|
|
278
|
+
`npx likec4 validate .` file counts; the table is in `src/core/c4/root-project/exclude.ts` and
|
|
279
|
+
pinned in `test/root-exclude.test.ts`. A directory the list already covers in
|
|
280
|
+
any of those spellings earns no second entry. `projects.exclude` carries `updated`, `entries`,
|
|
281
|
+
`added`, `removed` and `unreadable` — the last is `true` when the root file exists and loam could
|
|
282
|
+
not read an `exclude` list out of it, which is never the same answer as `entries: []`, and the list
|
|
283
|
+
is then reported untouched. Beside it, `--json` carries
|
|
284
|
+
`projects.created` (the per-service project files written) and `projects.removed` (POSIX paths
|
|
285
|
+
relative to the DOCS ROOT, deleted this run, sorted by path; `[]` otherwise) — and
|
|
286
|
+
`projects.created` is spelled the same way. Both are relative to the docs root, not to the
|
|
287
|
+
repository root, which differ whenever `docsDir` is a subdirectory. A fresh repository is
|
|
288
|
+
scaffolded with
|
|
289
|
+
`exclude: ["**/node_modules/**", "features/**"]`.
|
|
290
|
+
|
|
291
|
+
**Migrating a standalone model to the extending shape** is a documented recipe an agent performs;
|
|
292
|
+
loam does not rewrite a model, because the comments in a real one are somebody's and not loam's to
|
|
293
|
+
reflow. In order:
|
|
294
|
+
|
|
295
|
+
1. Write into `architecture/landscape.likec4` any element kind the model uses that the map does not
|
|
296
|
+
declare (`element database`, `element queue`), and any element the model reaches that the map
|
|
297
|
+
does not draw — an external system, a broker's topic — as the fleet's own declaration.
|
|
298
|
+
2. Delete the model's `specification` block.
|
|
299
|
+
3. Delete every element the map declares: the service's own element, and every re-declared partner.
|
|
300
|
+
4. Wrap what is left in `model { extend <fqn> { … } }`, `<fqn>` being the fully-qualified id of the
|
|
301
|
+
map's element for this service. Child ids inside the block stay relative; internal edges may be
|
|
302
|
+
written inside it.
|
|
303
|
+
5. Move the cross-boundary edges outside the `extend` block and spell both endpoints in full —
|
|
304
|
+
`marketplace.orderService.outbox -> kafka.orderEvents`.
|
|
305
|
+
6. Requalify every id that names one of those elements from outside the file: the `Covers:` lines in
|
|
306
|
+
`arch.spec.md`, the `of <element>` and `include` lines of any `views` block, and the hops of any
|
|
307
|
+
`#req-`-tagged flow beside the model. There is no suffix matching anywhere — `orderService.db`
|
|
308
|
+
does not answer for `marketplace.orderService.db` — so every model-local nested id has to be
|
|
309
|
+
rewritten. `health.yaml`'s `dependencies:` are the one place that usually needs no edit, because
|
|
310
|
+
a dependency resolves when an element of the own slice answers to it **by id, `metadata { service }`
|
|
311
|
+
binding or title**, and the slice holds the service's own elements plus every endpoint of the
|
|
312
|
+
model's own edges and each endpoint's ANCESTORS: `kafka` still answers through an edge naming
|
|
313
|
+
`kafka.orderEvents`, and a peer service still answers through the binding on the map's element.
|
|
314
|
+
Prefer the service id or a top-level system id there — those two spellings resolve under either
|
|
315
|
+
model shape, and a model-local nested id does not.
|
|
316
|
+
7. Delete `services/<…>/<svc>/likec4.config.json`.
|
|
317
|
+
8. Run `loam subsystem sync` (it rewrites the root `exclude`), then `loam validate --all`.
|
|
318
|
+
|
|
115
319
|
## Subsystems — a navigable tree under `services/`
|
|
116
320
|
|
|
117
321
|
`services/` may be one flat level (the compatibility case, and forever legal) or an unbounded tree
|
|
@@ -134,6 +338,13 @@ A directory under `services/` is classified **three ways, never two**:
|
|
|
134
338
|
subsystem's marker while another moves a service in, and the group directory would otherwise be
|
|
135
339
|
read as one empty service. The fleet is never reported smaller than it is.
|
|
136
340
|
|
|
341
|
+
A directory named for one of the service's own subdirectories (`adrs/`, `usecases/`, `ui/`) is
|
|
342
|
+
service interior wherever it sits: the walk never reads one as a service or a subsystem, and never
|
|
343
|
+
counts one as the subdirectory that makes its parent a group. Writing the documented
|
|
344
|
+
`services/<id>/usecases/<name>.likec4` slot into a directory that holds no artifact FILE yet used
|
|
345
|
+
to make the walk read `usecases` as a service and its parent as an unmarked group — three errors
|
|
346
|
+
earned by following the instruction `loam init` had just scaffolded.
|
|
347
|
+
|
|
137
348
|
`subsystem.yaml` is a **marker, not a manifest**:
|
|
138
349
|
|
|
139
350
|
```yaml
|
|
@@ -165,6 +376,30 @@ reports). The output is deterministic and line-oriented — subsystems sorted by
|
|
|
165
376
|
one `include` per line, no timestamps, no absolute paths — so it is byte-reproducible on any machine
|
|
166
377
|
and two concurrent moves into different groups merge in git without intervention.
|
|
167
378
|
|
|
379
|
+
**Styling.** Each generated view carries `global style subsystems` — after its `title` and
|
|
380
|
+
`description`, before its `include`/`group` — exactly when the `architecture/` project declares a
|
|
381
|
+
global style with that id: `global { styleGroup subsystems { style element.tag = #external { color gray } } }`
|
|
382
|
+
(or the single-rule `global { style subsystems … }`), in the landscape or in ANY `.likec4` under
|
|
383
|
+
`architecture/` the root `likec4.config.json`'s `exclude` does not cover — a `palette.likec4` beside
|
|
384
|
+
the map works, and one under an excluded path is loaded by neither the renderer nor loam.
|
|
385
|
+
`architecture/landscape.likec4` is the one exception: loam keeps the map in that set however the
|
|
386
|
+
list is written, because an architecture project with no documents in it reads as a fleet that
|
|
387
|
+
models nothing. With the targeted tag declared in the `specification` block.
|
|
388
|
+
Declaring it is the whole opt-in. loam reads only that the id is declared, never what the group
|
|
389
|
+
says, and writes nothing when it is not — a reference to an undeclared style is a parse error that
|
|
390
|
+
blanks the whole `architecture/` project in the renderer. When the project declares style groups
|
|
391
|
+
under other names only, the file opens with one comment line naming them and saying none is
|
|
392
|
+
`subsystems`, and the views carry the renderer's defaults; there is no single-group fallback.
|
|
393
|
+
Both halves restale the file once (`subsystem.views-stale`) until the next `loam subsystem sync`:
|
|
394
|
+
declaring or removing the group named `subsystems` changes whether the views carry the
|
|
395
|
+
`global style subsystems` line, and declaring or removing a group under ANY OTHER name changes the
|
|
396
|
+
disclosure comment that lists what is available. Whether a service's own views can borrow that group depends on the shape of
|
|
397
|
+
its model: a model that EXTENDS the map is in the root project, so a `global styleGroup` declared
|
|
398
|
+
under `architecture/` reaches the views beside it and referencing the id there is legal; a model
|
|
399
|
+
that STANDS ALONE is a project of its own, and a group declared under `architecture/` does not reach
|
|
400
|
+
it — those views need a declaration of their own, or the reference is the parse error that blanks
|
|
401
|
+
that project.
|
|
402
|
+
|
|
168
403
|
Each view carries the **label a human reads**: `title`, and `description` when the marker authors
|
|
169
404
|
one. The title is the subsystem's **path** — every marked ancestor's label and its own, joined with
|
|
170
405
|
` / `, so `services/platform/order-flow` reads `Platform / Order flow` — and each label is the
|
|
@@ -204,6 +439,85 @@ does not parse standalone (its `include` lines reference elements the landscape
|
|
|
204
439
|
holds **static** views, whose contents loam never reads in any document. Only the LikeC4 renderer,
|
|
205
440
|
which merges the whole `architecture/` project, resolves its includes.
|
|
206
441
|
|
|
442
|
+
`sync` refuses to rewrite this file while the `architecture/` project cannot be read. The render
|
|
443
|
+
needs the map's elements and its global-style census, and a tolerant "no elements" answer wrote a
|
|
444
|
+
file holding title and description alone — the `global style subsystems` line, every `include` and
|
|
445
|
+
the boundary comment gone — while reporting `updated … 1 subsystem view(s)` and answering `current`
|
|
446
|
+
ever after. So: a load error falls back to the landscape FILE when that parses on its own (a
|
|
447
|
+
duplicate `subsystems` group declared in two `architecture/` documents costs nothing), and when
|
|
448
|
+
that fails too the facts are UNKNOWN. With an existing views file, `sync` then writes nothing about
|
|
449
|
+
it, prints why, and reports the `--json` action `blocked`, with `blocked.reason` reading
|
|
450
|
+
`landscape-invalid` — exit 0, because `loam validate --all`'s `landscape.invalid` is the gating
|
|
451
|
+
surface. With no file yet, and inside a tree-changing verb's commit window (`new`/`move`/`rm`/
|
|
452
|
+
`rename`, whose contract is that the file lands with the change), the tolerant shape is written as
|
|
453
|
+
before and the same note is printed.
|
|
454
|
+
|
|
455
|
+
The tree's **second renderer file** is per service — `services/<…>/<svc>/likec4.config.json`,
|
|
456
|
+
which `loam subsystem sync` also writes, beside every **standalone** `model.likec4` that has none.
|
|
457
|
+
An extending model needs no such file and never gets one: it belongs to the root project, which is
|
|
458
|
+
where it parses, and a project file beside it is `service.likec4-config-stray` — which `sync`
|
|
459
|
+
DELETES, in the same journaled commit as everything else it writes. A standalone model
|
|
460
|
+
needs one because it cannot share a project with the map — it declares its own kinds, and inside
|
|
461
|
+
one project every declaration is a duplicate blamed on the map too — so `sync` also keeps the root
|
|
462
|
+
`exclude` covering exactly the directories of the models that stand alone. Before extending models
|
|
463
|
+
existed, `services/**` was excluded wholesale and every model needed the file; what that exclusion
|
|
464
|
+
cost went unstated for a release: an
|
|
465
|
+
adopted service was a box on the fleet map with nothing renderable inside it. Measured at the
|
|
466
|
+
pinned likec4, a project file INSIDE a service directory is registered as a project of its own
|
|
467
|
+
regardless of the exclusion, so a docs root holding standalone models loads `fleet` plus one
|
|
468
|
+
project per such service, each carrying LikeC4's own `index` view of the containers beside the
|
|
469
|
+
authored one, and a model that does not parse breaks only its own project. (An extending model
|
|
470
|
+
is inside `fleet`, so a model that does not parse breaks the root project instead — the trade the
|
|
471
|
+
shape makes, and `c4.fleet-project-invalid` is what names it before a reader meets it in the
|
|
472
|
+
renderer.) The file is **create-only for a standalone model** —
|
|
473
|
+
never compared, never rewritten — under the root config's ownership rule, not the
|
|
474
|
+
views file's: there is no `-stale` grade because nothing can make it stale. It is removed in
|
|
475
|
+
exactly one state, and only there: beside a model that EXTENDS the map. That nested project claims
|
|
476
|
+
the model out of the root project — the workspace loads two projects, the model is Invalid inside
|
|
477
|
+
its own one (the map's kinds are not in there with it) and `export json --project fleet` then loses
|
|
478
|
+
the service's containers. Which of the two the workspace does was measured both ways at the 1.59.2
|
|
479
|
+
pin: on a seeded fleet whose model extends a top-level id the containers went; on `examples/docs`
|
|
480
|
+
the root project kept the model and the export was byte-identical. What is true in every state is
|
|
481
|
+
the second project itself — `likec4 validate .` at the docs root refuses without `--project` from
|
|
482
|
+
the moment that file exists. So it is not a harmless leftover, and `loam subsystem sync` deletes
|
|
483
|
+
it. One beside a model that is GONE stays: that one really is the team's — though the same second
|
|
484
|
+
project it registers is there too, so `likec4 validate .` and `likec4 build` at the docs root need
|
|
485
|
+
`--project` while it stands (ROADMAP's "Known and carried, 2026-09-04"). Its bytes are a
|
|
486
|
+
function of the id alone (two keys, `name` and `title`), so placement never changes them and a
|
|
487
|
+
`subsystem move` carries the file with no write of its own — it rides inside the rename the move's
|
|
488
|
+
transaction already journals (in a git-backed repo, commit the files `sync` wrote first: `move`
|
|
489
|
+
refuses over untracked paths, `move-uncommitted`); a compare would only fight the keys LikeC4 lets a
|
|
490
|
+
team add (`title`, styles). `name` is the id with every
|
|
491
|
+
`.` folded to `-` (LikeC4's name grammar refuses the byte; loam's id grammar admits it), prefixed
|
|
492
|
+
`service-` when the result would equal the root project's `fleet` (a nested project of that name
|
|
493
|
+
is silently registered as `fleet-1`) or LikeC4's reserved `default`; `title` is the id verbatim,
|
|
494
|
+
and the title is what the renderer's project picker shows. The name is **not injective**, and
|
|
495
|
+
that is disclosed rather than escaped away: an id spelled billing.core beside a service literally
|
|
496
|
+
named billing-core, or `service-fleet` beside `fleet`, share a name and LikeC4 suffixes the later
|
|
497
|
+
one `-1`; no loam
|
|
498
|
+
finding grades it, because the picker still reads the id. Order is a gate — the root file first
|
|
499
|
+
(`loam init --create` writes it, `loam doctor` prints it), then `sync`: without the root file
|
|
500
|
+
nothing is written and the text output says so. No model, no file: a spec-only service is owed
|
|
501
|
+
nothing and gets its file on the first `sync` after its model lands. No legal id, no file either: a
|
|
502
|
+
directory whose name breaks the service-id grammar (`service.id-invalid`) would get a `name` the
|
|
503
|
+
renderer silently drops, and a create-only file the renderer ignores would silence the grade for
|
|
504
|
+
good, so `sync` skips it and the rename that clears `service.id-invalid` earns the file.
|
|
505
|
+
`validate --all` grades presence only — one warning per service with
|
|
506
|
+
a STANDALONE model and no file, `service.likec4-config-missing`, on the fleet `landscape` target,
|
|
507
|
+
never an error, never in `--service`, `status` or `doctor`; the fix is `loam subsystem sync`, never
|
|
508
|
+
a hand-written file. Its two siblings grade the extending shape's version of the same question —
|
|
509
|
+
`service.likec4-config-stray` for a project file that should not be there, `service.model-excluded`
|
|
510
|
+
for a root `exclude` entry that hides the model from the renderer — and `sync` is the repair for
|
|
511
|
+
both: it removes the stray and rewrites the entry. A third reads the same list and is nobody's to
|
|
512
|
+
rewrite: `landscape.excluded` says an entry covers `architecture/landscape.likec4` itself, so the
|
|
513
|
+
renderer loads no map at all — `sync` maintains only the `services/` entries and will not touch that
|
|
514
|
+
line, and the finding quotes it so its author can. It also suspends
|
|
515
|
+
`c4.fleet-project-invalid` for that run, because the renderer's project is exactly what the entry
|
|
516
|
+
broke and grading it would report the one line once per extending model. A
|
|
517
|
+
feature's `delta.likec4` is deliberately out of scope: the directory is transient and
|
|
518
|
+
`loam archive` moves it where a project file would still be registered, so every shipped feature
|
|
519
|
+
would keep a dead project in the list forever.
|
|
520
|
+
|
|
207
521
|
A move is **one journaled transaction over N directory renames plus the regenerated views file**
|
|
208
522
|
(`loam subsystem move <name>... --into <sub|.>`; `rename` is a one-rename move; whole subtrees move
|
|
209
523
|
as one rename). It refuses only when git reports uncommitted changes under a directory being moved
|
|
@@ -225,6 +539,7 @@ lives *today*.
|
|
|
225
539
|
| Capability | requirement `Capability:` (list; also `Capabilities:`) | `architecture/capabilities.yaml` OR `capabilities/<cap>/spec.md` | declared capability id, `/` allowed for nesting |
|
|
226
540
|
| Capability requirement | requirement `Realizes:` (list) | a requirement in `capabilities/<cap>/spec.md` | `<capability-id>#<Requirement-ID>`, split at the LAST `#`, with an optional `@<digest>` pin |
|
|
227
541
|
| Capability requirement (flow) | a `dynamic view` tag `#req-<slug>` | a requirement of the capability the view's `#cap-` tag names | the `Requirement-ID` with every character outside `[A-Za-z0-9_-]` flattened to `-` |
|
|
542
|
+
| Service requirement (flow) | a `dynamic view` tag `#req-<slug>` in a `.likec4` beside model.likec4, or inside it | a requirement of the service whose directory holds the file, from either spec axis | the `Requirement-ID` flattened as above |
|
|
228
543
|
|
|
229
544
|
These keys are independent: an operation id is not a permission and a service id is not inferred
|
|
230
545
|
from a feature directory. A file can therefore be structurally valid while a join is broken; the
|
|
@@ -647,7 +962,21 @@ There is no manifest of services: the directories under `services/` **are** the
|
|
|
647
962
|
why the two are graded differently;
|
|
648
963
|
- two elements resolving to the **same** `services/<svc>/` → `landscape.binding-duplicate`
|
|
649
964
|
(**warning**): every element→service join picks one of them arbitrarily, so the other's edges are
|
|
650
|
-
quietly filed under a service that does not own them
|
|
965
|
+
quietly filed under a service that does not own them;
|
|
966
|
+
- an element resolving to a `services/<svc>/` that no edge in the map touches, while that service's
|
|
967
|
+
`model.likec4` parses and declares at least one call across its own boundary →
|
|
968
|
+
`landscape.service-isolated` (**warning**, never gating): the service is drawn and invisible to
|
|
969
|
+
every cross-service check, and the finding names the attested calls to draw as edges on the
|
|
970
|
+
existing element, collapsed to the service. Evidence-gated on purpose — a service with no model,
|
|
971
|
+
an unparseable one (`c4.invalid`), or a model reaching nothing is silent here, because an edgeless
|
|
972
|
+
element that is TRUE is a fact and an invented edge is a dependency the fleet then plans against;
|
|
973
|
+
the adopt brief's `landscape.touched` is the only place that silent state is named. And BINARY:
|
|
974
|
+
one edge closes it, so a map drawing one of five attested calls is silent too — the check is
|
|
975
|
+
touched/untouched, never a set difference over the attested calls, because loam cannot tell a
|
|
976
|
+
call the map forgot from one the fleet deliberately does not draw at its own altitude. "Edge in
|
|
977
|
+
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
|
|
979
|
+
`#external` element is never the subject, whatever directory it is bound to.
|
|
651
980
|
|
|
652
981
|
**`architecture/landscape.likec4` is required, not optional.** Its absence is a finding, never a
|
|
653
982
|
skipped check: an **error** as soon as `services/` holds at least one service directory, and a
|
|
@@ -658,7 +987,7 @@ it cannot be silence: with no map, the whole cross-service layer — the C4↔AP
|
|
|
658
987
|
`landscape.service-unmodelled`, every derived view — computes nothing and *reports* nothing, which
|
|
659
988
|
reads exactly like a clean fleet. `loam init` scaffolds the file (empty, commented, parsing clean)
|
|
660
989
|
so a fresh docs repo starts from a real one rather than from that ambiguity, and `loam adopt` briefs
|
|
661
|
-
the
|
|
990
|
+
the write each service still owes it — the block, or the edges on an element no edge touches.
|
|
662
991
|
|
|
663
992
|
Absences inside a service directory are graded too. A directory with no `spec.md` is a **warning**
|
|
664
993
|
(`service.no-spec`) — partial adoption is a supported state, but the checks that file feeds are
|
|
@@ -946,7 +1275,9 @@ the source for tests.
|
|
|
946
1275
|
diff, tagged to the feature.
|
|
947
1276
|
- **`loam archive <FEAT>`** merges the delta into the living state on three axes — **requirements**
|
|
948
1277
|
(`spec.md`, and `arch.spec.md` beside it: the two files ride one merge code path, parameterized by
|
|
949
|
-
filename), **API** (`openapi.yaml`), **architecture** (`landscape.likec4
|
|
1278
|
+
filename), **API** (`openapi.yaml`), **architecture** (`landscape.likec4`, plus the
|
|
1279
|
+
`services/<…>/<svc>/model.likec4` of each service whose EXTENDING model owns an addition nested
|
|
1280
|
+
inside it) — then archives the
|
|
950
1281
|
feature, so the living state stays complete. Archived deltas are the evolution history (like
|
|
951
1282
|
`git log`). `--dry-run` prints the whole plan and writes nothing.
|
|
952
1283
|
- **Coherence gate:** `loam validate --feature` checks the three axes agree (C4 edge `op` ↔ OpenAPI
|
|
@@ -1106,10 +1437,16 @@ the CI escalation.
|
|
|
1106
1437
|
such entry into a false `covers.unknown` "typo" pointing at the spec, when the truth was the file.
|
|
1107
1438
|
- `health.dependency-unmodelled` (service scope): a `dependencies:` id that nothing in the service's
|
|
1108
1439
|
OWN `model.likec4` answers to — by element id, `metadata { service }` binding, or title; exact
|
|
1109
|
-
match, with a did-you-mean hint. Deliberately the service model and never the landscape
|
|
1440
|
+
match, with a did-you-mean hint. Deliberately the service model and never the landscape read on
|
|
1441
|
+
its own: the
|
|
1110
1442
|
landscape carries only what crosses the service boundary, the model carries everything the service
|
|
1111
|
-
touches (a private store
|
|
1112
|
-
pushing it there
|
|
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
|
|
1446
|
+
the set is its own SLICE: the elements it adds, plus the partners its own edges point at and their
|
|
1447
|
+
ancestors — so a dependency named `kafka` resolves through an edge that names `kafka.orderEvents`,
|
|
1448
|
+
and a peer service resolves through the `metadata { service }` binding on the map's element the
|
|
1449
|
+
model points at. A partner the model never reaches still does not answer, which is the check.
|
|
1113
1450
|
|
|
1114
1451
|
The rest of the machinery treats the axis as what it is — requirements. `loam archive` merges an
|
|
1115
1452
|
`arch.spec.md` delta into the living one through the same code path as `spec.md` (same delta
|
|
@@ -1497,6 +1834,21 @@ turn a mistake into a green fleet. And a run that could not READ the flows (a si
|
|
|
1497
1834
|
an `architecture/` that did not parse) suspends `capability.requirement-unrealized` entirely: loam
|
|
1498
1835
|
did not look, which is never the same answer as "there is nothing there".
|
|
1499
1836
|
|
|
1837
|
+
**A `#req-` tag inside a service's own project is scoped by the SERVICE, not by a capability.** A
|
|
1838
|
+
`dynamic view` beside model.likec4 carrying `#req-<slug>` resolves against the `Requirement-ID`s of
|
|
1839
|
+
that service's spec.md and arch.spec.md together — flattened by the same rule, an id both declare
|
|
1840
|
+
resolving once, and `many` when the two documents declare ids that flatten alike, because a
|
|
1841
|
+
Requirement-ID is unique inside ONE document and a flow beside the model has two.
|
|
1842
|
+
`usecase.requirement-unresolved` there has four arms: no document at all, no identified requirement,
|
|
1843
|
+
none flattening to the slug (close ids offered), or two that do. No `#cap-` tag belongs there
|
|
1844
|
+
(`usecase.capability-unresolved` says so, vocabulary or not); `usecase.step-unlinked` never fires
|
|
1845
|
+
on a hop whose caller and provider resolve to the same service — a service owes no operationId to
|
|
1846
|
+
itself — and still fires on a hop from such a flow into another service's element (a stand-in the
|
|
1847
|
+
model declares for a sibling, with no `metadata { op }`), exactly as on the fleet map; and a
|
|
1848
|
+
resolved service-local claim is the join being checked and nothing more today: it
|
|
1849
|
+
enters no capability `keptBy`, counts toward no `Covers:` or `c4.uncovered`, appears in no pack,
|
|
1850
|
+
`loam list capabilities`, `loam diff` or `loam delta`, and no feature carries one yet.
|
|
1851
|
+
|
|
1500
1852
|
**Both corpora are reported together, and neither is derived from the other.**
|
|
1501
1853
|
`loam list capabilities --json` carries `keptBy` (the flows keeping a promise) beside `realizedBy`
|
|
1502
1854
|
(the service requirements realizing it) on every `requirements[]` row, with the same three-state
|
|
@@ -1770,6 +2122,44 @@ unparseable delta. A living declaration sharing its line with another statement
|
|
|
1770
2122
|
splice map cannot see around — its statement scan runs to the end of the line) refuses the same way:
|
|
1771
2123
|
give each top-level declaration its own line, then re-run.
|
|
1772
2124
|
|
|
2125
|
+
**Which document an addition lands in.** The C4 merge asks it per declaration, not per feature. An
|
|
2126
|
+
element whose PARENT resolves to a service whose `model.likec4` extends the map is spliced into that
|
|
2127
|
+
model — and so is the interior a delta writes inside a service element the same archive introduces,
|
|
2128
|
+
since those containers carry no feature tag and would otherwise ride onto the map inside their
|
|
2129
|
+
parent's bytes. The parent chain has to be spelled in the map's own ids for either to happen. The
|
|
2130
|
+
splice goes inside the `extend <fqn> { … }` block for the parent — appended when the model has none, as
|
|
2131
|
+
the `model { }` block itself is appended to a model that has none (an empty file, a `views { }`-only
|
|
2132
|
+
file and a tags-only `specification { }` are all extending models, and all three are legal to
|
|
2133
|
+
archive into). A
|
|
2134
|
+
relationship goes to the model of the service owning its SOURCE when the source is interior to such
|
|
2135
|
+
a service, else to the model owning its TARGET, else to the map: a service-level edge stays where
|
|
2136
|
+
every fleet check reads it. A service with a STANDALONE model, or none yet, owns nothing — the map
|
|
2137
|
+
draws its containers, which is legal and is what every repository did before the extending shape.
|
|
2138
|
+
The merged model is proved the way the grader reads it (the `architecture/` documents with this
|
|
2139
|
+
archive's own merged landscape in place, plus the model) before anything is written, and it appears
|
|
2140
|
+
in the `--json` plan as an ordinary write, so `loam unarchive` restores it from the same
|
|
2141
|
+
`.loam-before` snapshot as any other overwritten file. Two limits are deliberate and each refuses at
|
|
2142
|
+
plan time with nothing written: a merged model that does not parse beside the map (`merge-failed`),
|
|
2143
|
+
and a container-level edge whose other end lives in a THIRD document — another service's interior
|
|
2144
|
+
resolves in neither this model nor the map, so that call is drawn service to service on the map. One
|
|
2145
|
+
more is silent: a delta that spells the parent under its own name rather than the map's (the legacy
|
|
2146
|
+
title-join) keeps landing on the map, because splicing its bytes into a document that shares the
|
|
2147
|
+
map's id namespace would carry ids nothing there resolves. It is the WHOLE chain that decides, not
|
|
2148
|
+
the outermost box — `marketplace.orderSvc.cache` under a map that spells the service
|
|
2149
|
+
`marketplace.orderService` is a title join and stays on the map, even though the group
|
|
2150
|
+
`marketplace` is the map's own. A block a delta writes for a NEW service is split — the box to the
|
|
2151
|
+
map, its containers to the model — unless the block also draws a relationship between those
|
|
2152
|
+
containers. Such an edge names its endpoints by their local names inside the block, so splitting it
|
|
2153
|
+
would leave the map naming children it no longer declares; the block rides whole instead, as it did
|
|
2154
|
+
before routing existed. The block decides once for everything written inside it: a container or an
|
|
2155
|
+
edge the delta tags with the feature follows its block rather than its own tag, so it is placed
|
|
2156
|
+
exactly once whichever way the block goes. Re-archiving is idempotent in the models
|
|
2157
|
+
too: an addition whose id, or whose title inside the same bound service, an extending model already
|
|
2158
|
+
declares is skipped exactly as one the map declares is. The title half is service-scoped in a model
|
|
2159
|
+
and global on the map — the map's global join is the fallback for documents written before
|
|
2160
|
+
`metadata { service }` existed, and a service's interior is where two boxes called `api` would
|
|
2161
|
+
collide.
|
|
2162
|
+
|
|
1773
2163
|
The OpenAPI merge carries components along with operations, and since the path-item/component
|
|
1774
2164
|
baselines it decides each one **by verdict rather than by reachability**: a component the feature
|
|
1775
2165
|
declares with a pin equal to its own content is a quote and is skipped (the living document keeps
|
|
@@ -2295,14 +2685,28 @@ through the forge, not through loam features:
|
|
|
2295
2685
|
**parsed** model and never computes a view — it reads what a view **declares** (a `dynamic view`'s
|
|
2296
2686
|
tags and ordered steps, already materialized by the parse loam pays for) and never what a view
|
|
2297
2687
|
**shows**. It renders nothing, and view *computation* is what is superlinear in edges. (If you
|
|
2298
|
-
want diagrams, add a `views` block and render it with LikeC4's own tooling — `npx likec4 start`
|
|
2299
|
-
the docs
|
|
2300
|
-
|
|
2688
|
+
want diagrams, add a `views` block and render it with LikeC4's own tooling — `npx likec4 start`
|
|
2689
|
+
at the docs root, which renders the fleet map AND every adopted service's model: an extending
|
|
2690
|
+
model is part of the root `fleet` project, beside the map, and a standalone one is the project its
|
|
2691
|
+
own `services/<svc>/likec4.config.json` declares (written by `loam subsystem sync`; the Layout
|
|
2692
|
+
section, "Two shapes of a service model", has the rule), and `npx likec4 start features/<FEAT>`
|
|
2693
|
+
for a delta, which is
|
|
2694
|
+
transient and gets no project — and scope the views, because an `include *` over a fleet-sized
|
|
2695
|
+
landscape takes minutes. Two costs arrive with the first STANDALONE service project, and only
|
|
2696
|
+
with it. Measured at the
|
|
2697
|
+
pinned likec4: with more than one project, `likec4 validate` at the docs root refuses without
|
|
2698
|
+
`--project <name>`, so a CI step that ran it bare changes (`likec4 build` and `likec4 export` take
|
|
2699
|
+
every project and need no flag). And the renderer's project list grows by one per such service
|
|
2700
|
+
beside `fleet` — LikeC4 projects are separate models, so a box on the fleet map is not a link into
|
|
2701
|
+
that service's project. A fleet whose models all extend the map pays neither: one project, one
|
|
2702
|
+
picker entry, and the containers of every adopted service reachable from the map's own boxes.
|
|
2703
|
+
`loam init`
|
|
2301
2704
|
scaffolds the landscape with no `views` block for that reason: loam should not plant a diagram it
|
|
2302
2705
|
never draws **— and never asks for one: a fleet that draws nothing owes no views block.** It also
|
|
2303
|
-
scaffolds a `likec4.config.json`
|
|
2304
|
-
|
|
2305
|
-
|
|
2706
|
+
scaffolds a `likec4.config.json` whose `exclude` is `["**/node_modules/**", "features/**"]`: a
|
|
2707
|
+
feature's `delta.likec4` re-declares the landscape's own identifiers and carries a `specification`
|
|
2708
|
+
block of its own, so it is read alone, and `services/` is deliberately NOT excluded — the models a
|
|
2709
|
+
fresh repository writes extend the map and the root project is the only place they parse.)
|
|
2306
2710
|
`loam validate --all --json` over the same 120 services used to be the outlier at ~13–14 s — one
|
|
2307
2711
|
fresh LikeC4 workspace per service; since the batched loader it parses the run's C4 documents in
|
|
2308
2712
|
one shared workspace and answers in **~0.7 s median** on the committed 120-service benchmark
|
|
@@ -2408,7 +2812,28 @@ repairing anything:
|
|
|
2408
2812
|
|
|
2409
2813
|
`loam-report` is a separate support protocol, generated as a command and skill in every profile
|
|
2410
2814
|
but never appended to the lifecycle. It writes
|
|
2411
|
-
`loam-reports/YYYY-MM-DD-<short-slug>.md`
|
|
2815
|
+
`loam-reports/NNN-YYYY-MM-DD-<short-slug>.md` — `NNN` an ORDINAL of at least three digits,
|
|
2816
|
+
zero-padded, one more
|
|
2817
|
+
than the highest already in the directory (`001` when it is empty or absent, and widening past
|
|
2818
|
+
three digits rather than reusing a number once a corpus gets there), so a report has a
|
|
2819
|
+
short handle a commit message, an issue or the next report can cite. The document carries a
|
|
2820
|
+
`- Status: open` line beside its classification, whose vocabulary is `open` / `sent` (handed to
|
|
2821
|
+
the author, set by whoever forwards it) / `fixed in <version>` / `superseded by <NNN>`, and
|
|
2822
|
+
`loam doctor` reads the directory: how many reports it holds, how many stand at each status, and
|
|
2823
|
+
which ordinal comes next. `doctor --json` carries that as the `reports` object — `reports.dir`
|
|
2824
|
+
(the ABSOLUTE path of the directory, `loam-reports/` included, so an agent writes inside it rather
|
|
2825
|
+
than joining the name on a second time), resolved beside the `loam.json` that parsed, or beside
|
|
2826
|
+
the one loam FOUND when it did not parse, and only beside the run directory when there is no
|
|
2827
|
+
config file at or above it — `doctor` is the preflight that runs precisely when the config may not
|
|
2828
|
+
resolve, so it counts the corpus that is there rather than handing out an ordinal five existing
|
|
2829
|
+
reports already used — `reports.present`, `reports.total`, `reports.next`, and
|
|
2830
|
+
`reports.entries[]` with a `file`, an `ordinal` (`null` for a name written before the numbering)
|
|
2831
|
+
and a `status`, in ordinal order, numerically, with unnumbered names last (byte order stopped
|
|
2832
|
+
being report order at exactly the width the ordinal widens to: `1000-…` sorts before `998-…`). The status is read from the report's HEADER FIELD BLOCK alone — the lines above
|
|
2833
|
+
the first `##` heading, quoted code skipped in both markdown spellings (a fence, closed only by a
|
|
2834
|
+
run of the same character at least as long as the one that opened it, and an indented block,
|
|
2835
|
+
which is how the protocol prints its own template) — so a report quoting this protocol's own
|
|
2836
|
+
template still counts as whatever its own header says. The body carries expected/actual behavior,
|
|
2412
2837
|
version, stable envelope and finding codes, relevant locations, write state, a minimal safe
|
|
2413
2838
|
reproduction, classification and missing evidence. Before writing, the agent redacts secrets and
|
|
2414
2839
|
home paths and reduces command output to the excerpt that proves the symptom. It never retries a
|
|
@@ -2517,20 +2942,43 @@ hand-merges the block. That resolution has a documented cost, not a surprise: a
|
|
|
2517
2942
|
landscape moves the file away from the bytes the archive's `.loam-before/` snapshot recorded, so a
|
|
2518
2943
|
later `loam unarchive` of that feature refuses with `snapshot-stale` and needs `--force` — the
|
|
2519
2944
|
resolver is trading undo precision for the merge, and should know it. The trigger for revisiting is
|
|
2520
|
-
written down so nobody relitigates it early
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
map
|
|
2945
|
+
written down so nobody relitigates it early — and half of what it once asked for has already
|
|
2946
|
+
arrived. Per-service internals now live in `services/<svc>/model.likec4` **by default**, and that is
|
|
2947
|
+
where a second team adopting a service writes, so it adds nothing to this file. It is a default and
|
|
2948
|
+
not a prohibition: the map MAY hold a service's interior, and sometimes must. A store a second
|
|
2949
|
+
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
|
|
2953
|
+
title-join lands its container on the map too. What an extending model owns is what it DECLARES,
|
|
2954
|
+
not everything below the service's element. `loam archive` routes rather than forbids — a delta's
|
|
2955
|
+
additions nested under a service whose model extends the map, with the whole parent chain spelled
|
|
2956
|
+
in the map's own ids, are spliced into that model's `extend <fqn> { … }` block instead of this
|
|
2957
|
+
file, and the merged model is proved to parse beside the merged map before anything is written. What is left of the
|
|
2958
|
+
trigger is the CROSS-SERVICE EDGE, which still exists twice — container-level in the model, service
|
|
2959
|
+
level here — because the spine, the platform census and every other fleet check read this file
|
|
2960
|
+
alone. So: when landscape merge conflicts become routine — weekly, not monthly — thin the map to
|
|
2961
|
+
elements plus cross-service edges and teach the fleet checks to read the edges out of the models
|
|
2962
|
+
that attest them. Until then, one file keeps the fleet map one diffable document, and
|
|
2963
|
+
`landscape.service-isolated` remains the nudge that carries an attested call up into it.
|
|
2524
2964
|
|
|
2525
2965
|
## Two flows
|
|
2526
2966
|
|
|
2527
2967
|
- **Bootstrap (reverse):** `loam adopt --service <id>` emits a **brief**, not an extraction — the
|
|
2528
2968
|
target paths (and which already exist, to be diffed and never overwritten), the grammar of each
|
|
2529
|
-
artifact, what the living
|
|
2969
|
+
artifact, what the living fleet map — the `architecture/` project, not the landscape file alone —
|
|
2970
|
+
already says about the service, the frontmatter to write, the
|
|
2530
2971
|
checks `loam validate --service <id>` will then run, and the ones that do not exist. One target is
|
|
2531
2972
|
not under `services/<id>/`: `architecture/landscape.likec4`, carried with `action: "edit"` and a
|
|
2532
|
-
`landscape.instruction` naming the
|
|
2533
|
-
nothing in the map resolves to the service
|
|
2973
|
+
`landscape.instruction` naming the write this service owes the fleet map — the element while
|
|
2974
|
+
nothing in the map resolves to the service, the EDGES while an element does and no edge touches it
|
|
2975
|
+
(`landscape.touched: false`, with `landscape.attested` listing the calls the service's own
|
|
2976
|
+
model.likec4 declares across its boundary); absent, and the instruction `null`, only once an
|
|
2977
|
+
element resolves and an edge touches it. `landscape.callersKnown` says whether loam could read WHO
|
|
2978
|
+
CALLS this service — true when the `architecture/` project parses, and true when it does not but
|
|
2979
|
+
`architecture/landscape.likec4` does. It is what an empty `landscape.expects` has to be read
|
|
2980
|
+
against: only a readable landscape makes "nothing calls this service" evidence, and it is the one
|
|
2981
|
+
fact `openapi.yaml`'s `required` is derived from. An agent reads the code and writes draft
|
|
2534
2982
|
`model.likec4` + `spec.md` + `openapi.yaml` + `adrs/`, `runbook.md`, `health.yaml`, and makes that
|
|
2535
2983
|
one addition to the shared map. A human promotes `draft` -> `verified` with `loam vouch`.
|
|
2536
2984
|
- **Forward (generative):** author `features/<FEAT>/delta.likec4` -> `loam delta <FEAT>` projects it
|
|
@@ -2556,9 +3004,9 @@ component-surface pins), `verify`,
|
|
|
2556
3004
|
transactional `archive` / `unarchive`, `vouch`, and the OpenSpec audit/migration on-ramp are
|
|
2557
3005
|
implemented, each with a `--json` contract.
|
|
2558
3006
|
|
|
2559
|
-
Known and unclosed, so nobody has to discover them: the
|
|
2560
|
-
|
|
2561
|
-
|
|
3007
|
+
Known and unclosed, so nobody has to discover them: the scheduled CI `stability` job has still to be
|
|
3008
|
+
observed green, even though behavior, coverage, the installed-package smoke and the local stress
|
|
3009
|
+
runner all pass. Both contract axes now merge a components-only delta: a feature
|
|
2562
3010
|
asyncapi.yaml declaring no channel/operation/message slot (`components.schemas`-only, say) merges
|
|
2563
3011
|
the component surfaces it declares, pinned by the root `x-loam-baselines` record, and they enter the
|
|
2564
3012
|
ref sweep like anything else the merge wrote — so a `$ref` whose target the delta itself defines
|