@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
|
@@ -1,25 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Fleet-shape advisories over the parsed landscape: the
|
|
3
|
-
*
|
|
4
|
-
* memory.
|
|
2
|
+
* Fleet-shape advisories over the parsed landscape: the way a fleet map stops
|
|
3
|
+
* answering questions as it grows, caught while the map is already in memory.
|
|
5
4
|
*
|
|
6
|
-
*
|
|
5
|
+
* The shape becomes visible at service three rather than service two.
|
|
7
6
|
* Ubiquitous infrastructure — logging Kafka, an identity provider, service
|
|
8
7
|
* discovery — takes one inbound edge per modelled service until the map is a
|
|
9
8
|
* hairball around it; deleting the element fixes the picture and loses "who
|
|
10
9
|
* depends on the Identity Provider", which is the question the map exists to
|
|
11
10
|
* answer during an incident. Tagging keeps both: the fleet view excludes
|
|
12
|
-
* `#platform`, and a platform view keeps the dependents.
|
|
13
|
-
* peer makes a claim its consumer count either supports or refutes: one
|
|
14
|
-
* consumer means the drawing is false — the store is that service's
|
|
15
|
-
* internals, not a system in its own right — while two or more mean the
|
|
16
|
-
* strongest coupling two services can have, which deserves to be stated
|
|
17
|
-
* rather than inferred.
|
|
11
|
+
* `#platform`, and a platform view keeps the dependents.
|
|
18
12
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
13
|
+
* The DATASTORE half of this pair moved to `../fleet/map/consumers.ts` when its
|
|
14
|
+
* consumer count stopped being a fact about the map alone: a private store now
|
|
15
|
+
* lives nested inside its owner's element, and the services reaching it are
|
|
16
|
+
* attested in their own models (E3/R1). The census both checks read is built
|
|
17
|
+
* there and handed in here as `consumers`, so a hub and a store can never
|
|
18
|
+
* disagree about who consumes what.
|
|
19
|
+
*
|
|
20
|
+
* loam still reads no code and decides no policy: the team chooses what counts
|
|
21
|
+
* as platform — this warning only names a shape that is usually wrong.
|
|
23
22
|
*/
|
|
24
23
|
import { existsSync } from "node:fs";
|
|
25
24
|
import { readFile } from "node:fs/promises";
|
|
@@ -45,76 +44,30 @@ import { EXTERNAL_TAG, PLATFORM_TAG } from "../../../core/vocabulary/maturity.js
|
|
|
45
44
|
* the shared hub starts making the map unreadable.
|
|
46
45
|
*/
|
|
47
46
|
export const PLATFORM_CANDIDATE_MIN_CONSUMERS = 3;
|
|
48
|
-
/** The element kind read as a datastore, compared case-insensitively. */
|
|
49
|
-
const DATASTORE_KIND = "database";
|
|
50
47
|
export function fleetShapeFindings(shape) {
|
|
51
|
-
const { drawn,
|
|
48
|
+
const { drawn, services } = shape;
|
|
52
49
|
const findings = [];
|
|
53
|
-
// Distinct consumer SERVICES of an element, its nested children included: an
|
|
54
|
-
// edge into `kafka.paymentEvents` is consumption of `kafka`. Persons, other
|
|
55
|
-
// external systems and unresolved sources never count — a consumer is a
|
|
56
|
-
// source the shared resolver files under a real services/<id>/, so two edges
|
|
57
|
-
// from one service are one consumer.
|
|
58
|
-
const consumersOf = (e) => {
|
|
59
|
-
const out = new Set();
|
|
60
|
-
for (const r of relationships) {
|
|
61
|
-
if (r.target !== e.id && !r.target.startsWith(`${e.id}.`))
|
|
62
|
-
continue;
|
|
63
|
-
const svc = resolve(r.source);
|
|
64
|
-
if (services.has(svc))
|
|
65
|
-
out.add(svc);
|
|
66
|
-
}
|
|
67
|
-
return [...out].sort();
|
|
68
|
-
};
|
|
69
50
|
for (const e of drawn) {
|
|
70
51
|
// An element that stands for a real service is the fleet itself, not its
|
|
71
|
-
// shape: many consumers is what a well-used service looks like
|
|
72
|
-
// datastore bound to a directory is graded by the binding checks instead.
|
|
52
|
+
// shape: many consumers is what a well-used service looks like.
|
|
73
53
|
if (e.service !== undefined || services.has(e.title))
|
|
74
54
|
continue;
|
|
75
|
-
|
|
76
|
-
if (e.tags.includes(EXTERNAL_TAG) &&
|
|
77
|
-
!e.tags.includes(PLATFORM_TAG) &&
|
|
78
|
-
consumers.length >= PLATFORM_CANDIDATE_MIN_CONSUMERS) {
|
|
79
|
-
findings.push({
|
|
80
|
-
severity: "warn",
|
|
81
|
-
code: "landscape.platform-candidate",
|
|
82
|
-
subject: e.title,
|
|
83
|
-
message: `landscape: '${e.title}' is consumed by ${consumers.length} services (${consumers.join(", ")}) ` +
|
|
84
|
-
`and is not tagged #platform — a hub like this takes one more edge per modelled service until ` +
|
|
85
|
-
`the fleet view is unreadable. Declare \`tag platform\` in the specification block and tag the ` +
|
|
86
|
-
`element (LikeC4 refuses an undeclared tag, so both steps are needed): the fleet view then ` +
|
|
87
|
-
`excludes it, and a platform view over \`include * -> element.tag = #platform\` keeps ` +
|
|
88
|
-
`"who depends on it" answerable`,
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
if (e.kind.toLowerCase() !== DATASTORE_KIND || consumers.length === 0)
|
|
55
|
+
if (!e.tags.includes(EXTERNAL_TAG) || e.tags.includes(PLATFORM_TAG))
|
|
92
56
|
continue;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
severity: "warn",
|
|
108
|
-
code: "landscape.datastore-shared",
|
|
109
|
-
subject: e.title,
|
|
110
|
-
message: `landscape: '${e.title}' is a datastore shared by ${consumers.length} services ` +
|
|
111
|
-
`(${consumers.join(", ")}) — the strongest coupling two services can have, and the hardest ` +
|
|
112
|
-
`to undo. Shared means the same DATA: if they read the same tables or keys, keep it drawn ` +
|
|
113
|
-
`and let this warning state the coupling; if they only share a host or cluster (two schemas, ` +
|
|
114
|
-
`two lock paths), that is operational blast radius — a runbook fact — and the honest model ` +
|
|
115
|
-
`is one private store per service`,
|
|
116
|
-
});
|
|
117
|
-
}
|
|
57
|
+
const consumers = shape.consumers(e.id);
|
|
58
|
+
if (consumers.length < PLATFORM_CANDIDATE_MIN_CONSUMERS)
|
|
59
|
+
continue;
|
|
60
|
+
findings.push({
|
|
61
|
+
severity: "warn",
|
|
62
|
+
code: "landscape.platform-candidate",
|
|
63
|
+
subject: e.title,
|
|
64
|
+
message: `landscape: '${e.title}' is consumed by ${consumers.length} services (${consumers.join(", ")}) ` +
|
|
65
|
+
`and is not tagged #platform — a hub like this takes one more edge per modelled service until ` +
|
|
66
|
+
`the fleet view is unreadable. Declare \`tag platform\` in the specification block and tag the ` +
|
|
67
|
+
`element (LikeC4 refuses an undeclared tag, so both steps are needed): the fleet view then ` +
|
|
68
|
+
`excludes it, and a platform view over \`include * -> element.tag = #platform\` keeps ` +
|
|
69
|
+
`"who depends on it" answerable`,
|
|
70
|
+
});
|
|
118
71
|
}
|
|
119
72
|
return findings;
|
|
120
73
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fleet-shape.js","sourceRoot":"","sources":["../../../../src/commands/validate/checks/fleet-shape.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Fleet-shape advisories over the parsed landscape: the two ways a fleet map\n * stops answering questions as it grows, caught while the map is already in\n * memory.\n *\n * Both shapes become visible at service three rather than service two.\n * Ubiquitous infrastructure — logging Kafka, an identity provider, service\n * discovery — takes one inbound edge per modelled service until the map is a\n * hairball around it; deleting the element fixes the picture and loses \"who\n * depends on the Identity Provider\", which is the question the map exists to\n * answer during an incident. Tagging keeps both: the fleet view excludes\n * `#platform`, and a platform view keeps the dependents. And a datastore drawn as a fleet-level\n * peer makes a claim its consumer count either supports or refutes: one\n * consumer means the drawing is false — the store is that service's\n * internals, not a system in its own right — while two or more mean the\n * strongest coupling two services can have, which deserves to be stated\n * rather than inferred.\n *\n * Everything here is O(relationships) over data the fleet target already\n * holds. loam still reads no code and decides no policy: the team chooses\n * what counts as platform and which data is truly shared — these warnings\n * only name the shapes that are usually wrong.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { type Elem, type LoadedDoc, type Rel } from \"../../../core/c4/likec4.js\";\nimport { type DeclaredService, type RawServiceId } from \"../../../core/kernel/ids/service.js\";\nimport { type Finding } from \"../../../core/vocabulary/report.js\";\nimport { type ServiceEntry } from \"../../../core/repo/entries.js\";\nimport { permissionsPath, servicePathsAt } from \"../../../core/repo/paths.js\";\nimport { readVocabulary } from \"../../../core/permissions/permissions.js\";\nimport { readCapabilityVocabulary } from \"../../../core/capabilities/capabilities.js\";\nimport {\n capabilityDocFindings,\n capabilityRequirementIndex,\n docMissingFindings,\n gradableCapabilityIds,\n invalidVocabularyFinding,\n unrealizedFindings,\n} from \"../../../core/capabilities/findings.js\";\nimport { useCaseRequirementClaims } from \"../../../core/usecases/capability.js\";\nimport type { ParsedView } from \"../../../core/c4/parsed/dynamic-views.js\";\nimport { capabilityRollup, usedCapabilities } from \"../../../core/capabilities/rollup.js\";\nimport { requirementUnrealizedFindings } from \"../../../core/capabilities/realizes/findings.js\";\nimport { parseRequirements } from \"../../../core/document/parse.js\";\nimport { type Requirement } from \"../../../core/document/spec.js\";\nimport { FleetContext } from \"../../../core/fleet-context.js\";\nimport { EXTERNAL_TAG, PLATFORM_TAG } from \"../../../core/vocabulary/maturity.js\";\nimport type { DocsDir } from \"../../../core/kernel/ids/dirs.js\";\n\n/**\n * Consumers at which an untagged external hub starts to warn. Three, not two:\n * two services sharing a dependency is a fact about those two services, the\n * same edge from three is a pattern about the element — and three is where\n * the shared hub starts making the map unreadable.\n */\nexport const PLATFORM_CANDIDATE_MIN_CONSUMERS = 3;\n\n/** The element kind read as a datastore, compared case-insensitively. */\nconst DATASTORE_KIND = \"database\";\n\nexport interface FleetShape {\n /** Service-LEVEL elements — the fleet map's own boxes (landscape.ts `drawn`). */\n drawn: Elem[];\n relationships: Rel[];\n /** The services/<id>/ directories that exist. */\n services: ReadonlySet<string>;\n /** The shared element→service resolver every edge join uses. */\n resolve: (id: string) => string;\n /**\n * Where a service id's directory actually sits, repo-relative and without a\n * trailing slash. Injected rather than joined: only the enumeration knows\n * whether a service is filed under a subsystem, and a fix instruction naming\n * the wrong directory is worse than one naming none.\n */\n pathOf: (id: string) => string;\n}\n\nexport function fleetShapeFindings(shape: FleetShape): Finding[] {\n const { drawn, relationships, services, resolve } = shape;\n const findings: Finding[] = [];\n\n // Distinct consumer SERVICES of an element, its nested children included: an\n // edge into `kafka.paymentEvents` is consumption of `kafka`. Persons, other\n // external systems and unresolved sources never count — a consumer is a\n // source the shared resolver files under a real services/<id>/, so two edges\n // from one service are one consumer.\n const consumersOf = (e: Elem): string[] => {\n const out = new Set<string>();\n for (const r of relationships) {\n if (r.target !== e.id && !r.target.startsWith(`${e.id}.`)) continue;\n const svc = resolve(r.source);\n if (services.has(svc)) out.add(svc);\n }\n return [...out].sort();\n };\n\n for (const e of drawn) {\n // An element that stands for a real service is the fleet itself, not its\n // shape: many consumers is what a well-used service looks like, and a\n // datastore bound to a directory is graded by the binding checks instead.\n if (e.service !== undefined || services.has(e.title)) continue;\n const consumers = consumersOf(e);\n\n if (\n e.tags.includes(EXTERNAL_TAG) &&\n !e.tags.includes(PLATFORM_TAG) &&\n consumers.length >= PLATFORM_CANDIDATE_MIN_CONSUMERS\n ) {\n findings.push({\n severity: \"warn\",\n code: \"landscape.platform-candidate\",\n subject: e.title,\n message:\n `landscape: '${e.title}' is consumed by ${consumers.length} services (${consumers.join(\", \")}) ` +\n `and is not tagged #platform — a hub like this takes one more edge per modelled service until ` +\n `the fleet view is unreadable. Declare \\`tag platform\\` in the specification block and tag the ` +\n `element (LikeC4 refuses an undeclared tag, so both steps are needed): the fleet view then ` +\n `excludes it, and a platform view over \\`include * -> element.tag = #platform\\` keeps ` +\n `\"who depends on it\" answerable`,\n });\n }\n\n if (e.kind.toLowerCase() !== DATASTORE_KIND || consumers.length === 0) continue;\n if (consumers.length === 1) {\n findings.push({\n severity: \"warn\",\n code: \"landscape.datastore-private\",\n subject: e.title,\n message:\n `landscape: '${e.title}' is a datastore with a single consumer at fleet level ` +\n `('${consumers[0]!}') — drawn as a peer it reads as a system in its own right, available to ` +\n `be depended on. Move it into ${shape.pathOf(consumers[0]!)}/model.likec4 as a nested container ` +\n `and delete it here, or add the second consumer's edge if another service really reaches ` +\n `the same data`,\n });\n } else {\n findings.push({\n severity: \"warn\",\n code: \"landscape.datastore-shared\",\n subject: e.title,\n message:\n `landscape: '${e.title}' is a datastore shared by ${consumers.length} services ` +\n `(${consumers.join(\", \")}) — the strongest coupling two services can have, and the hardest ` +\n `to undo. Shared means the same DATA: if they read the same tables or keys, keep it drawn ` +\n `and let this warning state the coupling; if they only share a host or cluster (two schemas, ` +\n `two lock paths), that is operational blast radius — a runbook fact — and the honest model ` +\n `is one private store per service`,\n });\n }\n }\n return findings;\n}\n\n/**\n * The #external element that declares it PUBLISHES this message, or null —\n * the landscape-side half of the event spine's fleet question, beside the\n * other checks that read tags off the drawn map.\n *\n * Positive evidence only: an edge carrying `metadata { publishes '<msg>' }`\n * whose source element — itself or a declared ancestor, since the edge may\n * point out of a topic nested inside the broker — is tagged #external and\n * resolves to no known service directory. The resolution guard is what keeps\n * a mis-tagged internal service reading as unproduced: a tag is cheap to\n * write, and a services/<id>/ directory outranks it.\n */\n/**\n * The authorization vocabulary graded against the fleet that uses it — the half\n * of the axis no single service can answer.\n *\n * `permissions.invalid` is an error and it is the only one reported when it\n * fires: a vocabulary nobody can read resolves nothing, so every `Requires:`\n * line in the fleet would follow it as `permissions.unknown`, and a hundred\n * findings about one broken file is a cascade, not a diagnosis.\n *\n * `permissions.unenforced` is the mirror of `api.ungoverned`, and it earns its\n * place for the same reason: a vocabulary is only worth what cites it. A\n * permission nothing requires is either a rule that was removed and left its\n * word behind, or a word nobody adopted — both are drift, and neither is\n * visible from inside the file. Warn, because the honest answer is sometimes\n * \"not modelled yet\".\n */\nexport async function permissionFindings(\n docsDir: DocsDir,\n services: ServiceEntry[],\n fleet: FleetContext | undefined,\n): Promise<Finding[]> {\n const vocabulary = await readVocabulary(permissionsPath(docsDir));\n if (!vocabulary.present) return [];\n if (vocabulary.invalid !== undefined) {\n return [\n {\n severity: \"error\",\n code: \"permissions.invalid\",\n message:\n `landscape: architecture/permissions.yaml does not read as a vocabulary — ${vocabulary.invalid}. ` +\n \"Every `Requires:` line in the fleet resolves against this file, so none of them can be graded until it parses. \" +\n \"The shape is `subjects: {<kind>: {description}}` then `permissions: {<kind>: {<name>: {description, owned_by, enforced_by}}}`.\",\n },\n ];\n }\n const used = new Set<string>();\n for (const entry of services) {\n const paths = servicePathsAt(entry.dir);\n for (const path of [paths.spec, paths.archSpec]) {\n // Existence first, and for BOTH readers: arch.spec.md is optional (most\n // of a legacy fleet has none) and spec.md is missing on a half-adopted\n // service. `FleetContext.readRequirements` throws ENOENT, which surfaces\n // as `repository-unavailable` and takes the whole `--all` run down — an\n // absent optional artifact must never be able to do that.\n if (!existsSync(path)) continue;\n const reqs = fleet === undefined ? await readRequirementsAt(path) : await fleet.readRequirements(path);\n for (const r of reqs) {\n if (r.kind !== \"REMOVED\") for (const p of r.requires) used.add(p);\n }\n }\n }\n const unenforced = [...vocabulary.byId.keys()].filter((id) => !used.has(id));\n if (unenforced.length === 0) return [];\n return [\n {\n severity: \"warn\",\n code: \"permissions.unenforced\",\n message: `landscape: ${unenforced.length} declared permission(s) that no requirement's \\`Requires:\\` line names — write the requirement that gates on each, or drop the declaration`,\n details: unenforced,\n },\n ];\n}\n\n/** The uncached read, for the `--service` path where no fleet context exists. */\nasync function readRequirementsAt(path: string): Promise<Requirement[]> {\n if (!existsSync(path)) return [];\n return parseRequirements(await readFile(path, \"utf8\"));\n}\n\n/**\n * The capability vocabulary graded against the fleet that realizes it — the\n * axis's fleet half, beside permissionFindings and shaped by the same two\n * verdicts. `capability.invalid` is a `validate --all` run's ONE finding about\n * an unreadable file — fleet scope, so single-target runs stay silent about\n * it — and it suppresses the rest of the family (the unknown\n * grades at every service target return [] for the same vocabulary — one\n * breach, one finding, never a cascade). `capability.unrealized` is one warn\n * PER declared-but-unnamed capability, subject = the id, because the roadmap's\n * criterion is 'one warning per capability, not one per service'. Holding\n * NEITHER `architecture/capabilities.yaml` nor `capabilities/` is total\n * silence: the fleet's own files are this axis's opt-in, either of them.\n *\n * The used set comes through capabilityRollup with the fleet's own cached\n * reader, so under --all the walk re-parses nothing the service targets have\n * not already paid for — and the rollup, the warn and `loam list capabilities`\n * can never disagree about what \"realized\" means.\n *\n * The AUTHORED documents are graded here too, and they ride this function\n * rather than a target of their own for `viewsStaleFindings`' reason one tree\n * over: a capability belongs to no service, so only the fleet run has the whole\n * vocabulary and the enumerated tree in view at once. Their reads are NOT\n * wrapped, deliberately — a `capabilities/<id>/spec.md` that is not UTF-8 takes\n * the run down exactly as a `services/<id>/spec.md` does, because a document\n * loam cannot decode is a refusal everywhere else in the product and a quiet\n * \"zero requirements\" here would grade a whole capability green on bytes\n * nobody read.\n */\nexport async function capabilityFleetFindings(\n docsDir: DocsDir,\n services: ServiceEntry[],\n fleet: FleetContext | undefined,\n flows: readonly ParsedView[] | null,\n): Promise<Finding[]> {\n const vocab = fleet === undefined ? await readCapabilityVocabulary(docsDir) : await fleet.capabilities(docsDir);\n if (!vocab.present) return [];\n const read = fleet === undefined ? readRequirementsAt : (p: string) => fleet.readRequirements(p);\n const authored: Finding[] = docMissingFindings(vocab.tree);\n for (const doc of vocab.tree.docs) authored.push(...capabilityDocFindings(await read(doc.spec), doc.id));\n\n const invalid = invalidVocabularyFinding(vocab);\n if (invalid !== null) return [invalid, ...authored];\n // A USE CASE IS THE OTHER WAY TO KEEP A PROMISE, and it is the only carrier a\n // cross-service criterion has: \"I enter a login and a password and I am in\"\n // belongs to no single service's spec, so a fleet could satisfy it perfectly\n // through a `#cap-`/`#req-` tagged flow and still be told nobody realizes it.\n //\n // The claims go INTO the rollup rather than beside it, so both unrealized\n // grades and `loam list capabilities` read one set of rows. `flows === null`\n // means loam could not READ the flows (no preload, or an `architecture/` that\n // did not parse); the key is then OMITTED rather than empty, and every\n // `keptBy` below is `undefined` — the three-state contract `rollup.ts` states.\n const index = await capabilityRequirementIndex(vocab, read);\n const keptByFlows =\n flows === null\n ? undefined\n : useCaseRequirementClaims(flows, gradableCapabilityIds(vocab), (c) => index.byCapability.get(c));\n const rows = await capabilityRollup({ services, vocab, read, ...(keptByFlows === undefined ? {} : { keptByFlows }) });\n\n // The two unrealized grades read the SAME rollup, which is what keeps them\n // from disagreeing about the word: a capability is unrealized when nothing\n // names it by any join, and one of its requirements is unrealized when nothing\n // keeps that promise. Both are warnings, and the second is the one that\n // survives a healthy-looking row — a capability with four requirements and\n // three kept reports nothing at all through the first.\n //\n // The suspension falls out of the DATA rather than a branch: `keptBy` absent\n // means nobody looked, and a promise nobody looked for is not a promise\n // reported unkept.\n const unrealizedRequirements = rows.flatMap((row) =>\n (row.requirements ?? [])\n .filter((req) => req.realizedBy.length === 0 && req.keptBy?.length === 0)\n .map((req) => ({ capability: row.id, id: req.id, name: req.name })),\n );\n return [\n ...authored,\n ...unrealizedFindings(vocab, usedCapabilities(rows)),\n ...requirementUnrealizedFindings(unrealizedRequirements),\n ];\n}\n\nexport function externalProducerOf(\n message: string,\n land: LoadedDoc | null,\n landSvcOf: ((id: string) => DeclaredService) | null,\n known: ReadonlySet<RawServiceId>,\n): string | null {\n if (land === null || land.errors.length > 0 || landSvcOf === null) return null;\n const knownIds: ReadonlySet<string> = known;\n const byId = new Map(land.elements.map((e) => [e.id, e]));\n for (const r of land.relationships) {\n if (r.publishes !== message) continue;\n if (knownIds.has(landSvcOf(r.source))) continue;\n for (let id = r.source; ; ) {\n const e = byId.get(id);\n if (e !== undefined && e.tags.includes(EXTERNAL_TAG)) return e.title;\n const dot = id.lastIndexOf(\".\");\n if (dot === -1) break;\n id = id.slice(0, dot);\n }\n }\n return null;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"fleet-shape.js","sourceRoot":"","sources":["../../../../src/commands/validate/checks/fleet-shape.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Fleet-shape advisories over the parsed landscape: the way a fleet map stops\n * answering questions as it grows, caught while the map is already in memory.\n *\n * The shape becomes visible at service three rather than service two.\n * Ubiquitous infrastructure — logging Kafka, an identity provider, service\n * discovery — takes one inbound edge per modelled service until the map is a\n * hairball around it; deleting the element fixes the picture and loses \"who\n * depends on the Identity Provider\", which is the question the map exists to\n * answer during an incident. Tagging keeps both: the fleet view excludes\n * `#platform`, and a platform view keeps the dependents.\n *\n * The DATASTORE half of this pair moved to `../fleet/map/consumers.ts` when its\n * consumer count stopped being a fact about the map alone: a private store now\n * lives nested inside its owner's element, and the services reaching it are\n * attested in their own models (E3/R1). The census both checks read is built\n * there and handed in here as `consumers`, so a hub and a store can never\n * disagree about who consumes what.\n *\n * loam still reads no code and decides no policy: the team chooses what counts\n * as platform — this warning only names a shape that is usually wrong.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { type Elem, type LoadedDoc } from \"../../../core/c4/likec4.js\";\nimport { type DeclaredService, type RawServiceId } from \"../../../core/kernel/ids/service.js\";\nimport { type Finding } from \"../../../core/vocabulary/report.js\";\nimport { type ServiceEntry } from \"../../../core/repo/entries.js\";\nimport { permissionsPath, servicePathsAt } from \"../../../core/repo/paths.js\";\nimport { readVocabulary } from \"../../../core/permissions/permissions.js\";\nimport { readCapabilityVocabulary } from \"../../../core/capabilities/capabilities.js\";\nimport {\n capabilityDocFindings,\n capabilityRequirementIndex,\n docMissingFindings,\n gradableCapabilityIds,\n invalidVocabularyFinding,\n unrealizedFindings,\n} from \"../../../core/capabilities/findings.js\";\nimport { useCaseRequirementClaims } from \"../../../core/usecases/capability.js\";\nimport type { ParsedView } from \"../../../core/c4/parsed/dynamic-views.js\";\nimport { capabilityRollup, usedCapabilities } from \"../../../core/capabilities/rollup.js\";\nimport { requirementUnrealizedFindings } from \"../../../core/capabilities/realizes/findings.js\";\nimport { parseRequirements } from \"../../../core/document/parse.js\";\nimport { type Requirement } from \"../../../core/document/spec.js\";\nimport { FleetContext } from \"../../../core/fleet-context.js\";\nimport { EXTERNAL_TAG, PLATFORM_TAG } from \"../../../core/vocabulary/maturity.js\";\nimport type { DocsDir } from \"../../../core/kernel/ids/dirs.js\";\n\n/**\n * Consumers at which an untagged external hub starts to warn. Three, not two:\n * two services sharing a dependency is a fact about those two services, the\n * same edge from three is a pattern about the element — and three is where\n * the shared hub starts making the map unreadable.\n */\nexport const PLATFORM_CANDIDATE_MIN_CONSUMERS = 3;\n\nexport interface FleetShape {\n /** Service-LEVEL elements — the fleet map's own boxes (landscape.ts `drawn`). */\n drawn: Elem[];\n /** The services/<id>/ directories that exist. */\n services: ReadonlySet<string>;\n /**\n * Distinct consumer SERVICES of an element, its nested children included —\n * `../fleet/map/consumers.ts` owns the derivation and both checks that read a\n * consumer count share this one instance of it.\n */\n consumers: (elementId: string) => string[];\n}\n\nexport function fleetShapeFindings(shape: FleetShape): Finding[] {\n const { drawn, services } = shape;\n const findings: Finding[] = [];\n\n for (const e of drawn) {\n // An element that stands for a real service is the fleet itself, not its\n // shape: many consumers is what a well-used service looks like.\n if (e.service !== undefined || services.has(e.title)) continue;\n if (!e.tags.includes(EXTERNAL_TAG) || e.tags.includes(PLATFORM_TAG)) continue;\n const consumers = shape.consumers(e.id);\n if (consumers.length < PLATFORM_CANDIDATE_MIN_CONSUMERS) continue;\n findings.push({\n severity: \"warn\",\n code: \"landscape.platform-candidate\",\n subject: e.title,\n message:\n `landscape: '${e.title}' is consumed by ${consumers.length} services (${consumers.join(\", \")}) ` +\n `and is not tagged #platform — a hub like this takes one more edge per modelled service until ` +\n `the fleet view is unreadable. Declare \\`tag platform\\` in the specification block and tag the ` +\n `element (LikeC4 refuses an undeclared tag, so both steps are needed): the fleet view then ` +\n `excludes it, and a platform view over \\`include * -> element.tag = #platform\\` keeps ` +\n `\"who depends on it\" answerable`,\n });\n }\n return findings;\n}\n\n/**\n * The #external element that declares it PUBLISHES this message, or null —\n * the landscape-side half of the event spine's fleet question, beside the\n * other checks that read tags off the drawn map.\n *\n * Positive evidence only: an edge carrying `metadata { publishes '<msg>' }`\n * whose source element — itself or a declared ancestor, since the edge may\n * point out of a topic nested inside the broker — is tagged #external and\n * resolves to no known service directory. The resolution guard is what keeps\n * a mis-tagged internal service reading as unproduced: a tag is cheap to\n * write, and a services/<id>/ directory outranks it.\n */\n/**\n * The authorization vocabulary graded against the fleet that uses it — the half\n * of the axis no single service can answer.\n *\n * `permissions.invalid` is an error and it is the only one reported when it\n * fires: a vocabulary nobody can read resolves nothing, so every `Requires:`\n * line in the fleet would follow it as `permissions.unknown`, and a hundred\n * findings about one broken file is a cascade, not a diagnosis.\n *\n * `permissions.unenforced` is the mirror of `api.ungoverned`, and it earns its\n * place for the same reason: a vocabulary is only worth what cites it. A\n * permission nothing requires is either a rule that was removed and left its\n * word behind, or a word nobody adopted — both are drift, and neither is\n * visible from inside the file. Warn, because the honest answer is sometimes\n * \"not modelled yet\".\n */\nexport async function permissionFindings(\n docsDir: DocsDir,\n services: ServiceEntry[],\n fleet: FleetContext | undefined,\n): Promise<Finding[]> {\n const vocabulary = await readVocabulary(permissionsPath(docsDir));\n if (!vocabulary.present) return [];\n if (vocabulary.invalid !== undefined) {\n return [\n {\n severity: \"error\",\n code: \"permissions.invalid\",\n message:\n `landscape: architecture/permissions.yaml does not read as a vocabulary — ${vocabulary.invalid}. ` +\n \"Every `Requires:` line in the fleet resolves against this file, so none of them can be graded until it parses. \" +\n \"The shape is `subjects: {<kind>: {description}}` then `permissions: {<kind>: {<name>: {description, owned_by, enforced_by}}}`.\",\n },\n ];\n }\n const used = new Set<string>();\n for (const entry of services) {\n const paths = servicePathsAt(entry.dir);\n for (const path of [paths.spec, paths.archSpec]) {\n // Existence first, and for BOTH readers: arch.spec.md is optional (most\n // of a legacy fleet has none) and spec.md is missing on a half-adopted\n // service. `FleetContext.readRequirements` throws ENOENT, which surfaces\n // as `repository-unavailable` and takes the whole `--all` run down — an\n // absent optional artifact must never be able to do that.\n if (!existsSync(path)) continue;\n const reqs = fleet === undefined ? await readRequirementsAt(path) : await fleet.readRequirements(path);\n for (const r of reqs) {\n if (r.kind !== \"REMOVED\") for (const p of r.requires) used.add(p);\n }\n }\n }\n const unenforced = [...vocabulary.byId.keys()].filter((id) => !used.has(id));\n if (unenforced.length === 0) return [];\n return [\n {\n severity: \"warn\",\n code: \"permissions.unenforced\",\n message: `landscape: ${unenforced.length} declared permission(s) that no requirement's \\`Requires:\\` line names — write the requirement that gates on each, or drop the declaration`,\n details: unenforced,\n },\n ];\n}\n\n/** The uncached read, for the `--service` path where no fleet context exists. */\nasync function readRequirementsAt(path: string): Promise<Requirement[]> {\n if (!existsSync(path)) return [];\n return parseRequirements(await readFile(path, \"utf8\"));\n}\n\n/**\n * The capability vocabulary graded against the fleet that realizes it — the\n * axis's fleet half, beside permissionFindings and shaped by the same two\n * verdicts. `capability.invalid` is a `validate --all` run's ONE finding about\n * an unreadable file — fleet scope, so single-target runs stay silent about\n * it — and it suppresses the rest of the family (the unknown\n * grades at every service target return [] for the same vocabulary — one\n * breach, one finding, never a cascade). `capability.unrealized` is one warn\n * PER declared-but-unnamed capability, subject = the id, because the roadmap's\n * criterion is 'one warning per capability, not one per service'. Holding\n * NEITHER `architecture/capabilities.yaml` nor `capabilities/` is total\n * silence: the fleet's own files are this axis's opt-in, either of them.\n *\n * The used set comes through capabilityRollup with the fleet's own cached\n * reader, so under --all the walk re-parses nothing the service targets have\n * not already paid for — and the rollup, the warn and `loam list capabilities`\n * can never disagree about what \"realized\" means.\n *\n * The AUTHORED documents are graded here too, and they ride this function\n * rather than a target of their own for `viewsStaleFindings`' reason one tree\n * over: a capability belongs to no service, so only the fleet run has the whole\n * vocabulary and the enumerated tree in view at once. Their reads are NOT\n * wrapped, deliberately — a `capabilities/<id>/spec.md` that is not UTF-8 takes\n * the run down exactly as a `services/<id>/spec.md` does, because a document\n * loam cannot decode is a refusal everywhere else in the product and a quiet\n * \"zero requirements\" here would grade a whole capability green on bytes\n * nobody read.\n */\nexport async function capabilityFleetFindings(\n docsDir: DocsDir,\n services: ServiceEntry[],\n fleet: FleetContext | undefined,\n flows: readonly ParsedView[] | null,\n): Promise<Finding[]> {\n const vocab = fleet === undefined ? await readCapabilityVocabulary(docsDir) : await fleet.capabilities(docsDir);\n if (!vocab.present) return [];\n const read = fleet === undefined ? readRequirementsAt : (p: string) => fleet.readRequirements(p);\n const authored: Finding[] = docMissingFindings(vocab.tree);\n for (const doc of vocab.tree.docs) authored.push(...capabilityDocFindings(await read(doc.spec), doc.id));\n\n const invalid = invalidVocabularyFinding(vocab);\n if (invalid !== null) return [invalid, ...authored];\n // A USE CASE IS THE OTHER WAY TO KEEP A PROMISE, and it is the only carrier a\n // cross-service criterion has: \"I enter a login and a password and I am in\"\n // belongs to no single service's spec, so a fleet could satisfy it perfectly\n // through a `#cap-`/`#req-` tagged flow and still be told nobody realizes it.\n //\n // The claims go INTO the rollup rather than beside it, so both unrealized\n // grades and `loam list capabilities` read one set of rows. `flows === null`\n // means loam could not READ the flows (no preload, or an `architecture/` that\n // did not parse); the key is then OMITTED rather than empty, and every\n // `keptBy` below is `undefined` — the three-state contract `rollup.ts` states.\n const index = await capabilityRequirementIndex(vocab, read);\n const keptByFlows =\n flows === null\n ? undefined\n : useCaseRequirementClaims(flows, gradableCapabilityIds(vocab), (c) => index.byCapability.get(c));\n const rows = await capabilityRollup({ services, vocab, read, ...(keptByFlows === undefined ? {} : { keptByFlows }) });\n\n // The two unrealized grades read the SAME rollup, which is what keeps them\n // from disagreeing about the word: a capability is unrealized when nothing\n // names it by any join, and one of its requirements is unrealized when nothing\n // keeps that promise. Both are warnings, and the second is the one that\n // survives a healthy-looking row — a capability with four requirements and\n // three kept reports nothing at all through the first.\n //\n // The suspension falls out of the DATA rather than a branch: `keptBy` absent\n // means nobody looked, and a promise nobody looked for is not a promise\n // reported unkept.\n const unrealizedRequirements = rows.flatMap((row) =>\n (row.requirements ?? [])\n .filter((req) => req.realizedBy.length === 0 && req.keptBy?.length === 0)\n .map((req) => ({ capability: row.id, id: req.id, name: req.name })),\n );\n return [\n ...authored,\n ...unrealizedFindings(vocab, usedCapabilities(rows)),\n ...requirementUnrealizedFindings(unrealizedRequirements),\n ];\n}\n\nexport function externalProducerOf(\n message: string,\n land: LoadedDoc | null,\n landSvcOf: ((id: string) => DeclaredService) | null,\n known: ReadonlySet<RawServiceId>,\n): string | null {\n if (land === null || land.errors.length > 0 || landSvcOf === null) return null;\n const knownIds: ReadonlySet<string> = known;\n const byId = new Map(land.elements.map((e) => [e.id, e]));\n for (const r of land.relationships) {\n if (r.publishes !== message) continue;\n if (knownIds.has(landSvcOf(r.source))) continue;\n for (let id = r.source; ; ) {\n const e = byId.get(id);\n if (e !== undefined && e.tags.includes(EXTERNAL_TAG)) return e.title;\n const dot = id.lastIndexOf(\".\");\n if (dot === -1) break;\n id = id.slice(0, dot);\n }\n }\n return null;\n}\n"]}
|
|
@@ -10,11 +10,13 @@
|
|
|
10
10
|
* concerned.
|
|
11
11
|
*
|
|
12
12
|
* Resolution is against the SERVICE'S OWN model — deliberately never the
|
|
13
|
-
* landscape. A
|
|
14
|
-
*
|
|
15
|
-
* landscape
|
|
16
|
-
*
|
|
17
|
-
*
|
|
13
|
+
* landscape. A store one service owns may be nested inside that service's
|
|
14
|
+
* element or drawn as a fleet-level peer, whichever the team chose (loam grades
|
|
15
|
+
* neither: `landscape.datastore-private`, which used to push it into the first,
|
|
16
|
+
* was retired 2026-09-07). Only the nested placement puts it in the landscape's
|
|
17
|
+
* interior at all, so the landscape is not a set this check could rely on, while
|
|
18
|
+
* the model carries everything the service touches — and that is the set the
|
|
19
|
+
* on-call file has to agree with. An id resolves by the same names every other join
|
|
18
20
|
* uses: the element's likec4 id, its `metadata { service }` binding, or its
|
|
19
21
|
* title. Exact match, no case folding — a fold would be a permanent
|
|
20
22
|
* equivalence semantic no other loam join has, able to silently join two
|
|
@@ -45,7 +47,8 @@ export function healthDependencyFindings(deps) {
|
|
|
45
47
|
message: `${service}: health.yaml declares dependency '${dep}' but nothing in ` +
|
|
46
48
|
`${modelPath} answers to that name — either a dependency nobody ` +
|
|
47
49
|
`modelled, or one that no longer exists. Model it (an external system as its own element ` +
|
|
48
|
-
`with the edge this service actually has; a
|
|
50
|
+
`with the edge this service actually has; a store this service owns as a nested container ` +
|
|
51
|
+
`or as its own element, whichever the map already does), or ` +
|
|
49
52
|
`delete the entry` +
|
|
50
53
|
(close.length > 0 ? `. Did you mean: ${close.join(", ")}?` : ""),
|
|
51
54
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"health-deps.js","sourceRoot":"","sources":["../../../../src/commands/validate/checks/health-deps.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"health-deps.js","sourceRoot":"","sources":["../../../../src/commands/validate/checks/health-deps.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACpD,OAAO,EAAa,MAAM,4BAA4B,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAC;AACrE,OAAO,EAAgB,MAAM,oCAAoC,CAAC;AAkBlE,MAAM,UAAU,wBAAwB,CAAC,IAAgB;IACvD,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC;IAC5D,yEAAyE;IACzE,4EAA4E;IAC5E,gCAAgC;IAChC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACrC,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,KAAK,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;QAC/B,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,GAAG,IAAI,cAAc,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC;YAAE,SAAS;QAC9E,MAAM,IAAI,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,cAAc,CAAC,CAAC,CAAW,CAAC,CAAC,CAAC,CAAC,CAAC;QACxF,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAClC,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ,EAAE,MAAM;YAChB,IAAI,EAAE,8BAA8B;YACpC,OAAO,EAAE,OAAO;YAChB,OAAO,EACL,GAAG,OAAO,sCAAsC,GAAG,mBAAmB;gBACtE,GAAG,SAAS,qDAAqD;gBACjE,0FAA0F;gBAC1F,2FAA2F;gBAC3F,6DAA6D;gBAC7D,kBAAkB;gBAClB,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,mBAAmB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;SACnE,CAAC,CAAC;IACL,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC","sourcesContent":["/**\n * The health.yaml → model reconciliation: every id the `dependencies:` block\n * declares must be something this service's OWN model answers to.\n *\n * Without this reconciliation, a disabled optional service registry can leave\n * the model, landscape, arch spec and runbook corrected while health.yaml still\n * declares `serviceRegistry` as a `critical: startup` dependency under a green\n * validator — in the file an on-call engineer reaches for first. Its\n * dependencies block was otherwise free text as far as validation was\n * concerned.\n *\n * Resolution is against the SERVICE'S OWN model — deliberately never the\n * landscape. A store one service owns may be nested inside that service's\n * element or drawn as a fleet-level peer, whichever the team chose (loam grades\n * neither: `landscape.datastore-private`, which used to push it into the first,\n * was retired 2026-09-07). Only the nested placement puts it in the landscape's\n * interior at all, so the landscape is not a set this check could rely on, while\n * the model carries everything the service touches — and that is the set the\n * on-call file has to agree with. An id resolves by the same names every other join\n * uses: the element's likec4 id, its `metadata { service }` binding, or its\n * title. Exact match, no case folding — a fold would be a permanent\n * equivalence semantic no other loam join has, able to silently join two\n * genuinely different names; the did-you-mean hint bridges the typo case\n * instead.\n */\nimport { closeIds } from \"../../../core/c4/arch.js\";\nimport { type Elem } from \"../../../core/c4/likec4.js\";\nimport { elementService } from \"../../../core/c4/resolve/service.js\";\nimport { type Finding } from \"../../../core/vocabulary/report.js\";\n\nexport interface HealthDeps {\n service: string;\n /**\n * The resolved path of the service's own `model.likec4` — the file this\n * check reads and the one its message names. Handed in rather than joined\n * from the id: `services/<id>/model.likec4` is right only for an unfiled\n * service, and every sibling finding in this command prints the resolved\n * path of the file it means.\n */\n modelPath: string;\n /** `dependencies:` ids, already muted by the caller when health.yaml is unreadable. */\n dependencies: string[];\n /** The service's own parsed model — empty when absent or invalid, which mutes the check. */\n elements: Elem[];\n}\n\nexport function healthDependencyFindings(deps: HealthDeps): Finding[] {\n const { service, dependencies, elements, modelPath } = deps;\n // An absent or unparsable model already has its own finding, and grading\n // health.yaml against a model nobody could read would manufacture warns out\n // of the wrong file's breakage.\n if (elements.length === 0) return [];\n const findings: Finding[] = [];\n for (const dep of dependencies) {\n if (elements.some((e) => e.id === dep || elementService(e) === dep)) continue;\n const pool = [...new Set(elements.flatMap((e) => [e.id, elementService(e) as string]))];\n const close = closeIds(dep, pool);\n findings.push({\n severity: \"warn\",\n code: \"health.dependency-unmodelled\",\n subject: service,\n message:\n `${service}: health.yaml declares dependency '${dep}' but nothing in ` +\n `${modelPath} answers to that name — either a dependency nobody ` +\n `modelled, or one that no longer exists. Model it (an external system as its own element ` +\n `with the edge this service actually has; a store this service owns as a nested container ` +\n `or as its own element, whichever the map already does), or ` +\n `delete the entry` +\n (close.length > 0 ? `. Did you mean: ${close.join(\", \")}?` : \"\"),\n });\n }\n return findings;\n}\n"]}
|
|
@@ -4,28 +4,13 @@
|
|
|
4
4
|
* A leaf module rather than a paragraph beside its first caller, because the
|
|
5
5
|
* checks are spread across packages and each of these is read from more than
|
|
6
6
|
* one of them: the dispatcher's rollup and the service target's sources check
|
|
7
|
-
* both name one finding code
|
|
8
|
-
* the same way. ACTOR_KINDS and EXTERNAL_TAG lived here too, until they
|
|
7
|
+
* both name one finding code. ACTOR_KINDS and EXTERNAL_TAG lived here too, until they
|
|
9
8
|
* reached a third copy (`core/gate/partners.ts`, `core/verify/checklist.ts`);
|
|
10
9
|
* their shared spelling is now `core/vocabulary/maturity.ts`, beside the
|
|
11
|
-
* landscape evidence that asks the same questions.
|
|
10
|
+
* landscape evidence that asks the same questions. The parser-diagnostic line
|
|
11
|
+
* (`errorText`) went the same way, to `core/c4/likec4.ts` beside the type it
|
|
12
|
+
* reads, once four hand-rolled copies had disagreed about the line base.
|
|
12
13
|
*/
|
|
13
|
-
import {} from "../../../core/c4/likec4.js";
|
|
14
14
|
/** The one code the rollup line counts; spelled once so the two cannot drift. */
|
|
15
15
|
export const UNVERIFIABLE = "sources.unverifiable-from-here";
|
|
16
|
-
/**
|
|
17
|
-
* One parser diagnostic as a finding's evidence line: `L12:` when the parser
|
|
18
|
-
* knew a line, because that lets a reader open the file at the fault instead of
|
|
19
|
-
* searching for it.
|
|
20
|
-
*
|
|
21
|
-
* Three other modules hand-roll the same ternary (`commands/show.ts`,
|
|
22
|
-
* `commands/delta.ts`, `core/c4/splice/landscape-merge.ts`). All four belong
|
|
23
|
-
* beside `LikeC4Error` in `core/c4/likec4.ts`. That used to be blocked by the
|
|
24
|
-
* file limit — nine lines of headroom — and is not any more: moving the resolver
|
|
25
|
-
* out to `core/c4/resolve/service.ts` left the loader around 100 lines under.
|
|
26
|
-
* The consolidation is now unblocked and simply owed.
|
|
27
|
-
*/
|
|
28
|
-
export function errorText(e) {
|
|
29
|
-
return typeof e.line === "number" ? `L${e.line}: ${e.message}` : e.message;
|
|
30
|
-
}
|
|
31
16
|
//# sourceMappingURL=vocabulary.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vocabulary.js","sourceRoot":"","sources":["../../../../src/commands/validate/checks/vocabulary.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"vocabulary.js","sourceRoot":"","sources":["../../../../src/commands/validate/checks/vocabulary.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,iFAAiF;AACjF,MAAM,CAAC,MAAM,YAAY,GAAG,gCAAgC,CAAC","sourcesContent":["/**\n * The handful of facts every validate check speaks, and none of them owns.\n *\n * A leaf module rather than a paragraph beside its first caller, because the\n * checks are spread across packages and each of these is read from more than\n * one of them: the dispatcher's rollup and the service target's sources check\n * both name one finding code. ACTOR_KINDS and EXTERNAL_TAG lived here too, until they\n * reached a third copy (`core/gate/partners.ts`, `core/verify/checklist.ts`);\n * their shared spelling is now `core/vocabulary/maturity.ts`, beside the\n * landscape evidence that asks the same questions. The parser-diagnostic line\n * (`errorText`) went the same way, to `core/c4/likec4.ts` beside the type it\n * reads, once four hand-rolled copies had disagreed about the line base.\n */\n\n/** The one code the rollup line counts; spelled once so the two cannot drift. */\nexport const UNVERIFIABLE = \"sources.unverifiable-from-here\";\n"]}
|
|
@@ -33,7 +33,7 @@ import { documentConflictFinding } from "../../core/conflict-markers.js";
|
|
|
33
33
|
import { featureCapabilityDeltas } from "../../core/capabilities/delta/tree.js";
|
|
34
34
|
import { docMissingFindings } from "../../core/capabilities/findings.js";
|
|
35
35
|
import { FleetContext } from "../../core/fleet-context.js";
|
|
36
|
-
import { errorText } from "
|
|
36
|
+
import { errorText } from "../../core/c4/likec4.js";
|
|
37
37
|
import { coverageFinding, repeatedListLineFindings } from "./checks/requirements.js";
|
|
38
38
|
import { deltaArchCoverage } from "./arch-coverage.js";
|
|
39
39
|
import { featureLinkFindings } from "./links/corpus.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"feature.js","sourceRoot":"","sources":["../../../src/commands/validate/feature.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,QAAQ,EAAuC,MAAM,yBAAyB,CAAC;AACxF,OAAO,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AAClE,OAAO,EAAwB,MAAM,kCAAkC,CAAC;AACxE,OAAO,EAAqB,MAAM,4BAA4B,CAAC;AAC/D,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC1E,OAAO,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AACzE,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5E,OAAO,EAAmC,MAAM,iCAAiC,CAAC;AAClF,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,EACL,qBAAqB,EACrB,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAoB,MAAM,6BAA6B,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AACrE,OAAO,EACL,0BAA0B,EAC1B,0BAA0B,GAC3B,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAC9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AACtE,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AACzE,OAAO,EAAE,uBAAuB,EAAE,MAAM,uCAAuC,CAAC;AAChF,OAAO,EAAE,kBAAkB,EAAE,MAAM,qCAAqC,CAAC;AACzE,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,eAAe,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AACrF,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAGxD,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,OAAgB,EAChB,OAAqB,EACrB,aAAgC,EAChC,KAAoB;IAEpB,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC;IAC/B,MAAM,SAAS,GAAG,OAAO,CAAC,EAAE,CAAC;IAE7B,wEAAwE;IACxE,6EAA6E;IAC7E,6EAA6E;IAC7E,IAAI,SAAS,GAAW,EAAE,CAAC;IAC3B,IAAI,UAAU,GAAU,EAAE,CAAC;IAC3B,IAAI,QAAQ,GAAW,EAAE,CAAC;IAC1B,IAAI,SAAS,GAAU,EAAE,CAAC;IAC1B,IAAI,QAA+B,CAAC;IACpC,MAAM,SAAS,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC,KAAK,CAAC;IACjD,IAAI,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC1B,MAAM,GAAG,GAAG,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;QAChG,QAAQ,GAAG,GAAG,CAAC;QACf,IAAI,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC1B,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,OAAO;gBACjB,IAAI,EAAE,eAAe;gBACrB,OAAO,EAAE,oBAAoB,GAAG,CAAC,MAAM,CAAC,MAAM,WAAW;gBACzD,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC;aACnC,CAAC,CAAC;QACL,CAAC;aAAM,CAAC;YACN,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC;YACxB,SAAS,GAAG,GAAG,CAAC,aAAa,CAAC;YAC9B,SAAS,GAAG,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC;YACnE,UAAU,GAAG,GAAG,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC;YACzE,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,IAAI;gBACd,IAAI,EAAE,aAAa;gBACnB,OAAO,EAAE,uBAAuB,GAAG,CAAC,QAAQ,CAAC,MAAM,eAAe,GAAG,CAAC,aAAa,CAAC,MAAM,iBAAiB;aAC5G,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,iBAAiB,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC;IAEnE,6EAA6E;IAC7E,4EAA4E;IAC5E,iEAAiE;IACjE,0EAA0E;IAC1E,6EAA6E;IAC7E,8EAA8E;IAC9E,8EAA8E;IAC9E,uDAAuD;IACvD,EAAE;IACF,uEAAuE;IACvE,6EAA6E;IAC7E,wEAAwE;IACxE,0EAA0E;IAC1E,6EAA6E;IAC7E,2EAA2E;IAC3E,4EAA4E;IAC5E,0CAA0C;IAC1C,MAAM,eAAe,GAAG,MAAM,mBAAmB,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;IACrE,MAAM,UAAU,GAAwB,IAAI,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC;IAC/E,MAAM,aAAa,GAAG,QAAQ,KAAK,SAAS,IAAI,QAAQ,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC;IAC7E,MAAM,UAAU,GAAG,IAAI,GAAG,CAAS,MAAM,oBAAoB,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC;IAC/E,MAAM,eAAe,GAAG,aAAa;QACnC,CAAC,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC/E,CAAC,CAAC,EAAE,CAAC;IACP,0EAA0E;IAC1E,kDAAkD;IAClD,MAAM,OAAO,GACX,eAAe,CAAC,MAAM,GAAG,CAAC,IAAI,aAAa,CAAC,OAAO,CAAC,CAAC,IAAI,KAAK,IAAI;QAChE,CAAC,CAAC,CAAC,MAAM,YAAY,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACvD,CAAC,CAAC,EAAE,CAAC;IACT,8EAA8E;IAC9E,8DAA8D;IAC9D,KAAK,MAAM,GAAG,IAAI,eAAe;QAAE,QAAQ,CAAC,IAAI,CAAC,0BAA0B,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC;IAE3F,+EAA+E;IAC/E,+EAA+E;IAC/E,0EAA0E;IAC1E,+EAA+E;IAC/E,+EAA+E;IAC/E,6EAA6E;IAC7E,+DAA+D;IAC/D,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,0BAA0B,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IAExE,8EAA8E;IAC9E,oEAAoE;IACpE,IAAI,YAAY,GAAG,EAAE,CAAC;IACtB,MAAM,UAAU,GAA6D,EAAE,CAAC;IAChF,KAAK,MAAM,GAAG,IAAI,eAAe,EAAE,CAAC;QAClC,MAAM,CAAC,GAAG,gBAAgB,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;QAC5C,IAAI,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;YACvB,MAAM,GAAG,GAAG,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,QAAQ,CAAC,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;YAChG,YAAY,IAAI,IAAI,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC;YACzC,MAAM,IAAI,GAAG,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,gBAAgB,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;YACjG,sEAAsE;YACtE,wEAAwE;YACxE,qEAAqE;YACrE,oEAAoE;YACpE,yBAAyB;YACzB,MAAM,QAAQ,GAAG,uBAAuB,CAAC,GAAG,GAAG,WAAW,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;YACtE,IAAI,QAAQ,KAAK,IAAI;gBAAE,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAC/C,QAAQ,CAAC,IAAI,CAAC,EAAE,GAAG,eAAe,CAAC,GAAG,GAAG,gBAAgB,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC;YAClF,QAAQ,CAAC,IAAI,CAAC,GAAG,gBAAgB,CAAC,GAAG,GAAG,gBAAgB,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC;YACtE,QAAQ,CAAC,IAAI,CAAC,GAAG,qBAAqB,CAAC,GAAG,GAAG,gBAAgB,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC;YAC3E,QAAQ,CAAC,IAAI,CAAC,GAAG,gBAAgB,CAAC,GAAG,GAAG,gBAAgB,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC;YACtE,yEAAyE;YACzE,yEAAyE;YACzE,QAAQ,CAAC,IAAI,CAAC,GAAG,wBAAwB,CAAC,IAAI,EAAE,GAAG,GAAG,WAAW,EAAE,GAAG,CAAC,CAAC,CAAC;QAC3E,CAAC;QACD,IAAI,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC3B,MAAM,GAAG,GAAG,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,QAAQ,CAAC,CAAC,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;YACxG,YAAY,IAAI,IAAI,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC;YACzC,MAAM,IAAI,GAAG,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,gBAAgB,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;YACrG,UAAU,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC;YACxC,MAAM,QAAQ,GAAG,uBAAuB,CAAC,GAAG,GAAG,gBAAgB,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;YAC3E,IAAI,QAAQ,KAAK,IAAI;gBAAE,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAC/C,QAAQ,CAAC,IAAI,CAAC,EAAE,GAAG,eAAe,CAAC,GAAG,GAAG,qBAAqB,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC;YACvF,QAAQ,CAAC,IAAI,CAAC,GAAG,gBAAgB,CAAC,GAAG,GAAG,qBAAqB,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC;YAC3E,QAAQ,CAAC,IAAI,CAAC,GAAG,qBAAqB,CAAC,GAAG,GAAG,qBAAqB,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC;YAChF,QAAQ,CAAC,IAAI,CAAC,GAAG,gBAAgB,CAAC,GAAG,GAAG,qBAAqB,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC;YAC3E,QAAQ,CAAC,IAAI,CAAC,GAAG,wBAAwB,CAAC,IAAI,EAAE,GAAG,GAAG,gBAAgB,EAAE,GAAG,CAAC,CAAC,CAAC;QAChF,CAAC;IACH,CAAC;IAED,4EAA4E;IAC5E,yEAAyE;IACzE,0EAA0E;IAC1E,4EAA4E;IAC5E,8EAA8E;IAC9E,qEAAqE;IACrE,0EAA0E;IAC1E,4BAA4B;IAC5B,MAAM,gBAAgB,GACpB,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,uBAAuB,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,uBAAuB,CAAC,UAAU,CAAC,CAAC;IACpH,8EAA8E;IAC9E,8EAA8E;IAC9E,+EAA+E;IAC/E,4EAA4E;IAC5E,2EAA2E;IAC3E,6FAA6F;IAC7F,0EAA0E;IAC1E,oEAAoE;IACpE,QAAQ,CAAC,IAAI,CAAC,GAAG,kBAAkB,CAAC,gBAAgB,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;IACxE,KAAK,MAAM,GAAG,IAAI,gBAAgB,CAAC,IAAI,EAAE,CAAC;QACxC,MAAM,GAAG,GAAG,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACpG,MAAM,QAAQ,GAAG,uBAAuB,CAAC,cAAc,GAAG,CAAC,EAAE,WAAW,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;QACvF,IAAI,QAAQ,KAAK,IAAI;YAAE,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACjD,CAAC;IAED,4EAA4E;IAC5E,yEAAyE;IACzE,2EAA2E;IAC3E,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,mBAAmB,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC;IAElG,QAAQ,CAAC,IAAI,CACX,GAAG,CAAC,MAAM,iBAAiB,CAAC;QAC1B,OAAO;QACP,QAAQ;QACR,aAAa,EAAE,SAAS;QACxB,SAAS;QACT,UAAU;QACV,UAAU;QACV,eAAe;QACf,YAAY;QACZ,aAAa;QACb,KAAK;KACN,CAAC,CAAC,CACJ,CAAC;IAGF,oEAAoE;IACpE,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,cAAc,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;IACpH,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ,EAAE,IAAI;YACd,IAAI,EAAE,cAAc;YACpB,OAAO,EAAE,gDAAgD;YACzD,IAAI,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE;SACnC,CAAC,CAAC;IACL,CAAC;SAAM,CAAC;QACN,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;YACvB,QAAQ,CAAC,IAAI,CAAC,EAAE,GAAG,YAAY,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,EAAE,CAAC,CAAC;QACnF,CAAC;IACH,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,YAAY,OAAO,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,CAAC;AAC3F,CAAC","sourcesContent":["/**\n * The feature target: a delta graded on its own, before anything merges.\n *\n * Everything here reads the feature directory — delta.likec4, the per-service\n * spec.md/arch.spec.md deltas, the provenance stamp — and grades the documents\n * a merge is about to fold into living ones. A delta is held to the same\n * document rules as the file it becomes (`./checks/requirements.js`), because\n * grading it more leniently is how a defect acquires a signature saying it was\n * reviewed.\n *\n * The one question that needs the LIVING fleet — what in this delta is actually\n * new architecture — is `./arch-coverage.js`.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { loadFile, type Elem, type LoadedDoc, type Rel } from \"../../core/c4/likec4.js\";\nimport { elementService } from \"../../core/c4/resolve/service.js\";\nimport { type PathableService } from \"../../core/kernel/ids/service.js\";\nimport { type FeatureEntry } from \"../../core/repo/entries.js\";\nimport { featurePaths, featureSpecPaths } from \"../../core/repo/paths.js\";\nimport { enumeratedServiceIds } from \"../../core/repo/service-target.js\";\nimport { docsRepoState } from \"../../core/repo/state.js\";\nimport { featureSpecServices, listServices } from \"../../core/repo/repo.js\";\nimport { type Finding, type TargetReport } from \"../../core/vocabulary/report.js\";\nimport { parseRequirements } from \"../../core/document/parse.js\";\nimport {\n assertionlessFindings,\n examplesFindings,\n steplessFindings,\n} from \"../../core/document/scenarios.js\";\nimport { type Requirement } from \"../../core/document/spec.js\";\nimport { featureCoherence } from \"../../core/coherence/coherence.js\";\nimport {\n deltaServiceUnknownFinding,\n invalidSpecServiceFindings,\n} from \"../../core/coherence/living.js\";\nimport { issueFinding } from \"../../core/vocabulary/issue.js\";\nimport { featureProvenance } from \"../../core/provenance/findings.js\";\nimport { documentConflictFinding } from \"../../core/conflict-markers.js\";\nimport { featureCapabilityDeltas } from \"../../core/capabilities/delta/tree.js\";\nimport { docMissingFindings } from \"../../core/capabilities/findings.js\";\nimport { FleetContext } from \"../../core/fleet-context.js\";\nimport { errorText } from \"./checks/vocabulary.js\";\nimport { coverageFinding, repeatedListLineFindings } from \"./checks/requirements.js\";\nimport { deltaArchCoverage } from \"./arch-coverage.js\";\nimport { featureLinkFindings } from \"./links/corpus.js\";\nimport type { DocsDir } from \"../../core/kernel/ids/dirs.js\";\n\nexport async function validateFeature(\n docsDir: DocsDir,\n feature: FeatureEntry,\n preloadedLand?: LoadedDoc | null,\n fleet?: FleetContext,\n): Promise<TargetReport> {\n const findings: Finding[] = [];\n const featureDir = feature.dir;\n const featureId = feature.id;\n\n // delta.likec4 parse + collect tagged edges. The loaded doc is kept and\n // handed to featureCoherence below — loading it is a Langium workspace spin,\n // and paying it twice per feature was the dominant cost of `validate --all`.\n let taggedEls: Elem[] = [];\n let taggedRels: Rel[] = [];\n let elements: Elem[] = [];\n let deltaRels: Rel[] = [];\n let deltaDoc: LoadedDoc | undefined;\n const deltaPath = featurePaths(featureDir).delta;\n if (existsSync(deltaPath)) {\n const res = fleet === undefined ? await loadFile(deltaPath) : await fleet.loadLikeC4(deltaPath);\n deltaDoc = res;\n if (res.errors.length > 0) {\n findings.push({\n severity: \"error\",\n code: \"delta.invalid\",\n message: `delta.likec4 has ${res.errors.length} error(s)`,\n details: res.errors.map(errorText),\n });\n } else {\n elements = res.elements;\n deltaRels = res.relationships;\n taggedEls = res.elements.filter((e) => e.tags.includes(featureId));\n taggedRels = res.relationships.filter((r) => r.tags.includes(featureId));\n findings.push({\n severity: \"ok\",\n code: \"delta.valid\",\n message: `delta.likec4 valid (${res.elements.length} elements · ${res.relationships.length} relationships)`,\n });\n }\n }\n\n findings.push(...(await featureProvenance(featureDir, featureId)));\n\n // Who this feature is allowed to address. `specs/<svc>/` is what the archive\n // materialises `services/<svc>/` from, and nothing used to ask whether that\n // name means anything: one wrong character in `--touches` passed\n // `validate --all` with zero errors, and archive then created the phantom\n // directory. A delta may legitimately name a service that does not exist yet\n // — but only one it INTRODUCES itself, in its own tagged C4. A delta that did\n // not parse proves neither (`delta.invalid` is that finding), so the question\n // is suspended there rather than answered by guessing.\n //\n // Existence is enumeration MEMBERSHIP through the no-services-tolerant\n // spelling, never `existsSync(services/<svc>/)` at the root: a service filed\n // into a subsystem exists wherever the tree walk found it, and the root\n // probe graded every filed service `delta.service-unknown`. The tolerance\n // carries the old carve-out forward — `validate --feature` is allowed to run\n // in a docs repo with no services/ at all, where enumerating is a refusal;\n // `enumeratedServiceIds` swallows exactly that refusal to an empty list, in\n // which every service is honestly absent.\n const featureServices = await featureSpecServices(featureDir, fleet);\n const introduces: ReadonlySet<string> = new Set(taggedEls.map(elementService));\n const deltaReadable = deltaDoc === undefined || deltaDoc.errors.length === 0;\n const enumerated = new Set<string>(await enumeratedServiceIds(docsDir, fleet));\n const unknownServices = deltaReadable\n ? featureServices.filter((svc) => !enumerated.has(svc) && !introduces.has(svc))\n : [];\n // The near-miss hint, on the same rule `service.unknown` uses — a typo is\n // only diagnosable against the ids that DO exist.\n const closeTo =\n unknownServices.length > 0 && docsRepoState(docsDir).kind === \"ok\"\n ? (await listServices(docsDir, fleet)).map((s) => s.id)\n : [];\n // The finding is coherence/living.ts's — the same words archive refuses with,\n // because it is the same conclusion about the same directory.\n for (const svc of unknownServices) findings.push(deltaServiceUnknownFinding(svc, closeTo));\n\n // The grammar half of the same guarantee. `delta.service-unknown` asks whether\n // the directory names a service anyone knows; this asks whether the NAME could\n // ever be one. The two are independent on purpose: a tagged element whose\n // title matches the directory answers the first question, which is exactly how\n // `specs/Payment Service/` used to validate green and archive into a directory\n // no loam command can address. Not suspended on an unreadable delta either —\n // no reading of the architecture axis can make the name legal.\n findings.push(...(await invalidSpecServiceFindings(featureDir, fleet)));\n\n // Requirement coverage across every per-service delta — the business spec and\n // the arch spec through the same check — and collect scenario text.\n let scenarioText = \"\";\n const archDeltas: Array<{ service: PathableService; reqs: Requirement[] }> = [];\n for (const svc of featureServices) {\n const p = featureSpecPaths(featureDir, svc);\n if (existsSync(p.spec)) {\n const raw = fleet === undefined ? await readFile(p.spec, \"utf8\") : await fleet.readText(p.spec);\n scenarioText += \"\\n\" + raw.toLowerCase();\n const reqs = fleet === undefined ? parseRequirements(raw) : await fleet.readRequirements(p.spec);\n // Both document-level breaches carry into the living spec through the\n // merge, so a delta is graded for them exactly as a living document is:\n // conflict markers merge as prose under someone's requirement, and a\n // stepless scenario merges as a requirement the coverage rule calls\n // covered forever after.\n const conflict = documentConflictFinding(`${svc}: spec.md`, svc, raw);\n if (conflict !== null) findings.push(conflict);\n findings.push({ ...coverageFinding(`${svc}: requirements`, reqs), subject: svc });\n findings.push(...steplessFindings(`${svc}: requirements`, svc, reqs));\n findings.push(...assertionlessFindings(`${svc}: requirements`, svc, reqs));\n findings.push(...examplesFindings(`${svc}: requirements`, svc, reqs));\n // The keep-last quirk loses lines in a delta exactly as in a living spec\n // — and a delta's lost Operations: line then merges into the living one.\n findings.push(...repeatedListLineFindings(reqs, `${svc}: spec.md`, svc));\n }\n if (existsSync(p.archSpec)) {\n const raw = fleet === undefined ? await readFile(p.archSpec, \"utf8\") : await fleet.readText(p.archSpec);\n scenarioText += \"\\n\" + raw.toLowerCase();\n const reqs = fleet === undefined ? parseRequirements(raw) : await fleet.readRequirements(p.archSpec);\n archDeltas.push({ service: svc, reqs });\n const conflict = documentConflictFinding(`${svc}: arch.spec.md`, svc, raw);\n if (conflict !== null) findings.push(conflict);\n findings.push({ ...coverageFinding(`${svc}: arch requirements`, reqs), subject: svc });\n findings.push(...steplessFindings(`${svc}: arch requirements`, svc, reqs));\n findings.push(...assertionlessFindings(`${svc}: arch requirements`, svc, reqs));\n findings.push(...examplesFindings(`${svc}: arch requirements`, svc, reqs));\n findings.push(...repeatedListLineFindings(reqs, `${svc}: arch.spec.md`, svc));\n }\n }\n\n // The BUSINESS corpus's delta documents, graded for the same document-level\n // breach as the service ones and for the same reason: a conflicted delta\n // parses as prose, so every other check reads it as a valid document, and\n // `loam archive` then re-serializes both sides of somebody's merge into the\n // LIVING capability document. It is worse there than one tree over — once the\n // feature is archived, nothing walks the delta again, and the living\n // capability documents are only conflict-scanned while some feature still\n // carries a delta for them.\n const capabilityDeltas =\n fleet === undefined ? await featureCapabilityDeltas(featureDir) : await fleet.featureCapabilityDeltas(featureDir);\n // The half-created directory, on the FEATURE side. The living tree has warned\n // about `capabilities/<id>/` with no document since the axis landed; the same\n // `mkdir` inside `features/<FEAT>/capabilities/` earned nothing, and it is the\n // quieter of the two — an empty directory is not a delta, so the walk finds\n // no document, the merge carries nothing, and the business change ships as\n // zero at exit 0. Same code, feature-scoped message (`../../core/capabilities/findings.ts`).\n // `dirName`, not the id: the directory is `features/FEAT-1-split/`, and a\n // message spelling `features/FEAT-1/` names a path nobody can open.\n findings.push(...docMissingFindings(capabilityDeltas, feature.dirName));\n for (const doc of capabilityDeltas.docs) {\n const raw = fleet === undefined ? await readFile(doc.spec, \"utf8\") : await fleet.readText(doc.spec);\n const conflict = documentConflictFinding(`capability ${doc.id}: spec.md`, doc.id, raw);\n if (conflict !== null) findings.push(conflict);\n }\n\n // The links this feature's own documents write — the intent, each service's\n // two spec deltas, its ADRs and its capability deltas. Same place in the\n // order as the service target's: after every join loam defined a line for.\n findings.push(...(await featureLinkFindings({ docsDir, subject: featureId, fleet }, featureDir)));\n\n findings.push(\n ...(await deltaArchCoverage({\n docsDir,\n elements,\n relationships: deltaRels,\n taggedEls,\n taggedRels,\n archDeltas,\n featureServices,\n scenarioText,\n preloadedLand,\n fleet,\n })),\n );\n\n\n // Coherence — cross-axis consistency (C4 ↔ requirements ↔ OpenAPI).\n const issues = await featureCoherence({ docsDir, featureDir, featureId, preloadedDelta: deltaDoc, context: fleet });\n if (issues.length === 0) {\n findings.push({\n severity: \"ok\",\n code: \"coherence.ok\",\n message: \"coherence: ✓ C4 · requirements · OpenAPI agree\",\n text: { indent: 2, marker: false },\n });\n } else {\n for (const i of issues) {\n findings.push({ ...issueFinding(i), text: { indent: 4, header: \"coherence:\" } });\n }\n }\n\n return { kind: \"feature\", id: featureId, path: `features/${feature.dirName}`, findings };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"feature.js","sourceRoot":"","sources":["../../../src/commands/validate/feature.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The feature target: a delta graded on its own, before anything merges.\n *\n * Everything here reads the feature directory — delta.likec4, the per-service\n * spec.md/arch.spec.md deltas, the provenance stamp — and grades the documents\n * a merge is about to fold into living ones. A delta is held to the same\n * document rules as the file it becomes (`./checks/requirements.js`), because\n * grading it more leniently is how a defect acquires a signature saying it was\n * reviewed.\n *\n * The one question that needs the LIVING fleet — what in this delta is actually\n * new architecture — is `./arch-coverage.js`.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { loadFile, type Elem, type LoadedDoc, type Rel } from \"../../core/c4/likec4.js\";\nimport { elementService } from \"../../core/c4/resolve/service.js\";\nimport { type PathableService } from \"../../core/kernel/ids/service.js\";\nimport { type FeatureEntry } from \"../../core/repo/entries.js\";\nimport { featurePaths, featureSpecPaths } from \"../../core/repo/paths.js\";\nimport { enumeratedServiceIds } from \"../../core/repo/service-target.js\";\nimport { docsRepoState } from \"../../core/repo/state.js\";\nimport { featureSpecServices, listServices } from \"../../core/repo/repo.js\";\nimport { type Finding, type TargetReport } from \"../../core/vocabulary/report.js\";\nimport { parseRequirements } from \"../../core/document/parse.js\";\nimport {\n assertionlessFindings,\n examplesFindings,\n steplessFindings,\n} from \"../../core/document/scenarios.js\";\nimport { type Requirement } from \"../../core/document/spec.js\";\nimport { featureCoherence } from \"../../core/coherence/coherence.js\";\nimport {\n deltaServiceUnknownFinding,\n invalidSpecServiceFindings,\n} from \"../../core/coherence/living.js\";\nimport { issueFinding } from \"../../core/vocabulary/issue.js\";\nimport { featureProvenance } from \"../../core/provenance/findings.js\";\nimport { documentConflictFinding } from \"../../core/conflict-markers.js\";\nimport { featureCapabilityDeltas } from \"../../core/capabilities/delta/tree.js\";\nimport { docMissingFindings } from \"../../core/capabilities/findings.js\";\nimport { FleetContext } from \"../../core/fleet-context.js\";\nimport { errorText } from \"../../core/c4/likec4.js\";\nimport { coverageFinding, repeatedListLineFindings } from \"./checks/requirements.js\";\nimport { deltaArchCoverage } from \"./arch-coverage.js\";\nimport { featureLinkFindings } from \"./links/corpus.js\";\nimport type { DocsDir } from \"../../core/kernel/ids/dirs.js\";\n\nexport async function validateFeature(\n docsDir: DocsDir,\n feature: FeatureEntry,\n preloadedLand?: LoadedDoc | null,\n fleet?: FleetContext,\n): Promise<TargetReport> {\n const findings: Finding[] = [];\n const featureDir = feature.dir;\n const featureId = feature.id;\n\n // delta.likec4 parse + collect tagged edges. The loaded doc is kept and\n // handed to featureCoherence below — loading it is a Langium workspace spin,\n // and paying it twice per feature was the dominant cost of `validate --all`.\n let taggedEls: Elem[] = [];\n let taggedRels: Rel[] = [];\n let elements: Elem[] = [];\n let deltaRels: Rel[] = [];\n let deltaDoc: LoadedDoc | undefined;\n const deltaPath = featurePaths(featureDir).delta;\n if (existsSync(deltaPath)) {\n const res = fleet === undefined ? await loadFile(deltaPath) : await fleet.loadLikeC4(deltaPath);\n deltaDoc = res;\n if (res.errors.length > 0) {\n findings.push({\n severity: \"error\",\n code: \"delta.invalid\",\n message: `delta.likec4 has ${res.errors.length} error(s)`,\n details: res.errors.map(errorText),\n });\n } else {\n elements = res.elements;\n deltaRels = res.relationships;\n taggedEls = res.elements.filter((e) => e.tags.includes(featureId));\n taggedRels = res.relationships.filter((r) => r.tags.includes(featureId));\n findings.push({\n severity: \"ok\",\n code: \"delta.valid\",\n message: `delta.likec4 valid (${res.elements.length} elements · ${res.relationships.length} relationships)`,\n });\n }\n }\n\n findings.push(...(await featureProvenance(featureDir, featureId)));\n\n // Who this feature is allowed to address. `specs/<svc>/` is what the archive\n // materialises `services/<svc>/` from, and nothing used to ask whether that\n // name means anything: one wrong character in `--touches` passed\n // `validate --all` with zero errors, and archive then created the phantom\n // directory. A delta may legitimately name a service that does not exist yet\n // — but only one it INTRODUCES itself, in its own tagged C4. A delta that did\n // not parse proves neither (`delta.invalid` is that finding), so the question\n // is suspended there rather than answered by guessing.\n //\n // Existence is enumeration MEMBERSHIP through the no-services-tolerant\n // spelling, never `existsSync(services/<svc>/)` at the root: a service filed\n // into a subsystem exists wherever the tree walk found it, and the root\n // probe graded every filed service `delta.service-unknown`. The tolerance\n // carries the old carve-out forward — `validate --feature` is allowed to run\n // in a docs repo with no services/ at all, where enumerating is a refusal;\n // `enumeratedServiceIds` swallows exactly that refusal to an empty list, in\n // which every service is honestly absent.\n const featureServices = await featureSpecServices(featureDir, fleet);\n const introduces: ReadonlySet<string> = new Set(taggedEls.map(elementService));\n const deltaReadable = deltaDoc === undefined || deltaDoc.errors.length === 0;\n const enumerated = new Set<string>(await enumeratedServiceIds(docsDir, fleet));\n const unknownServices = deltaReadable\n ? featureServices.filter((svc) => !enumerated.has(svc) && !introduces.has(svc))\n : [];\n // The near-miss hint, on the same rule `service.unknown` uses — a typo is\n // only diagnosable against the ids that DO exist.\n const closeTo =\n unknownServices.length > 0 && docsRepoState(docsDir).kind === \"ok\"\n ? (await listServices(docsDir, fleet)).map((s) => s.id)\n : [];\n // The finding is coherence/living.ts's — the same words archive refuses with,\n // because it is the same conclusion about the same directory.\n for (const svc of unknownServices) findings.push(deltaServiceUnknownFinding(svc, closeTo));\n\n // The grammar half of the same guarantee. `delta.service-unknown` asks whether\n // the directory names a service anyone knows; this asks whether the NAME could\n // ever be one. The two are independent on purpose: a tagged element whose\n // title matches the directory answers the first question, which is exactly how\n // `specs/Payment Service/` used to validate green and archive into a directory\n // no loam command can address. Not suspended on an unreadable delta either —\n // no reading of the architecture axis can make the name legal.\n findings.push(...(await invalidSpecServiceFindings(featureDir, fleet)));\n\n // Requirement coverage across every per-service delta — the business spec and\n // the arch spec through the same check — and collect scenario text.\n let scenarioText = \"\";\n const archDeltas: Array<{ service: PathableService; reqs: Requirement[] }> = [];\n for (const svc of featureServices) {\n const p = featureSpecPaths(featureDir, svc);\n if (existsSync(p.spec)) {\n const raw = fleet === undefined ? await readFile(p.spec, \"utf8\") : await fleet.readText(p.spec);\n scenarioText += \"\\n\" + raw.toLowerCase();\n const reqs = fleet === undefined ? parseRequirements(raw) : await fleet.readRequirements(p.spec);\n // Both document-level breaches carry into the living spec through the\n // merge, so a delta is graded for them exactly as a living document is:\n // conflict markers merge as prose under someone's requirement, and a\n // stepless scenario merges as a requirement the coverage rule calls\n // covered forever after.\n const conflict = documentConflictFinding(`${svc}: spec.md`, svc, raw);\n if (conflict !== null) findings.push(conflict);\n findings.push({ ...coverageFinding(`${svc}: requirements`, reqs), subject: svc });\n findings.push(...steplessFindings(`${svc}: requirements`, svc, reqs));\n findings.push(...assertionlessFindings(`${svc}: requirements`, svc, reqs));\n findings.push(...examplesFindings(`${svc}: requirements`, svc, reqs));\n // The keep-last quirk loses lines in a delta exactly as in a living spec\n // — and a delta's lost Operations: line then merges into the living one.\n findings.push(...repeatedListLineFindings(reqs, `${svc}: spec.md`, svc));\n }\n if (existsSync(p.archSpec)) {\n const raw = fleet === undefined ? await readFile(p.archSpec, \"utf8\") : await fleet.readText(p.archSpec);\n scenarioText += \"\\n\" + raw.toLowerCase();\n const reqs = fleet === undefined ? parseRequirements(raw) : await fleet.readRequirements(p.archSpec);\n archDeltas.push({ service: svc, reqs });\n const conflict = documentConflictFinding(`${svc}: arch.spec.md`, svc, raw);\n if (conflict !== null) findings.push(conflict);\n findings.push({ ...coverageFinding(`${svc}: arch requirements`, reqs), subject: svc });\n findings.push(...steplessFindings(`${svc}: arch requirements`, svc, reqs));\n findings.push(...assertionlessFindings(`${svc}: arch requirements`, svc, reqs));\n findings.push(...examplesFindings(`${svc}: arch requirements`, svc, reqs));\n findings.push(...repeatedListLineFindings(reqs, `${svc}: arch.spec.md`, svc));\n }\n }\n\n // The BUSINESS corpus's delta documents, graded for the same document-level\n // breach as the service ones and for the same reason: a conflicted delta\n // parses as prose, so every other check reads it as a valid document, and\n // `loam archive` then re-serializes both sides of somebody's merge into the\n // LIVING capability document. It is worse there than one tree over — once the\n // feature is archived, nothing walks the delta again, and the living\n // capability documents are only conflict-scanned while some feature still\n // carries a delta for them.\n const capabilityDeltas =\n fleet === undefined ? await featureCapabilityDeltas(featureDir) : await fleet.featureCapabilityDeltas(featureDir);\n // The half-created directory, on the FEATURE side. The living tree has warned\n // about `capabilities/<id>/` with no document since the axis landed; the same\n // `mkdir` inside `features/<FEAT>/capabilities/` earned nothing, and it is the\n // quieter of the two — an empty directory is not a delta, so the walk finds\n // no document, the merge carries nothing, and the business change ships as\n // zero at exit 0. Same code, feature-scoped message (`../../core/capabilities/findings.ts`).\n // `dirName`, not the id: the directory is `features/FEAT-1-split/`, and a\n // message spelling `features/FEAT-1/` names a path nobody can open.\n findings.push(...docMissingFindings(capabilityDeltas, feature.dirName));\n for (const doc of capabilityDeltas.docs) {\n const raw = fleet === undefined ? await readFile(doc.spec, \"utf8\") : await fleet.readText(doc.spec);\n const conflict = documentConflictFinding(`capability ${doc.id}: spec.md`, doc.id, raw);\n if (conflict !== null) findings.push(conflict);\n }\n\n // The links this feature's own documents write — the intent, each service's\n // two spec deltas, its ADRs and its capability deltas. Same place in the\n // order as the service target's: after every join loam defined a line for.\n findings.push(...(await featureLinkFindings({ docsDir, subject: featureId, fleet }, featureDir)));\n\n findings.push(\n ...(await deltaArchCoverage({\n docsDir,\n elements,\n relationships: deltaRels,\n taggedEls,\n taggedRels,\n archDeltas,\n featureServices,\n scenarioText,\n preloadedLand,\n fleet,\n })),\n );\n\n\n // Coherence — cross-axis consistency (C4 ↔ requirements ↔ OpenAPI).\n const issues = await featureCoherence({ docsDir, featureDir, featureId, preloadedDelta: deltaDoc, context: fleet });\n if (issues.length === 0) {\n findings.push({\n severity: \"ok\",\n code: \"coherence.ok\",\n message: \"coherence: ✓ C4 · requirements · OpenAPI agree\",\n text: { indent: 2, marker: false },\n });\n } else {\n for (const i of issues) {\n findings.push({ ...issueFinding(i), text: { indent: 4, header: \"coherence:\" } });\n }\n }\n\n return { kind: \"feature\", id: featureId, path: `features/${feature.dirName}`, findings };\n}\n"]}
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* emits a finding, or knows what a target is.
|
|
11
11
|
*/
|
|
12
12
|
import {} from "../../../core/c4/likec4.js";
|
|
13
|
-
import {
|
|
13
|
+
import { properAncestorIds } from "../../../core/kernel/ids/fqn/ancestors.js";
|
|
14
|
+
import { ACTOR_KINDS, EXTERNAL_TAG, INFRA_KINDS } from "../../../core/vocabulary/maturity.js";
|
|
14
15
|
/**
|
|
15
16
|
* Does this element stand for a `services/<id>/` directory — an explicit
|
|
16
17
|
* `metadata { service }` binding, or a title naming a real one? The question
|
|
@@ -35,21 +36,22 @@ export function standsForService(e, services) {
|
|
|
35
36
|
*/
|
|
36
37
|
export function serviceLevelElements(elements, services) {
|
|
37
38
|
const byId = new Map(elements.map((e) => [e.id, e]));
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return
|
|
47
|
-
};
|
|
39
|
+
/**
|
|
40
|
+
* The ancestors this document actually DECLARES, nearest first. The chain
|
|
41
|
+
* itself is `core/kernel/ids/fqn/ancestors.ts`'s — the one spelling of it —
|
|
42
|
+
* and the filter is this function's own: an id may name a level nothing
|
|
43
|
+
* declares, and an undeclared level answers for no service.
|
|
44
|
+
*/
|
|
45
|
+
const ancestorsOf = (e) => properAncestorIds(e.id).flatMap((id) => {
|
|
46
|
+
const parent = byId.get(id);
|
|
47
|
+
return parent === undefined ? [] : [parent];
|
|
48
|
+
});
|
|
48
49
|
return elements.filter((e) => !ancestorsOf(e).some((p) => standsForService(p, services)));
|
|
49
50
|
}
|
|
50
51
|
/**
|
|
51
52
|
* The drawn SYSTEMS: service-level elements minus everything the map itself
|
|
52
|
-
* says is not a service. Actor kinds model people;
|
|
53
|
+
* says is not a service. Actor kinds model people; INFRA kinds model the
|
|
54
|
+
* datastores, queues and buckets the fleet RUNS; `#external` is somebody
|
|
53
55
|
* else's system; and an element that CONTAINS a service is a grouping — a
|
|
54
56
|
* domain, a boundary, an enterprise — not a service nobody adopted. There is
|
|
55
57
|
* nothing to bind on a grouping and nothing to tag #external, so asking for
|
|
@@ -58,12 +60,34 @@ export function serviceLevelElements(elements, services) {
|
|
|
58
60
|
* the scorecard's `c4.elements` census counts it — one predicate, two readers,
|
|
59
61
|
* zero drift.
|
|
60
62
|
*
|
|
63
|
+
* The INFRA skip is the newest and the one with a measured cost behind it. A
|
|
64
|
+
* `database` the fleet OWNS and draws at fleet level is neither a person nor
|
|
65
|
+
* somebody else's system, so it used to earn `landscape.service-undocumented`
|
|
66
|
+
* — "bind it with `metadata { service '<id>' }`, or tag it `#external`" —
|
|
67
|
+
* whose two remedies are both FALSE of it: there is no directory to bind, and
|
|
68
|
+
* it is ours. Measured on a one-service fixture drawing an owned `database` at
|
|
69
|
+
* fleet level (2026-09-07): that warning plus `landscape.datastore-private`
|
|
70
|
+
* plus `gate.partner-undocumented` on its one consumer, three findings and no
|
|
71
|
+
* correct fix, which is why both `examples/docs` and this repo's own suite
|
|
72
|
+
* tagged an owned Redis `#external` to buy silence.
|
|
73
|
+
*
|
|
74
|
+
* `standsForService` wins over the kind, and that guard is load-bearing rather
|
|
75
|
+
* than defensive: a `database` bound with `metadata { service '<id>' }`, or one
|
|
76
|
+
* whose title names a real `services/<id>/`, IS an adopted service — dropping
|
|
77
|
+
* it here would take it out of the scorecard's `c4.elements` denominator and
|
|
78
|
+
* out of the binding passes at once. The kind is the fallback for an element
|
|
79
|
+
* nothing else answers for, never an override.
|
|
80
|
+
*
|
|
61
81
|
* The `#external` skip here is exactly what `./kind-tags.ts` guards: read off a
|
|
62
|
-
* tag a KIND declared, it can empty this census for a whole fleet at once.
|
|
82
|
+
* tag a KIND declared, it can empty this census for a whole fleet at once. The
|
|
83
|
+
* INFRA skip is NOT guarded there and does not need to be — a kind is what an
|
|
84
|
+
* element already is, not a claim declared over the fleet, and `#infrastructure`
|
|
85
|
+
* is deliberately not a tag loam grades on (`GRADED_TAGS`).
|
|
63
86
|
*/
|
|
64
87
|
export function drawnSystems(elements, services) {
|
|
65
88
|
return serviceLevelElements(elements, services).filter((e) => !e.tags.includes(EXTERNAL_TAG) &&
|
|
66
89
|
!ACTOR_KINDS.has(e.kind.toLowerCase()) &&
|
|
90
|
+
!(INFRA_KINDS.has(e.kind.toLowerCase()) && !standsForService(e, services)) &&
|
|
67
91
|
!elements.some((c) => c.id.startsWith(`${e.id}.`) && standsForService(c, services)));
|
|
68
92
|
}
|
|
69
93
|
//# sourceMappingURL=census.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"census.js","sourceRoot":"","sources":["../../../../src/commands/validate/fleet/census.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAa,MAAM,4BAA4B,CAAC;AACvD,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,sCAAsC,CAAC;
|
|
1
|
+
{"version":3,"file":"census.js","sourceRoot":"","sources":["../../../../src/commands/validate/fleet/census.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAa,MAAM,4BAA4B,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,2CAA2C,CAAC;AAC9E,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,sCAAsC,CAAC;AAE9F;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,CAAO,EAAE,QAA6B;IACrE,OAAO,CAAC,CAAC,OAAO,KAAK,SAAS,IAAI,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;AAC1D,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,oBAAoB,CAAC,QAAgB,EAAE,QAA6B;IAClF,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IACrD;;;;;OAKG;IACH,MAAM,WAAW,GAAG,CAAC,CAAO,EAAU,EAAE,CACtC,iBAAiB,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE;QACrC,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC5B,OAAO,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;IACL,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,gBAAgB,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC;AAC5F,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,UAAU,YAAY,CAAC,QAAgB,EAAE,QAA6B;IAC1E,OAAO,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,MAAM,CACpD,CAAC,CAAC,EAAE,EAAE,CACJ,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC;QAC9B,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;QACtC,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;QAC1E,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,gBAAgB,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,CACtF,CAAC;AACJ,CAAC","sourcesContent":["/**\n * The fleet map's census: which drawn elements answer for a `services/<id>/`.\n *\n * Three predicates, in one place because every exemption in the fleet target,\n * the scorecard's `c4.elements` count and the kind-tag check all have to answer\n * the same question the same way — a second spelling of \"is this element a\n * service\" is how a map and a rollup start disagreeing about the same fleet.\n * They lived in `./landscape.ts` beside their first reader until that file hit\n * its line limit; the seam was already there, since nothing here reads a file,\n * emits a finding, or knows what a target is.\n */\nimport { type Elem } from \"../../../core/c4/likec4.js\";\nimport { properAncestorIds } from \"../../../core/kernel/ids/fqn/ancestors.js\";\nimport { ACTOR_KINDS, EXTERNAL_TAG, INFRA_KINDS } from \"../../../core/vocabulary/maturity.js\";\n\n/**\n * Does this element stand for a `services/<id>/` directory — an explicit\n * `metadata { service }` binding, or a title naming a real one? The question\n * every exemption in `./landscape.ts`, the scorecard's census and\n * `./kind-tags.ts` all ask, exported so they cannot answer it differently.\n */\nexport function standsForService(e: Elem, services: ReadonlySet<string>): boolean {\n return e.service !== undefined || services.has(e.title);\n}\n\n/**\n * Service-LEVEL elements: everything not drawn inside something that is\n * already a service.\n *\n * Depth is not a fact about a service. This used to keep only top-level\n * elements (`!e.id.includes(\".\")`), which ordinary C4 breaks the moment it\n * groups services under a parent: every nested element was thrown away, so a\n * bound service read as unmodelled — an ERROR, on every service in the fleet\n * — and a binding written one level down was never checked at all. The tree\n * is walked instead, the way `serviceResolver` walks it for edges: a binding\n * wins at any depth, then a title naming a real services/<id>/, and what sits\n * INSIDE one of those is that service's container, not a service of its own.\n */\nexport function serviceLevelElements(elements: Elem[], services: ReadonlySet<string>): Elem[] {\n const byId = new Map(elements.map((e) => [e.id, e]));\n /**\n * The ancestors this document actually DECLARES, nearest first. The chain\n * itself is `core/kernel/ids/fqn/ancestors.ts`'s — the one spelling of it —\n * and the filter is this function's own: an id may name a level nothing\n * declares, and an undeclared level answers for no service.\n */\n const ancestorsOf = (e: Elem): Elem[] =>\n properAncestorIds(e.id).flatMap((id) => {\n const parent = byId.get(id);\n return parent === undefined ? [] : [parent];\n });\n return elements.filter((e) => !ancestorsOf(e).some((p) => standsForService(p, services)));\n}\n\n/**\n * The drawn SYSTEMS: service-level elements minus everything the map itself\n * says is not a service. Actor kinds model people; INFRA kinds model the\n * datastores, queues and buckets the fleet RUNS; `#external` is somebody\n * else's system; and an element that CONTAINS a service is a grouping — a\n * domain, a boundary, an enterprise — not a service nobody adopted. There is\n * nothing to bind on a grouping and nothing to tag #external, so asking for\n * either would be a warning with no correct fix; the services under it answer\n * for themselves. `validateLandscape`'s undocumented pass walks this set, and\n * the scorecard's `c4.elements` census counts it — one predicate, two readers,\n * zero drift.\n *\n * The INFRA skip is the newest and the one with a measured cost behind it. A\n * `database` the fleet OWNS and draws at fleet level is neither a person nor\n * somebody else's system, so it used to earn `landscape.service-undocumented`\n * — \"bind it with `metadata { service '<id>' }`, or tag it `#external`\" —\n * whose two remedies are both FALSE of it: there is no directory to bind, and\n * it is ours. Measured on a one-service fixture drawing an owned `database` at\n * fleet level (2026-09-07): that warning plus `landscape.datastore-private`\n * plus `gate.partner-undocumented` on its one consumer, three findings and no\n * correct fix, which is why both `examples/docs` and this repo's own suite\n * tagged an owned Redis `#external` to buy silence.\n *\n * `standsForService` wins over the kind, and that guard is load-bearing rather\n * than defensive: a `database` bound with `metadata { service '<id>' }`, or one\n * whose title names a real `services/<id>/`, IS an adopted service — dropping\n * it here would take it out of the scorecard's `c4.elements` denominator and\n * out of the binding passes at once. The kind is the fallback for an element\n * nothing else answers for, never an override.\n *\n * The `#external` skip here is exactly what `./kind-tags.ts` guards: read off a\n * tag a KIND declared, it can empty this census for a whole fleet at once. The\n * INFRA skip is NOT guarded there and does not need to be — a kind is what an\n * element already is, not a claim declared over the fleet, and `#infrastructure`\n * is deliberately not a tag loam grades on (`GRADED_TAGS`).\n */\nexport function drawnSystems(elements: Elem[], services: ReadonlySet<string>): Elem[] {\n return serviceLevelElements(elements, services).filter(\n (e) =>\n !e.tags.includes(EXTERNAL_TAG) &&\n !ACTOR_KINDS.has(e.kind.toLowerCase()) &&\n !(INFRA_KINDS.has(e.kind.toLowerCase()) && !standsForService(e, services)) &&\n !elements.some((c) => c.id.startsWith(`${e.id}.`) && standsForService(c, services)),\n );\n}\n"]}
|