@ybotok/loam 0.2.0-alpha.4 → 0.2.0-alpha.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +947 -0
- package/MIGRATING-from-OpenSpec.md +7 -1
- package/QUICKSTART.md +8 -3
- package/README.md +20 -13
- package/ROADMAP.md +329 -22
- package/SCHEMA.md +472 -24
- package/WORKFLOW.md +23 -10
- package/dist/commands/adopt/render.js +47 -8
- package/dist/commands/adopt/render.js.map +1 -1
- package/dist/commands/archive/plan/landscape.js +45 -1
- package/dist/commands/archive/plan/landscape.js.map +1 -1
- package/dist/commands/archive/plan/model/extending.js +85 -0
- package/dist/commands/archive/plan/model/extending.js.map +1 -0
- package/dist/commands/archive/run.js +5 -2
- package/dist/commands/archive/run.js.map +1 -1
- package/dist/commands/context/print.js +11 -1
- package/dist/commands/context/print.js.map +1 -1
- package/dist/commands/doctor.js +46 -0
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/explore.js +12 -2
- package/dist/commands/explore.js.map +1 -1
- package/dist/commands/list/views.js +5 -1
- package/dist/commands/list/views.js.map +1 -1
- package/dist/commands/seed/plan.js +97 -3
- package/dist/commands/seed/plan.js.map +1 -1
- package/dist/commands/seed/seed.js +7 -2
- package/dist/commands/seed/seed.js.map +1 -1
- package/dist/commands/show/feature.js +2 -1
- package/dist/commands/show/feature.js.map +1 -1
- package/dist/commands/show/marks.js +3 -5
- package/dist/commands/show/marks.js.map +1 -1
- package/dist/commands/show/service.js +39 -18
- package/dist/commands/show/service.js.map +1 -1
- package/dist/commands/subsystem/sync.js +184 -39
- package/dist/commands/subsystem/sync.js.map +1 -1
- package/dist/commands/subsystem/txn/exclude.js +132 -0
- package/dist/commands/subsystem/txn/exclude.js.map +1 -0
- package/dist/commands/subsystem/txn/report.js +124 -0
- package/dist/commands/subsystem/txn/report.js.map +1 -0
- package/dist/commands/subsystem/txn/txn.js +29 -3
- package/dist/commands/subsystem/txn/txn.js.map +1 -1
- package/dist/commands/subsystem/txn/views.js +113 -16
- package/dist/commands/subsystem/txn/views.js.map +1 -1
- package/dist/commands/validate/arch-coverage.js +16 -9
- package/dist/commands/validate/arch-coverage.js.map +1 -1
- package/dist/commands/validate/checks/fleet-shape.js +30 -77
- package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
- package/dist/commands/validate/checks/vocabulary.js +4 -19
- package/dist/commands/validate/checks/vocabulary.js.map +1 -1
- package/dist/commands/validate/feature.js +1 -1
- package/dist/commands/validate/feature.js.map +1 -1
- package/dist/commands/validate/fleet/census.js +11 -10
- package/dist/commands/validate/fleet/census.js.map +1 -1
- package/dist/commands/validate/fleet/landscape.js +128 -114
- package/dist/commands/validate/fleet/landscape.js.map +1 -1
- package/dist/commands/validate/fleet/load.js +66 -0
- package/dist/commands/validate/fleet/load.js.map +1 -1
- package/dist/commands/validate/fleet/map/attest.js +103 -0
- package/dist/commands/validate/fleet/map/attest.js.map +1 -0
- package/dist/commands/validate/fleet/map/bindings.js +96 -0
- package/dist/commands/validate/fleet/map/bindings.js.map +1 -0
- package/dist/commands/validate/fleet/map/consumers.js +298 -0
- package/dist/commands/validate/fleet/map/consumers.js.map +1 -0
- package/dist/commands/validate/fleet/map/isolation.js +116 -0
- package/dist/commands/validate/fleet/map/isolation.js.map +1 -0
- package/dist/commands/validate/fleet/map/usecases.js +56 -0
- package/dist/commands/validate/fleet/map/usecases.js.map +1 -0
- package/dist/commands/validate/fleet/usecases/steps.js +49 -17
- package/dist/commands/validate/fleet/usecases/steps.js.map +1 -1
- package/dist/commands/validate/fleet/views/fleet-project.js +217 -0
- package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -0
- package/dist/commands/validate/fleet/views/ids.js +13 -9
- package/dist/commands/validate/fleet/views/ids.js.map +1 -1
- package/dist/commands/validate/fleet/views/projects.js +278 -0
- package/dist/commands/validate/fleet/views/projects.js.map +1 -0
- package/dist/commands/validate/fleet/views/root-documents.js +97 -0
- package/dist/commands/validate/fleet/views/root-documents.js.map +1 -0
- package/dist/commands/validate/fleet/views/stale.js +2 -2
- package/dist/commands/validate/fleet/views/stale.js.map +1 -1
- package/dist/commands/validate/service/model/diverged.js +226 -0
- package/dist/commands/validate/service/model/diverged.js.map +1 -0
- package/dist/commands/validate/service/model/grade.js +163 -0
- package/dist/commands/validate/service/model/grade.js.map +1 -0
- package/dist/commands/validate/service/service.js +110 -34
- package/dist/commands/validate/service/service.js.map +1 -1
- package/dist/commands/validate/service/specs.js +39 -5
- package/dist/commands/validate/service/specs.js.map +1 -1
- package/dist/commands/validate/service/spine.js +41 -3
- package/dist/commands/validate/service/spine.js.map +1 -1
- package/dist/commands/validate/service/usecases/tags.js +113 -0
- package/dist/commands/validate/service/usecases/tags.js.map +1 -0
- package/dist/commands/validate/service/usecases/usecases.js +209 -0
- package/dist/commands/validate/service/usecases/usecases.js.map +1 -0
- package/dist/commands/validate/validate.js +10 -24
- package/dist/commands/validate/validate.js.map +1 -1
- package/dist/core/agent/agents-md/artifacts.js +1 -0
- package/dist/core/agent/agents-md/artifacts.js.map +1 -1
- package/dist/core/agent/agents-md/command-map.js +18 -2
- package/dist/core/agent/agents-md/command-map.js.map +1 -1
- package/dist/core/agent/agents-md/cycle.js +24 -9
- package/dist/core/agent/agents-md/cycle.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/context.js +4 -3
- package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -1
- package/dist/core/agent/agents-md/map/subsystem.js +51 -2
- package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
- package/dist/core/agent/agents-md/map/usecases.js +45 -3
- package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
- package/dist/core/agent/agents-md.js +14 -4
- package/dist/core/agent/agents-md.js.map +1 -1
- package/dist/core/agent/support/report.js +39 -5
- package/dist/core/agent/support/report.js.map +1 -1
- package/dist/core/agent/workflows/adopt.js +125 -6
- package/dist/core/agent/workflows/adopt.js.map +1 -1
- package/dist/core/agent/workflows/check.js +35 -19
- package/dist/core/agent/workflows/check.js.map +1 -1
- package/dist/core/agent/workflows/closing.js +3 -1
- package/dist/core/agent/workflows/closing.js.map +1 -1
- package/dist/core/brief/brief.js +63 -11
- package/dist/core/brief/brief.js.map +1 -1
- package/dist/core/brief/checks.js +138 -3
- package/dist/core/brief/checks.js.map +1 -1
- package/dist/core/brief/landscape.js +212 -129
- package/dist/core/brief/landscape.js.map +1 -1
- package/dist/core/brief/map/extend.js +81 -0
- package/dist/core/brief/map/extend.js.map +1 -0
- package/dist/core/brief/map/owed.js +284 -0
- package/dist/core/brief/map/owed.js.map +1 -0
- package/dist/core/brief/targets.js +30 -26
- package/dist/core/brief/targets.js.map +1 -1
- package/dist/core/brief/unchecked.js +12 -2
- package/dist/core/brief/unchecked.js.map +1 -1
- package/dist/core/c4/likec4.js +23 -1
- package/dist/core/c4/likec4.js.map +1 -1
- package/dist/core/c4/parsed/deployment.js +5 -3
- package/dist/core/c4/parsed/deployment.js.map +1 -1
- package/dist/core/c4/parsed/view-ids.js +39 -1
- package/dist/core/c4/parsed/view-ids.js.map +1 -1
- package/dist/core/c4/prefetch/fleet.js +142 -0
- package/dist/core/c4/prefetch/fleet.js.map +1 -0
- package/dist/core/c4/project/architecture.js +52 -4
- package/dist/core/c4/project/architecture.js.map +1 -1
- package/dist/core/c4/project/batch.js +140 -0
- package/dist/core/c4/project/batch.js.map +1 -0
- package/dist/core/c4/project/documents.js +9 -4
- package/dist/core/c4/project/documents.js.map +1 -1
- package/dist/core/c4/project/load.js +77 -38
- package/dist/core/c4/project/load.js.map +1 -1
- package/dist/core/c4/project/staged.js +46 -12
- package/dist/core/c4/project/staged.js.map +1 -1
- package/dist/core/c4/resolve/attested.js +0 -0
- package/dist/core/c4/resolve/attested.js.map +1 -0
- package/dist/core/c4/resolve/service.js +8 -17
- package/dist/core/c4/resolve/service.js.map +1 -1
- package/dist/core/c4/resolve/steps.js +18 -0
- package/dist/core/c4/resolve/steps.js.map +1 -1
- package/dist/core/c4/root-project/exclude.js +352 -0
- package/dist/core/c4/root-project/exclude.js.map +1 -0
- package/dist/core/c4/service-model/documents.js +47 -0
- package/dist/core/c4/service-model/documents.js.map +1 -0
- package/dist/core/c4/service-model/fleet/extending.js +82 -0
- package/dist/core/c4/service-model/fleet/extending.js.map +1 -0
- package/dist/core/c4/service-model/load.js +165 -0
- package/dist/core/c4/service-model/load.js.map +1 -0
- package/dist/core/c4/service-model/renderer.js +128 -0
- package/dist/core/c4/service-model/renderer.js.map +1 -0
- package/dist/core/c4/service-model/shape.js +126 -0
- package/dist/core/c4/service-model/shape.js.map +1 -0
- package/dist/core/c4/service-model/slice.js +188 -0
- package/dist/core/c4/service-model/slice.js.map +1 -0
- package/dist/core/c4/source-scan.js +26 -0
- package/dist/core/c4/source-scan.js.map +1 -1
- package/dist/core/c4/splice/authored-source.js +60 -13
- package/dist/core/c4/splice/authored-source.js.map +1 -1
- package/dist/core/c4/splice/contract.js.map +1 -1
- package/dist/core/c4/splice/identity/declared.js +87 -0
- package/dist/core/c4/splice/identity/declared.js.map +1 -0
- package/dist/core/c4/splice/identity/edges.js +17 -0
- package/dist/core/c4/splice/identity/edges.js.map +1 -1
- package/dist/core/c4/splice/identity/existing.js +80 -0
- package/dist/core/c4/splice/identity/existing.js.map +1 -0
- package/dist/core/c4/splice/identity/interior.js +80 -0
- package/dist/core/c4/splice/identity/interior.js.map +1 -0
- package/dist/core/c4/splice/identity/owner.js +50 -0
- package/dist/core/c4/splice/identity/owner.js.map +1 -0
- package/dist/core/c4/splice/landscape-merge.js +154 -100
- package/dist/core/c4/splice/landscape-merge.js.map +1 -1
- package/dist/core/c4/splice/model/merge.js +140 -0
- package/dist/core/c4/splice/model/merge.js.map +1 -0
- package/dist/core/c4/splice/placement.js +14 -4
- package/dist/core/c4/splice/placement.js.map +1 -1
- package/dist/core/c4/workspace.js +2 -1
- package/dist/core/c4/workspace.js.map +1 -1
- package/dist/core/coherence/coherence.js +7 -0
- package/dist/core/coherence/coherence.js.map +1 -1
- package/dist/core/delta/delta.js +9 -0
- package/dist/core/delta/delta.js.map +1 -1
- package/dist/core/deployment/delta.js +4 -0
- package/dist/core/deployment/delta.js.map +1 -1
- package/dist/core/diff/base-state.js +21 -5
- package/dist/core/diff/base-state.js.map +1 -1
- package/dist/core/diff/victims.js +10 -1
- package/dist/core/diff/victims.js.map +1 -1
- package/dist/core/docs.js +31 -22
- package/dist/core/docs.js.map +1 -1
- package/dist/core/doctor/config.js +7 -3
- package/dist/core/doctor/config.js.map +1 -1
- package/dist/core/doctor/doctor.js +48 -9
- package/dist/core/doctor/doctor.js.map +1 -1
- package/dist/core/doctor/report.js.map +1 -1
- package/dist/core/doctor/reports/scan.js +242 -0
- package/dist/core/doctor/reports/scan.js.map +1 -0
- package/dist/core/explain/families.js +1 -1
- package/dist/core/explain/families.js.map +1 -1
- package/dist/core/explain/refusals.js +1 -1
- package/dist/core/explain/refusals.js.map +1 -1
- package/dist/core/explore/explore.js +44 -9
- package/dist/core/explore/explore.js.map +1 -1
- package/dist/core/fleet-context.js +47 -0
- package/dist/core/fleet-context.js.map +1 -1
- package/dist/core/gate/partners.js +5 -9
- package/dist/core/gate/partners.js.map +1 -1
- package/dist/core/kernel/ids/fqn/ancestors.js +65 -0
- package/dist/core/kernel/ids/fqn/ancestors.js.map +1 -0
- package/dist/core/pack/living.js +47 -13
- package/dist/core/pack/living.js.map +1 -1
- package/dist/core/pack/pack.js +9 -6
- package/dist/core/pack/pack.js.map +1 -1
- package/dist/core/projection/arch-slice.js +2 -2
- package/dist/core/projection/arch-slice.js.map +1 -1
- package/dist/core/repo/paths.js +40 -0
- package/dist/core/repo/paths.js.map +1 -1
- package/dist/core/repo/tree/render/projects.js +182 -0
- package/dist/core/repo/tree/render/projects.js.map +1 -0
- package/dist/core/repo/tree/render/stale.js +2 -3
- package/dist/core/repo/tree/render/stale.js.map +1 -1
- package/dist/core/repo/tree/render/views.js +51 -3
- package/dist/core/repo/tree/render/views.js.map +1 -1
- package/dist/core/repo/tree/walk.js +45 -2
- package/dist/core/repo/tree/walk.js.map +1 -1
- package/dist/core/scaffold/landscape.js +219 -9
- package/dist/core/scaffold/landscape.js.map +1 -1
- package/dist/core/staging/writes.js.map +1 -1
- package/dist/core/usecases/delta/claims.js +1 -0
- package/dist/core/usecases/delta/claims.js.map +1 -1
- package/dist/core/usecases/delta/overlay.js +4 -0
- package/dist/core/usecases/delta/overlay.js.map +1 -1
- package/dist/core/usecases/fleet.js +18 -6
- package/dist/core/usecases/fleet.js.map +1 -1
- package/dist/core/usecases/operations.js +7 -0
- package/dist/core/usecases/operations.js.map +1 -1
- package/dist/core/usecases/service/exclude.js +119 -0
- package/dist/core/usecases/service/exclude.js.map +1 -0
- package/dist/core/usecases/service/flows.js +161 -0
- package/dist/core/usecases/service/flows.js.map +1 -0
- package/dist/core/vocabulary/maturity.js +10 -0
- package/dist/core/vocabulary/maturity.js.map +1 -1
- package/examples/README.md +23 -0
- package/examples/docs/architecture/landscape.likec4 +41 -0
- package/examples/docs/architecture/usecases/order-notification.likec4 +5 -2
- package/examples/docs/likec4.config.json +0 -1
- package/examples/docs/services/checkout-web/model.likec4 +39 -56
- package/examples/docs/services/order-service/arch.spec.md +13 -6
- package/examples/docs/services/order-service/model.likec4 +53 -67
- package/examples/docs/services/payment-service/arch.spec.md +7 -4
- package/examples/docs/services/payment-service/model.likec4 +32 -33
- package/examples/docs/services/platform/identity-service/arch.spec.md +7 -4
- package/examples/docs/services/platform/identity-service/model.likec4 +29 -33
- package/examples/docs/services/platform/notification-service/health.yaml +8 -3
- package/examples/docs/services/platform/notification-service/model.likec4 +39 -59
- package/package.json +1 -1
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A service's OWN use cases: the `dynamic view`s its LikeC4 project declares,
|
|
3
|
+
* read the way the renderer reads that project.
|
|
4
|
+
*
|
|
5
|
+
* THE SLOT IS THE PROJECT. `loam subsystem sync` writes a
|
|
6
|
+
* `services/<…>/<svc>/likec4.config.json` that registers every `.likec4` under
|
|
7
|
+
* the service directory as one LikeC4 project, so the renderer already shows a
|
|
8
|
+
* `views.likec4` beside the model, or a `usecases/<name>.likec4` under it. loam
|
|
9
|
+
* reads exactly that set — `model.likec4` plus every sibling, recursively, minus
|
|
10
|
+
* whatever that project file's own `exclude` drops (`./exclude.ts`) — and
|
|
11
|
+
* nothing else, because a slot narrower than the renderer's would leave a flow
|
|
12
|
+
* that renders and is graded by nothing, which is the report this module
|
|
13
|
+
* answers: an intra-service flow behind an `arch.spec.md` requirement whose
|
|
14
|
+
* `Covers:` names containers had no home. `architecture/usecases/` is a
|
|
15
|
+
* separate project that cannot resolve a container (one such file turned a
|
|
16
|
+
* whole fleet `landscape.invalid`), and the author's workaround beside the
|
|
17
|
+
* model was invisible to every check. The read is NOT gated on the project
|
|
18
|
+
* file's presence: that file is create-only and a team may delete it, its
|
|
19
|
+
* absence has a warning of its own (`service.likec4-config-missing`), and a
|
|
20
|
+
* flow the renderer cannot show yet is still a flow loam can grade.
|
|
21
|
+
*
|
|
22
|
+
* MODEL FACTS STAY SINGLE-FILE. `c4.valid`'s counts, `Covers:` resolution,
|
|
23
|
+
* `health.dependency-unmodelled` and every other model reader keep the
|
|
24
|
+
* single-file load the service target already made; the project is loaded here
|
|
25
|
+
* for the VIEWS and their `sourcePath` only, and only when there is something
|
|
26
|
+
* to grade. A sibling that declares elements extends what a hop may name, and
|
|
27
|
+
* that is the renderer's reading too.
|
|
28
|
+
*
|
|
29
|
+
* TWO SHAPES, ONE SLOT. A model that stands alone is a project of its own
|
|
30
|
+
* directory, exactly as above. A model that EXTENDS the fleet map is not: its
|
|
31
|
+
* containers are declared inside an element the map owns, so a flow beside it
|
|
32
|
+
* naming one of those containers only resolves in a project that also holds the
|
|
33
|
+
* map (`c4/service-model/documents.ts`, `serviceFlowDocuments`). That project is
|
|
34
|
+
* staged from the DOCS ROOT, so every view comes back spelled
|
|
35
|
+
* `services/<tree>/usecases/x.likec4` — and the service prefix is stripped here,
|
|
36
|
+
* because every finding this scan feeds names a file relative to the service
|
|
37
|
+
* directory and a reader must not have to work out which spelling they were
|
|
38
|
+
* given. The same prefix is the FILTER: the map's own `architecture/usecases/`
|
|
39
|
+
* flows are in that project too, and without it every service in the fleet would
|
|
40
|
+
* be graded for every fleet-level use case.
|
|
41
|
+
*
|
|
42
|
+
* THE CHEAP GATE, in order, so a fleet without the axis pays one recursive
|
|
43
|
+
* readdir per service and nothing else:
|
|
44
|
+
*
|
|
45
|
+
* 1. no sibling → nothing new is loaded, and the model's own tagged views are
|
|
46
|
+
* graded at zero cost against the project the model was READ in. For a
|
|
47
|
+
* standalone model that is the single-file load already in hand, which IS
|
|
48
|
+
* its whole project; for an extending one it is `ServiceModel.project`, the
|
|
49
|
+
* map and this model together — NOT the service's own slice of it, which is
|
|
50
|
+
* what the caller passes as `model` and what every count and `Covers:` line
|
|
51
|
+
* is answered from. A hop backed by a map edge is backed wherever the view
|
|
52
|
+
* is written, and grading the two arms against different documents made one
|
|
53
|
+
* view `usecase.step-unbacked` in model.likec4 and clean in a sibling. A
|
|
54
|
+
* standalone model's views came back named `source.c4`
|
|
55
|
+
* (`core/usecases/place.ts` records the rule) and an extending model's carry
|
|
56
|
+
* their docs-relative path, so the file is spelled here, where it is known,
|
|
57
|
+
* for both: `model.likec4`;
|
|
58
|
+
* 2. siblings, no tagged view in the model, and no reserved prefix in any
|
|
59
|
+
* document (`mentionsTagPrefix`, the fleet's own byte gate) → nothing opted
|
|
60
|
+
* in, no workspace. An untagged `views.likec4` is somebody's hand-drawn
|
|
61
|
+
* diagram, never graded, exactly as an untagged fleet view is — and its
|
|
62
|
+
* parse errors are the renderer's business until a view opts the project
|
|
63
|
+
* in;
|
|
64
|
+
* 3. otherwise `loadProject` over model + siblings, the way the renderer reads
|
|
65
|
+
* them. `own` and the project's views are never merged: when siblings
|
|
66
|
+
* exist the project is the whole answer, which is what keeps one view from
|
|
67
|
+
* being graded twice.
|
|
68
|
+
*
|
|
69
|
+
* A REQUEST RECORD rather than three parameters, for the reason
|
|
70
|
+
* docs/CODE-STYLE.md gives: the paths, the single-file doc and the fleet set
|
|
71
|
+
* are only ever passed together, and a caller handing in a doc loaded from
|
|
72
|
+
* some other path is exactly the inconsistent pair a record makes
|
|
73
|
+
* unrepresentable.
|
|
74
|
+
*/
|
|
75
|
+
import { relative } from "node:path";
|
|
76
|
+
import { architectureDocuments } from "../../c4/project/documents.js";
|
|
77
|
+
import { asLoadedDoc, loadProject } from "../../c4/project/load.js";
|
|
78
|
+
import { serviceFlowDocuments } from "../../c4/service-model/documents.js";
|
|
79
|
+
import { ARTIFACT_FILES } from "../../repo/paths.js";
|
|
80
|
+
import { isUseCase, mentionsTagPrefix } from "../fleet.js";
|
|
81
|
+
import { keepIncluded, serviceProjectExclude } from "./exclude.js";
|
|
82
|
+
/**
|
|
83
|
+
* The service's declared use cases, with the model they are drawn over.
|
|
84
|
+
*
|
|
85
|
+
* Reads only, throws nothing: every failure is the `unreadable` arm. The caller
|
|
86
|
+
* has already established that `model.likec4` parses on its own — a project
|
|
87
|
+
* whose model is broken is `c4.invalid`'s business, and loading it again here
|
|
88
|
+
* would only repeat that cascade under a second code.
|
|
89
|
+
*/
|
|
90
|
+
export async function readServiceFlows(req) {
|
|
91
|
+
const found = await architectureDocuments(req.paths.dir, [req.paths.model]);
|
|
92
|
+
// What the RENDERER holds, for the standalone shape where this directory's
|
|
93
|
+
// own `likec4.config.json` IS the project config: a sibling its `exclude`
|
|
94
|
+
// covers is in no project loam may claim to be reading (`./exclude.ts`). The
|
|
95
|
+
// extending shape reads the root project instead, so its list does not apply
|
|
96
|
+
// — a per-service config beside an extending model is `service.likec4-config-stray`.
|
|
97
|
+
const siblings = req.extending === undefined
|
|
98
|
+
? keepIncluded(req.paths.dir, await serviceProjectExclude(req.paths.dir), found)
|
|
99
|
+
: found;
|
|
100
|
+
// The scope of the no-sibling arm, and it is the PROJECT for both shapes: the
|
|
101
|
+
// single-file load for a standalone model (which is its whole project), and
|
|
102
|
+
// the map-plus-model load for an extending one. Reading `req.model` there for
|
|
103
|
+
// an extending model read its own slice instead, so one view was graded two
|
|
104
|
+
// different ways depending on which file it was written in.
|
|
105
|
+
const alone = req.extending?.project ?? req.model;
|
|
106
|
+
const scope = { elements: alone.elements, relationships: alone.relationships, known: req.known };
|
|
107
|
+
const own = (req.model.views ?? []).filter(isUseCase);
|
|
108
|
+
if (siblings.length === 0) {
|
|
109
|
+
if (own.length === 0)
|
|
110
|
+
return { kind: "none" };
|
|
111
|
+
return { kind: "read", views: own.map((view) => ({ ...view, sourcePath: ARTIFACT_FILES.model })), model: scope };
|
|
112
|
+
}
|
|
113
|
+
if (own.length === 0 && !(await mentionsTagPrefix(siblings)))
|
|
114
|
+
return { kind: "none" };
|
|
115
|
+
const base = req.extending?.docsDir ?? req.paths.dir;
|
|
116
|
+
const documents = req.extending === undefined
|
|
117
|
+
? [req.paths.model, ...siblings]
|
|
118
|
+
: await serviceFlowDocuments(req.extending.docsDir, req.paths);
|
|
119
|
+
const project = await loadProject(base, documents);
|
|
120
|
+
if (!project.clean) {
|
|
121
|
+
// Each error already carries the real path of its document (`loadProject`
|
|
122
|
+
// rewrites the staged path back), and the flattening is `asLoadedDoc`'s —
|
|
123
|
+
// the fleet's own path-sorted order, taken from the one place it is
|
|
124
|
+
// spelled rather than re-spelled here: the caller names the FIRST broken
|
|
125
|
+
// file, and a second sort rule would let the service target name a
|
|
126
|
+
// different first file than the fleet target names for the same project.
|
|
127
|
+
return { kind: "unreadable", errors: asLoadedDoc(project).errors };
|
|
128
|
+
}
|
|
129
|
+
// The prefix the service's own documents are spelled under inside this
|
|
130
|
+
// project: empty for a standalone model (the project IS the directory, so
|
|
131
|
+
// every view in it is this service's) and `services/<tree>/` for an extending
|
|
132
|
+
// one, whose project also holds the whole fleet map.
|
|
133
|
+
const prefix = req.extending === undefined ? "" : `${relative(base, req.paths.dir).split(/[\\/]/).join("/")}/`;
|
|
134
|
+
return {
|
|
135
|
+
kind: "read",
|
|
136
|
+
views: rebase(project.views.filter(isUseCase), prefix),
|
|
137
|
+
model: { elements: project.elements, relationships: project.relationships, known: req.known },
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The views this SERVICE declares, spelled relative to its own directory.
|
|
142
|
+
*
|
|
143
|
+
* With an empty prefix — the standalone shape — this is the identity, including
|
|
144
|
+
* for a view whose `sourcePath` is absent: that project holds nothing but the
|
|
145
|
+
* service's own files, so there is nothing to attribute and nothing to strip.
|
|
146
|
+
*
|
|
147
|
+
* With a prefix, it is both the filter and the rebase, and both halves are one
|
|
148
|
+
* rule about which document belongs to whom. The project also holds the fleet
|
|
149
|
+
* map, so `architecture/usecases/checkout.likec4` is in it — and without the
|
|
150
|
+
* filter every service in the fleet would be graded for every fleet-level use
|
|
151
|
+
* case. A view carrying no `sourcePath` cannot be attributed at all and is
|
|
152
|
+
* dropped for the same reason: the path is the only evidence there is.
|
|
153
|
+
*/
|
|
154
|
+
function rebase(views, prefix) {
|
|
155
|
+
if (prefix === "")
|
|
156
|
+
return [...views];
|
|
157
|
+
return views
|
|
158
|
+
.filter((view) => view.sourcePath?.startsWith(prefix) === true)
|
|
159
|
+
.map((view) => ({ ...view, sourcePath: (view.sourcePath ?? "").slice(prefix.length) }));
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=flows.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flows.js","sourceRoot":"","sources":["../../../../src/core/usecases/service/flows.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A service's OWN use cases: the `dynamic view`s its LikeC4 project declares,\n * read the way the renderer reads that project.\n *\n * THE SLOT IS THE PROJECT. `loam subsystem sync` writes a\n * `services/<…>/<svc>/likec4.config.json` that registers every `.likec4` under\n * the service directory as one LikeC4 project, so the renderer already shows a\n * `views.likec4` beside the model, or a `usecases/<name>.likec4` under it. loam\n * reads exactly that set — `model.likec4` plus every sibling, recursively, minus\n * whatever that project file's own `exclude` drops (`./exclude.ts`) — and\n * nothing else, because a slot narrower than the renderer's would leave a flow\n * that renders and is graded by nothing, which is the report this module\n * answers: an intra-service flow behind an `arch.spec.md` requirement whose\n * `Covers:` names containers had no home. `architecture/usecases/` is a\n * separate project that cannot resolve a container (one such file turned a\n * whole fleet `landscape.invalid`), and the author's workaround beside the\n * model was invisible to every check. The read is NOT gated on the project\n * file's presence: that file is create-only and a team may delete it, its\n * absence has a warning of its own (`service.likec4-config-missing`), and a\n * flow the renderer cannot show yet is still a flow loam can grade.\n *\n * MODEL FACTS STAY SINGLE-FILE. `c4.valid`'s counts, `Covers:` resolution,\n * `health.dependency-unmodelled` and every other model reader keep the\n * single-file load the service target already made; the project is loaded here\n * for the VIEWS and their `sourcePath` only, and only when there is something\n * to grade. A sibling that declares elements extends what a hop may name, and\n * that is the renderer's reading too.\n *\n * TWO SHAPES, ONE SLOT. A model that stands alone is a project of its own\n * directory, exactly as above. A model that EXTENDS the fleet map is not: its\n * containers are declared inside an element the map owns, so a flow beside it\n * naming one of those containers only resolves in a project that also holds the\n * map (`c4/service-model/documents.ts`, `serviceFlowDocuments`). That project is\n * staged from the DOCS ROOT, so every view comes back spelled\n * `services/<tree>/usecases/x.likec4` — and the service prefix is stripped here,\n * because every finding this scan feeds names a file relative to the service\n * directory and a reader must not have to work out which spelling they were\n * given. The same prefix is the FILTER: the map's own `architecture/usecases/`\n * flows are in that project too, and without it every service in the fleet would\n * be graded for every fleet-level use case.\n *\n * THE CHEAP GATE, in order, so a fleet without the axis pays one recursive\n * readdir per service and nothing else:\n *\n * 1. no sibling → nothing new is loaded, and the model's own tagged views are\n * graded at zero cost against the project the model was READ in. For a\n * standalone model that is the single-file load already in hand, which IS\n * its whole project; for an extending one it is `ServiceModel.project`, the\n * map and this model together — NOT the service's own slice of it, which is\n * what the caller passes as `model` and what every count and `Covers:` line\n * is answered from. A hop backed by a map edge is backed wherever the view\n * is written, and grading the two arms against different documents made one\n * view `usecase.step-unbacked` in model.likec4 and clean in a sibling. A\n * standalone model's views came back named `source.c4`\n * (`core/usecases/place.ts` records the rule) and an extending model's carry\n * their docs-relative path, so the file is spelled here, where it is known,\n * for both: `model.likec4`;\n * 2. siblings, no tagged view in the model, and no reserved prefix in any\n * document (`mentionsTagPrefix`, the fleet's own byte gate) → nothing opted\n * in, no workspace. An untagged `views.likec4` is somebody's hand-drawn\n * diagram, never graded, exactly as an untagged fleet view is — and its\n * parse errors are the renderer's business until a view opts the project\n * in;\n * 3. otherwise `loadProject` over model + siblings, the way the renderer reads\n * them. `own` and the project's views are never merged: when siblings\n * exist the project is the whole answer, which is what keeps one view from\n * being graded twice.\n *\n * A REQUEST RECORD rather than three parameters, for the reason\n * docs/CODE-STYLE.md gives: the paths, the single-file doc and the fleet set\n * are only ever passed together, and a caller handing in a doc loaded from\n * some other path is exactly the inconsistent pair a record makes\n * unrepresentable.\n */\nimport { relative } from \"node:path\";\nimport type { LikeC4Error, LoadedDoc } from \"../../c4/likec4.js\";\nimport type { ParsedView } from \"../../c4/parsed/dynamic-views.js\";\nimport { architectureDocuments } from \"../../c4/project/documents.js\";\nimport { asLoadedDoc, loadProject } from \"../../c4/project/load.js\";\nimport { serviceFlowDocuments } from \"../../c4/service-model/documents.js\";\nimport type { StepScope } from \"../../c4/resolve/steps.js\";\nimport type { DocsDir } from \"../../kernel/ids/dirs.js\";\nimport { ARTIFACT_FILES, type ServicePaths } from \"../../repo/paths.js\";\nimport { isUseCase, mentionsTagPrefix } from \"../fleet.js\";\nimport { keepIncluded, serviceProjectExclude } from \"./exclude.js\";\n\nexport interface ServiceFlowRequest {\n paths: ServicePaths;\n /**\n * The single-file load the service target already made and has verified\n * parses. Its views are the project when no sibling exists; its elements are\n * the model a no-sibling flow is graded over.\n */\n model: LoadedDoc;\n /** The enumerated fleet, for `serviceResolver`'s reason: without it a container resolves to a service that has never existed. */\n known: ReadonlySet<string>;\n /**\n * Set only when this model EXTENDS the fleet map, and then both halves of it\n * — a record rather than two optional fields, for docs/CODE-STYLE.md's reason:\n * they are only ever passed together, and a caller that supplied one without\n * the other is exactly the inconsistent pair a record makes unrepresentable.\n *\n * `docsDir` decides which documents the sibling project holds (the map as\n * well, or the directory alone) and which root they are staged from, and\n * therefore how LikeC4 spells every `sourcePath` that comes back.\n *\n * `project` is the per-service project the model was READ in — the map plus\n * this model (`ServiceModel.project`) — and it is the scope a hop resolves\n * against when there is no sibling file. `model` above is then the service's\n * own SLICE of that project, which is the right answer for every count and\n * every `Covers:` line and the wrong one here: a hop backed by a MAP edge is\n * backed, and grading it against the slice reported `usecase.step-unbacked`\n * (exit 1) for a view in model.likec4 that was clean the moment the same view\n * moved to a sibling file, where the loaded project holds the map. One\n * question, two answers, decided by which file the author happened to write\n * the view in.\n *\n * Absent means the standalone shape — the service directory is the project and\n * the single-file load IS that project, exactly as it has always been.\n */\n extending?: { docsDir: DocsDir; project: LoadedDoc };\n}\n\n/**\n * What a service's flows are, or the honest refusal to say.\n *\n * Three variants rather than a record with flags, for `UseCaseScan`'s reason\n * one package up: a project that did not parse has no elements to resolve\n * against and no views to grade, and a caller handed `{ views: [], unreadable:\n * true }` is one `.length` check away from reporting a service as flow-free\n * when nobody could read it. `none` is the gate's answer — nothing opted in,\n * nothing was loaded — and it is distinct from `read` with no views (a project\n * that mentions a prefix in prose and declares no tagged view), because only\n * the second one cost a workspace.\n */\nexport type ServiceFlowScan =\n | { kind: \"none\" }\n | {\n kind: \"read\";\n /** The reserved-tag views ONLY, each carrying a `sourcePath` relative to the SERVICE directory. */\n views: ParsedView[];\n /** What a hop is attributed against — `known` required, as `UseCaseScan.model` requires it. */\n model: StepScope & { known: ReadonlySet<string> };\n }\n | {\n kind: \"unreadable\";\n /**\n * Every error the project raised, each carrying the real absolute path of\n * the document that raised it, sorted by that path — so a caller can name\n * the file, and a project with two broken documents reports them in a\n * stable order.\n */\n errors: LikeC4Error[];\n };\n\n/**\n * The service's declared use cases, with the model they are drawn over.\n *\n * Reads only, throws nothing: every failure is the `unreadable` arm. The caller\n * has already established that `model.likec4` parses on its own — a project\n * whose model is broken is `c4.invalid`'s business, and loading it again here\n * would only repeat that cascade under a second code.\n */\nexport async function readServiceFlows(req: ServiceFlowRequest): Promise<ServiceFlowScan> {\n const found = await architectureDocuments(req.paths.dir, [req.paths.model]);\n // What the RENDERER holds, for the standalone shape where this directory's\n // own `likec4.config.json` IS the project config: a sibling its `exclude`\n // covers is in no project loam may claim to be reading (`./exclude.ts`). The\n // extending shape reads the root project instead, so its list does not apply\n // — a per-service config beside an extending model is `service.likec4-config-stray`.\n const siblings =\n req.extending === undefined\n ? keepIncluded(req.paths.dir, await serviceProjectExclude(req.paths.dir), found)\n : found;\n // The scope of the no-sibling arm, and it is the PROJECT for both shapes: the\n // single-file load for a standalone model (which is its whole project), and\n // the map-plus-model load for an extending one. Reading `req.model` there for\n // an extending model read its own slice instead, so one view was graded two\n // different ways depending on which file it was written in.\n const alone = req.extending?.project ?? req.model;\n const scope = { elements: alone.elements, relationships: alone.relationships, known: req.known };\n const own = (req.model.views ?? []).filter(isUseCase);\n if (siblings.length === 0) {\n if (own.length === 0) return { kind: \"none\" };\n return { kind: \"read\", views: own.map((view) => ({ ...view, sourcePath: ARTIFACT_FILES.model })), model: scope };\n }\n if (own.length === 0 && !(await mentionsTagPrefix(siblings))) return { kind: \"none\" };\n\n const base = req.extending?.docsDir ?? req.paths.dir;\n const documents =\n req.extending === undefined\n ? [req.paths.model, ...siblings]\n : await serviceFlowDocuments(req.extending.docsDir, req.paths);\n const project = await loadProject(base, documents);\n if (!project.clean) {\n // Each error already carries the real path of its document (`loadProject`\n // rewrites the staged path back), and the flattening is `asLoadedDoc`'s —\n // the fleet's own path-sorted order, taken from the one place it is\n // spelled rather than re-spelled here: the caller names the FIRST broken\n // file, and a second sort rule would let the service target name a\n // different first file than the fleet target names for the same project.\n return { kind: \"unreadable\", errors: asLoadedDoc(project).errors };\n }\n // The prefix the service's own documents are spelled under inside this\n // project: empty for a standalone model (the project IS the directory, so\n // every view in it is this service's) and `services/<tree>/` for an extending\n // one, whose project also holds the whole fleet map.\n const prefix = req.extending === undefined ? \"\" : `${relative(base, req.paths.dir).split(/[\\\\/]/).join(\"/\")}/`;\n return {\n kind: \"read\",\n views: rebase(project.views.filter(isUseCase), prefix),\n model: { elements: project.elements, relationships: project.relationships, known: req.known },\n };\n}\n\n/**\n * The views this SERVICE declares, spelled relative to its own directory.\n *\n * With an empty prefix — the standalone shape — this is the identity, including\n * for a view whose `sourcePath` is absent: that project holds nothing but the\n * service's own files, so there is nothing to attribute and nothing to strip.\n *\n * With a prefix, it is both the filter and the rebase, and both halves are one\n * rule about which document belongs to whom. The project also holds the fleet\n * map, so `architecture/usecases/checkout.likec4` is in it — and without the\n * filter every service in the fleet would be graded for every fleet-level use\n * case. A view carrying no `sourcePath` cannot be attributed at all and is\n * dropped for the same reason: the path is the only evidence there is.\n */\nfunction rebase(views: readonly ParsedView[], prefix: string): ParsedView[] {\n if (prefix === \"\") return [...views];\n return views\n .filter((view) => view.sourcePath?.startsWith(prefix) === true)\n .map((view) => ({ ...view, sourcePath: (view.sourcePath ?? \"\").slice(prefix.length) }));\n}\n"]}
|
|
@@ -129,6 +129,16 @@ export function landscapeEvidence(req) {
|
|
|
129
129
|
const inbound = [];
|
|
130
130
|
const outbound = [];
|
|
131
131
|
for (const r of relationships) {
|
|
132
|
+
// An edge whose BOTH endpoints resolve to this service is its own internal
|
|
133
|
+
// wiring (`svc.web -> svc.db`): filed as inbound it made the service its own
|
|
134
|
+
// caller — `← <id> "reads"` in `loam context` (verification 2026-09-04) —
|
|
135
|
+
// and, through `apiExpected` below, told a service nobody calls that it owed
|
|
136
|
+
// an OpenAPI contract. `core/brief/landscape.ts` drops it for the same
|
|
137
|
+
// reason. It still TOUCHES the service, which is why the brief's `touched`
|
|
138
|
+
// and `landscape.service-isolated` read the relationships rather than these
|
|
139
|
+
// two lists.
|
|
140
|
+
if (svcOf(r.source) === id && svcOf(r.target) === id)
|
|
141
|
+
continue;
|
|
132
142
|
const edge = { op: r.op ?? null, title: r.title ?? null };
|
|
133
143
|
if (svcOf(r.target) === id)
|
|
134
144
|
inbound.push({ service: svcOf(r.source), ...edge });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"maturity.js","sourceRoot":"","sources":["../../../src/core/vocabulary/maturity.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The adoption-maturity ladder: one monotone word for how far a service's\n * documentation has got, and what stands between it and the next rung.\n *\n * This lived inside `commands/list.ts` while `list` was the only caller. It\n * moved here when `explore` needed the same answer, because the alternative is\n * the failure `core/kernel/ids/service.ts` already records for the service-id grammar: a\n * second, subtly different copy, so two commands grade the same directory\n * differently and nobody can tell which one is lying. The rung is a fleet dial\n * — `list` prints it per service and in a rollup, `explore` prints it for a\n * service somebody is about to build against — and a dial with two readings is\n * not a dial.\n */\nimport type { ServiceEntry } from \"../repo/entries.js\";\n\n/** The ladder, in order. Each rung stands on every rung below it. */\nexport const MATURITY_LADDER = [\"empty\", \"partial\", \"documented\", \"sourced\", \"vouched\"] as const;\nexport type Maturity = (typeof MATURITY_LADDER)[number];\n\n/**\n * A service entry plus the two facts the directory enumeration cannot know on\n * its own. Both change what a rung MEANS, and both are fleet-level: they need\n * the landscape, not `services/<id>/`.\n */\nexport interface MaturityInput {\n entry: ServiceEntry;\n /** `arch.spec.md` — the architecture-obligations axis, absent from ServiceEntry.has. */\n archSpec: boolean;\n /**\n * Does anything in the fleet call an operation on this service? True unless\n * the landscape PROVES otherwise, exactly as `validate`'s no-openapi grace\n * reads it: a missing or unparseable landscape proves nothing.\n */\n apiExpected: boolean;\n}\n\n/**\n * How far a service's documentation has got. Derived from artifact PRESENCE and\n * provenance state only — the data an enumeration already holds — never from\n * what the artifacts say. COMPLETENESS of adopted docs is explicitly on the\n * unchecked list (brief/unchecked.ts): a service with one endpoint documented out of\n * thirty climbs this ladder exactly as fast as a thorough one, which is why no\n * rung is called \"adopted\".\n *\n * empty services/<id>/ exists, no artifact is in it\n * partial some artifacts, but not the required set\n * documented the artifacts the adopt brief marks required are present\n * sourced the living spec declares `sources` — something ties it to code\n * vouched status: verified with a sources_digest behind it — a person\n * stamped it. `verified` with no digest is a claim with nothing\n * behind it and stays below this rung.\n *\n * `openapi.yaml` is required ONLY where an API is expected. The rung used to\n * demand it of everything, which permanently pinned every worker, cron and\n * consumer in the fleet at `partial` — services that are fully documented and\n * vouched for, reported as unfinished forever, leaving the rollup permanently\n * below its true documentation state. The evidence is the same one\n * `validate` uses to keep `service.no-openapi` quiet, so a service cannot be\n * green in one command and unfinished in the other.\n */\nexport function serviceMaturity(v: MaturityInput): Maturity {\n const s = v.entry;\n if (!Object.values(s.has).some(Boolean) && !v.archSpec && s.adrs === 0) return \"empty\";\n if (!(s.has.model && s.has.spec && (!v.apiExpected || s.has.openapi))) return \"partial\";\n if (!s.sources.declared) return \"documented\";\n if (!(s.status === \"verified\" && s.sources.stamped)) return \"sourced\";\n return \"vouched\";\n}\n\n/**\n * What stands between this service and `vouched`, named as the thing to go and\n * do. One rung's worth at a time: telling somebody at `empty` that they also\n * need a vouch is noise, and the vouch is not even possible yet.\n */\nexport function maturityGaps(v: MaturityInput): string[] {\n const s = v.entry;\n const files = [\n ...(s.has.model ? [] : [\"model.likec4\"]),\n ...(s.has.spec ? [] : [\"spec.md\"]),\n ...(v.apiExpected && !s.has.openapi ? [\"openapi.yaml\"] : []),\n ];\n if (files.length > 0) return files;\n if (!s.sources.declared) return [\"sources: in the spec.md frontmatter\"];\n if (!(s.status === \"verified\" && s.sources.stamped)) return [`\\`loam vouch --service ${s.id}\\``];\n return [];\n}\n\n/** Counts per rung, every rung present — a stable shape a fleet dashboard can diff. */\nexport function maturityRollup(graded: readonly { maturity: Maturity }[]): Record<Maturity, number> {\n const out = Object.fromEntries(MATURITY_LADDER.map((m) => [m, 0])) as Record<Maturity, number>;\n for (const v of graded) out[v.maturity] += 1;\n return out;\n}\n\n/* ------------------------------------------------------------------ */\n/* What the fleet map proves about one service */\n/* ------------------------------------------------------------------ */\n\n/**\n * C4 kinds that model people, and the tag marking an element as somebody\n * else's system. A person is never a service directory, and `#external` is\n * undocumented on purpose — every census and exemption over the fleet map\n * asks both questions.\n *\n * Extracted on the third strike: `commands/validate/checks/vocabulary.ts`,\n * `core/gate/partners.ts` and `core/verify/checklist.ts` each carried a copy.\n * Their real home is `core/c4/likec4.ts` beside the model they describe, but\n * that file sits against the 300-line limit and its package holds five files\n * — so the trio lives here, beside `landscapeEvidence`, which reads the same\n * map for the same per-service questions.\n *\n * `PLATFORM_TAG` joined them from `commands/validate/checks/fleet-shape.ts`,\n * where it was declared beside its one reader. It moved for the rule above the\n * others came for: `fleet/kind-tags.ts` needs to know which tag names loam\n * grades on, and reaching into fleet-shape.ts for a five-character constant\n * would import a module that reads permissions, capabilities and every\n * service's requirements to get it. The two tags loam gives meaning to belong\n * in one place anyway — that list IS what the kind-tag check is about.\n */\nexport const ACTOR_KINDS = new Set([\"person\", \"actor\", \"user\"]);\nexport const EXTERNAL_TAG = \"external\";\n/** Tag marking ubiquitous infrastructure; the scaffolded fleet view excludes it. */\nexport const PLATFORM_TAG = \"platform\";\n\n/**\n * The tags loam GRADES on, as opposed to the ones it merely carries.\n *\n * A tag in this list changes what loam concludes about a fleet: `#external`\n * exempts an element from the landscape↔services reconciliation entirely, and\n * `#platform` silences the hub warning. Every other tag on an element is the\n * author's own vocabulary and loam does not read it.\n *\n * The list exists because since LikeC4 1.59.0 a tag can be declared on a KIND\n * — `specification { element softwareSystem { #external } }` — and LikeC4\n * applies it to every element of that kind. A kind-wide declaration of a tag on\n * this list switches loam's grading off for the whole fleet at once, which is\n * what `kindTagFindings` refuses. A tag NOT on this list is safe to declare\n * kind-wide, so the check must ask exactly this question and no broader one.\n */\nexport const GRADED_TAGS: readonly string[] = [EXTERNAL_TAG, PLATFORM_TAG];\n\n/** One end of a call the fleet map draws, as every per-service view spells it. */\nexport interface LandscapeEdge {\n service: string;\n op: string | null;\n title: string | null;\n}\n\n/**\n * The slice of a parsed relationship the partition reads. Structural on\n * purpose — the smallest parameter type the derivation needs — so this module\n * stays a leaf: `core/c4/likec4.ts`'s `Rel` satisfies it without an import\n * edge from the vocabulary package into the parser.\n */\nexport interface EdgeSource {\n source: string;\n target: string;\n op?: string;\n title?: string;\n}\n\nexport interface LandscapeEvidenceRequest {\n /** The service being asked about. */\n id: string;\n /** Whether the landscape parsed — what makes its silence evidence rather than absence. */\n parses: boolean;\n /** The parsed relationships; empty when the landscape is absent or unparseable. */\n relationships: readonly EdgeSource[];\n /** Every element id in the map, for the `modelled` probe. */\n elementIds: readonly string[];\n /** The shared resolver (`core/c4/resolve/service.ts` `serviceResolver`), widened to plain strings. */\n svcOf: (id: string) => string;\n}\n\nexport interface LandscapeEvidence {\n inbound: LandscapeEdge[];\n outbound: LandscapeEdge[];\n /** Whether anything in the fleet map resolves to this service. */\n modelled: boolean;\n /** The `apiExpected` input the ladder above consumes. */\n apiExpected: boolean;\n}\n\n/**\n * The fleet map's evidence about one service: the edges into and out of it,\n * whether the map draws it at all, and whether an API is expected of it.\n *\n * One spelling, because it had grown three. `explore`'s describe and the\n * context pack's living slice partitioned the same relationships with the\n * same positive-evidence rule line for line — an inbound edge carrying an\n * operation proves somebody calls the service; a landscape that is absent or\n * does not parse proves NOTHING about who calls it, so the contract is still\n * owed — and a rule spelled twice is how the same service grades `partial` in\n * one command and `documented` in another. `list`'s serviceViews\n * (`commands/list/views.ts`) derives the same rule fleet-wide in ONE pass —\n * a single `called` set for every service, not a partition per service —\n * so that spelling deliberately stays there, cross-referenced from both\n * sides: calling this per service inside a fleet loop would re-walk the\n * whole relationship list once per service.\n */\nexport function landscapeEvidence(req: LandscapeEvidenceRequest): LandscapeEvidence {\n const { id, parses, relationships, svcOf } = req;\n const inbound: LandscapeEdge[] = [];\n const outbound: LandscapeEdge[] = [];\n for (const r of relationships) {\n const edge = { op: r.op ?? null, title: r.title ?? null };\n if (svcOf(r.target) === id) inbound.push({ service: svcOf(r.source), ...edge });\n else if (svcOf(r.source) === id) outbound.push({ service: svcOf(r.target), ...edge });\n }\n return {\n inbound,\n outbound,\n modelled: req.elementIds.some((e) => svcOf(e) === id),\n apiExpected: !parses || inbound.some((e) => e.op !== null),\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"maturity.js","sourceRoot":"","sources":["../../../src/core/vocabulary/maturity.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The adoption-maturity ladder: one monotone word for how far a service's\n * documentation has got, and what stands between it and the next rung.\n *\n * This lived inside `commands/list.ts` while `list` was the only caller. It\n * moved here when `explore` needed the same answer, because the alternative is\n * the failure `core/kernel/ids/service.ts` already records for the service-id grammar: a\n * second, subtly different copy, so two commands grade the same directory\n * differently and nobody can tell which one is lying. The rung is a fleet dial\n * — `list` prints it per service and in a rollup, `explore` prints it for a\n * service somebody is about to build against — and a dial with two readings is\n * not a dial.\n */\nimport type { ServiceEntry } from \"../repo/entries.js\";\n\n/** The ladder, in order. Each rung stands on every rung below it. */\nexport const MATURITY_LADDER = [\"empty\", \"partial\", \"documented\", \"sourced\", \"vouched\"] as const;\nexport type Maturity = (typeof MATURITY_LADDER)[number];\n\n/**\n * A service entry plus the two facts the directory enumeration cannot know on\n * its own. Both change what a rung MEANS, and both are fleet-level: they need\n * the landscape, not `services/<id>/`.\n */\nexport interface MaturityInput {\n entry: ServiceEntry;\n /** `arch.spec.md` — the architecture-obligations axis, absent from ServiceEntry.has. */\n archSpec: boolean;\n /**\n * Does anything in the fleet call an operation on this service? True unless\n * the landscape PROVES otherwise, exactly as `validate`'s no-openapi grace\n * reads it: a missing or unparseable landscape proves nothing.\n */\n apiExpected: boolean;\n}\n\n/**\n * How far a service's documentation has got. Derived from artifact PRESENCE and\n * provenance state only — the data an enumeration already holds — never from\n * what the artifacts say. COMPLETENESS of adopted docs is explicitly on the\n * unchecked list (brief/unchecked.ts): a service with one endpoint documented out of\n * thirty climbs this ladder exactly as fast as a thorough one, which is why no\n * rung is called \"adopted\".\n *\n * empty services/<id>/ exists, no artifact is in it\n * partial some artifacts, but not the required set\n * documented the artifacts the adopt brief marks required are present\n * sourced the living spec declares `sources` — something ties it to code\n * vouched status: verified with a sources_digest behind it — a person\n * stamped it. `verified` with no digest is a claim with nothing\n * behind it and stays below this rung.\n *\n * `openapi.yaml` is required ONLY where an API is expected. The rung used to\n * demand it of everything, which permanently pinned every worker, cron and\n * consumer in the fleet at `partial` — services that are fully documented and\n * vouched for, reported as unfinished forever, leaving the rollup permanently\n * below its true documentation state. The evidence is the same one\n * `validate` uses to keep `service.no-openapi` quiet, so a service cannot be\n * green in one command and unfinished in the other.\n */\nexport function serviceMaturity(v: MaturityInput): Maturity {\n const s = v.entry;\n if (!Object.values(s.has).some(Boolean) && !v.archSpec && s.adrs === 0) return \"empty\";\n if (!(s.has.model && s.has.spec && (!v.apiExpected || s.has.openapi))) return \"partial\";\n if (!s.sources.declared) return \"documented\";\n if (!(s.status === \"verified\" && s.sources.stamped)) return \"sourced\";\n return \"vouched\";\n}\n\n/**\n * What stands between this service and `vouched`, named as the thing to go and\n * do. One rung's worth at a time: telling somebody at `empty` that they also\n * need a vouch is noise, and the vouch is not even possible yet.\n */\nexport function maturityGaps(v: MaturityInput): string[] {\n const s = v.entry;\n const files = [\n ...(s.has.model ? [] : [\"model.likec4\"]),\n ...(s.has.spec ? [] : [\"spec.md\"]),\n ...(v.apiExpected && !s.has.openapi ? [\"openapi.yaml\"] : []),\n ];\n if (files.length > 0) return files;\n if (!s.sources.declared) return [\"sources: in the spec.md frontmatter\"];\n if (!(s.status === \"verified\" && s.sources.stamped)) return [`\\`loam vouch --service ${s.id}\\``];\n return [];\n}\n\n/** Counts per rung, every rung present — a stable shape a fleet dashboard can diff. */\nexport function maturityRollup(graded: readonly { maturity: Maturity }[]): Record<Maturity, number> {\n const out = Object.fromEntries(MATURITY_LADDER.map((m) => [m, 0])) as Record<Maturity, number>;\n for (const v of graded) out[v.maturity] += 1;\n return out;\n}\n\n/* ------------------------------------------------------------------ */\n/* What the fleet map proves about one service */\n/* ------------------------------------------------------------------ */\n\n/**\n * C4 kinds that model people, and the tag marking an element as somebody\n * else's system. A person is never a service directory, and `#external` is\n * undocumented on purpose — every census and exemption over the fleet map\n * asks both questions.\n *\n * Extracted on the third strike: `commands/validate/checks/vocabulary.ts`,\n * `core/gate/partners.ts` and `core/verify/checklist.ts` each carried a copy.\n * Their real home is `core/c4/likec4.ts` beside the model they describe, but\n * that file sits against the 300-line limit and its package holds five files\n * — so the trio lives here, beside `landscapeEvidence`, which reads the same\n * map for the same per-service questions.\n *\n * `PLATFORM_TAG` joined them from `commands/validate/checks/fleet-shape.ts`,\n * where it was declared beside its one reader. It moved for the rule above the\n * others came for: `fleet/kind-tags.ts` needs to know which tag names loam\n * grades on, and reaching into fleet-shape.ts for a five-character constant\n * would import a module that reads permissions, capabilities and every\n * service's requirements to get it. The two tags loam gives meaning to belong\n * in one place anyway — that list IS what the kind-tag check is about.\n */\nexport const ACTOR_KINDS = new Set([\"person\", \"actor\", \"user\"]);\nexport const EXTERNAL_TAG = \"external\";\n/** Tag marking ubiquitous infrastructure; the scaffolded fleet view excludes it. */\nexport const PLATFORM_TAG = \"platform\";\n\n/**\n * The tags loam GRADES on, as opposed to the ones it merely carries.\n *\n * A tag in this list changes what loam concludes about a fleet: `#external`\n * exempts an element from the landscape↔services reconciliation entirely, and\n * `#platform` silences the hub warning. Every other tag on an element is the\n * author's own vocabulary and loam does not read it.\n *\n * The list exists because since LikeC4 1.59.0 a tag can be declared on a KIND\n * — `specification { element softwareSystem { #external } }` — and LikeC4\n * applies it to every element of that kind. A kind-wide declaration of a tag on\n * this list switches loam's grading off for the whole fleet at once, which is\n * what `kindTagFindings` refuses. A tag NOT on this list is safe to declare\n * kind-wide, so the check must ask exactly this question and no broader one.\n */\nexport const GRADED_TAGS: readonly string[] = [EXTERNAL_TAG, PLATFORM_TAG];\n\n/** One end of a call the fleet map draws, as every per-service view spells it. */\nexport interface LandscapeEdge {\n service: string;\n op: string | null;\n title: string | null;\n}\n\n/**\n * The slice of a parsed relationship the partition reads. Structural on\n * purpose — the smallest parameter type the derivation needs — so this module\n * stays a leaf: `core/c4/likec4.ts`'s `Rel` satisfies it without an import\n * edge from the vocabulary package into the parser.\n */\nexport interface EdgeSource {\n source: string;\n target: string;\n op?: string;\n title?: string;\n}\n\nexport interface LandscapeEvidenceRequest {\n /** The service being asked about. */\n id: string;\n /** Whether the landscape parsed — what makes its silence evidence rather than absence. */\n parses: boolean;\n /** The parsed relationships; empty when the landscape is absent or unparseable. */\n relationships: readonly EdgeSource[];\n /** Every element id in the map, for the `modelled` probe. */\n elementIds: readonly string[];\n /** The shared resolver (`core/c4/resolve/service.ts` `serviceResolver`), widened to plain strings. */\n svcOf: (id: string) => string;\n}\n\nexport interface LandscapeEvidence {\n inbound: LandscapeEdge[];\n outbound: LandscapeEdge[];\n /** Whether anything in the fleet map resolves to this service. */\n modelled: boolean;\n /** The `apiExpected` input the ladder above consumes. */\n apiExpected: boolean;\n}\n\n/**\n * The fleet map's evidence about one service: the edges into and out of it,\n * whether the map draws it at all, and whether an API is expected of it.\n *\n * One spelling, because it had grown three. `explore`'s describe and the\n * context pack's living slice partitioned the same relationships with the\n * same positive-evidence rule line for line — an inbound edge carrying an\n * operation proves somebody calls the service; a landscape that is absent or\n * does not parse proves NOTHING about who calls it, so the contract is still\n * owed — and a rule spelled twice is how the same service grades `partial` in\n * one command and `documented` in another. `list`'s serviceViews\n * (`commands/list/views.ts`) derives the same rule fleet-wide in ONE pass —\n * a single `called` set for every service, not a partition per service —\n * so that spelling deliberately stays there, cross-referenced from both\n * sides: calling this per service inside a fleet loop would re-walk the\n * whole relationship list once per service.\n */\nexport function landscapeEvidence(req: LandscapeEvidenceRequest): LandscapeEvidence {\n const { id, parses, relationships, svcOf } = req;\n const inbound: LandscapeEdge[] = [];\n const outbound: LandscapeEdge[] = [];\n for (const r of relationships) {\n // An edge whose BOTH endpoints resolve to this service is its own internal\n // wiring (`svc.web -> svc.db`): filed as inbound it made the service its own\n // caller — `← <id> \"reads\"` in `loam context` (verification 2026-09-04) —\n // and, through `apiExpected` below, told a service nobody calls that it owed\n // an OpenAPI contract. `core/brief/landscape.ts` drops it for the same\n // reason. It still TOUCHES the service, which is why the brief's `touched`\n // and `landscape.service-isolated` read the relationships rather than these\n // two lists.\n if (svcOf(r.source) === id && svcOf(r.target) === id) continue;\n const edge = { op: r.op ?? null, title: r.title ?? null };\n if (svcOf(r.target) === id) inbound.push({ service: svcOf(r.source), ...edge });\n else if (svcOf(r.source) === id) outbound.push({ service: svcOf(r.target), ...edge });\n }\n return {\n inbound,\n outbound,\n modelled: req.elementIds.some((e) => svcOf(e) === id),\n apiExpected: !parses || inbound.some((e) => e.op !== null),\n };\n}\n"]}
|
package/examples/README.md
CHANGED
|
@@ -149,6 +149,29 @@ and `validate --all` answers `subsystem.views-stale`. (`order-service` and `paym
|
|
|
149
149
|
unfiled deliberately: FEAT-088's committed version-2 snapshot restores them by literal path, and the
|
|
150
150
|
`loam unarchive FEAT-088` walkthrough must keep working byte-for-byte.)
|
|
151
151
|
|
|
152
|
+
**All five models are EXTENDING models, and that is the shape this example teaches.** None of them
|
|
153
|
+
declares a `specification` block, none re-declares a partner, and none has a `likec4.config.json`
|
|
154
|
+
beside it. Each one says what is inside its service with `extend marketplace.<svc> { … }` on the
|
|
155
|
+
element `architecture/landscape.likec4` already binds to its directory, so `kafka`, `stripe`,
|
|
156
|
+
`notification-provider` and every peer service are declared exactly once — in the map — and no two
|
|
157
|
+
documents can disagree about their tags. The root `likec4.config.json` therefore excludes only
|
|
158
|
+
`**/node_modules/**` and `features/**`: `npx likec4 validate examples/docs` names ONE project,
|
|
159
|
+
`fleet`, holding the map and all five models, and needs no `--project` flag. The other shape — a
|
|
160
|
+
model that declares its own `specification` and is parsed alone, which is what every model here was
|
|
161
|
+
before — is still legal and still supported; SCHEMA.md's "Two shapes of a service model" has both,
|
|
162
|
+
the eight grades in its table — seven that came with the pair, plus the one it narrowed
|
|
163
|
+
(`service.likec4-config-missing`, now standalone-only) — and the
|
|
164
|
+
ordered recipe for migrating between them.
|
|
165
|
+
|
|
166
|
+
Two consequences are visible in the tree and worth reading for. Element ids carry their full path,
|
|
167
|
+
so `services/order-service/arch.spec.md` writes `Covers: marketplace.orderService.db` — the
|
|
168
|
+
container has no shorter name, because the model extends an element nested in the `marketplace`
|
|
169
|
+
group. And a cross-boundary call is written **twice** on purpose: container-level in the model
|
|
170
|
+
(`marketplace.notificationService.sender -> notificationProvider`) and service-level in the map
|
|
171
|
+
(`marketplace.notificationService -> notificationProvider`), because every fleet check — the spine,
|
|
172
|
+
`landscape.service-isolated`, the platform census — reads the map alone. ROADMAP.md keeps that
|
|
173
|
+
remainder written down rather than hidden.
|
|
174
|
+
|
|
152
175
|
Plus one count rather than a finding: `sourcesUnverifiableFromHere: 4`. Four specs name paths in
|
|
153
176
|
their own service repositories, and this is none of those repositories — the fleet gate reports the
|
|
154
177
|
blind spot instead of resolving it. Running `loam validate --service <id>` inside each service repo
|
|
@@ -8,6 +8,16 @@
|
|
|
8
8
|
// service needs a directory (warning). An element is tied to its directory by
|
|
9
9
|
// `metadata { service '<id>' }`, or by its title when there is no binding.
|
|
10
10
|
// Systems that are not ours carry #external and are exempt.
|
|
11
|
+
//
|
|
12
|
+
// This file is also the SPECIFICATION every service model borrows. Each
|
|
13
|
+
// `services/<…>/<svc>/model.likec4` here is an EXTENDING model: it declares no
|
|
14
|
+
// kinds and no partners, and says what is inside its service with
|
|
15
|
+
// `extend marketplace.<svc> { … }` on the element below that binds to its
|
|
16
|
+
// directory. The root `likec4.config.json` therefore does NOT exclude
|
|
17
|
+
// `services/**` — the map and every model are ONE LikeC4 project, so an element
|
|
18
|
+
// is declared exactly once and no two documents can disagree about its tags.
|
|
19
|
+
// SCHEMA.md, "Two shapes of a service model", has both shapes and the rule that
|
|
20
|
+
// picks between them; a kind or tag a model needs is added HERE, once.
|
|
11
21
|
specification {
|
|
12
22
|
// A grouping element — it stands for no service and owns no directory. The
|
|
13
23
|
// service checks WALK the tree rather than reading only top-level elements:
|
|
@@ -20,7 +30,18 @@ specification {
|
|
|
20
30
|
// Declared here because archive SPLICES a delta's additions verbatim: FEAT-101's
|
|
21
31
|
// new system carries its `api` container along, and the kind has to be legal in
|
|
22
32
|
// THIS document for the merged landscape to parse (archive refuses otherwise).
|
|
33
|
+
// Only a NEW service arrives this way — one that already has an extending model
|
|
34
|
+
// takes the addition into that model instead, and this file never grows an
|
|
35
|
+
// interior.
|
|
23
36
|
element container
|
|
37
|
+
// Declared here for the SERVICE MODELS, and nothing in this file uses it: a
|
|
38
|
+
// store lives inside the service that owns it (`landscape.datastore-private`
|
|
39
|
+
// is the check that pushes one back in), so every `database` element in this
|
|
40
|
+
// fleet is written in a `services/<svc>/model.likec4` under
|
|
41
|
+
// `extend marketplace.<svc>`. An extending model declares no kinds of its
|
|
42
|
+
// own, so a kind it needs and this block does not declare is ONE parse error
|
|
43
|
+
// on the model — which is what this line prevents.
|
|
44
|
+
element database
|
|
24
45
|
element person
|
|
25
46
|
// A topic is an element, and the kind carries #external rather than each
|
|
26
47
|
// instance doing it: LikeC4 does NOT inherit tags, so a topic nested inside
|
|
@@ -137,6 +158,18 @@ model {
|
|
|
137
158
|
description 'CRM of record — the fleet reads its events and writes none'
|
|
138
159
|
}
|
|
139
160
|
|
|
161
|
+
// Declared HERE rather than in notification-service's model, and that is the
|
|
162
|
+
// rule an extending model turns on: a system a service reaches is the fleet's
|
|
163
|
+
// element, declared once, and a model that declared its own copy would be
|
|
164
|
+
// `c4.element-unowned` (an extending model may only add elements under the
|
|
165
|
+
// element that resolves to its own service). One consumer, so it never
|
|
166
|
+
// becomes `landscape.platform-candidate`; #external, so no
|
|
167
|
+
// services/notification-provider/ is asked for.
|
|
168
|
+
notificationProvider = softwareSystem 'notification-provider' {
|
|
169
|
+
#external
|
|
170
|
+
description 'Third-party email and SMS gateway — quota-limited, and outside this fleet'
|
|
171
|
+
}
|
|
172
|
+
|
|
140
173
|
customer -> marketplace.checkoutWeb 'Uses'
|
|
141
174
|
|
|
142
175
|
// The synchronous spine. Every one of these resolves against the TARGET's
|
|
@@ -185,6 +218,14 @@ model {
|
|
|
185
218
|
// it raises no warning because its title does not claim to be a call into a
|
|
186
219
|
// fleet service.
|
|
187
220
|
marketplace.paymentService -> stripe 'Authorizes cards'
|
|
221
|
+
// The second half of the rule the extending shape keeps: the container that
|
|
222
|
+
// really makes the call is drawn in the service's own model
|
|
223
|
+
// (`marketplace.notificationService.sender -> notificationProvider`), and the
|
|
224
|
+
// SERVICE-level twin is drawn here, because every fleet check — the spine,
|
|
225
|
+
// `landscape.service-isolated`, the platform census — reads this file alone.
|
|
226
|
+
// The edge exists twice on purpose; SCHEMA.md says so where it says the map
|
|
227
|
+
// is the one document holding every cross-service edge.
|
|
228
|
+
marketplace.notificationService -> notificationProvider 'Sends email and SMS'
|
|
188
229
|
|
|
189
230
|
// The event spine, the async mirror of `metadata { op }`: the name must be a
|
|
190
231
|
// message the PRODUCER's own asyncapi.yaml declares an `action: send` for, or
|
|
@@ -14,7 +14,10 @@
|
|
|
14
14
|
// read as ONE LikeC4 project, so the steps below resolve against the landscape's
|
|
15
15
|
// elements even though this file declares none — which also means exactly one
|
|
16
16
|
// file in this directory declares the `specification` block, and it is not this
|
|
17
|
-
// one.
|
|
17
|
+
// one. (The renderer's root project is WIDER than what loam grades here: it also
|
|
18
|
+
// holds every `services/<svc>/model.likec4`, because each of them extends the
|
|
19
|
+
// map. A tag declared twice across that whole set is `c4.fleet-project-invalid`,
|
|
20
|
+
// which is why a tag belongs in the landscape's one block.)
|
|
18
21
|
//
|
|
19
22
|
// THE TAG MUST BE THE FIRST THING IN THE VIEW BODY. Written after `title` it is
|
|
20
23
|
// five parse errors, none of which mentions tags. That is the mistake every
|
|
@@ -30,7 +33,7 @@
|
|
|
30
33
|
// `usecase.step-unlinked` is met except the one that matters, which is that a
|
|
31
34
|
// person is not a caller. Somebody clicking a button owes no operationId. Delete
|
|
32
35
|
// that exemption from the checker and this file is what notices: the example
|
|
33
|
-
// fleet's warning count moves from
|
|
36
|
+
// fleet's warning count moves from ten to eleven and test/examples.test.ts
|
|
34
37
|
// fails.
|
|
35
38
|
//
|
|
36
39
|
// The hop from checkout-web to order-service is DELIBERATELY NOT DRAWN, and its
|
|
@@ -1,27 +1,24 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// checkout-web's own C4 — what is INSIDE the element the fleet map binds to
|
|
2
|
+
// services/checkout-web/. A UI is a first-class C4 system: it has containers, it
|
|
3
|
+
// has runtime dependencies, and its pages (ui/pages/*.page.yaml) hang off the
|
|
4
|
+
// operations the edges below name.
|
|
4
5
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
6
|
+
// An EXTENDING model: no `specification` block, no partner copies. The kinds and
|
|
7
|
+
// every element this file points at — `customer`, `marketplace.orderService`,
|
|
8
|
+
// `marketplace.paymentService` — are declared once in
|
|
9
|
+
// architecture/landscape.likec4, and the root `likec4.config.json` does not
|
|
10
|
+
// exclude `services/**`, so the map and every model are read as ONE LikeC4
|
|
11
|
+
// project. `extend` takes the fully-qualified id, because the element is nested
|
|
12
|
+
// in the `marketplace` group.
|
|
13
|
+
//
|
|
14
|
+
// Note what this file does NOT do: it does not declare `customer = person`. The
|
|
15
|
+
// actor is the map's, and the edge below simply points at it. That is the rule
|
|
16
|
+
// the shape turns on — an element declared here outside
|
|
17
|
+
// `marketplace.checkoutWeb` is `c4.element-unowned` — and it is also what stops
|
|
18
|
+
// two documents from holding two different descriptions of the same person.
|
|
19
|
+
// SCHEMA.md, "Two shapes of a service model", has the other shape.
|
|
15
20
|
model {
|
|
16
|
-
|
|
17
|
-
// the pages exist for this person, and the flows in ui/pages/ start at them.
|
|
18
|
-
// The landscape draws the same customer -> checkout-web edge at fleet scale;
|
|
19
|
-
// a person is exempt from the service/directory cross-check either way.
|
|
20
|
-
customer = person 'Customer'
|
|
21
|
-
|
|
22
|
-
checkoutWeb = softwareSystem 'checkout-web' {
|
|
23
|
-
description 'Customer-facing checkout UI'
|
|
24
|
-
|
|
21
|
+
extend marketplace.checkoutWeb {
|
|
25
22
|
spa = container 'spa' {
|
|
26
23
|
description 'Checkout SPA — cart, payment and order status pages'
|
|
27
24
|
technology 'React'
|
|
@@ -37,37 +34,21 @@ model {
|
|
|
37
34
|
technology 'Node.js'
|
|
38
35
|
}
|
|
39
36
|
|
|
40
|
-
customer -> spa 'Buys'
|
|
41
37
|
spa -> bff 'Calls the checkout BFF'
|
|
42
38
|
}
|
|
43
39
|
|
|
44
|
-
// The
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
// `c4.no-relationships` would be right to say a model that reaches nothing is
|
|
50
|
-
// almost never true.
|
|
51
|
-
orderService = softwareSystem 'order-service' {
|
|
52
|
-
description 'Owns the order lifecycle'
|
|
53
|
-
// Bind rather than rely on the title. A renamed box silently stops matching
|
|
54
|
-
// its directory everywhere the two are joined — in the landscape that costs
|
|
55
|
-
// `landscape.binding-unknown` (error) when the id is wrong, and here it is
|
|
56
|
-
// what `health.dependency-unmodelled` resolves a declared dependency
|
|
57
|
-
// through in the services that have a health.yaml. checkout-web has none:
|
|
58
|
-
// a UI is nobody's runtime dependency, so it declares no SLIs, no alerts
|
|
59
|
-
// and no dependency graph, and owes no arch.spec.md to cover them.
|
|
60
|
-
metadata {
|
|
61
|
-
service 'order-service'
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
paymentService = softwareSystem 'payment-service' {
|
|
65
|
-
description 'Owns payment authorization/capture'
|
|
66
|
-
metadata {
|
|
67
|
-
service 'payment-service'
|
|
68
|
-
}
|
|
69
|
-
}
|
|
40
|
+
// The person is the MAP's element and the hop into this UI is drawn from it.
|
|
41
|
+
// The landscape draws the coarser `customer -> marketplace.checkoutWeb` twin
|
|
42
|
+
// at fleet scale; this one says which container the click reaches. A person is
|
|
43
|
+
// exempt from the service/directory cross-check either way.
|
|
44
|
+
customer -> marketplace.checkoutWeb.spa 'Buys'
|
|
70
45
|
|
|
46
|
+
// The services this one actually calls, drawn with the edge the service really
|
|
47
|
+
// has — the landscape carries only the boundary crossing (checkout-web ->
|
|
48
|
+
// order-service), while the edges below say WHICH container makes the call.
|
|
49
|
+
// Without them this file would be two boxes and a hop, and `c4.no-relationships`
|
|
50
|
+
// would be right to say a model that reaches nothing is almost never true.
|
|
51
|
+
//
|
|
71
52
|
// `metadata { op }` is the C4 half of the operation join: the exact
|
|
72
53
|
// operationId order-service's openapi.yaml declares. What `loam validate`
|
|
73
54
|
// grades is the LANDSCAPE's twin of each edge below — `spine.op-undefined`
|
|
@@ -77,12 +58,12 @@ model {
|
|
|
77
58
|
// nothing would catch it if they did. Two calls to one service are two edges,
|
|
78
59
|
// not one edge with a list — a view has to be able to show that the status
|
|
79
60
|
// page and the checkout page stand on different operations.
|
|
80
|
-
checkoutWeb.bff -> orderService 'Calls createOrder' {
|
|
61
|
+
marketplace.checkoutWeb.bff -> marketplace.orderService 'Calls createOrder' {
|
|
81
62
|
metadata {
|
|
82
63
|
op 'createOrder'
|
|
83
64
|
}
|
|
84
65
|
}
|
|
85
|
-
checkoutWeb.bff -> orderService 'Calls getOrder' {
|
|
66
|
+
marketplace.checkoutWeb.bff -> marketplace.orderService 'Calls getOrder' {
|
|
86
67
|
metadata {
|
|
87
68
|
op 'getOrder'
|
|
88
69
|
}
|
|
@@ -94,16 +75,18 @@ model {
|
|
|
94
75
|
// for an edge that SAYS it calls something and names no operation, so nothing
|
|
95
76
|
// can check the call still exists. Free here and one warning there, which is
|
|
96
77
|
// the asymmetry worth knowing: the fleet map is what the spine reads.
|
|
97
|
-
checkoutWeb.spa -> paymentService 'Calls the payment API'
|
|
78
|
+
marketplace.checkoutWeb.spa -> marketplace.paymentService 'Calls the payment API'
|
|
98
79
|
}
|
|
99
80
|
|
|
100
81
|
// Views are the LikeC4 renderer's, never loam's — no check reads this block.
|
|
101
|
-
// Scoped to the
|
|
102
|
-
// rather than redrawing the fleet, which architecture/landscape.likec4
|
|
82
|
+
// Scoped to the service's element so the page answers "what does the checkout UI
|
|
83
|
+
// stand on" rather than redrawing the fleet, which architecture/landscape.likec4
|
|
84
|
+
// already does. This file is part of the root project: `npx likec4 start` at the
|
|
85
|
+
// docs root draws it, with no per-service project file to write.
|
|
103
86
|
views {
|
|
104
|
-
view checkout_web of checkoutWeb {
|
|
87
|
+
view checkout_web of marketplace.checkoutWeb {
|
|
105
88
|
title 'checkout-web — containers and the services they call'
|
|
106
89
|
include *
|
|
107
|
-
include orderService, paymentService
|
|
90
|
+
include marketplace.orderService, marketplace.paymentService
|
|
108
91
|
}
|
|
109
92
|
}
|
|
@@ -25,9 +25,16 @@ this service's own `model.likec4` plus the landscape (`covers.unknown`, with a d
|
|
|
25
25
|
hint, is the typo guard — a mistyped entry otherwise costs exactly the coverage it was written
|
|
26
26
|
for), and every `slis[].name` and `alerts[].name` in `health.yaml` must be named by one of them
|
|
27
27
|
(`health.uncovered`, or a signal ships to the on-call rotation with nothing testing it). Edge
|
|
28
|
-
entries below are spelled as this model draws them — `orderService.outbox ->
|
|
29
|
-
— because the endpoint that matters is the container; `order-service ->
|
|
30
|
-
resolves too, through the `metadata { service }` binding on the
|
|
28
|
+
entries below are spelled as this model draws them — `marketplace.orderService.outbox ->
|
|
29
|
+
kafka.orderEvents` — because the endpoint that matters is the container; `order-service ->
|
|
30
|
+
payment-service` resolves too, through the `metadata { service }` binding on the map's element.
|
|
31
|
+
|
|
32
|
+
Element ids here carry the FULL path, `marketplace.` and all. That is a consequence of the model
|
|
33
|
+
shape rather than a style: `model.likec4` extends the element the fleet map declares inside the
|
|
34
|
+
`marketplace` group, so the container it adds is `marketplace.orderService.db` and no shorter
|
|
35
|
+
spelling of it exists. A service whose model declares its own `specification` and is parsed alone
|
|
36
|
+
mints shorter ids and writes them here — see SCHEMA.md, "Two shapes of a service model", which also
|
|
37
|
+
lists requalifying these lines as a step of the migration between the two.
|
|
31
38
|
|
|
32
39
|
## Requirements
|
|
33
40
|
|
|
@@ -40,7 +47,7 @@ key rather than placing a second order or requesting a second authorization. che
|
|
|
40
47
|
retry on a gateway timeout is not a rare path — it is the normal shape of a mobile checkout —
|
|
41
48
|
and without the key a customer's flaky connection charges the card twice.
|
|
42
49
|
|
|
43
|
-
Covers: orderService.db, orderService.domain -> paymentService
|
|
50
|
+
Covers: marketplace.orderService.db, marketplace.orderService.domain -> marketplace.paymentService
|
|
44
51
|
|
|
45
52
|
#### Scenario: checkout-web retries a placement it never saw acknowledged
|
|
46
53
|
- **Given** an order placed under `Idempotency-Key` "ik-8842" with an authorization already requested
|
|
@@ -63,7 +70,7 @@ database and then to kafka. A dual write has no failure mode in which both sides
|
|
|
63
70
|
between them either announces an order nobody stored or stores one nobody hears about, and
|
|
64
71
|
notification-service is the service that pays for the second case.
|
|
65
72
|
|
|
66
|
-
Covers: orderService.outbox, orderService.outbox -> kafka.orderEvents
|
|
73
|
+
Covers: marketplace.orderService.outbox, marketplace.orderService.outbox -> kafka.orderEvents
|
|
67
74
|
|
|
68
75
|
#### Scenario: The broker is unavailable when an order is placed
|
|
69
76
|
- **Given** an accepted order whose `OrderPlaced` row is still in the outbox table
|
|
@@ -86,7 +93,7 @@ already recorded. The producer's outbox promises at-least-once delivery and a re
|
|
|
86
93
|
redelivers whatever was in flight, so a consumer without a ledger is a consumer that confirms
|
|
87
94
|
an order it already confirmed — and, once ORD-CANCEL has moved on, resurrects a cancelled one.
|
|
88
95
|
|
|
89
|
-
Covers: orderService.consumer, kafka.paymentEvents -> orderService.consumer
|
|
96
|
+
Covers: marketplace.orderService.consumer, kafka.paymentEvents -> marketplace.orderService.consumer
|
|
90
97
|
Consumes: payment.PaymentAuthorized
|
|
91
98
|
|
|
92
99
|
#### Scenario: The same authorization is redelivered after a consumer rebalance
|