@ybotok/loam 0.2.0-alpha.5 → 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 +1063 -0
- package/MIGRATING-from-OpenSpec.md +7 -4
- package/QUICKSTART.md +8 -3
- package/README.md +13 -7
- package/ROADMAP.md +285 -11
- package/SCHEMA.md +566 -45
- 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 +53 -23
- 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/commit.js +34 -1
- package/dist/commands/seed/commit.js.map +1 -1
- package/dist/commands/seed/plan.js +158 -13
- 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/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 +238 -87
- 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 +174 -0
- package/dist/commands/subsystem/txn/report.js.map +1 -0
- package/dist/commands/subsystem/txn/txn.js +126 -22
- package/dist/commands/subsystem/txn/txn.js.map +1 -1
- package/dist/commands/subsystem/txn/views.js +159 -19
- 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/health-deps.js +9 -6
- package/dist/commands/validate/checks/health-deps.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 +37 -13
- package/dist/commands/validate/fleet/census.js.map +1 -1
- package/dist/commands/validate/fleet/landscape.js +131 -120
- 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 +102 -0
- package/dist/commands/validate/fleet/map/attest.js.map +1 -0
- package/dist/commands/validate/fleet/map/bindings.js +109 -0
- package/dist/commands/validate/fleet/map/bindings.js.map +1 -0
- package/dist/commands/validate/fleet/map/consumers.js +219 -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/scorecard/scorecard.js.map +1 -1
- 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 +45 -15
- package/dist/commands/validate/fleet/views/ids.js.map +1 -1
- package/dist/commands/validate/fleet/views/projects.js +258 -70
- package/dist/commands/validate/fleet/views/projects.js.map +1 -1
- package/dist/commands/validate/fleet/views/root-documents.js +99 -0
- package/dist/commands/validate/fleet/views/root-documents.js.map +1 -0
- package/dist/commands/validate/fleet/views/stale.js +32 -15
- 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 +19 -5
- 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 +70 -17
- 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/refusals.js +5 -3
- package/dist/core/agent/agents-md/refusals.js.map +1 -1
- package/dist/core/agent/agents-md.js +26 -7
- 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 +123 -14
- package/dist/core/agent/workflows/adopt.js.map +1 -1
- package/dist/core/agent/workflows/check.js +34 -20
- 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/agent/workflows/reference/spine.js +12 -6
- package/dist/core/agent/workflows/reference/spine.js.map +1 -1
- package/dist/core/brief/brief.js +88 -19
- package/dist/core/brief/brief.js.map +1 -1
- package/dist/core/brief/checks.js +133 -7
- 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 +94 -29
- package/dist/core/brief/targets.js.map +1 -1
- package/dist/core/brief/unchecked.js +13 -3
- 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 +77 -7
- 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 +12 -6
- package/dist/core/c4/project/documents.js.map +1 -1
- package/dist/core/c4/project/load.js +85 -42
- package/dist/core/c4/project/load.js.map +1 -1
- package/dist/core/c4/project/staged.js +53 -15
- 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 +16 -10
- 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 +29 -24
- 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 +36 -8
- 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/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/checks.js +5 -1
- package/dist/core/gate/checks.js.map +1 -1
- package/dist/core/gate/partners.js +65 -26
- 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 +45 -38
- 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/projects.js +93 -35
- package/dist/core/repo/tree/render/projects.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 +20 -4
- package/dist/core/repo/tree/render/stale.js.map +1 -1
- package/dist/core/repo/tree/render/views.js +149 -32
- 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/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 +51 -1
- package/dist/core/vocabulary/maturity.js.map +1 -1
- package/examples/README.md +30 -8
- package/examples/docs/architecture/landscape.likec4 +47 -0
- package/examples/docs/architecture/services.likec4 +70 -0
- package/examples/docs/architecture/subsystems.likec4 +3 -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 +58 -70
- package/examples/docs/services/payment-service/arch.spec.md +7 -4
- package/examples/docs/services/payment-service/model.likec4 +38 -39
- package/examples/docs/services/platform/identity-service/arch.spec.md +7 -4
- package/examples/docs/services/platform/identity-service/model.likec4 +35 -37
- package/examples/docs/services/platform/notification-service/health.yaml +8 -3
- package/examples/docs/services/platform/notification-service/model.likec4 +43 -64
- package/package.json +1 -1
- package/examples/docs/services/checkout-web/likec4.config.json +0 -4
- package/examples/docs/services/order-service/likec4.config.json +0 -4
- package/examples/docs/services/payment-service/likec4.config.json +0 -4
- package/examples/docs/services/platform/identity-service/likec4.config.json +0 -4
- package/examples/docs/services/platform/notification-service/likec4.config.json +0 -4
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]
|
|
@@ -71,12 +72,13 @@ docs/
|
|
|
71
72
|
spec.md narrative, then `## Requirements` — `Requirement-ID:` REQUIRED
|
|
72
73
|
glossary/<term>.md the system's domain vocabulary, one file per term; nesting allowed [authored, opt-in]
|
|
73
74
|
services/<svc>/ at the root (unfiled), or inside subsystem directories at any depth
|
|
74
|
-
model.likec4 boundary C4 (containers/components) [adopt]
|
|
75
|
-
likec4.config.json one LikeC4 project per service model, create-only [GENERATED: loam subsystem sync]
|
|
75
|
+
model.likec4 boundary C4 (containers/components); extends the map, or stands alone [adopt]
|
|
76
|
+
likec4.config.json one LikeC4 project per STANDALONE service model, create-only [GENERATED: loam subsystem sync]
|
|
76
77
|
spec.md living requirements (Requirement/Scenario) [adopt]
|
|
77
78
|
arch.spec.md living ARCHITECTURE requirements (outbox, retries, alerts; Covers:) [authored]
|
|
78
79
|
openapi.yaml API contract [adopt / authored]
|
|
79
80
|
asyncapi.yaml async contract (AsyncAPI 3) [adopt / authored]
|
|
81
|
+
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]
|
|
80
82
|
adrs/NNNN-*.md MADR decisions [adopt-seed / authored]
|
|
81
83
|
runbook.md operational runbook [adopt-draft / authored]
|
|
82
84
|
health.yaml SLI/SLO, checks, critical deps [authored]
|
|
@@ -106,6 +108,38 @@ tagged with the capability it realizes. A flow that spans services does not belo
|
|
|
106
108
|
service's directory, and the view's steps join to the landscape's own relationships, which is what
|
|
107
109
|
makes the flow checkable rather than merely drawn.
|
|
108
110
|
|
|
111
|
+
A flow that stays INSIDE one service — the hop sequence behind an arch.spec.md requirement whose
|
|
112
|
+
`Covers:` names containers — cannot live under `architecture/`: loam parses that directory as its
|
|
113
|
+
OWN document set — the map plus its siblings, and nothing under `services/` — so a step naming a
|
|
114
|
+
container does not resolve there, and one such file takes the whole fleet map down
|
|
115
|
+
(`landscape.invalid` plus one `spine.landscape-invalid` per service). That holds however the models
|
|
116
|
+
are shaped: the renderer's root project may also hold every extending model and draw such a step
|
|
117
|
+
perfectly, and loam still grades the map alone, because the map is the document every cross-service
|
|
118
|
+
check reads. It lives beside
|
|
119
|
+
the model instead: every `.likec4` under `services/<…>/<svc>/` other than model.likec4, plus any
|
|
120
|
+
tagged `dynamic view` inside model.likec4 itself, is that service's own LikeC4 project — for an
|
|
121
|
+
extending model, the map plus those documents; for a standalone one, the set
|
|
122
|
+
`services/<svc>/likec4.config.json` registers for the renderer — and `loam validate --service <id>`
|
|
123
|
+
(and `--all`, per service) reads exactly that set, whether or not the project file exists.
|
|
124
|
+
`usecases/<name>.likec4` is the convention; `views.likec4` reads the same. Tag the view
|
|
125
|
+
`#req-<Requirement-ID>` of a requirement in that service's spec.md or arch.spec.md. The tag has to
|
|
126
|
+
be DECLARED somewhere the project can see: for a standalone model that is model.likec4's own
|
|
127
|
+
`specification` — the one block that project has; for an extending model it is
|
|
128
|
+
`architecture/landscape.likec4`'s block, or a tags-only `specification { tag req-… }` beside the
|
|
129
|
+
`extend` (legal, and it lands the tag in the project's specification). Declaring it in BOTH is a
|
|
130
|
+
duplicate, and the code depends on which document carries the second declaration — the finding
|
|
131
|
+
lands where the file is, not at fleet altitude: `c4.invalid` (an ERROR, and the model's grading is
|
|
132
|
+
suspended behind it) when the tags-only block is in model.likec4, `usecase.flow-invalid` when it is
|
|
133
|
+
in a sibling. `c4.fleet-project-invalid` is the fleet-scope class one step out: TWO SERVICES each
|
|
134
|
+
declaring the same tag locally, where no per-service project sees the collision.
|
|
135
|
+
model.likec4 stays THE model:
|
|
136
|
+
`c4.valid`, `Covers:` and every other model check read it alone, and a sibling that does not parse
|
|
137
|
+
is one `usecase.flow-invalid` while the model keeps grading. Files loam does not read: a `.likec4`
|
|
138
|
+
in a subsystem directory, in a feature directory outside `delta.likec4`/`usecases/`/`deployment/`,
|
|
139
|
+
the generated `subsystems.likec4` and `services.likec4`, a sibling the service's own `likec4.config.json` `exclude`
|
|
140
|
+
covers (it is in no project the renderer loads either), and every UNTAGGED `dynamic view`
|
|
141
|
+
anywhere — a diagram, never a use case.
|
|
142
|
+
|
|
109
143
|
A feature **brings** one the way it brings every other axis: `features/<FEAT>/usecases/<name>.likec4`
|
|
110
144
|
is a views-only document that `loam archive` copies into `architecture/usecases/` and
|
|
111
145
|
`loam unarchive` takes back. Create-only, and graded before the copy against the map the same merge
|
|
@@ -113,6 +147,189 @@ would leave behind — so a flow may draw a hop into a service the feature's own
|
|
|
113
147
|
and a hop into an element the merge does NOT land is refused at plan time rather than archived into
|
|
114
148
|
a map the next reader's `loam validate --all` fails on.
|
|
115
149
|
|
|
150
|
+
### Two shapes of a service model
|
|
151
|
+
|
|
152
|
+
`services/<…>/<svc>/model.likec4` comes in two shapes, and **the file's own grammar decides which**
|
|
153
|
+
— never a config key, never a flag. loam scans the bytes (comments and strings masked) for a
|
|
154
|
+
top-level `specification { }` block containing an `element` declaration:
|
|
155
|
+
|
|
156
|
+
- an **extending model** declares no element kind. It is parsed INSIDE the fleet's `architecture/`
|
|
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
|
|
159
|
+
the map already binds to the directory. `<fqn>` is the FULLY-QUALIFIED id: `extend orderService`
|
|
160
|
+
resolves to nothing when the element is `marketplace.orderService`. No `specification` is needed,
|
|
161
|
+
no partner is re-declared, and `model {}` or no model at all are both fine. `extend` accepts
|
|
162
|
+
`#tags` and a `metadata { }` block on the element it opens (merged into it — the map's `service`
|
|
163
|
+
binding survives) and REFUSES `description`, which belongs on the map's declaration. This is the
|
|
164
|
+
shape `loam adopt` briefs, and the shape `loam init --create` scaffolds a repository for.
|
|
165
|
+
- a **standalone model** declares at least one `element <kind>` inside a `specification { }` block.
|
|
166
|
+
It is parsed ALONE, exactly as every model was before this shape existed; it may re-declare the
|
|
167
|
+
partners it names; and it renders only as a LikeC4 project of its own. Legal forever — a service
|
|
168
|
+
whose docs are reviewed from its own repository, a model adopted before the fleet had a map — and
|
|
169
|
+
not what a new model is written as.
|
|
170
|
+
|
|
171
|
+
**Who declares kinds and tags.** In the extending shape, `architecture/landscape.likec4` is the one
|
|
172
|
+
`specification` block: a kind a model needs and the map does not declare (`element database`,
|
|
173
|
+
`element queue`) is added THERE, once, and a missing one is a single parse error on the model. A
|
|
174
|
+
tags-only `specification { tag req-CANCEL }` beside an `extend` is legal and lands the tag in the
|
|
175
|
+
project's specification — but a tag the map already declares, declared again there, is a duplicate
|
|
176
|
+
blamed on both files. In the standalone shape the model declares everything it uses, which is
|
|
177
|
+
exactly why its copies of the map's elements can drift.
|
|
178
|
+
|
|
179
|
+
**Seven grades come with the pair**, and one existing grade is narrowed:
|
|
180
|
+
|
|
181
|
+
| code | shape | what it says |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
189
|
+
| `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 |
|
|
190
|
+
| `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 |
|
|
191
|
+
|
|
192
|
+
`c4.invalid` and `c4.valid` are unchanged codes with adjusted scope: for an extending model every
|
|
193
|
+
error the per-service project raises is THIS model's, including one the parser blames on the
|
|
194
|
+
landscape, because the map parsed clean on its own; and `c4.valid`'s message reads
|
|
195
|
+
`extends the fleet map — N elements · M relationships`, followed by ` · K tagged flow(s) graded`
|
|
196
|
+
when the service's own project declared any (the one surface a healthy intra-service flow appears
|
|
197
|
+
on). When the map itself does not parse, an
|
|
198
|
+
extending model cannot be read at all: neither code is emitted, and `spine.landscape-invalid` says
|
|
199
|
+
so with the clause ` — and model.likec4 extends it, so the model cannot be read either`.
|
|
200
|
+
|
|
201
|
+
Every other model-dependent check reads the same slice it always read — the service's OWN elements
|
|
202
|
+
and relationships. For an extending model that is what the project holds minus what the map alone
|
|
203
|
+
holds, plus the **partners** its own edges point at and their ancestors, so `health.yaml`'s
|
|
204
|
+
`kafka` still resolves when the only edge names `kafka.orderEvents`. Nothing reads the map's edges
|
|
205
|
+
as the model's.
|
|
206
|
+
|
|
207
|
+
**What loam grades is still the `architecture/` project alone.** The renderer's root project is
|
|
208
|
+
WIDER than the fleet map: it also holds every extending model. loam's fleet checks — the spine, the
|
|
209
|
+
bindings, the fleet-shape advisories, and the steps of every `architecture/usecases/` flow — read
|
|
210
|
+
the landscape plus its `architecture/` siblings and nothing else, whatever shape the models take.
|
|
211
|
+
So a cross-service flow names SERVICE elements: a hop naming a container an extending model
|
|
212
|
+
declares renders perfectly and is `landscape.invalid` for loam. That is deliberate, and the reason
|
|
213
|
+
is blast radius — a service's model is graded on its own merits, and one broken model must never
|
|
214
|
+
blank the fleet's own map. A container-level flow has its own home beside the model that declares
|
|
215
|
+
the container. The same holds for a feature's `features/<FEAT>/usecases/` flow, and the gate says
|
|
216
|
+
so before the merge rather than after it: the preview stages no service model, and it ROUTES a
|
|
217
|
+
nested addition into the model that owns that service's interior exactly as `loam archive` would.
|
|
218
|
+
So a hop naming `<service-fqn>.<container>` resolves in neither the preview nor the tree the archive
|
|
219
|
+
leaves, and `usecase.flow-invalid` refuses — where the un-routed preview resolved it, passed the
|
|
220
|
+
gate `--approve` cannot override, and landed a flow the next `loam validate --all` failed on.
|
|
221
|
+
|
|
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
|
|
245
|
+
consumer a service's EXTENDING model attests exactly as it counts one the map draws, so a nested
|
|
246
|
+
store two services reach is `landscape.datastore-shared` — the store's owner plus every service
|
|
247
|
+
whose model draws an outbound call at it or below it. The second consumer draws that call as a
|
|
248
|
+
container-level edge to `<owner-fqn>.<store>` in its OWN model, and the service-level twin on the
|
|
249
|
+
map as usual. The two placements give the store the same id,
|
|
250
|
+
`<owner-fqn>.<store>`, so moving the declaration OUT of the owner's `extend` block into the owner's
|
|
251
|
+
element on the map rewrites no edge and no `Covers:` line — measured on `examples/docs`, where
|
|
252
|
+
moving `db` out of order-service's `extend` block into `orderService` on the map left the model at
|
|
253
|
+
12 elements · 8 relationships, left `Covers: marketplace.orderService.db` in its `arch.spec.md`
|
|
254
|
+
resolving, and left checkout-web's container-level edge to it resolving. The move BACK is free only
|
|
255
|
+
while no other service names the store: with checkout-web's edge drawn, putting `db` back into
|
|
256
|
+
order-service's `extend` block is `c4.invalid` on checkout-web's model ("Could not resolve
|
|
257
|
+
reference to Referenceable named 'db'"), or `landscape.invalid` plus one `spine.landscape-invalid`
|
|
258
|
+
per service when that second edge is drawn on the map. `landscape.datastore-shared` needs a second
|
|
259
|
+
consumer to fire, so it always prints in the state where map→extend breaks, and its message says
|
|
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
|
|
264
|
+
EXTENDING models only, because their ids are the map's fully-qualified ids and the join is exact,
|
|
265
|
+
while a standalone model's ids are its own file's and are never matched to a landscape element.
|
|
266
|
+
|
|
267
|
+
**Renderer consequences.** The root project holds the map **and** every extending model, so one
|
|
268
|
+
`npx likec4 start` at the docs root draws the fleet and the inside of every adopted service, and
|
|
269
|
+
the project picker holds one entry. A standalone model is the exception, and it costs two files:
|
|
270
|
+
ONE entry in the root `likec4.config.json`'s `exclude` covering its directory (its kinds would be
|
|
271
|
+
duplicates of the map's inside one project), plus its own `likec4.config.json`, which is what makes
|
|
272
|
+
it a project of its own.
|
|
273
|
+
`loam subsystem sync` maintains both halves — it writes the per-service file for standalone models
|
|
274
|
+
only, and rewrites the root `exclude` so that it holds `services/<tree>/**` for exactly the models
|
|
275
|
+
that stand alone. The rewrite removes the wholesale `services/**` and every `services/<tree>/**`
|
|
276
|
+
naming a directory that IS a service, keeps every other entry in the order the team wrote it (a
|
|
277
|
+
`services/legacy/**` for a directory that is no service survives untouched), and appends the
|
|
278
|
+
standalone entries sorted. It runs when at least one extending model exists, or when a standalone
|
|
279
|
+
model's directory is not covered — so a repository whose models all stand alone behind `services/**`
|
|
280
|
+
is never touched, and a fresh scaffold that gains its first standalone model earns the entry on the
|
|
281
|
+
next sync. `--json` reports it as `projects.exclude`, with what was added and removed; the text view
|
|
282
|
+
prints one line when it rewrote. An entry covers a directory when it names that directory —
|
|
283
|
+
`<dir>`, `<dir>/`, `<dir>/*`, `<dir>/**` and `<dir>/**/*` all do, and a wildcard segment in the
|
|
284
|
+
middle (`services/*/legacy`) matches one level — or when it is a raw path prefix of it from a `/`
|
|
285
|
+
boundary, which is what the renderer measures: `services/pay` covers `services/payment-service`,
|
|
286
|
+
while `services/pay/**` covers nothing there. An entry is normalised the way the renderer
|
|
287
|
+
normalises it before anything is matched: a leading `./`, an inner `/./` and an inner `//` are
|
|
288
|
+
resolved away, a TRAILING `/` is not (bare `architecture` also hides `architecture-old/`,
|
|
289
|
+
`architecture/` does not), `.//` leaves a literal `.` directory and hides nothing, and a BACKSLASH
|
|
290
|
+
is a literal character to the matcher, never a separator — `services\order-service\**` hides
|
|
291
|
+
nothing at all, so loam neither grades on it nor rewrites it. Measured at the 1.59.2 pin against
|
|
292
|
+
`npx likec4 validate .` file counts; the table is in `src/core/c4/root-project/exclude.ts` and
|
|
293
|
+
pinned in `test/root-exclude.test.ts`. A directory the list already covers in
|
|
294
|
+
any of those spellings earns no second entry. `projects.exclude` carries `updated`, `entries`,
|
|
295
|
+
`added`, `removed` and `unreadable` — the last is `true` when the root file exists and loam could
|
|
296
|
+
not read an `exclude` list out of it, which is never the same answer as `entries: []`, and the list
|
|
297
|
+
is then reported untouched. Beside it, `--json` carries
|
|
298
|
+
`projects.created` (the per-service project files written) and `projects.removed` (POSIX paths
|
|
299
|
+
relative to the DOCS ROOT, deleted this run, sorted by path; `[]` otherwise) — and
|
|
300
|
+
`projects.created` is spelled the same way. Both are relative to the docs root, not to the
|
|
301
|
+
repository root, which differ whenever `docsDir` is a subdirectory. A fresh repository is
|
|
302
|
+
scaffolded with
|
|
303
|
+
`exclude: ["**/node_modules/**", "features/**"]`.
|
|
304
|
+
|
|
305
|
+
**Migrating a standalone model to the extending shape** is a documented recipe an agent performs;
|
|
306
|
+
loam does not rewrite a model, because the comments in a real one are somebody's and not loam's to
|
|
307
|
+
reflow. In order:
|
|
308
|
+
|
|
309
|
+
1. Write into `architecture/landscape.likec4` any element kind the model uses that the map does not
|
|
310
|
+
declare (`element database`, `element queue`), and any element the model reaches that the map
|
|
311
|
+
does not draw — an external system, a broker's topic — as the fleet's own declaration.
|
|
312
|
+
2. Delete the model's `specification` block.
|
|
313
|
+
3. Delete every element the map declares: the service's own element, and every re-declared partner.
|
|
314
|
+
4. Wrap what is left in `model { extend <fqn> { … } }`, `<fqn>` being the fully-qualified id of the
|
|
315
|
+
map's element for this service. Child ids inside the block stay relative; internal edges may be
|
|
316
|
+
written inside it.
|
|
317
|
+
5. Move the cross-boundary edges outside the `extend` block and spell both endpoints in full —
|
|
318
|
+
`marketplace.orderService.outbox -> kafka.orderEvents`.
|
|
319
|
+
6. Requalify every id that names one of those elements from outside the file: the `Covers:` lines in
|
|
320
|
+
`arch.spec.md`, the `of <element>` and `include` lines of any `views` block, and the hops of any
|
|
321
|
+
`#req-`-tagged flow beside the model. There is no suffix matching anywhere — `orderService.db`
|
|
322
|
+
does not answer for `marketplace.orderService.db` — so every model-local nested id has to be
|
|
323
|
+
rewritten. `health.yaml`'s `dependencies:` are the one place that usually needs no edit, because
|
|
324
|
+
a dependency resolves when an element of the own slice answers to it **by id, `metadata { service }`
|
|
325
|
+
binding or title**, and the slice holds the service's own elements plus every endpoint of the
|
|
326
|
+
model's own edges and each endpoint's ANCESTORS: `kafka` still answers through an edge naming
|
|
327
|
+
`kafka.orderEvents`, and a peer service still answers through the binding on the map's element.
|
|
328
|
+
Prefer the service id or a top-level system id there — those two spellings resolve under either
|
|
329
|
+
model shape, and a model-local nested id does not.
|
|
330
|
+
7. Delete `services/<…>/<svc>/likec4.config.json`.
|
|
331
|
+
8. Run `loam subsystem sync` (it rewrites the root `exclude`), then `loam validate --all`.
|
|
332
|
+
|
|
116
333
|
## Subsystems — a navigable tree under `services/`
|
|
117
334
|
|
|
118
335
|
`services/` may be one flat level (the compatibility case, and forever legal) or an unbounded tree
|
|
@@ -135,6 +352,13 @@ A directory under `services/` is classified **three ways, never two**:
|
|
|
135
352
|
subsystem's marker while another moves a service in, and the group directory would otherwise be
|
|
136
353
|
read as one empty service. The fleet is never reported smaller than it is.
|
|
137
354
|
|
|
355
|
+
A directory named for one of the service's own subdirectories (`adrs/`, `usecases/`, `ui/`) is
|
|
356
|
+
service interior wherever it sits: the walk never reads one as a service or a subsystem, and never
|
|
357
|
+
counts one as the subdirectory that makes its parent a group. Writing the documented
|
|
358
|
+
`services/<id>/usecases/<name>.likec4` slot into a directory that holds no artifact FILE yet used
|
|
359
|
+
to make the walk read `usecases` as a service and its parent as an unmarked group — three errors
|
|
360
|
+
earned by following the instruction `loam init` had just scaffolded.
|
|
361
|
+
|
|
138
362
|
`subsystem.yaml` is a **marker, not a manifest**:
|
|
139
363
|
|
|
140
364
|
```yaml
|
|
@@ -166,6 +390,30 @@ reports). The output is deterministic and line-oriented — subsystems sorted by
|
|
|
166
390
|
one `include` per line, no timestamps, no absolute paths — so it is byte-reproducible on any machine
|
|
167
391
|
and two concurrent moves into different groups merge in git without intervention.
|
|
168
392
|
|
|
393
|
+
**Styling.** Each generated view carries `global style subsystems` — after its `title` and
|
|
394
|
+
`description`, before its `include`/`group` — exactly when the `architecture/` project declares a
|
|
395
|
+
global style with that id: `global { styleGroup subsystems { style element.tag = #external { color gray } } }`
|
|
396
|
+
(or the single-rule `global { style subsystems … }`), in the landscape or in ANY `.likec4` under
|
|
397
|
+
`architecture/` the root `likec4.config.json`'s `exclude` does not cover — a `palette.likec4` beside
|
|
398
|
+
the map works, and one under an excluded path is loaded by neither the renderer nor loam.
|
|
399
|
+
`architecture/landscape.likec4` is the one exception: loam keeps the map in that set however the
|
|
400
|
+
list is written, because an architecture project with no documents in it reads as a fleet that
|
|
401
|
+
models nothing. With the targeted tag declared in the `specification` block.
|
|
402
|
+
Declaring it is the whole opt-in. loam reads only that the id is declared, never what the group
|
|
403
|
+
says, and writes nothing when it is not — a reference to an undeclared style is a parse error that
|
|
404
|
+
blanks the whole `architecture/` project in the renderer. When the project declares style groups
|
|
405
|
+
under other names only, the file opens with one comment line naming them and saying none is
|
|
406
|
+
`subsystems`, and the views carry the renderer's defaults; there is no single-group fallback.
|
|
407
|
+
Both halves restale the file once (`subsystem.views-stale`) until the next `loam subsystem sync`:
|
|
408
|
+
declaring or removing the group named `subsystems` changes whether the views carry the
|
|
409
|
+
`global style subsystems` line, and declaring or removing a group under ANY OTHER name changes the
|
|
410
|
+
disclosure comment that lists what is available. Whether a service's own views can borrow that group depends on the shape of
|
|
411
|
+
its model: a model that EXTENDS the map is in the root project, so a `global styleGroup` declared
|
|
412
|
+
under `architecture/` reaches the views beside it and referencing the id there is legal; a model
|
|
413
|
+
that STANDS ALONE is a project of its own, and a group declared under `architecture/` does not reach
|
|
414
|
+
it — those views need a declaration of their own, or the reference is the parse error that blanks
|
|
415
|
+
that project.
|
|
416
|
+
|
|
169
417
|
Each view carries the **label a human reads**: `title`, and `description` when the marker authors
|
|
170
418
|
one. The title is the subsystem's **path** — every marked ancestor's label and its own, joined with
|
|
171
419
|
` / `, so `services/platform/order-flow` reads `Platform / Order flow` — and each label is the
|
|
@@ -199,23 +447,161 @@ the file behind.
|
|
|
199
447
|
`loam subsystem sync` is the one regenerator (it also *removes* the file when the tree has no
|
|
200
448
|
subsystems), every `loam subsystem` write verb regenerates it inside the same transaction, and
|
|
201
449
|
`validate --all` grades staleness by **content comparison** (line endings normalized; a CRLF
|
|
202
|
-
checkout changes no fact) —
|
|
203
|
-
`subsystem.views-stale`,
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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.
|
|
456
|
+
|
|
457
|
+
`sync` refuses to rewrite this file while the `architecture/` project cannot be read. The render
|
|
458
|
+
needs the map's elements and its global-style census, and a tolerant "no elements" answer wrote a
|
|
459
|
+
file holding title and description alone — the `global style subsystems` line, every `include` and
|
|
460
|
+
the boundary comment gone — while reporting `updated … 1 subsystem view(s)` and answering `current`
|
|
461
|
+
ever after. So: a load error falls back to the landscape FILE when that parses on its own (a
|
|
462
|
+
duplicate `subsystems` group declared in two `architecture/` documents costs nothing), and when
|
|
463
|
+
that fails too the facts are UNKNOWN. With an existing views file, `sync` then writes nothing about
|
|
464
|
+
it, prints why, and reports the `--json` action `blocked`, with `blocked.reason` reading
|
|
465
|
+
`landscape-invalid` — exit 0, because `loam validate --all`'s `landscape.invalid` is the gating
|
|
466
|
+
surface. With no file yet, and inside a tree-changing verb's commit window (`new`/`move`/`rm`/
|
|
467
|
+
`rename`, whose contract is that the file lands with the change), the tolerant shape is written as
|
|
468
|
+
before and the same note is printed.
|
|
469
|
+
|
|
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`,
|
|
575
|
+
which `loam subsystem sync` also writes, beside every **standalone** `model.likec4` that has none.
|
|
576
|
+
An extending model needs no such file and never gets one: it belongs to the root project, which is
|
|
577
|
+
where it parses, and a project file beside it is `service.likec4-config-stray` — which `sync`
|
|
578
|
+
DELETES, in the same journaled commit as everything else it writes. A standalone model
|
|
579
|
+
needs one because it cannot share a project with the map — it declares its own kinds, and inside
|
|
580
|
+
one project every declaration is a duplicate blamed on the map too — so `sync` also keeps the root
|
|
581
|
+
`exclude` covering exactly the directories of the models that stand alone. Before extending models
|
|
582
|
+
existed, `services/**` was excluded wholesale and every model needed the file; what that exclusion
|
|
583
|
+
cost went unstated for a release: an
|
|
212
584
|
adopted service was a box on the fleet map with nothing renderable inside it. Measured at the
|
|
213
585
|
pinned likec4, a project file INSIDE a service directory is registered as a project of its own
|
|
214
|
-
regardless of
|
|
215
|
-
project per service, each carrying LikeC4's own `index` view of the containers beside the
|
|
216
|
-
one, and a model that does not parse breaks only its own project.
|
|
217
|
-
|
|
218
|
-
|
|
586
|
+
regardless of the exclusion, so a docs root holding standalone models loads `fleet` plus one
|
|
587
|
+
project per such service, each carrying LikeC4's own `index` view of the containers beside the
|
|
588
|
+
authored one, and a model that does not parse breaks only its own project. (An extending model
|
|
589
|
+
is inside `fleet`, so a model that does not parse breaks the root project instead — the trade the
|
|
590
|
+
shape makes, and `c4.fleet-project-invalid` is what names it before a reader meets it in the
|
|
591
|
+
renderer.) The file is **create-only for a standalone model** —
|
|
592
|
+
never compared, never rewritten — under the root config's ownership rule, not the
|
|
593
|
+
views file's: there is no `-stale` grade because nothing can make it stale. It is removed in
|
|
594
|
+
exactly one state, and only there: beside a model that EXTENDS the map. That nested project claims
|
|
595
|
+
the model out of the root project — the workspace loads two projects, the model is Invalid inside
|
|
596
|
+
its own one (the map's kinds are not in there with it) and `export json --project fleet` then loses
|
|
597
|
+
the service's containers. Which of the two the workspace does was measured both ways at the 1.59.2
|
|
598
|
+
pin: on a seeded fleet whose model extends a top-level id the containers went; on `examples/docs`
|
|
599
|
+
the root project kept the model and the export was byte-identical. What is true in every state is
|
|
600
|
+
the second project itself — `likec4 validate .` at the docs root refuses without `--project` from
|
|
601
|
+
the moment that file exists. So it is not a harmless leftover, and `loam subsystem sync` deletes
|
|
602
|
+
it. One beside a model that is GONE stays: that one really is the team's — though the same second
|
|
603
|
+
project it registers is there too, so `likec4 validate .` and `likec4 build` at the docs root need
|
|
604
|
+
`--project` while it stands (ROADMAP's "Known and carried, 2026-09-04"). Its bytes are a
|
|
219
605
|
function of the id alone (two keys, `name` and `title`), so placement never changes them and a
|
|
220
606
|
`subsystem move` carries the file with no write of its own — it rides inside the rename the move's
|
|
221
607
|
transaction already journals (in a git-backed repo, commit the files `sync` wrote first: `move`
|
|
@@ -234,11 +620,19 @@ nothing is written and the text output says so. No model, no file: a spec-only s
|
|
|
234
620
|
nothing and gets its file on the first `sync` after its model lands. No legal id, no file either: a
|
|
235
621
|
directory whose name breaks the service-id grammar (`service.id-invalid`) would get a `name` the
|
|
236
622
|
renderer silently drops, and a create-only file the renderer ignores would silence the grade for
|
|
237
|
-
good, so `sync` skips it and the rename that clears `service.id-invalid` earns the file.
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
623
|
+
good, so `sync` skips it and the rename that clears `service.id-invalid` earns the file.
|
|
624
|
+
`validate --all` grades presence only — one warning per service with
|
|
625
|
+
a STANDALONE model and no file, `service.likec4-config-missing`, on the fleet `landscape` target,
|
|
626
|
+
never an error, never in `--service`, `status` or `doctor`; the fix is `loam subsystem sync`, never
|
|
627
|
+
a hand-written file. Its two siblings grade the extending shape's version of the same question —
|
|
628
|
+
`service.likec4-config-stray` for a project file that should not be there, `service.model-excluded`
|
|
629
|
+
for a root `exclude` entry that hides the model from the renderer — and `sync` is the repair for
|
|
630
|
+
both: it removes the stray and rewrites the entry. A third reads the same list and is nobody's to
|
|
631
|
+
rewrite: `landscape.excluded` says an entry covers `architecture/landscape.likec4` itself, so the
|
|
632
|
+
renderer loads no map at all — `sync` maintains only the `services/` entries and will not touch that
|
|
633
|
+
line, and the finding quotes it so its author can. It also suspends
|
|
634
|
+
`c4.fleet-project-invalid` for that run, because the renderer's project is exactly what the entry
|
|
635
|
+
broke and grading it would report the one line once per extending model. A
|
|
242
636
|
feature's `delta.likec4` is deliberately out of scope: the directory is transient and
|
|
243
637
|
`loam archive` moves it where a project file would still be registered, so every shipped feature
|
|
244
638
|
would keep a dead project in the list forever.
|
|
@@ -264,6 +658,7 @@ lives *today*.
|
|
|
264
658
|
| Capability | requirement `Capability:` (list; also `Capabilities:`) | `architecture/capabilities.yaml` OR `capabilities/<cap>/spec.md` | declared capability id, `/` allowed for nesting |
|
|
265
659
|
| 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 |
|
|
266
660
|
| 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 `-` |
|
|
661
|
+
| 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 |
|
|
267
662
|
|
|
268
663
|
These keys are independent: an operation id is not a permission and a service id is not inferred
|
|
269
664
|
from a feature directory. A file can therefore be structurally valid while a join is broken; the
|
|
@@ -686,7 +1081,21 @@ There is no manifest of services: the directories under `services/` **are** the
|
|
|
686
1081
|
why the two are graded differently;
|
|
687
1082
|
- two elements resolving to the **same** `services/<svc>/` → `landscape.binding-duplicate`
|
|
688
1083
|
(**warning**): every element→service join picks one of them arbitrarily, so the other's edges are
|
|
689
|
-
quietly filed under a service that does not own them
|
|
1084
|
+
quietly filed under a service that does not own them;
|
|
1085
|
+
- an element resolving to a `services/<svc>/` that no edge in the map touches, while that service's
|
|
1086
|
+
`model.likec4` parses and declares at least one call across its own boundary →
|
|
1087
|
+
`landscape.service-isolated` (**warning**, never gating): the service is drawn and invisible to
|
|
1088
|
+
every cross-service check, and the finding names the attested calls to draw as edges on the
|
|
1089
|
+
existing element, collapsed to the service. Evidence-gated on purpose — a service with no model,
|
|
1090
|
+
an unparseable one (`c4.invalid`), or a model reaching nothing is silent here, because an edgeless
|
|
1091
|
+
element that is TRUE is a fact and an invented edge is a dependency the fleet then plans against;
|
|
1092
|
+
the adopt brief's `landscape.touched` is the only place that silent state is named. And BINARY:
|
|
1093
|
+
one edge closes it, so a map drawing one of five attested calls is silent too — the check is
|
|
1094
|
+
touched/untouched, never a set difference over the attested calls, because loam cannot tell a
|
|
1095
|
+
call the map forgot from one the fleet deliberately does not draw at its own altitude. "Edge in
|
|
1096
|
+
the map" means an edge in the `architecture/` PROJECT — the landscape plus every other `.likec4`
|
|
1097
|
+
under it, minus the generated views files (`subsystems.likec4`, `services.likec4`) — and `loam adopt` reads the same set. An
|
|
1098
|
+
`#external` element is never the subject, whatever directory it is bound to.
|
|
690
1099
|
|
|
691
1100
|
**`architecture/landscape.likec4` is required, not optional.** Its absence is a finding, never a
|
|
692
1101
|
skipped check: an **error** as soon as `services/` holds at least one service directory, and a
|
|
@@ -697,7 +1106,7 @@ it cannot be silence: with no map, the whole cross-service layer — the C4↔AP
|
|
|
697
1106
|
`landscape.service-unmodelled`, every derived view — computes nothing and *reports* nothing, which
|
|
698
1107
|
reads exactly like a clean fleet. `loam init` scaffolds the file (empty, commented, parsing clean)
|
|
699
1108
|
so a fresh docs repo starts from a real one rather than from that ambiguity, and `loam adopt` briefs
|
|
700
|
-
the
|
|
1109
|
+
the write each service still owes it — the block, or the edges on an element no edge touches.
|
|
701
1110
|
|
|
702
1111
|
Absences inside a service directory are graded too. A directory with no `spec.md` is a **warning**
|
|
703
1112
|
(`service.no-spec`) — partial adoption is a supported state, but the checks that file feeds are
|
|
@@ -985,7 +1394,9 @@ the source for tests.
|
|
|
985
1394
|
diff, tagged to the feature.
|
|
986
1395
|
- **`loam archive <FEAT>`** merges the delta into the living state on three axes — **requirements**
|
|
987
1396
|
(`spec.md`, and `arch.spec.md` beside it: the two files ride one merge code path, parameterized by
|
|
988
|
-
filename), **API** (`openapi.yaml`), **architecture** (`landscape.likec4
|
|
1397
|
+
filename), **API** (`openapi.yaml`), **architecture** (`landscape.likec4`, plus the
|
|
1398
|
+
`services/<…>/<svc>/model.likec4` of each service whose EXTENDING model owns an addition nested
|
|
1399
|
+
inside it) — then archives the
|
|
989
1400
|
feature, so the living state stays complete. Archived deltas are the evolution history (like
|
|
990
1401
|
`git log`). `--dry-run` prints the whole plan and writes nothing.
|
|
991
1402
|
- **Coherence gate:** `loam validate --feature` checks the three axes agree (C4 edge `op` ↔ OpenAPI
|
|
@@ -1145,10 +1556,16 @@ the CI escalation.
|
|
|
1145
1556
|
such entry into a false `covers.unknown` "typo" pointing at the spec, when the truth was the file.
|
|
1146
1557
|
- `health.dependency-unmodelled` (service scope): a `dependencies:` id that nothing in the service's
|
|
1147
1558
|
OWN `model.likec4` answers to — by element id, `metadata { service }` binding, or title; exact
|
|
1148
|
-
match, with a did-you-mean hint. Deliberately the service model and never the landscape
|
|
1559
|
+
match, with a did-you-mean hint. Deliberately the service model and never the landscape read on
|
|
1560
|
+
its own: the
|
|
1149
1561
|
landscape carries only what crosses the service boundary, the model carries everything the service
|
|
1150
|
-
touches (a private store
|
|
1151
|
-
|
|
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
|
|
1565
|
+
the set is its own SLICE: the elements it adds, plus the partners its own edges point at and their
|
|
1566
|
+
ancestors — so a dependency named `kafka` resolves through an edge that names `kafka.orderEvents`,
|
|
1567
|
+
and a peer service resolves through the `metadata { service }` binding on the map's element the
|
|
1568
|
+
model points at. A partner the model never reaches still does not answer, which is the check.
|
|
1152
1569
|
|
|
1153
1570
|
The rest of the machinery treats the axis as what it is — requirements. `loam archive` merges an
|
|
1154
1571
|
`arch.spec.md` delta into the living one through the same code path as `spec.md` (same delta
|
|
@@ -1536,6 +1953,21 @@ turn a mistake into a green fleet. And a run that could not READ the flows (a si
|
|
|
1536
1953
|
an `architecture/` that did not parse) suspends `capability.requirement-unrealized` entirely: loam
|
|
1537
1954
|
did not look, which is never the same answer as "there is nothing there".
|
|
1538
1955
|
|
|
1956
|
+
**A `#req-` tag inside a service's own project is scoped by the SERVICE, not by a capability.** A
|
|
1957
|
+
`dynamic view` beside model.likec4 carrying `#req-<slug>` resolves against the `Requirement-ID`s of
|
|
1958
|
+
that service's spec.md and arch.spec.md together — flattened by the same rule, an id both declare
|
|
1959
|
+
resolving once, and `many` when the two documents declare ids that flatten alike, because a
|
|
1960
|
+
Requirement-ID is unique inside ONE document and a flow beside the model has two.
|
|
1961
|
+
`usecase.requirement-unresolved` there has four arms: no document at all, no identified requirement,
|
|
1962
|
+
none flattening to the slug (close ids offered), or two that do. No `#cap-` tag belongs there
|
|
1963
|
+
(`usecase.capability-unresolved` says so, vocabulary or not); `usecase.step-unlinked` never fires
|
|
1964
|
+
on a hop whose caller and provider resolve to the same service — a service owes no operationId to
|
|
1965
|
+
itself — and still fires on a hop from such a flow into another service's element (a stand-in the
|
|
1966
|
+
model declares for a sibling, with no `metadata { op }`), exactly as on the fleet map; and a
|
|
1967
|
+
resolved service-local claim is the join being checked and nothing more today: it
|
|
1968
|
+
enters no capability `keptBy`, counts toward no `Covers:` or `c4.uncovered`, appears in no pack,
|
|
1969
|
+
`loam list capabilities`, `loam diff` or `loam delta`, and no feature carries one yet.
|
|
1970
|
+
|
|
1539
1971
|
**Both corpora are reported together, and neither is derived from the other.**
|
|
1540
1972
|
`loam list capabilities --json` carries `keptBy` (the flows keeping a promise) beside `realizedBy`
|
|
1541
1973
|
(the service requirements realizing it) on every `requirements[]` row, with the same three-state
|
|
@@ -1809,6 +2241,44 @@ unparseable delta. A living declaration sharing its line with another statement
|
|
|
1809
2241
|
splice map cannot see around — its statement scan runs to the end of the line) refuses the same way:
|
|
1810
2242
|
give each top-level declaration its own line, then re-run.
|
|
1811
2243
|
|
|
2244
|
+
**Which document an addition lands in.** The C4 merge asks it per declaration, not per feature. An
|
|
2245
|
+
element whose PARENT resolves to a service whose `model.likec4` extends the map is spliced into that
|
|
2246
|
+
model — and so is the interior a delta writes inside a service element the same archive introduces,
|
|
2247
|
+
since those containers carry no feature tag and would otherwise ride onto the map inside their
|
|
2248
|
+
parent's bytes. The parent chain has to be spelled in the map's own ids for either to happen. The
|
|
2249
|
+
splice goes inside the `extend <fqn> { … }` block for the parent — appended when the model has none, as
|
|
2250
|
+
the `model { }` block itself is appended to a model that has none (an empty file, a `views { }`-only
|
|
2251
|
+
file and a tags-only `specification { }` are all extending models, and all three are legal to
|
|
2252
|
+
archive into). A
|
|
2253
|
+
relationship goes to the model of the service owning its SOURCE when the source is interior to such
|
|
2254
|
+
a service, else to the model owning its TARGET, else to the map: a service-level edge stays where
|
|
2255
|
+
every fleet check reads it. A service with a STANDALONE model, or none yet, owns nothing — the map
|
|
2256
|
+
draws its containers, which is legal and is what every repository did before the extending shape.
|
|
2257
|
+
The merged model is proved the way the grader reads it (the `architecture/` documents with this
|
|
2258
|
+
archive's own merged landscape in place, plus the model) before anything is written, and it appears
|
|
2259
|
+
in the `--json` plan as an ordinary write, so `loam unarchive` restores it from the same
|
|
2260
|
+
`.loam-before` snapshot as any other overwritten file. Two limits are deliberate and each refuses at
|
|
2261
|
+
plan time with nothing written: a merged model that does not parse beside the map (`merge-failed`),
|
|
2262
|
+
and a container-level edge whose other end lives in a THIRD document — another service's interior
|
|
2263
|
+
resolves in neither this model nor the map, so that call is drawn service to service on the map. One
|
|
2264
|
+
more is silent: a delta that spells the parent under its own name rather than the map's (the legacy
|
|
2265
|
+
title-join) keeps landing on the map, because splicing its bytes into a document that shares the
|
|
2266
|
+
map's id namespace would carry ids nothing there resolves. It is the WHOLE chain that decides, not
|
|
2267
|
+
the outermost box — `marketplace.orderSvc.cache` under a map that spells the service
|
|
2268
|
+
`marketplace.orderService` is a title join and stays on the map, even though the group
|
|
2269
|
+
`marketplace` is the map's own. A block a delta writes for a NEW service is split — the box to the
|
|
2270
|
+
map, its containers to the model — unless the block also draws a relationship between those
|
|
2271
|
+
containers. Such an edge names its endpoints by their local names inside the block, so splitting it
|
|
2272
|
+
would leave the map naming children it no longer declares; the block rides whole instead, as it did
|
|
2273
|
+
before routing existed. The block decides once for everything written inside it: a container or an
|
|
2274
|
+
edge the delta tags with the feature follows its block rather than its own tag, so it is placed
|
|
2275
|
+
exactly once whichever way the block goes. Re-archiving is idempotent in the models
|
|
2276
|
+
too: an addition whose id, or whose title inside the same bound service, an extending model already
|
|
2277
|
+
declares is skipped exactly as one the map declares is. The title half is service-scoped in a model
|
|
2278
|
+
and global on the map — the map's global join is the fallback for documents written before
|
|
2279
|
+
`metadata { service }` existed, and a service's interior is where two boxes called `api` would
|
|
2280
|
+
collide.
|
|
2281
|
+
|
|
1812
2282
|
The OpenAPI merge carries components along with operations, and since the path-item/component
|
|
1813
2283
|
baselines it decides each one **by verdict rather than by reachability**: a component the feature
|
|
1814
2284
|
declares with a pin equal to its own content is a quote and is skipped (the living document keeps
|
|
@@ -2335,21 +2805,27 @@ through the forge, not through loam features:
|
|
|
2335
2805
|
tags and ordered steps, already materialized by the parse loam pays for) and never what a view
|
|
2336
2806
|
**shows**. It renders nothing, and view *computation* is what is superlinear in edges. (If you
|
|
2337
2807
|
want diagrams, add a `views` block and render it with LikeC4's own tooling — `npx likec4 start`
|
|
2338
|
-
at the docs root, which renders the fleet map AND every adopted service's model
|
|
2339
|
-
|
|
2340
|
-
|
|
2808
|
+
at the docs root, which renders the fleet map AND every adopted service's model: an extending
|
|
2809
|
+
model is part of the root `fleet` project, beside the map, and a standalone one is the project its
|
|
2810
|
+
own `services/<svc>/likec4.config.json` declares (written by `loam subsystem sync`; the Layout
|
|
2811
|
+
section, "Two shapes of a service model", has the rule), and `npx likec4 start features/<FEAT>`
|
|
2812
|
+
for a delta, which is
|
|
2341
2813
|
transient and gets no project — and scope the views, because an `include *` over a fleet-sized
|
|
2342
|
-
landscape takes minutes. Two costs arrive with the first service project
|
|
2814
|
+
landscape takes minutes. Two costs arrive with the first STANDALONE service project, and only
|
|
2815
|
+
with it. Measured at the
|
|
2343
2816
|
pinned likec4: with more than one project, `likec4 validate` at the docs root refuses without
|
|
2344
2817
|
`--project <name>`, so a CI step that ran it bare changes (`likec4 build` and `likec4 export` take
|
|
2345
|
-
every project and need no flag). And the renderer's project list grows by one per
|
|
2818
|
+
every project and need no flag). And the renderer's project list grows by one per such service
|
|
2346
2819
|
beside `fleet` — LikeC4 projects are separate models, so a box on the fleet map is not a link into
|
|
2347
|
-
that service's project.
|
|
2820
|
+
that service's project. A fleet whose models all extend the map pays neither: one project, one
|
|
2821
|
+
picker entry, and the containers of every adopted service reachable from the map's own boxes.
|
|
2822
|
+
`loam init`
|
|
2348
2823
|
scaffolds the landscape with no `views` block for that reason: loam should not plant a diagram it
|
|
2349
2824
|
never draws **— and never asks for one: a fleet that draws nothing owes no views block.** It also
|
|
2350
|
-
scaffolds a `likec4.config.json`
|
|
2351
|
-
|
|
2352
|
-
|
|
2825
|
+
scaffolds a `likec4.config.json` whose `exclude` is `["**/node_modules/**", "features/**"]`: a
|
|
2826
|
+
feature's `delta.likec4` re-declares the landscape's own identifiers and carries a `specification`
|
|
2827
|
+
block of its own, so it is read alone, and `services/` is deliberately NOT excluded — the models a
|
|
2828
|
+
fresh repository writes extend the map and the root project is the only place they parse.)
|
|
2353
2829
|
`loam validate --all --json` over the same 120 services used to be the outlier at ~13–14 s — one
|
|
2354
2830
|
fresh LikeC4 workspace per service; since the batched loader it parses the run's C4 documents in
|
|
2355
2831
|
one shared workspace and answers in **~0.7 s median** on the committed 120-service benchmark
|
|
@@ -2455,7 +2931,28 @@ repairing anything:
|
|
|
2455
2931
|
|
|
2456
2932
|
`loam-report` is a separate support protocol, generated as a command and skill in every profile
|
|
2457
2933
|
but never appended to the lifecycle. It writes
|
|
2458
|
-
`loam-reports/YYYY-MM-DD-<short-slug>.md`
|
|
2934
|
+
`loam-reports/NNN-YYYY-MM-DD-<short-slug>.md` — `NNN` an ORDINAL of at least three digits,
|
|
2935
|
+
zero-padded, one more
|
|
2936
|
+
than the highest already in the directory (`001` when it is empty or absent, and widening past
|
|
2937
|
+
three digits rather than reusing a number once a corpus gets there), so a report has a
|
|
2938
|
+
short handle a commit message, an issue or the next report can cite. The document carries a
|
|
2939
|
+
`- Status: open` line beside its classification, whose vocabulary is `open` / `sent` (handed to
|
|
2940
|
+
the author, set by whoever forwards it) / `fixed in <version>` / `superseded by <NNN>`, and
|
|
2941
|
+
`loam doctor` reads the directory: how many reports it holds, how many stand at each status, and
|
|
2942
|
+
which ordinal comes next. `doctor --json` carries that as the `reports` object — `reports.dir`
|
|
2943
|
+
(the ABSOLUTE path of the directory, `loam-reports/` included, so an agent writes inside it rather
|
|
2944
|
+
than joining the name on a second time), resolved beside the `loam.json` that parsed, or beside
|
|
2945
|
+
the one loam FOUND when it did not parse, and only beside the run directory when there is no
|
|
2946
|
+
config file at or above it — `doctor` is the preflight that runs precisely when the config may not
|
|
2947
|
+
resolve, so it counts the corpus that is there rather than handing out an ordinal five existing
|
|
2948
|
+
reports already used — `reports.present`, `reports.total`, `reports.next`, and
|
|
2949
|
+
`reports.entries[]` with a `file`, an `ordinal` (`null` for a name written before the numbering)
|
|
2950
|
+
and a `status`, in ordinal order, numerically, with unnumbered names last (byte order stopped
|
|
2951
|
+
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
|
|
2952
|
+
the first `##` heading, quoted code skipped in both markdown spellings (a fence, closed only by a
|
|
2953
|
+
run of the same character at least as long as the one that opened it, and an indented block,
|
|
2954
|
+
which is how the protocol prints its own template) — so a report quoting this protocol's own
|
|
2955
|
+
template still counts as whatever its own header says. The body carries expected/actual behavior,
|
|
2459
2956
|
version, stable envelope and finding codes, relevant locations, write state, a minimal safe
|
|
2460
2957
|
reproduction, classification and missing evidence. Before writing, the agent redacts secrets and
|
|
2461
2958
|
home paths and reduces command output to the excerpt that proves the symptom. It never retries a
|
|
@@ -2564,20 +3061,44 @@ hand-merges the block. That resolution has a documented cost, not a surprise: a
|
|
|
2564
3061
|
landscape moves the file away from the bytes the archive's `.loam-before/` snapshot recorded, so a
|
|
2565
3062
|
later `loam unarchive` of that feature refuses with `snapshot-stale` and needs `--force` — the
|
|
2566
3063
|
resolver is trading undo precision for the merge, and should know it. The trigger for revisiting is
|
|
2567
|
-
written down so nobody relitigates it early
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
map
|
|
3064
|
+
written down so nobody relitigates it early — and half of what it once asked for has already
|
|
3065
|
+
arrived. Per-service internals now live in `services/<svc>/model.likec4` **by default**, and that is
|
|
3066
|
+
where a second team adopting a service writes, so it adds nothing to this file. It is a default and
|
|
3067
|
+
not a prohibition: the map MAY hold a service's interior, and sometimes must. A store a second
|
|
3068
|
+
service will reach has to be declared where that consumer's project can see it — a per-service
|
|
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
|
|
3073
|
+
title-join lands its container on the map too. What an extending model owns is what it DECLARES,
|
|
3074
|
+
not everything below the service's element. `loam archive` routes rather than forbids — a delta's
|
|
3075
|
+
additions nested under a service whose model extends the map, with the whole parent chain spelled
|
|
3076
|
+
in the map's own ids, are spliced into that model's `extend <fqn> { … }` block instead of this
|
|
3077
|
+
file, and the merged model is proved to parse beside the merged map before anything is written. What is left of the
|
|
3078
|
+
trigger is the CROSS-SERVICE EDGE, which still exists twice — container-level in the model, service
|
|
3079
|
+
level here — because the spine, the platform census and every other fleet check read this file
|
|
3080
|
+
alone. So: when landscape merge conflicts become routine — weekly, not monthly — thin the map to
|
|
3081
|
+
elements plus cross-service edges and teach the fleet checks to read the edges out of the models
|
|
3082
|
+
that attest them. Until then, one file keeps the fleet map one diffable document, and
|
|
3083
|
+
`landscape.service-isolated` remains the nudge that carries an attested call up into it.
|
|
2571
3084
|
|
|
2572
3085
|
## Two flows
|
|
2573
3086
|
|
|
2574
3087
|
- **Bootstrap (reverse):** `loam adopt --service <id>` emits a **brief**, not an extraction — the
|
|
2575
3088
|
target paths (and which already exist, to be diffed and never overwritten), the grammar of each
|
|
2576
|
-
artifact, what the living
|
|
3089
|
+
artifact, what the living fleet map — the `architecture/` project, not the landscape file alone —
|
|
3090
|
+
already says about the service, the frontmatter to write, the
|
|
2577
3091
|
checks `loam validate --service <id>` will then run, and the ones that do not exist. One target is
|
|
2578
3092
|
not under `services/<id>/`: `architecture/landscape.likec4`, carried with `action: "edit"` and a
|
|
2579
|
-
`landscape.instruction` naming the
|
|
2580
|
-
nothing in the map resolves to the service
|
|
3093
|
+
`landscape.instruction` naming the write this service owes the fleet map — the element while
|
|
3094
|
+
nothing in the map resolves to the service, the EDGES while an element does and no edge touches it
|
|
3095
|
+
(`landscape.touched: false`, with `landscape.attested` listing the calls the service's own
|
|
3096
|
+
model.likec4 declares across its boundary); absent, and the instruction `null`, only once an
|
|
3097
|
+
element resolves and an edge touches it. `landscape.callersKnown` says whether loam could read WHO
|
|
3098
|
+
CALLS this service — true when the `architecture/` project parses, and true when it does not but
|
|
3099
|
+
`architecture/landscape.likec4` does. It is what an empty `landscape.expects` has to be read
|
|
3100
|
+
against: only a readable landscape makes "nothing calls this service" evidence, and it is the one
|
|
3101
|
+
fact `openapi.yaml`'s `required` is derived from. An agent reads the code and writes draft
|
|
2581
3102
|
`model.likec4` + `spec.md` + `openapi.yaml` + `adrs/`, `runbook.md`, `health.yaml`, and makes that
|
|
2582
3103
|
one addition to the shared map. A human promotes `draft` -> `verified` with `loam vouch`.
|
|
2583
3104
|
- **Forward (generative):** author `features/<FEAT>/delta.likec4` -> `loam delta <FEAT>` projects it
|