@ybotok/loam 0.2.0-alpha.4 → 0.2.0-alpha.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +947 -0
- package/MIGRATING-from-OpenSpec.md +7 -1
- package/QUICKSTART.md +8 -3
- package/README.md +20 -13
- package/ROADMAP.md +329 -22
- package/SCHEMA.md +472 -24
- package/WORKFLOW.md +23 -10
- package/dist/commands/adopt/render.js +47 -8
- package/dist/commands/adopt/render.js.map +1 -1
- package/dist/commands/archive/plan/landscape.js +45 -1
- package/dist/commands/archive/plan/landscape.js.map +1 -1
- package/dist/commands/archive/plan/model/extending.js +85 -0
- package/dist/commands/archive/plan/model/extending.js.map +1 -0
- package/dist/commands/archive/run.js +5 -2
- package/dist/commands/archive/run.js.map +1 -1
- package/dist/commands/context/print.js +11 -1
- package/dist/commands/context/print.js.map +1 -1
- package/dist/commands/doctor.js +46 -0
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/explore.js +12 -2
- package/dist/commands/explore.js.map +1 -1
- package/dist/commands/list/views.js +5 -1
- package/dist/commands/list/views.js.map +1 -1
- package/dist/commands/seed/plan.js +97 -3
- package/dist/commands/seed/plan.js.map +1 -1
- package/dist/commands/seed/seed.js +7 -2
- package/dist/commands/seed/seed.js.map +1 -1
- package/dist/commands/show/feature.js +2 -1
- package/dist/commands/show/feature.js.map +1 -1
- package/dist/commands/show/marks.js +3 -5
- package/dist/commands/show/marks.js.map +1 -1
- package/dist/commands/show/service.js +39 -18
- package/dist/commands/show/service.js.map +1 -1
- package/dist/commands/subsystem/sync.js +184 -39
- package/dist/commands/subsystem/sync.js.map +1 -1
- package/dist/commands/subsystem/txn/exclude.js +132 -0
- package/dist/commands/subsystem/txn/exclude.js.map +1 -0
- package/dist/commands/subsystem/txn/report.js +124 -0
- package/dist/commands/subsystem/txn/report.js.map +1 -0
- package/dist/commands/subsystem/txn/txn.js +29 -3
- package/dist/commands/subsystem/txn/txn.js.map +1 -1
- package/dist/commands/subsystem/txn/views.js +113 -16
- package/dist/commands/subsystem/txn/views.js.map +1 -1
- package/dist/commands/validate/arch-coverage.js +16 -9
- package/dist/commands/validate/arch-coverage.js.map +1 -1
- package/dist/commands/validate/checks/fleet-shape.js +30 -77
- package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
- package/dist/commands/validate/checks/vocabulary.js +4 -19
- package/dist/commands/validate/checks/vocabulary.js.map +1 -1
- package/dist/commands/validate/feature.js +1 -1
- package/dist/commands/validate/feature.js.map +1 -1
- package/dist/commands/validate/fleet/census.js +11 -10
- package/dist/commands/validate/fleet/census.js.map +1 -1
- package/dist/commands/validate/fleet/landscape.js +128 -114
- package/dist/commands/validate/fleet/landscape.js.map +1 -1
- package/dist/commands/validate/fleet/load.js +66 -0
- package/dist/commands/validate/fleet/load.js.map +1 -1
- package/dist/commands/validate/fleet/map/attest.js +103 -0
- package/dist/commands/validate/fleet/map/attest.js.map +1 -0
- package/dist/commands/validate/fleet/map/bindings.js +96 -0
- package/dist/commands/validate/fleet/map/bindings.js.map +1 -0
- package/dist/commands/validate/fleet/map/consumers.js +298 -0
- package/dist/commands/validate/fleet/map/consumers.js.map +1 -0
- package/dist/commands/validate/fleet/map/isolation.js +116 -0
- package/dist/commands/validate/fleet/map/isolation.js.map +1 -0
- package/dist/commands/validate/fleet/map/usecases.js +56 -0
- package/dist/commands/validate/fleet/map/usecases.js.map +1 -0
- package/dist/commands/validate/fleet/usecases/steps.js +49 -17
- package/dist/commands/validate/fleet/usecases/steps.js.map +1 -1
- package/dist/commands/validate/fleet/views/fleet-project.js +217 -0
- package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -0
- package/dist/commands/validate/fleet/views/ids.js +13 -9
- package/dist/commands/validate/fleet/views/ids.js.map +1 -1
- package/dist/commands/validate/fleet/views/projects.js +278 -0
- package/dist/commands/validate/fleet/views/projects.js.map +1 -0
- package/dist/commands/validate/fleet/views/root-documents.js +97 -0
- package/dist/commands/validate/fleet/views/root-documents.js.map +1 -0
- package/dist/commands/validate/fleet/views/stale.js +2 -2
- package/dist/commands/validate/fleet/views/stale.js.map +1 -1
- package/dist/commands/validate/service/model/diverged.js +226 -0
- package/dist/commands/validate/service/model/diverged.js.map +1 -0
- package/dist/commands/validate/service/model/grade.js +163 -0
- package/dist/commands/validate/service/model/grade.js.map +1 -0
- package/dist/commands/validate/service/service.js +110 -34
- package/dist/commands/validate/service/service.js.map +1 -1
- package/dist/commands/validate/service/specs.js +39 -5
- package/dist/commands/validate/service/specs.js.map +1 -1
- package/dist/commands/validate/service/spine.js +41 -3
- package/dist/commands/validate/service/spine.js.map +1 -1
- package/dist/commands/validate/service/usecases/tags.js +113 -0
- package/dist/commands/validate/service/usecases/tags.js.map +1 -0
- package/dist/commands/validate/service/usecases/usecases.js +209 -0
- package/dist/commands/validate/service/usecases/usecases.js.map +1 -0
- package/dist/commands/validate/validate.js +10 -24
- package/dist/commands/validate/validate.js.map +1 -1
- package/dist/core/agent/agents-md/artifacts.js +1 -0
- package/dist/core/agent/agents-md/artifacts.js.map +1 -1
- package/dist/core/agent/agents-md/command-map.js +18 -2
- package/dist/core/agent/agents-md/command-map.js.map +1 -1
- package/dist/core/agent/agents-md/cycle.js +24 -9
- package/dist/core/agent/agents-md/cycle.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/context.js +4 -3
- package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -1
- package/dist/core/agent/agents-md/map/subsystem.js +51 -2
- package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
- package/dist/core/agent/agents-md/map/usecases.js +45 -3
- package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
- package/dist/core/agent/agents-md.js +14 -4
- package/dist/core/agent/agents-md.js.map +1 -1
- package/dist/core/agent/support/report.js +39 -5
- package/dist/core/agent/support/report.js.map +1 -1
- package/dist/core/agent/workflows/adopt.js +125 -6
- package/dist/core/agent/workflows/adopt.js.map +1 -1
- package/dist/core/agent/workflows/check.js +35 -19
- package/dist/core/agent/workflows/check.js.map +1 -1
- package/dist/core/agent/workflows/closing.js +3 -1
- package/dist/core/agent/workflows/closing.js.map +1 -1
- package/dist/core/brief/brief.js +63 -11
- package/dist/core/brief/brief.js.map +1 -1
- package/dist/core/brief/checks.js +138 -3
- package/dist/core/brief/checks.js.map +1 -1
- package/dist/core/brief/landscape.js +212 -129
- package/dist/core/brief/landscape.js.map +1 -1
- package/dist/core/brief/map/extend.js +81 -0
- package/dist/core/brief/map/extend.js.map +1 -0
- package/dist/core/brief/map/owed.js +284 -0
- package/dist/core/brief/map/owed.js.map +1 -0
- package/dist/core/brief/targets.js +30 -26
- package/dist/core/brief/targets.js.map +1 -1
- package/dist/core/brief/unchecked.js +12 -2
- package/dist/core/brief/unchecked.js.map +1 -1
- package/dist/core/c4/likec4.js +23 -1
- package/dist/core/c4/likec4.js.map +1 -1
- package/dist/core/c4/parsed/deployment.js +5 -3
- package/dist/core/c4/parsed/deployment.js.map +1 -1
- package/dist/core/c4/parsed/view-ids.js +39 -1
- package/dist/core/c4/parsed/view-ids.js.map +1 -1
- package/dist/core/c4/prefetch/fleet.js +142 -0
- package/dist/core/c4/prefetch/fleet.js.map +1 -0
- package/dist/core/c4/project/architecture.js +52 -4
- package/dist/core/c4/project/architecture.js.map +1 -1
- package/dist/core/c4/project/batch.js +140 -0
- package/dist/core/c4/project/batch.js.map +1 -0
- package/dist/core/c4/project/documents.js +9 -4
- package/dist/core/c4/project/documents.js.map +1 -1
- package/dist/core/c4/project/load.js +77 -38
- package/dist/core/c4/project/load.js.map +1 -1
- package/dist/core/c4/project/staged.js +46 -12
- package/dist/core/c4/project/staged.js.map +1 -1
- package/dist/core/c4/resolve/attested.js +0 -0
- package/dist/core/c4/resolve/attested.js.map +1 -0
- package/dist/core/c4/resolve/service.js +8 -17
- package/dist/core/c4/resolve/service.js.map +1 -1
- package/dist/core/c4/resolve/steps.js +18 -0
- package/dist/core/c4/resolve/steps.js.map +1 -1
- package/dist/core/c4/root-project/exclude.js +352 -0
- package/dist/core/c4/root-project/exclude.js.map +1 -0
- package/dist/core/c4/service-model/documents.js +47 -0
- package/dist/core/c4/service-model/documents.js.map +1 -0
- package/dist/core/c4/service-model/fleet/extending.js +82 -0
- package/dist/core/c4/service-model/fleet/extending.js.map +1 -0
- package/dist/core/c4/service-model/load.js +165 -0
- package/dist/core/c4/service-model/load.js.map +1 -0
- package/dist/core/c4/service-model/renderer.js +128 -0
- package/dist/core/c4/service-model/renderer.js.map +1 -0
- package/dist/core/c4/service-model/shape.js +126 -0
- package/dist/core/c4/service-model/shape.js.map +1 -0
- package/dist/core/c4/service-model/slice.js +188 -0
- package/dist/core/c4/service-model/slice.js.map +1 -0
- package/dist/core/c4/source-scan.js +26 -0
- package/dist/core/c4/source-scan.js.map +1 -1
- package/dist/core/c4/splice/authored-source.js +60 -13
- package/dist/core/c4/splice/authored-source.js.map +1 -1
- package/dist/core/c4/splice/contract.js.map +1 -1
- package/dist/core/c4/splice/identity/declared.js +87 -0
- package/dist/core/c4/splice/identity/declared.js.map +1 -0
- package/dist/core/c4/splice/identity/edges.js +17 -0
- package/dist/core/c4/splice/identity/edges.js.map +1 -1
- package/dist/core/c4/splice/identity/existing.js +80 -0
- package/dist/core/c4/splice/identity/existing.js.map +1 -0
- package/dist/core/c4/splice/identity/interior.js +80 -0
- package/dist/core/c4/splice/identity/interior.js.map +1 -0
- package/dist/core/c4/splice/identity/owner.js +50 -0
- package/dist/core/c4/splice/identity/owner.js.map +1 -0
- package/dist/core/c4/splice/landscape-merge.js +154 -100
- package/dist/core/c4/splice/landscape-merge.js.map +1 -1
- package/dist/core/c4/splice/model/merge.js +140 -0
- package/dist/core/c4/splice/model/merge.js.map +1 -0
- package/dist/core/c4/splice/placement.js +14 -4
- package/dist/core/c4/splice/placement.js.map +1 -1
- package/dist/core/c4/workspace.js +2 -1
- package/dist/core/c4/workspace.js.map +1 -1
- package/dist/core/coherence/coherence.js +7 -0
- package/dist/core/coherence/coherence.js.map +1 -1
- package/dist/core/delta/delta.js +9 -0
- package/dist/core/delta/delta.js.map +1 -1
- package/dist/core/deployment/delta.js +4 -0
- package/dist/core/deployment/delta.js.map +1 -1
- package/dist/core/diff/base-state.js +21 -5
- package/dist/core/diff/base-state.js.map +1 -1
- package/dist/core/diff/victims.js +10 -1
- package/dist/core/diff/victims.js.map +1 -1
- package/dist/core/docs.js +31 -22
- package/dist/core/docs.js.map +1 -1
- package/dist/core/doctor/config.js +7 -3
- package/dist/core/doctor/config.js.map +1 -1
- package/dist/core/doctor/doctor.js +48 -9
- package/dist/core/doctor/doctor.js.map +1 -1
- package/dist/core/doctor/report.js.map +1 -1
- package/dist/core/doctor/reports/scan.js +242 -0
- package/dist/core/doctor/reports/scan.js.map +1 -0
- package/dist/core/explain/families.js +1 -1
- package/dist/core/explain/families.js.map +1 -1
- package/dist/core/explain/refusals.js +1 -1
- package/dist/core/explain/refusals.js.map +1 -1
- package/dist/core/explore/explore.js +44 -9
- package/dist/core/explore/explore.js.map +1 -1
- package/dist/core/fleet-context.js +47 -0
- package/dist/core/fleet-context.js.map +1 -1
- package/dist/core/gate/partners.js +5 -9
- package/dist/core/gate/partners.js.map +1 -1
- package/dist/core/kernel/ids/fqn/ancestors.js +65 -0
- package/dist/core/kernel/ids/fqn/ancestors.js.map +1 -0
- package/dist/core/pack/living.js +47 -13
- package/dist/core/pack/living.js.map +1 -1
- package/dist/core/pack/pack.js +9 -6
- package/dist/core/pack/pack.js.map +1 -1
- package/dist/core/projection/arch-slice.js +2 -2
- package/dist/core/projection/arch-slice.js.map +1 -1
- package/dist/core/repo/paths.js +40 -0
- package/dist/core/repo/paths.js.map +1 -1
- package/dist/core/repo/tree/render/projects.js +182 -0
- package/dist/core/repo/tree/render/projects.js.map +1 -0
- package/dist/core/repo/tree/render/stale.js +2 -3
- package/dist/core/repo/tree/render/stale.js.map +1 -1
- package/dist/core/repo/tree/render/views.js +51 -3
- package/dist/core/repo/tree/render/views.js.map +1 -1
- package/dist/core/repo/tree/walk.js +45 -2
- package/dist/core/repo/tree/walk.js.map +1 -1
- package/dist/core/scaffold/landscape.js +219 -9
- package/dist/core/scaffold/landscape.js.map +1 -1
- package/dist/core/staging/writes.js.map +1 -1
- package/dist/core/usecases/delta/claims.js +1 -0
- package/dist/core/usecases/delta/claims.js.map +1 -1
- package/dist/core/usecases/delta/overlay.js +4 -0
- package/dist/core/usecases/delta/overlay.js.map +1 -1
- package/dist/core/usecases/fleet.js +18 -6
- package/dist/core/usecases/fleet.js.map +1 -1
- package/dist/core/usecases/operations.js +7 -0
- package/dist/core/usecases/operations.js.map +1 -1
- package/dist/core/usecases/service/exclude.js +119 -0
- package/dist/core/usecases/service/exclude.js.map +1 -0
- package/dist/core/usecases/service/flows.js +161 -0
- package/dist/core/usecases/service/flows.js.map +1 -0
- package/dist/core/vocabulary/maturity.js +10 -0
- package/dist/core/vocabulary/maturity.js.map +1 -1
- package/examples/README.md +23 -0
- package/examples/docs/architecture/landscape.likec4 +41 -0
- package/examples/docs/architecture/usecases/order-notification.likec4 +5 -2
- package/examples/docs/likec4.config.json +0 -1
- package/examples/docs/services/checkout-web/model.likec4 +39 -56
- package/examples/docs/services/order-service/arch.spec.md +13 -6
- package/examples/docs/services/order-service/model.likec4 +53 -67
- package/examples/docs/services/payment-service/arch.spec.md +7 -4
- package/examples/docs/services/payment-service/model.likec4 +32 -33
- package/examples/docs/services/platform/identity-service/arch.spec.md +7 -4
- package/examples/docs/services/platform/identity-service/model.likec4 +29 -33
- package/examples/docs/services/platform/notification-service/health.yaml +8 -3
- package/examples/docs/services/platform/notification-service/model.likec4 +39 -59
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projects.js","sourceRoot":"","sources":["../../../../../src/commands/validate/fleet/views/projects.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The four ways a service's `model.likec4` and the renderer's project files can\n * disagree, graded as one warning per service on the fleet target — plus the\n * one way the same root config can hide the fleet MAP, graded once for the\n * fleet (`landscape.excluded`, at the foot of this module).\n *\n * They are one walk because they are one question — \"can the renderer actually\n * show what this model says?\" — asked of the two shapes a model has. A model\n * that STANDS ALONE (declares its own element kinds) is parsed alone: it needs a\n * project file of its own, and the root project must exclude its directory. A\n * model that EXTENDS the fleet map declares no kinds and lives IN the root\n * project: it must NOT be excluded, and a project file beside it TAKES it out of\n * there — measured at the 1.59.2 pin, the nested project claims the model, which\n * then does not resolve (the map's kinds are not in there with it) and the fleet\n * project loses the service's interior. Each shape therefore owes the opposite of\n * the other, and each of the four codes below is one half of one of those pairs:\n *\n * - `service.likec4-config-missing` — standalone, no project file.\n * - `service.likec4-config-stray` — extending, project file present.\n * - `service.model-excluded` — extending, root `exclude` covers it. Two\n * questions, not one: the DIRECTORY (`excludingEntry`, which `subsystem sync`\n * can repair) and, where that says no, the model FILE (`excludingPath`, which\n * it cannot — the message says so).\n * - `service.model-unexcluded` — standalone, root `exclude` does not.\n *\n * `landscape.excluded` is the fifth, and it is the same question asked one\n * level up: the root `exclude` covering `architecture/landscape.likec4` itself.\n *\n * WHY WARNINGS. Every fact loam grades about a model — its parse, its bindings,\n * its edges into the spine — holds exactly as it did in all five states, so\n * nothing here may gate. The fifth is only true because the architecture loader\n * keeps the map whatever the root `exclude` says (`core/c4/project/architecture.ts`);\n * before it did, an excluded map blanked every cross-service grade in the run. ROADMAP's clause that rendering \"must stay outside\n * validation so view computation cannot slow or change the gate\" is about\n * computing views; this check computes nothing: one `existsSync` per service and\n * one read of the root config. And the sibling that grades the ROOT file's\n * absence (`doctor.likec4-config-missing`, `core/doctor/doctor.ts`) calls a repo\n * without it \"only unrenderable\" and warns; these are the same condition one\n * directory down and earn the same severity.\n *\n * WHY `validate --all` AND NOT `doctor`. `doctor` is the step-0 preflight of the\n * adopt protocol — wiring, the config, the fleet map's readability — and the\n * protocol's step 5 is the `validate --all` run whose findings the agent\n * branches on. The report that motivated the first of these codes had its own\n * `doctor --json` answer `healthy: true` with 0 findings on a tree where every\n * adopted model was unrenderable from the root: the model is written in step 3,\n * so its renderability is a fact that first exists at step 5, and a preflight\n * that ran before the model did could not have said otherwise. Never in\n * `validate --service`, `status` or `doctor`: the writer is a fleet verb\n * (`subsystem sync`) and the findings belong beside the files that verb owns.\n *\n * WHY PER SERVICE. `subject` is the service id so a `--json` worklist filters by\n * team the way `landscape.service-unmodelled` already lets it — one fleet\n * finding listing every gap would have to be re-parsed to find out whose it is.\n * The `scope` location is the service's tree path, through `serviceTreePath` on\n * the ENTRY the survey hands back — never a join by id: these grades are emitted\n * before the map's early returns precisely so they hold on a broken tree, and a\n * tree with two services sharing a leaf name under different subsystems\n * (`subsystem.name-collision`) is exactly such a tree, still enumerated in full.\n * Joined by id, both findings named the first directory while `subsystem sync`\n * wrote both files at the right paths.\n *\n * WHY THE ROOT GATE, and it does double duty. Semantically: `subsystem sync`\n * writes nothing — no per-service file, no rewritten `exclude` — until the root\n * `likec4.config.json` exists (the docs root is then a LikeC4 workspace and a\n * nested project is registered inside it — measured at the 1.59.2 pin), so a\n * repo without the root file has `doctor` telling it to write that first and\n * nothing to sync into; grading a gap the writer would not fill is a loop no\n * command can clear. The same rule silences a directory whose name is not a\n * legal service id: the survey skips it (`render/projects.ts` says why),\n * `service.id-invalid` already names it, and a warning here would advertise a\n * repair `sync` refuses to perform. Fixture-wise: `makeProject` writes NO root\n * file, so every existing fixture stays silent under this gate, and the suites\n * that pin a fleet's warning set exactly (`test/examples.test.ts`,\n * `test/self-model.test.ts`) ship the files their shapes call for and keep their\n * counts.\n *\n * The exclude half has a SECOND gate, inside `readRootExclude`: a root config\n * loam cannot read an `exclude` list out of answers `null`, and both grades that\n * read it stay silent. loam does not know what the renderer will do with a file\n * it could not parse, and asserting an exclusion on evidence it does not have is\n * worse than saying nothing.\n */\nimport { existsSync } from \"node:fs\";\nimport { relative, resolve } from \"node:path\";\nimport type { ModelShape } from \"../../../../core/c4/service-model/shape.js\";\nimport { excludingEntry, excludingPath, readRootExclude } from \"../../../../core/c4/root-project/exclude.js\";\nimport { serviceTreePath, type DocsDir } from \"../../../../core/kernel/ids/dirs.js\";\nimport type { ServiceEntry } from \"../../../../core/repo/entries.js\";\nimport { ARTIFACT_FILES, landscapePath, LIKEC4_PROJECT_FILENAME, rootProjectPath, serviceRenderPaths } from \"../../../../core/repo/paths.js\";\nimport { surveyProjects } from \"../../../../core/repo/tree/render/projects.js\";\nimport type { Finding } from \"../../../../core/vocabulary/report.js\";\n\n/**\n * The five grades, and the one FACT out of them a second check has to branch on.\n *\n * `mapExcluded` is `landscape.excluded`'s own condition, handed out rather than\n * re-derived: `c4.fleet-project-invalid` loads the fleet project the way the\n * RENDERER does — without the architecture loader's floor under the map — so the\n * same entry that hides the map makes every extending model's `extend` fail to\n * resolve, and one bad line in `exclude` came back as 161 warnings on a\n * five-service fleet. Two spellings of \"is the map excluded\" would be two\n * chances for the cause and its cascade to disagree about whether they are\n * looking at the same state, which is exactly the pairing the boolean exists to\n * keep.\n */\nexport interface ProjectGrade {\n findings: Finding[];\n /** True when the root `exclude` covers `architecture/landscape.likec4` — see above. */\n mapExcluded: boolean;\n}\n\n/**\n * `entries` is the fleet enumeration at whatever depth each service lives, and\n * `shapes` is the shape of every model in it, keyed by resolved path\n * (`FleetContext.modelShapes`). The shape map is built over the FULL enumeration\n * rather than a `--base` narrowing: a model missing from it would read as\n * standalone, which is the wrong half of every pair above.\n *\n * The predicate handed into the survey is the ONE spelling of \"has a model,\n * lacks a project file\" that `subsystem sync` also fills, so the grader can\n * never name a gap the writer would leave.\n */\nexport async function projectFindings(\n docsDir: DocsDir,\n entries: readonly ServiceEntry[],\n shapes: ReadonlyMap<string, ModelShape>,\n): Promise<ProjectGrade> {\n // No root project file, no root project — and no `exclude` list either, so the\n // map is not excluded by anything loam can see. `false` here is a fact, not a\n // default: `readRootExclude` would answer `null` one line down and every grade\n // below it stays silent for the same reason.\n if (!existsSync(rootProjectPath(docsDir))) return { findings: [], mapExcluded: false };\n const survey = surveyProjects(entries, serviceRenderPaths, (model) => shapes.get(resolve(model)) !== \"extending\");\n const exclude = await readRootExclude(docsDir);\n const findings: Finding[] = [];\n // The ONE derivation. The boolean is read off the finding rather than computed\n // beside it, so a fleet where `c4.fleet-project-invalid` is suspended is by\n // construction a fleet where `landscape.excluded` says why.\n const excluded = mapExcluded(docsDir, exclude);\n findings.push(...excluded);\n for (const model of survey.models) {\n const tree = serviceTreePath(model.service);\n // Everything the four share, so each push below is its code and its\n // sentence and nothing else. `scope`, not `primary`: two of the four are\n // about a file that does not exist yet, and the actionable place is the\n // directory either way.\n const at = {\n severity: \"warn\" as const,\n subject: model.service.id,\n locations: [{ path: tree, role: \"scope\" as const }],\n };\n if (model.standalone && !model.configured) {\n findings.push({\n ...at,\n code: \"service.likec4-config-missing\",\n message:\n `${model.service.id}: ${tree}/${ARTIFACT_FILES.model} exists and ${tree}/${LIKEC4_PROJECT_FILENAME} does not — ` +\n \"a model that stands alone (declares its own `specification`) renders only as a project of its own, \" +\n \"and nothing renders it from the docs root now. Run `loam subsystem sync`; it writes the file (create-only) — never write it by hand.\",\n });\n }\n if (!model.standalone && model.configured) {\n findings.push({\n ...at,\n code: \"service.likec4-config-stray\",\n message:\n `${model.service.id}: ${tree}/${ARTIFACT_FILES.model} extends the fleet map and ${tree}/${LIKEC4_PROJECT_FILENAME} ` +\n \"sits beside it — the renderer registers that file as a project of its own rooted at that directory, so \" +\n \"the docs root stops being a one-project workspace (`likec4 validate .` then refuses without `--project`), \" +\n \"and wherever that nested project claims the model it parses alone, out of the root project where the \" +\n \"map's kinds are: the model does not resolve and `export json --project fleet` loses this service's \" +\n \"containers (measured, likec4 1.59.2 — which of the two happens depends on the workspace, and neither is \" +\n \"a state this file buys anything in). Run `loam subsystem sync`; it deletes the file (or delete it \" +\n \"yourself). loam never writes one beside an extending model\",\n });\n }\n // `excludingEntry` reports the entry VERBATIM because the repair is to\n // delete or narrow THAT line, and a fleet that files its services under\n // subsystems is excluded by `services/platform/**` rather than by anything\n // naming the service — a message that did not quote the entry would send\n // its reader looking for a line that is not there.\n const entry = exclude === null ? null : excludingEntry(exclude, tree);\n // THE SECOND QUESTION, and the state that was silent until it was asked:\n // an entry shaped like a FILE hides the model without covering the\n // directory. `services/**/model.likec4` and the bare `**/model.likec4` each\n // took every extending model out of the renderer on `examples/docs` — 3\n // source files of 8, five drill-down views gone from the export — with the\n // whole run reporting 0 errors and not one line containing the word\n // \"exclude\" (re-verification 2026-09-04, area C item 5). It is asked ONLY\n // where the directory question said no, because that arm names an entry\n // `subsystem sync` can take back and this one names an entry it cannot.\n const fileEntry =\n exclude === null || entry !== null ? null : excludingPath(exclude, `${tree}/${ARTIFACT_FILES.model}`);\n const hiding = entry ?? fileEntry;\n if (exclude !== null && !model.standalone && hiding !== null) {\n findings.push({\n ...at,\n code: \"service.model-excluded\",\n message:\n `${model.service.id}: ${tree}/${ARTIFACT_FILES.model} extends the fleet map and the root ` +\n `${LIKEC4_PROJECT_FILENAME} excludes it ('${hiding}') — the renderer never loads it, so the service is ` +\n \"a box with nothing inside it from the docs root. \" +\n (entry === null\n ? // The repair is an EDIT, not a command, and saying otherwise is the\n // loop `landscape.excluded` already refuses to open: sync recomputes\n // only the `services/<tree>/**` entries and would report success\n // having changed nothing.\n \"That entry hides the model file without covering \" +\n `${tree}/, so \\`loam subsystem sync\\` cannot repair it: sync maintains only the ` +\n \"`services/` directory entries and leaves every other line exactly as the team wrote it. Narrow that \" +\n \"entry to the paths it was written for, or delete it\"\n : \"Run `loam subsystem sync`: it rewrites the root project's `exclude` to cover exactly the models \" +\n \"that stand alone\"),\n });\n }\n if (exclude !== null && model.standalone && entry === null) {\n findings.push({\n ...at,\n code: \"service.model-unexcluded\",\n message:\n `${model.service.id}: ${tree}/${ARTIFACT_FILES.model} stands alone (declares its own specification) and ` +\n `the root ${LIKEC4_PROJECT_FILENAME} does not exclude ${tree}/ — the renderer merges it into the map ` +\n \"and reports every kind and element it declares as a duplicate, blanking the whole root project. \" +\n `Run \\`loam subsystem sync\\`: it adds ${tree}/** to the root project's \\`exclude\\``,\n });\n }\n }\n return { findings, mapExcluded: excluded.length > 0 };\n}\n\n/**\n * `landscape.excluded` — the root `exclude` covers the fleet MAP itself.\n *\n * WHY IT EXISTS AT ALL, and why only now. The architecture loader used to apply\n * the root `exclude` with no floor under it, so an entry covering\n * `architecture/landscape.likec4` — an `architecture/*.likec4` written for a\n * palette, say — left that project EMPTY, and an empty project parses with zero\n * errors. Every reader downstream took that for \"the map declares nothing\":\n * `landscape.service-unmodelled` on every service, `c4.invalid` on every\n * extending model, `subsystem sync` cutting a good generated file back to a\n * title (verification 2026-09-04, review C). The loader now keeps the map\n * whatever the list says, so loam has stopped asserting those falsehoods — but\n * the RENDERER still applies the entry, and it has no floor. Nothing told the\n * author that the fleet map they are grading is a map nobody can open. This is\n * that sentence, and it is the only finding in the run that names it.\n *\n * WARN, NEVER GATING, for the reason the module banner gives: with the loader's\n * floor in place, every fact loam grades holds identically either way. The gap\n * is a renderer gap, exactly like the four per-service grades above.\n *\n * THE SAME ROOT GATE covers it — the caller returns early with no root\n * `likec4.config.json`, and a config loam cannot read an `exclude` list out of\n * answers `null`, which says nothing rather than guessing. It is gated on the\n * map EXISTING as well: with no file there, `landscape.missing` is the finding\n * and an entry covering a path nobody wrote is not a repair anyone owes.\n *\n * `excludingPath`, not `excludingEntry`: the map is a FILE, and this must fire\n * exactly when `architectureProjectDocuments` would have dropped it — same\n * list, same matcher, same argument. Asking the directory question instead\n * would answer for `architecture/` as a whole and miss `architecture/*.likec4`,\n * which is the spelling that caused the defect.\n *\n * The repair is NOT `loam subsystem sync`, and the message says so where the\n * four grades above name it: sync recomputes only the `services/<tree>/**`\n * entries and leaves every other line in the order the team wrote it, so it\n * would report success and change nothing. The four had a writer; this one has\n * an author.\n *\n * AND IT SUSPENDS `c4.fleet-project-invalid`, which is the cascade discipline\n * `permissions.invalid` and `capability.invalid` already state: a grade that\n * RESOLVES against a document nobody loaded is not a diagnosis. That check\n * builds the fleet project the way the renderer does — no floor under the map —\n * so the very entry named here takes the map out of it and every `extend` in\n * every extending model then resolves against nothing. Measured on\n * `examples/docs` with `architecture/*.likec4` in the list: one\n * `landscape.excluded` and **161** `c4.fleet-project-invalid`, all of them one\n * line's fault. The message carries that clause so the reader knows the fleet\n * project is unread rather than clean, and the caller reads the same fact off\n * `ProjectGrade.mapExcluded` — see `./fleet-project.ts`'s `mapExcluded` gate.\n */\nfunction mapExcluded(docsDir: DocsDir, exclude: readonly string[] | null): Finding[] {\n const map = landscapePath(docsDir);\n if (exclude === null || !existsSync(map)) return [];\n const rel = relative(docsDir, map).split(/[\\\\/]/).join(\"/\");\n const entry = excludingPath(exclude, rel);\n if (entry === null) return [];\n return [\n {\n severity: \"warn\",\n code: \"landscape.excluded\",\n // Quoted VERBATIM, for `service.model-excluded`'s reason one level down:\n // the repair is to edit THAT line, and an entry loam re-spelled is a line\n // its author would go looking for and not find.\n message:\n `landscape: the root ${LIKEC4_PROJECT_FILENAME}'s \\`exclude\\` covers ${rel} ('${entry}') — the renderer never ` +\n \"loads the fleet map, so it draws no map at all and every model that EXTENDS the map resolves against \" +\n \"nothing in the root project, while loam goes on grading the map it reads from disk. `c4.fleet-project-invalid` \" +\n \"is NOT graded while this entry stands: that check loads the fleet project the way the renderer does, so it \" +\n \"would report the same one line as an unresolved reference in every extending model rather than as this. \" +\n \"Remove that entry, or \" +\n `narrow it to the paths it was written for, and the fleet project is graded again. \\`loam subsystem sync\\` ` +\n \"will not do it: that command maintains \" +\n \"only the `services/` entries in the list and leaves every other line exactly as the team wrote it\",\n locations: [{ path: rel, role: \"primary\" }],\n },\n ];\n}\n"]}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The documents the RENDERER's root project loads, enumerated the way the
|
|
3
|
+
* renderer enumerates them: everything under the docs root that the root
|
|
4
|
+
* `likec4.config.json` does not exclude.
|
|
5
|
+
*
|
|
6
|
+
* It exists because `./fleet-project.ts` used to build the set BY SHAPE — the
|
|
7
|
+
* `architecture/` project, each extending `model.likec4`, and every `.likec4`
|
|
8
|
+
* inside a service directory — and that is three roots, not a tree. A `.likec4`
|
|
9
|
+
* one directory ABOVE a service (`services/platform/notes.likec4`, the parent of
|
|
10
|
+
* service directories) was in none of them: `loam validate --all` came back
|
|
11
|
+
* byte-identical to baseline and never named the file, while `npx likec4
|
|
12
|
+
* validate --project fleet .` reported Invalid over nine files and thirteen
|
|
13
|
+
* errors (verification 2026-09-04, R2). The same file one directory lower was a
|
|
14
|
+
* loud error. SCHEMA, `loam-codes` and `explain` all said loam loads "every
|
|
15
|
+
* `.likec4` the root project reads"; this is that sentence, executed.
|
|
16
|
+
*
|
|
17
|
+
* FOUR THINGS COME OUT, and each is a rule rather than a filter of convenience:
|
|
18
|
+
*
|
|
19
|
+
* - The GENERATED `architecture/subsystems.likec4`, for `c4/project/documents.ts`'s
|
|
20
|
+
* reason — docs/DESIGN.md rule 26 makes its staleness a byte compare, and a
|
|
21
|
+
* stale one naming a removed element would blank this load and report a
|
|
22
|
+
* cascade whose repair is `loam subsystem sync`.
|
|
23
|
+
* - Anything the root `exclude` covers, read through `c4/root-project/exclude.ts`
|
|
24
|
+
* — the renderer does not load it, so grading it would be loam reporting a
|
|
25
|
+
* failure nobody can see. A root config loam cannot read an `exclude` list out
|
|
26
|
+
* of covers nothing: guessing an exclusion is worse than loading one document
|
|
27
|
+
* too many.
|
|
28
|
+
* - Every STANDALONE service directory. Such a model declares its own kinds, so
|
|
29
|
+
* the root project must exclude it and `service.model-unexcluded` is the grade
|
|
30
|
+
* for a root that does not — one finding naming the entry to add, rather than
|
|
31
|
+
* a duplicate-kind cascade under a second code.
|
|
32
|
+
* - `features/`. A feature's `delta.likec4` carries a `specification` block of
|
|
33
|
+
* its own and the directory is transient — the scaffolded root config excludes
|
|
34
|
+
* it for exactly that reason, and a fleet mid-feature would otherwise read as
|
|
35
|
+
* a fleet whose map is declared twice. loam drops it whether or not the entry
|
|
36
|
+
* is still there; `loam validate --feature` is where a delta is graded.
|
|
37
|
+
*
|
|
38
|
+
* Dot-directories and `node_modules` are not walked at all. Those are loam's own
|
|
39
|
+
* machinery (`.loam-before` holds a verbatim copy of the landscape, which would
|
|
40
|
+
* read as the map declared twice) and a package tree, never authored documents —
|
|
41
|
+
* the same thing the scaffold's own `node_modules` glob says.
|
|
42
|
+
*/
|
|
43
|
+
import { readdir } from "node:fs/promises";
|
|
44
|
+
import { join, relative, resolve, sep } from "node:path";
|
|
45
|
+
import { excludingPath, readRootExclude } from "../../../../core/c4/root-project/exclude.js";
|
|
46
|
+
import { subsystemViewsPath } from "../../../../core/repo/paths.js";
|
|
47
|
+
/** The directory name loam never walks, beside every dot-directory. */
|
|
48
|
+
const PACKAGES = "node_modules";
|
|
49
|
+
/** The one subtree loam drops whatever the root config says — see the banner. */
|
|
50
|
+
const FEATURES = "features";
|
|
51
|
+
/**
|
|
52
|
+
* Every `.likec4` the renderer's root project reads and loam can grade, absolute
|
|
53
|
+
* and sorted. `standalone` is each standalone service's directory, absolute.
|
|
54
|
+
*/
|
|
55
|
+
export async function rootProjectDocuments(docsDir, standalone) {
|
|
56
|
+
const exclude = await readRootExclude(docsDir);
|
|
57
|
+
const generated = resolve(subsystemViewsPath(docsDir));
|
|
58
|
+
const excluded = standalone.map((dir) => `${resolve(dir)}${sep}`);
|
|
59
|
+
const root = resolve(docsDir);
|
|
60
|
+
const found = [];
|
|
61
|
+
const keeps = (path) => {
|
|
62
|
+
if (resolve(path) === generated)
|
|
63
|
+
return false;
|
|
64
|
+
if (excluded.some((dir) => resolve(path).startsWith(dir)))
|
|
65
|
+
return false;
|
|
66
|
+
const rel = relative(root, path).split(/[\\/]/);
|
|
67
|
+
if (rel[0] === FEATURES)
|
|
68
|
+
return false;
|
|
69
|
+
return exclude === null || excludingPath(exclude, rel.join("/")) === null;
|
|
70
|
+
};
|
|
71
|
+
const walk = async (at) => {
|
|
72
|
+
let entries;
|
|
73
|
+
try {
|
|
74
|
+
entries = await readdir(at, { withFileTypes: true });
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// A directory that cannot be read contributes nothing: this grade runs
|
|
78
|
+
// inside `validate --all` over a whole fleet, and the one behaviour it may
|
|
79
|
+
// never have is failing the run it was added to.
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
const path = join(at, entry.name);
|
|
84
|
+
if (entry.isDirectory()) {
|
|
85
|
+
if (entry.name.startsWith(".") || entry.name === PACKAGES)
|
|
86
|
+
continue;
|
|
87
|
+
await walk(path);
|
|
88
|
+
}
|
|
89
|
+
else if (entry.name.endsWith(".likec4") && keeps(path)) {
|
|
90
|
+
found.push(path);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
await walk(root);
|
|
95
|
+
return found.sort();
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=root-documents.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"root-documents.js","sourceRoot":"","sources":["../../../../../src/commands/validate/fleet/views/root-documents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,6CAA6C,CAAC;AAE7F,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AAEpE,uEAAuE;AACvE,MAAM,QAAQ,GAAG,cAAc,CAAC;AAEhC,iFAAiF;AACjF,MAAM,QAAQ,GAAG,UAAU,CAAC;AAE5B;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,OAAgB,EAAE,UAA6B;IACxF,MAAM,OAAO,GAAG,MAAM,eAAe,CAAC,OAAO,CAAC,CAAC;IAC/C,MAAM,SAAS,GAAG,OAAO,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC,CAAC;IACvD,MAAM,QAAQ,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,EAAE,CAAC,CAAC;IAClE,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9B,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,KAAK,GAAG,CAAC,IAAY,EAAW,EAAE;QACtC,IAAI,OAAO,CAAC,IAAI,CAAC,KAAK,SAAS;YAAE,OAAO,KAAK,CAAC;QAC9C,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;YAAE,OAAO,KAAK,CAAC;QACxE,MAAM,GAAG,GAAG,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAChD,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;QACtC,OAAO,OAAO,KAAK,IAAI,IAAI,aAAa,CAAC,OAAO,EAAE,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,IAAI,CAAC;IAC5E,CAAC,CAAC;IACF,MAAM,IAAI,GAAG,KAAK,EAAE,EAAU,EAAiB,EAAE;QAC/C,IAAI,OAAO,CAAC;QACZ,IAAI,CAAC;YACH,OAAO,GAAG,MAAM,OAAO,CAAC,EAAE,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QACvD,CAAC;QAAC,MAAM,CAAC;YACP,uEAAuE;YACvE,2EAA2E;YAC3E,iDAAiD;YACjD,OAAO;QACT,CAAC;QACD,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;YAClC,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;gBACxB,IAAI,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ;oBAAE,SAAS;gBACpE,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC;YACnB,CAAC;iBAAM,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;gBACzD,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACnB,CAAC;QACH,CAAC;IACH,CAAC,CAAC;IACF,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC;IACjB,OAAO,KAAK,CAAC,IAAI,EAAE,CAAC;AACtB,CAAC","sourcesContent":["/**\n * The documents the RENDERER's root project loads, enumerated the way the\n * renderer enumerates them: everything under the docs root that the root\n * `likec4.config.json` does not exclude.\n *\n * It exists because `./fleet-project.ts` used to build the set BY SHAPE — the\n * `architecture/` project, each extending `model.likec4`, and every `.likec4`\n * inside a service directory — and that is three roots, not a tree. A `.likec4`\n * one directory ABOVE a service (`services/platform/notes.likec4`, the parent of\n * service directories) was in none of them: `loam validate --all` came back\n * byte-identical to baseline and never named the file, while `npx likec4\n * validate --project fleet .` reported Invalid over nine files and thirteen\n * errors (verification 2026-09-04, R2). The same file one directory lower was a\n * loud error. SCHEMA, `loam-codes` and `explain` all said loam loads \"every\n * `.likec4` the root project reads\"; this is that sentence, executed.\n *\n * FOUR THINGS COME OUT, and each is a rule rather than a filter of convenience:\n *\n * - The GENERATED `architecture/subsystems.likec4`, for `c4/project/documents.ts`'s\n * reason — docs/DESIGN.md rule 26 makes its staleness a byte compare, and a\n * stale one naming a removed element would blank this load and report a\n * cascade whose repair is `loam subsystem sync`.\n * - Anything the root `exclude` covers, read through `c4/root-project/exclude.ts`\n * — the renderer does not load it, so grading it would be loam reporting a\n * failure nobody can see. A root config loam cannot read an `exclude` list out\n * of covers nothing: guessing an exclusion is worse than loading one document\n * too many.\n * - Every STANDALONE service directory. Such a model declares its own kinds, so\n * the root project must exclude it and `service.model-unexcluded` is the grade\n * for a root that does not — one finding naming the entry to add, rather than\n * a duplicate-kind cascade under a second code.\n * - `features/`. A feature's `delta.likec4` carries a `specification` block of\n * its own and the directory is transient — the scaffolded root config excludes\n * it for exactly that reason, and a fleet mid-feature would otherwise read as\n * a fleet whose map is declared twice. loam drops it whether or not the entry\n * is still there; `loam validate --feature` is where a delta is graded.\n *\n * Dot-directories and `node_modules` are not walked at all. Those are loam's own\n * machinery (`.loam-before` holds a verbatim copy of the landscape, which would\n * read as the map declared twice) and a package tree, never authored documents —\n * the same thing the scaffold's own `node_modules` glob says.\n */\nimport { readdir } from \"node:fs/promises\";\nimport { join, relative, resolve, sep } from \"node:path\";\nimport { excludingPath, readRootExclude } from \"../../../../core/c4/root-project/exclude.js\";\nimport type { DocsDir } from \"../../../../core/kernel/ids/dirs.js\";\nimport { subsystemViewsPath } from \"../../../../core/repo/paths.js\";\n\n/** The directory name loam never walks, beside every dot-directory. */\nconst PACKAGES = \"node_modules\";\n\n/** The one subtree loam drops whatever the root config says — see the banner. */\nconst FEATURES = \"features\";\n\n/**\n * Every `.likec4` the renderer's root project reads and loam can grade, absolute\n * and sorted. `standalone` is each standalone service's directory, absolute.\n */\nexport async function rootProjectDocuments(docsDir: DocsDir, standalone: readonly string[]): Promise<string[]> {\n const exclude = await readRootExclude(docsDir);\n const generated = resolve(subsystemViewsPath(docsDir));\n const excluded = standalone.map((dir) => `${resolve(dir)}${sep}`);\n const root = resolve(docsDir);\n const found: string[] = [];\n const keeps = (path: string): boolean => {\n if (resolve(path) === generated) return false;\n if (excluded.some((dir) => resolve(path).startsWith(dir))) return false;\n const rel = relative(root, path).split(/[\\\\/]/);\n if (rel[0] === FEATURES) return false;\n return exclude === null || excludingPath(exclude, rel.join(\"/\")) === null;\n };\n const walk = async (at: string): Promise<void> => {\n let entries;\n try {\n entries = await readdir(at, { withFileTypes: true });\n } catch {\n // A directory that cannot be read contributes nothing: this grade runs\n // inside `validate --all` over a whole fleet, and the one behaviour it may\n // never have is failing the run it was added to.\n return;\n }\n for (const entry of entries) {\n const path = join(at, entry.name);\n if (entry.isDirectory()) {\n if (entry.name.startsWith(\".\") || entry.name === PACKAGES) continue;\n await walk(path);\n } else if (entry.name.endsWith(\".likec4\") && keeps(path)) {\n found.push(path);\n }\n }\n };\n await walk(root);\n return found.sort();\n}\n"]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { subsystemViewsPath } from "../../../../core/repo/paths.js";
|
|
2
2
|
import { viewsState } from "../../../../core/repo/tree/render/stale.js";
|
|
3
|
-
export async function viewsStaleFindings(docsDir, tree,
|
|
4
|
-
const { actual, expected, agrees } = await viewsState(subsystemViewsPath(docsDir), tree,
|
|
3
|
+
export async function viewsStaleFindings(docsDir, tree, map) {
|
|
4
|
+
const { actual, expected, agrees } = await viewsState(subsystemViewsPath(docsDir), tree, map);
|
|
5
5
|
if (agrees)
|
|
6
6
|
return [];
|
|
7
7
|
const state = actual === null
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stale.js","sourceRoot":"","sources":["../../../../../src/commands/validate/fleet/views/stale.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"stale.js","sourceRoot":"","sources":["../../../../../src/commands/validate/fleet/views/stale.ts"],"names":[],"mappings":"AAuCA,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AACpE,OAAO,EAAE,UAAU,EAAE,MAAM,4CAA4C,CAAC;AAKxE,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,OAAgB,EAChB,IAAe,EACf,GAAa;IAEb,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,UAAU,CAAC,kBAAkB,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC;IAC9F,IAAI,MAAM;QAAE,OAAO,EAAE,CAAC;IACtB,MAAM,KAAK,GACT,MAAM,KAAK,IAAI;QACb,CAAC,CAAC,uDAAuD;QACzD,CAAC,CAAC,QAAQ,KAAK,IAAI;YACjB,CAAC,CAAC,4EAA4E;YAC9E,CAAC,CAAC,yBAAyB,CAAC;IAClC,OAAO;QACL;YACE,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,uBAAuB;YAC7B,OAAO,EAAE,gCAAgC;YACzC,wEAAwE;YACxE,uEAAuE;YACvE,wDAAwD;YACxD,qEAAqE;YACrE,sEAAsE;YACtE,sEAAsE;YACtE,mCAAmC;YACnC,SAAS,EAAE;gBACT,MAAM,KAAK,IAAI;oBACb,CAAC,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,OAAgB,EAAE;oBAClD,CAAC,CAAC,EAAE,IAAI,EAAE,gCAAgC,EAAE,IAAI,EAAE,SAAkB,EAAE;aACzE;YACD,OAAO,EACL,8CAA8C,KAAK,KAAK;gBACxD,2GAA2G;gBAC3G,oEAAoE;SACvE;KACF,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Staleness of the GENERATED `architecture/subsystems.likec4` against the\n * tree: exactly ONE error on exactly one file, repaired by exactly one\n * command (`loam subsystem sync`).\n *\n * A content compare, deliberately — never a parse. A views-only document does\n * not parse standalone (its `include` lines reference elements the landscape\n * defines; the spike in `core/repo/tree/render/views.ts` records it), and no check\n * anywhere reads the file's MEANING: the file is a scoping convenience for\n * the LikeC4 renderer, so the only question loam owes it is \"is this what the\n * tree renders to\". Rule 26 leaves this untouched: what loam may\n * read is a `dynamic view`'s declared steps out of a document it already\n * parses — never a static view's `include` predicates, and never this\n * generated file at all. Absent counts as a state of its own: a fleet\n * with subsystems and no file is stale, and a fleet with NO subsystems and a\n * leftover file is stale too — the render contract says the file must then\n * not exist, or a group deleted months ago keeps a view forever.\n *\n * The compare is over CONTENT, not BYTES, and `viewsAgree` in\n * `core/repo/tree/render/views.ts` both performs it and records why: an ordinary\n * Windows clone hands this file back with CRLF and not one fact in it changed,\n * and under a byte compare the advertised repair is a loop rather than a\n * diagnosis. The rule lives beside the generator because the module that mints\n * the bytes is the one that owes an answer about them.\n *\n * Graded only when the landscape PARSED (the caller holds that gate): the\n * expected bytes resolve members through the landscape's element→service\n * join, and grading them against a map that is missing or unreadable would\n * cascade a second error behind `landscape.missing`/`landscape.invalid`,\n * pointing at a file whose repair command would only re-derive the same\n * incomplete answer.\n *\n * The map arrives as the parsed document's facts — its elements AND the\n * global style ids it declares — because the expected bytes now depend on\n * both: a view references `global style subsystems` exactly when the project\n * declares that id, so a fleet that declares it sees this finding once, until\n * the next `sync` writes the line.\n */\nimport type { DocsDir } from \"../../../../core/kernel/ids/dirs.js\";\nimport { subsystemViewsPath } from \"../../../../core/repo/paths.js\";\nimport { viewsState } from \"../../../../core/repo/tree/render/stale.js\";\nimport type { MapFacts } from \"../../../../core/repo/tree/render/views.js\";\nimport type { FleetTree } from \"../../../../core/repo/tree/walk.js\";\nimport type { Finding } from \"../../../../core/vocabulary/report.js\";\n\nexport async function viewsStaleFindings(\n docsDir: DocsDir,\n tree: FleetTree,\n map: MapFacts,\n): Promise<Finding[]> {\n const { actual, expected, agrees } = await viewsState(subsystemViewsPath(docsDir), tree, map);\n if (agrees) return [];\n const state =\n actual === null\n ? \"does not exist, but the tree has subsystems to mirror\"\n : expected === null\n ? \"exists, but the tree has no subsystems — the generated file must be absent\"\n : \"does not match the tree\";\n return [\n {\n severity: \"error\",\n code: \"subsystem.views-stale\",\n subject: \"architecture/subsystems.likec4\",\n // The file the message names, so an editor jumping to the finding lands\n // on it. Without a location the report's fallback files this under the\n // TARGET, which is the landscape — a reader was sent to\n // architecture/landscape.likec4 to fix a file the sentence beside it\n // named as architecture/subsystems.likec4. A file that does not exist\n // yet cannot be `primary`: the actionable place is then the directory\n // the regenerator will write into.\n locations: [\n actual === null\n ? { path: \"architecture\", role: \"scope\" as const }\n : { path: \"architecture/subsystems.likec4\", role: \"primary\" as const },\n ],\n message:\n `subsystems: architecture/subsystems.likec4 ${state} — ` +\n `it is generated, one view per subsystem, and every derived scope drawn from it is wrong until it agrees. ` +\n \"Run `loam subsystem sync` to regenerate it; never edit it by hand.\",\n },\n ];\n}\n"]}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { ARTIFACT_FILES } from "../../../../core/repo/paths.js";
|
|
2
|
+
import { serviceLevelElements } from "../../fleet/census.js";
|
|
3
|
+
/**
|
|
4
|
+
* How the OTHER declaration is named.
|
|
5
|
+
*
|
|
6
|
+
* The document, not the file: `Elem` carries no source path, and the map is a
|
|
7
|
+
* PROJECT — `architecture/landscape.likec4` plus every other `.likec4` under
|
|
8
|
+
* `architecture/` — so naming the landscape file was a guess that sent an author
|
|
9
|
+
* to a document that does not declare the element at all.
|
|
10
|
+
*/
|
|
11
|
+
const MAP = "the fleet map (architecture/)";
|
|
12
|
+
export function divergedFindings(check) {
|
|
13
|
+
const { service, model, architecture, known } = check;
|
|
14
|
+
// An extending model declares each of these elements exactly once by
|
|
15
|
+
// construction — there is no copy to diverge — and the shapes' other grade
|
|
16
|
+
// (`c4.element-unowned`) is what asks its question.
|
|
17
|
+
if (model.shape !== "standalone")
|
|
18
|
+
return [];
|
|
19
|
+
if (model.doc.errors.length > 0)
|
|
20
|
+
return [];
|
|
21
|
+
if (architecture === null || architecture.errors.length > 0)
|
|
22
|
+
return [];
|
|
23
|
+
const index = joinIndex(architecture.elements, known);
|
|
24
|
+
const findings = [];
|
|
25
|
+
for (const mine of model.doc.elements) {
|
|
26
|
+
const theirs = counterpart(index, mine);
|
|
27
|
+
if (theirs === undefined)
|
|
28
|
+
continue;
|
|
29
|
+
const fields = differing(mine, theirs);
|
|
30
|
+
if (fields.length === 0)
|
|
31
|
+
continue;
|
|
32
|
+
// Where "copy the map's declaration verbatim" is a no-op: the tag sets are
|
|
33
|
+
// all that differ (so the two kinds are equal, and the block to open can be
|
|
34
|
+
// named) and copying the map's literal tags across would leave them still
|
|
35
|
+
// differing. `fields` is checked first because copying DOES fix a title or a
|
|
36
|
+
// binding, so the verbatim remedy stays right whenever one of those differs.
|
|
37
|
+
const specs = { mine: model.doc.specification, theirs: architecture.specification };
|
|
38
|
+
const inherited = fields.length === 1 && fields[0] === "tags" && copyIsNoOp(mine, theirs, specs)
|
|
39
|
+
? declaringSide(mine.kind, specs)
|
|
40
|
+
: null;
|
|
41
|
+
findings.push({
|
|
42
|
+
severity: "warn",
|
|
43
|
+
code: "c4.declaration-diverged",
|
|
44
|
+
subject: service,
|
|
45
|
+
message: `${service}: ${ARTIFACT_FILES.model} declares '${mine.id}' as ${describe(mine)} and ` +
|
|
46
|
+
`${MAP} declares ${theirs.id === mine.id ? "it" : `'${theirs.id}'`} as ${describe(theirs)} — ` +
|
|
47
|
+
remedy(fields, mine.kind, inherited),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return findings;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* What the message says after the two declarations: which fields differ, and
|
|
54
|
+
* what to do about it. `inherited` is null when copying the map's declaration
|
|
55
|
+
* fixes this, and otherwise the clause naming which document's `specification`
|
|
56
|
+
* puts the tags on the kind — the side an author has to open.
|
|
57
|
+
*/
|
|
58
|
+
function remedy(fields, kind, inherited) {
|
|
59
|
+
// "the kind differs" / "the title differs" / "the tags differ": the verb
|
|
60
|
+
// agrees with the last field named, and `tags` is the one plural noun in the
|
|
61
|
+
// set. It shipped as "the kind differ", and a sentence a reader trips over is
|
|
62
|
+
// one they stop reading.
|
|
63
|
+
const plural = fields.length > 1 || fields[fields.length - 1] === "tags";
|
|
64
|
+
const named = `the ${fields.join(", ")} differ${plural ? "" : "s"}`;
|
|
65
|
+
// The two kinds are equal whenever `tags` is the only differing field, so the
|
|
66
|
+
// block to open can be named exactly.
|
|
67
|
+
if (inherited !== null) {
|
|
68
|
+
return (`${named}, and copying the map's declaration would not clear it: the sets come apart on ` +
|
|
69
|
+
`\`specification { element ${kind} { … } }\`, ${inherited}. Either match the two specification ` +
|
|
70
|
+
'blocks on that kind, or migrate the model to extend the map (SCHEMA.md, "Two shapes of a service ' +
|
|
71
|
+
'model") so there is one specification and one declaration');
|
|
72
|
+
}
|
|
73
|
+
return (`${named}, so two documents are two authorities on one element. Copy the map's declaration verbatim, or ` +
|
|
74
|
+
'migrate the model to extend the map (SCHEMA.md, "Two shapes of a service model") so it is declared once');
|
|
75
|
+
}
|
|
76
|
+
function joinIndex(elements, known) {
|
|
77
|
+
// Which map elements are the fleet's own boxes rather than somebody's
|
|
78
|
+
// interior, by the ONE predicate every other fleet check asks it with
|
|
79
|
+
// (`fleet/census.ts`): a binding or a title naming a real services/<id>/ makes
|
|
80
|
+
// an element a service at any depth, and what sits inside one is that
|
|
81
|
+
// service's container. Everything else is fleet-level — a top-level element,
|
|
82
|
+
// or a box the map draws inside a plain grouping element — see the header.
|
|
83
|
+
const fleetLevel = new Set(serviceLevelElements([...elements], known).map((e) => e.id));
|
|
84
|
+
return {
|
|
85
|
+
byId: unique(elements, (e) => [e.id]),
|
|
86
|
+
byBinding: unique(elements, (e) => (e.service === undefined ? [] : [e.service])),
|
|
87
|
+
// Every DOTTED tail of the id, so a model's `paymentService.api` reaches the
|
|
88
|
+
// map's `marketplace.paymentService.api`: a tail carrying a dot names the
|
|
89
|
+
// owner as well as the leaf, so the two documents are talking about one
|
|
90
|
+
// element.
|
|
91
|
+
//
|
|
92
|
+
// The bare LAST segment names only the leaf, and it is emitted for
|
|
93
|
+
// FLEET-LEVEL boxes alone. This shipped unconditional, and the comment
|
|
94
|
+
// here claimed it was not: a standalone model's own `db`, `api` or `agent`
|
|
95
|
+
// joined whatever `<something>.<that name>` the map happened to hold — one
|
|
96
|
+
// service's private cache told to copy another service's store, verbatim.
|
|
97
|
+
// loam's own meta/docs hit it (`agent` ↔ `loam.core.agent`, a container of
|
|
98
|
+
// src/core/agent/), which is the case that made the rung wrong rather than
|
|
99
|
+
// merely risky (verification 2026-09-04).
|
|
100
|
+
//
|
|
101
|
+
// A box the map draws inside a plain GROUP still emits its bare segment,
|
|
102
|
+
// and deliberately: `infra.cache` is the fleet's cache, so a model's own
|
|
103
|
+
// top-level `cache` is a second meaning for one name and worth the warning.
|
|
104
|
+
// Grouping is a drawing convenience, not ownership — only a SERVICE owns an
|
|
105
|
+
// interior — so the guard is "no ancestor stands for a service" and not
|
|
106
|
+
// "the id carries no dot".
|
|
107
|
+
bySuffix: unique(elements, (e) => {
|
|
108
|
+
const segments = e.id.split(".");
|
|
109
|
+
const tails = segments.map((_, i) => segments.slice(i).join("."));
|
|
110
|
+
return fleetLevel.has(e.id) ? tails : tails.slice(0, -1);
|
|
111
|
+
}),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/** An index holding only the keys exactly ONE element answers to — a shared key joins nothing. */
|
|
115
|
+
function unique(elements, keys) {
|
|
116
|
+
const claims = new Map();
|
|
117
|
+
for (const element of elements) {
|
|
118
|
+
for (const key of keys(element)) {
|
|
119
|
+
const held = claims.get(key);
|
|
120
|
+
if (held === undefined)
|
|
121
|
+
claims.set(key, [element]);
|
|
122
|
+
else
|
|
123
|
+
held.push(element);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const index = new Map();
|
|
127
|
+
for (const [key, held] of claims)
|
|
128
|
+
if (held.length === 1)
|
|
129
|
+
index.set(key, held[0]);
|
|
130
|
+
return index;
|
|
131
|
+
}
|
|
132
|
+
/** The map's declaration of the element this model declares, or undefined when nothing joins. */
|
|
133
|
+
function counterpart(index, mine) {
|
|
134
|
+
const byId = index.byId.get(mine.id);
|
|
135
|
+
if (byId !== undefined)
|
|
136
|
+
return byId;
|
|
137
|
+
// The binding is the strongest evidence after the id: both documents wrote
|
|
138
|
+
// `metadata { service '<id>' }`, so both mean the same directory.
|
|
139
|
+
const bound = mine.service === undefined ? undefined : index.byBinding.get(mine.service);
|
|
140
|
+
if (bound !== undefined)
|
|
141
|
+
return bound;
|
|
142
|
+
return index.bySuffix.get(mine.id);
|
|
143
|
+
}
|
|
144
|
+
/** Which of the four compared fields disagree, in a fixed order so two runs read alike. */
|
|
145
|
+
function differing(mine, theirs) {
|
|
146
|
+
const fields = [];
|
|
147
|
+
if (mine.kind !== theirs.kind)
|
|
148
|
+
fields.push("kind");
|
|
149
|
+
if (mine.title !== theirs.title)
|
|
150
|
+
fields.push("title");
|
|
151
|
+
// A SET, sorted: tag order is authoring order and means nothing, and a model
|
|
152
|
+
// that lists the map's two tags the other way round is the same declaration.
|
|
153
|
+
if (tagList(mine) !== tagList(theirs))
|
|
154
|
+
fields.push("tags");
|
|
155
|
+
if (mine.service !== theirs.service)
|
|
156
|
+
fields.push("service binding");
|
|
157
|
+
return fields;
|
|
158
|
+
}
|
|
159
|
+
function tagList(e) {
|
|
160
|
+
return [...new Set(e.tags)].sort().join(" ");
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Would "copy the map's declaration verbatim" leave the tag sets still
|
|
164
|
+
* differing? Answered by SIMULATING the copy, which is the only way to be right
|
|
165
|
+
* about it in both directions.
|
|
166
|
+
*
|
|
167
|
+
* `Elem.tags` is what LikeC4 hands over, and since 1.59.0 that includes every
|
|
168
|
+
* tag the element's kind declares (`core/c4/parsed/specification.ts` says why
|
|
169
|
+
* loam reads the block at all). Copying the map's DECLARATION copies its literal
|
|
170
|
+
* tags into the model and nothing else — the model keeps its own specification —
|
|
171
|
+
* so the model's element would come back carrying `mine's kind defaults ∪ the
|
|
172
|
+
* map's literal tags`, against the map's `its kind defaults ∪ the same literal
|
|
173
|
+
* tags`. Those differ exactly when the two kind defaults differ outside the
|
|
174
|
+
* copied literals, and then the remedy is a no-op.
|
|
175
|
+
*
|
|
176
|
+
* The first version compared the two LITERAL tag lists and switched arms only
|
|
177
|
+
* when they were equal, which caught one direction of the same case and missed
|
|
178
|
+
* its mirror: a model writing `#external` on an element whose map twin inherits
|
|
179
|
+
* `#external #platform` from `element topic { … }` was told to copy the map's
|
|
180
|
+
* bare line, and doing exactly that produced the OTHER arm of this same message
|
|
181
|
+
* (verification 2026-09-04, second pass, examples/docs order-service).
|
|
182
|
+
*/
|
|
183
|
+
function copyIsNoOp(mine, theirs, specs) {
|
|
184
|
+
const copied = literalTags(theirs, specs.theirs);
|
|
185
|
+
const after = union(kindTags(mine.kind, specs.mine), copied);
|
|
186
|
+
const before = union(kindTags(theirs.kind, specs.theirs), copied);
|
|
187
|
+
return after !== before;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Which document's `specification` puts the tags on the kind — the half of the
|
|
191
|
+
* message that tells an author which file to open. "Both, differently" is a real
|
|
192
|
+
* state (each block declares its own tag on the same kind) and gets its own
|
|
193
|
+
* words rather than a guess at one side.
|
|
194
|
+
*/
|
|
195
|
+
function declaringSide(kind, specs) {
|
|
196
|
+
const mine = kindTags(kind, specs.mine);
|
|
197
|
+
const theirs = kindTags(kind, specs.theirs);
|
|
198
|
+
if (mine.length > 0 && theirs.length > 0)
|
|
199
|
+
return "which the two documents declare different tags on";
|
|
200
|
+
if (theirs.length > 0)
|
|
201
|
+
return `which ${MAP} declares tags on and ${ARTIFACT_FILES.model} does not`;
|
|
202
|
+
if (mine.length > 0)
|
|
203
|
+
return `which ${ARTIFACT_FILES.model} declares tags on and ${MAP} does not`;
|
|
204
|
+
// No kind declares anything and the copy still fails: not reachable from the
|
|
205
|
+
// predicate above (with equal kinds and no defaults the copy always clears
|
|
206
|
+
// it), and a sentence that names no file beats one that names the wrong file.
|
|
207
|
+
return "which the two documents' blocks disagree on";
|
|
208
|
+
}
|
|
209
|
+
/** The tags `spec` puts on every element of `kind`, sorted — the defaults an element cannot see. */
|
|
210
|
+
function kindTags(kind, spec) {
|
|
211
|
+
return [...new Set(spec?.elementKindTags[kind] ?? [])].sort();
|
|
212
|
+
}
|
|
213
|
+
/** A tag SET as one comparable string; see `tagList` for why order is dropped. */
|
|
214
|
+
function union(a, b) {
|
|
215
|
+
return [...new Set([...a, ...b])].sort().join(" ");
|
|
216
|
+
}
|
|
217
|
+
function literalTags(e, spec) {
|
|
218
|
+
const inherited = new Set(kindTags(e.kind, spec));
|
|
219
|
+
return [...new Set(e.tags)].filter((tag) => !inherited.has(tag)).sort();
|
|
220
|
+
}
|
|
221
|
+
/** One element as the message names it — every compared field, in the order `differing` reports them. */
|
|
222
|
+
function describe(e) {
|
|
223
|
+
const tags = tagList(e);
|
|
224
|
+
return `${e.kind} '${e.title}' [${tags === "" ? "no tags" : tags}, bound to ${e.service ?? "unbound"}]`;
|
|
225
|
+
}
|
|
226
|
+
//# sourceMappingURL=diverged.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diverged.js","sourceRoot":"","sources":["../../../../../src/commands/validate/service/model/diverged.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `c4.declaration-diverged` — the check the \"every shared element is declared\n * once per document\" report asked for.\n *\n * A STANDALONE model has to re-declare every element it talks about: it is\n * parsed alone, so `kafka`, `uaa` and each peer service appear a second time,\n * with a second kind, a second title, a second tag list and a second binding.\n * Nothing compared the copies. Measured on the fleet that filed the report: 78\n * double declarations across 56 services, and the copies had already drifted —\n * `#platform` was on the map's elements and missing from the models'.\n *\n * WHAT IS COMPARED, and why exactly these four. `kind`, `title`, the tag SET and\n * the `metadata { service }` binding each change what loam concludes or what a\n * view shows: the kind decides which specification rule applies and which\n * exemptions (`ACTOR_KINDS`, `#external`) fire, the title is a resolver fallback\n * when nothing is bound, a tag is graded (`#external`, `#obl-…`), and the\n * binding decides which directory an edge is filed under. `description` is\n * deliberately NOT compared: it is prose, nothing joins on it, and reporting two\n * differently-worded sentences as a divergence would train a reader to ignore\n * the code.\n *\n * WHICH TWO DECLARATIONS ARE ONE ELEMENT is `counterpart` below, and it is the\n * half the first release got wrong: it joined on the literal id, so in a GROUPED\n * map — one that nests its services under `marketplace`, as loam's own example\n * does — a model's `paymentService` never met the map's\n * `marketplace.paymentService` and the check was silent on every fleet that\n * groups (verification 2026-09-04, W3). The three rungs are ordered by how much\n * they assume: the id, then the `metadata { service }` binding both sides wrote,\n * then a unique dotted TAIL of the map's id. Ambiguity is never resolved by\n * picking one — a key two map elements answer to joins nothing, because a\n * divergence reported against the wrong peer is worse than the silence. And the\n * BARE last segment is a tail only when the map element it names is a\n * FLEET-LEVEL box: see `joinIndex`, where the first release of the tail rung\n * joined one service's `db` to another service's.\n *\n * WHAT \"FLEET-LEVEL\" MEANS, spelled once here because three pages spelled it\n * differently and all three were wrong (verification 2026-09-04, second pass):\n * a map element NO ANCESTOR of which stands for a service — `serviceLevelElements`,\n * the one predicate every fleet check asks this question with. That is a\n * top-level element, and equally a child of a plain grouping element: a map\n * that draws `infra = group 'Infra' { cache = database … }` is drawing the\n * fleet's cache, at fleet level, and a standalone model that declares its own\n * top-level `cache` IS shadowing that box — the two documents give one name two\n * meanings, which is the thing this code exists to report. What the rung must\n * never reach is an element drawn INSIDE a service's own element: that is\n * somebody's interior, it belongs to that service, and a model's private `db`\n * being told to copy another service's `db` verbatim is the wrong-join class\n * the tail rung was restricted for in the first place.\n *\n * SILENT WHEN THE MAP DOES NOT PARSE, and when the model does not: a diff\n * against half a document is invention, and both states already have a finding\n * of their own.\n *\n * The repair has two forms on purpose. Copying the map's declaration verbatim\n * keeps the standalone shape, which is legal forever; migrating the model to\n * extend the map removes the copy altogether, which is the only fix that cannot\n * drift again. Except where the first repair is a no-op, and `copyIsNoOp`\n * decides that by SIMULATING it rather than by pattern-matching one shape of it:\n * a tag difference that survives the copy comes off the two documents'\n * `specification` blocks, so the message names the block instead.\n */\nimport type { Elem, LoadedDoc } from \"../../../../core/c4/likec4.js\";\nimport type { DocSpecification } from \"../../../../core/c4/parsed/specification.js\";\nimport type { ServiceModel } from \"../../../../core/c4/service-model/load.js\";\nimport type { PathableService } from \"../../../../core/kernel/ids/service.js\";\nimport { ARTIFACT_FILES } from \"../../../../core/repo/paths.js\";\nimport type { Finding } from \"../../../../core/vocabulary/report.js\";\nimport { serviceLevelElements } from \"../../fleet/census.js\";\n\n/**\n * How the OTHER declaration is named.\n *\n * The document, not the file: `Elem` carries no source path, and the map is a\n * PROJECT — `architecture/landscape.likec4` plus every other `.likec4` under\n * `architecture/` — so naming the landscape file was a guess that sent an author\n * to a document that does not declare the element at all.\n */\nconst MAP = \"the fleet map (architecture/)\";\n\nexport interface DivergenceCheck {\n service: PathableService;\n model: ServiceModel;\n /** The `architecture/` project — the map as every other fleet check reads it. Null when the repo has none. */\n architecture: LoadedDoc | null;\n /**\n * Every `services/<id>/` that exists — the census's positive evidence, which\n * is what tells a map's fleet-level box from a container drawn inside a\n * service. Only the bare-last-segment tail join reads it; see `joinIndex`.\n */\n known: ReadonlySet<string>;\n}\n\nexport function divergedFindings(check: DivergenceCheck): Finding[] {\n const { service, model, architecture, known } = check;\n // An extending model declares each of these elements exactly once by\n // construction — there is no copy to diverge — and the shapes' other grade\n // (`c4.element-unowned`) is what asks its question.\n if (model.shape !== \"standalone\") return [];\n if (model.doc.errors.length > 0) return [];\n if (architecture === null || architecture.errors.length > 0) return [];\n\n const index = joinIndex(architecture.elements, known);\n const findings: Finding[] = [];\n for (const mine of model.doc.elements) {\n const theirs = counterpart(index, mine);\n if (theirs === undefined) continue;\n const fields = differing(mine, theirs);\n if (fields.length === 0) continue;\n // Where \"copy the map's declaration verbatim\" is a no-op: the tag sets are\n // all that differ (so the two kinds are equal, and the block to open can be\n // named) and copying the map's literal tags across would leave them still\n // differing. `fields` is checked first because copying DOES fix a title or a\n // binding, so the verbatim remedy stays right whenever one of those differs.\n const specs = { mine: model.doc.specification, theirs: architecture.specification };\n const inherited =\n fields.length === 1 && fields[0] === \"tags\" && copyIsNoOp(mine, theirs, specs)\n ? declaringSide(mine.kind, specs)\n : null;\n findings.push({\n severity: \"warn\",\n code: \"c4.declaration-diverged\",\n subject: service,\n message:\n `${service}: ${ARTIFACT_FILES.model} declares '${mine.id}' as ${describe(mine)} and ` +\n `${MAP} declares ${theirs.id === mine.id ? \"it\" : `'${theirs.id}'`} as ${describe(theirs)} — ` +\n remedy(fields, mine.kind, inherited),\n });\n }\n return findings;\n}\n\n/**\n * What the message says after the two declarations: which fields differ, and\n * what to do about it. `inherited` is null when copying the map's declaration\n * fixes this, and otherwise the clause naming which document's `specification`\n * puts the tags on the kind — the side an author has to open.\n */\nfunction remedy(fields: string[], kind: string, inherited: string | null): string {\n // \"the kind differs\" / \"the title differs\" / \"the tags differ\": the verb\n // agrees with the last field named, and `tags` is the one plural noun in the\n // set. It shipped as \"the kind differ\", and a sentence a reader trips over is\n // one they stop reading.\n const plural = fields.length > 1 || fields[fields.length - 1] === \"tags\";\n const named = `the ${fields.join(\", \")} differ${plural ? \"\" : \"s\"}`;\n // The two kinds are equal whenever `tags` is the only differing field, so the\n // block to open can be named exactly.\n if (inherited !== null) {\n return (\n `${named}, and copying the map's declaration would not clear it: the sets come apart on ` +\n `\\`specification { element ${kind} { … } }\\`, ${inherited}. Either match the two specification ` +\n 'blocks on that kind, or migrate the model to extend the map (SCHEMA.md, \"Two shapes of a service ' +\n 'model\") so there is one specification and one declaration'\n );\n }\n return (\n `${named}, so two documents are two authorities on one element. Copy the map's declaration verbatim, or ` +\n 'migrate the model to extend the map (SCHEMA.md, \"Two shapes of a service model\") so it is declared once'\n );\n}\n\n/** The map's elements, indexed by each key `counterpart` may join on — ambiguous keys dropped. */\ninterface JoinIndex {\n byId: Map<string, Elem>;\n byBinding: Map<string, Elem>;\n bySuffix: Map<string, Elem>;\n}\n\nfunction joinIndex(elements: readonly Elem[], known: ReadonlySet<string>): JoinIndex {\n // Which map elements are the fleet's own boxes rather than somebody's\n // interior, by the ONE predicate every other fleet check asks it with\n // (`fleet/census.ts`): a binding or a title naming a real services/<id>/ makes\n // an element a service at any depth, and what sits inside one is that\n // service's container. Everything else is fleet-level — a top-level element,\n // or a box the map draws inside a plain grouping element — see the header.\n const fleetLevel = new Set(serviceLevelElements([...elements], known).map((e) => e.id));\n return {\n byId: unique(elements, (e) => [e.id]),\n byBinding: unique(elements, (e) => (e.service === undefined ? [] : [e.service as string])),\n // Every DOTTED tail of the id, so a model's `paymentService.api` reaches the\n // map's `marketplace.paymentService.api`: a tail carrying a dot names the\n // owner as well as the leaf, so the two documents are talking about one\n // element.\n //\n // The bare LAST segment names only the leaf, and it is emitted for\n // FLEET-LEVEL boxes alone. This shipped unconditional, and the comment\n // here claimed it was not: a standalone model's own `db`, `api` or `agent`\n // joined whatever `<something>.<that name>` the map happened to hold — one\n // service's private cache told to copy another service's store, verbatim.\n // loam's own meta/docs hit it (`agent` ↔ `loam.core.agent`, a container of\n // src/core/agent/), which is the case that made the rung wrong rather than\n // merely risky (verification 2026-09-04).\n //\n // A box the map draws inside a plain GROUP still emits its bare segment,\n // and deliberately: `infra.cache` is the fleet's cache, so a model's own\n // top-level `cache` is a second meaning for one name and worth the warning.\n // Grouping is a drawing convenience, not ownership — only a SERVICE owns an\n // interior — so the guard is \"no ancestor stands for a service\" and not\n // \"the id carries no dot\".\n bySuffix: unique(elements, (e) => {\n const segments = e.id.split(\".\");\n const tails = segments.map((_, i) => segments.slice(i).join(\".\"));\n return fleetLevel.has(e.id) ? tails : tails.slice(0, -1);\n }),\n };\n}\n\n/** An index holding only the keys exactly ONE element answers to — a shared key joins nothing. */\nfunction unique(elements: readonly Elem[], keys: (e: Elem) => string[]): Map<string, Elem> {\n const claims = new Map<string, Elem[]>();\n for (const element of elements) {\n for (const key of keys(element)) {\n const held = claims.get(key);\n if (held === undefined) claims.set(key, [element]);\n else held.push(element);\n }\n }\n const index = new Map<string, Elem>();\n for (const [key, held] of claims) if (held.length === 1) index.set(key, held[0]!);\n return index;\n}\n\n/** The map's declaration of the element this model declares, or undefined when nothing joins. */\nfunction counterpart(index: JoinIndex, mine: Elem): Elem | undefined {\n const byId = index.byId.get(mine.id);\n if (byId !== undefined) return byId;\n // The binding is the strongest evidence after the id: both documents wrote\n // `metadata { service '<id>' }`, so both mean the same directory.\n const bound = mine.service === undefined ? undefined : index.byBinding.get(mine.service as string);\n if (bound !== undefined) return bound;\n return index.bySuffix.get(mine.id);\n}\n\n/** Which of the four compared fields disagree, in a fixed order so two runs read alike. */\nfunction differing(mine: Elem, theirs: Elem): string[] {\n const fields: string[] = [];\n if (mine.kind !== theirs.kind) fields.push(\"kind\");\n if (mine.title !== theirs.title) fields.push(\"title\");\n // A SET, sorted: tag order is authoring order and means nothing, and a model\n // that lists the map's two tags the other way round is the same declaration.\n if (tagList(mine) !== tagList(theirs)) fields.push(\"tags\");\n if (mine.service !== theirs.service) fields.push(\"service binding\");\n return fields;\n}\n\nfunction tagList(e: Elem): string {\n return [...new Set(e.tags)].sort().join(\" \");\n}\n\n/** The two documents' `specification` blocks, as `copyIsNoOp` and `declaringSide` read them. */\ninterface SpecPair {\n mine: DocSpecification | undefined;\n theirs: DocSpecification | undefined;\n}\n\n/**\n * Would \"copy the map's declaration verbatim\" leave the tag sets still\n * differing? Answered by SIMULATING the copy, which is the only way to be right\n * about it in both directions.\n *\n * `Elem.tags` is what LikeC4 hands over, and since 1.59.0 that includes every\n * tag the element's kind declares (`core/c4/parsed/specification.ts` says why\n * loam reads the block at all). Copying the map's DECLARATION copies its literal\n * tags into the model and nothing else — the model keeps its own specification —\n * so the model's element would come back carrying `mine's kind defaults ∪ the\n * map's literal tags`, against the map's `its kind defaults ∪ the same literal\n * tags`. Those differ exactly when the two kind defaults differ outside the\n * copied literals, and then the remedy is a no-op.\n *\n * The first version compared the two LITERAL tag lists and switched arms only\n * when they were equal, which caught one direction of the same case and missed\n * its mirror: a model writing `#external` on an element whose map twin inherits\n * `#external #platform` from `element topic { … }` was told to copy the map's\n * bare line, and doing exactly that produced the OTHER arm of this same message\n * (verification 2026-09-04, second pass, examples/docs order-service).\n */\nfunction copyIsNoOp(mine: Elem, theirs: Elem, specs: SpecPair): boolean {\n const copied = literalTags(theirs, specs.theirs);\n const after = union(kindTags(mine.kind, specs.mine), copied);\n const before = union(kindTags(theirs.kind, specs.theirs), copied);\n return after !== before;\n}\n\n/**\n * Which document's `specification` puts the tags on the kind — the half of the\n * message that tells an author which file to open. \"Both, differently\" is a real\n * state (each block declares its own tag on the same kind) and gets its own\n * words rather than a guess at one side.\n */\nfunction declaringSide(kind: string, specs: SpecPair): string {\n const mine = kindTags(kind, specs.mine);\n const theirs = kindTags(kind, specs.theirs);\n if (mine.length > 0 && theirs.length > 0) return \"which the two documents declare different tags on\";\n if (theirs.length > 0) return `which ${MAP} declares tags on and ${ARTIFACT_FILES.model} does not`;\n if (mine.length > 0) return `which ${ARTIFACT_FILES.model} declares tags on and ${MAP} does not`;\n // No kind declares anything and the copy still fails: not reachable from the\n // predicate above (with equal kinds and no defaults the copy always clears\n // it), and a sentence that names no file beats one that names the wrong file.\n return \"which the two documents' blocks disagree on\";\n}\n\n/** The tags `spec` puts on every element of `kind`, sorted — the defaults an element cannot see. */\nfunction kindTags(kind: string, spec: DocSpecification | undefined): string[] {\n return [...new Set(spec?.elementKindTags[kind] ?? [])].sort();\n}\n\n/** A tag SET as one comparable string; see `tagList` for why order is dropped. */\nfunction union(a: readonly string[], b: readonly string[]): string {\n return [...new Set([...a, ...b])].sort().join(\" \");\n}\n\nfunction literalTags(e: Elem, spec: DocSpecification | undefined): string[] {\n const inherited = new Set(kindTags(e.kind, spec));\n return [...new Set(e.tags)].filter((tag) => !inherited.has(tag)).sort();\n}\n\n/** One element as the message names it — every compared field, in the order `differing` reports them. */\nfunction describe(e: Elem): string {\n const tags = tagList(e);\n return `${e.kind} '${e.title}' [${tags === \"\" ? \"no tags\" : tags}, bound to ${e.service ?? \"unbound\"}]`;\n}\n"]}
|