@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
|
@@ -170,12 +170,16 @@ export async function readFeatureDeployments(req) {
|
|
|
170
170
|
const docs = await featureDeployments(req.featureDir);
|
|
171
171
|
if (docs.length === 0)
|
|
172
172
|
return { kind: "read" };
|
|
173
|
+
// AFTER the early return: the fleet read is paid only by a feature that
|
|
174
|
+
// actually brings a topology document.
|
|
175
|
+
const models = req.models === undefined ? undefined : await req.models();
|
|
173
176
|
const staged = await stageMergedProject({
|
|
174
177
|
docsDir: req.docsDir,
|
|
175
178
|
featureDir: req.featureDir,
|
|
176
179
|
featureId: req.featureId,
|
|
177
180
|
documents: docs.map((doc) => ({ rel: doc.rel, path: doc.path })),
|
|
178
181
|
...(req.load === undefined ? {} : { load: req.load }),
|
|
182
|
+
...(models === undefined ? {} : { models }),
|
|
179
183
|
});
|
|
180
184
|
return staged.kind === "unreadable" ? { kind: "unreadable", errors: staged.errors } : { kind: "read" };
|
|
181
185
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"delta.js","sourceRoot":"","sources":["../../../src/core/deployment/delta.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A feature's own topology: `features/<FEAT>/deployment/<name>.likec4`.\n *\n * Until this slot existed a change that stood up a standby cluster had to be\n * written in two places at once — the requirement in the feature, the topology\n * edited straight into `architecture/` — because `delta.likec4` refuses a\n * `deployment { }` block and always will: that document re-declares the\n * landscape's own identifiers and carries its own `specification`, so it cannot\n * be staged beside the map in one LikeC4 project. A document that declares\n * NEITHER can, which is exactly why the use-case axis put its flows in files of\n * their own, and why this mirrors it rather than inventing a shape.\n *\n * WHAT MAKES IT WORK, and it was measured before the axis was written: LikeC4\n * `extend` resolves across documents of one project. A feature writes\n *\n * deployment {\n * extend eu {\n * dcC = datacenter 'DC-C' { k8sC = cluster 'cluster-c' { … } }\n * }\n * eu.dcA.k8sA -> eu.dcC.k8sC 'async replication'\n * }\n *\n * and the living region gains a datacenter with the living file untouched. So\n * the merge is a copy, not a splice, and `unarchive` needs nothing new — a\n * create is undone by deleting, which the snapshot manifest already records for\n * every write in the plan.\n *\n * CREATE-ONLY, for `core/usecases/delta/flows.ts`'s reasons rather than a\n * weaker version of them. A requirement delta merges INTO a living document and\n * two features editing one requirement would overwrite each other silently; a\n * topology document has no such algebra — the file is one block, the merge is a\n * whole-file copy, and there is nothing to merge partially. Rewriting a living\n * one through a feature would be a silent whole-file replacement with no pin to\n * collide on, where editing `architecture/<name>.likec4` directly in the same\n * pull request produces an ordinary git conflict a human resolves.\n *\n * ## Graded against the map its own merge would leave behind\n *\n * `ARCH-LOAM-FEATURE-CORPUS`, and this axis needs it more literally than the\n * use-case one does: a feature that stands a service up in a new cluster\n * declares that service in its `delta.likec4` and instances it here, so the\n * element the topology names exists ONLY post-merge. Read against the living\n * landscape it is unresolved, LikeC4 refuses the whole project, and loam would\n * report a hole against a file whose only fault is arriving with the change\n * that makes it true.\n *\n * The staging is `core/c4/project/staged.ts`, shared with the use-case slot\n * rather than copied — the two would have disagreed first about the merge\n * preview, which is the one thing both refusals rest on.\n */\nimport { existsSync } from \"node:fs\";\nimport { basename, join, relative, resolve } from \"node:path\";\nimport { architectureDir } from \"../c4/project/architecture.js\";\nimport { stageMergedProject } from \"../c4/project/staged.js\";\nimport { type LoadedDoc } from \"../c4/likec4.js\";\nimport { architectureDocuments } from \"../c4/project/documents.js\";\nimport { featureDeploymentDir } from \"../repo/authored/paths.js\";\nimport { reservedArchitectureName } from \"../repo/paths.js\";\nimport type { DocsDir, FeatureDir } from \"../kernel/ids/dirs.js\";\nimport { type Issue } from \"../vocabulary/issue.js\";\n\n/** One topology document a feature carries. */\nexport interface FeatureDeployment {\n /**\n * The path INSIDE the feature's `deployment/` directory, `/`-separated — the\n * id this axis has. Nesting is spelled by the tree, as it is for every other\n * authored tree in the repo.\n */\n rel: string;\n /** The absolute path of the authored file. */\n path: string;\n}\n\n/**\n * The topology documents one feature carries, ordered by path.\n *\n * Zero readdirs when the feature has no `deployment/` directory, which is every\n * feature in a fleet that has not adopted the axis: `architectureDocuments`\n * answers an absent directory with an empty list rather than a throw. That\n * directory's existence is the whole per-feature opt-in.\n */\nexport async function featureDeployments(featureDir: FeatureDir): Promise<FeatureDeployment[]> {\n const dir = featureDeploymentDir(featureDir);\n const paths = await architectureDocuments(dir);\n const base = resolve(dir);\n return paths.map((path) => ({ rel: relative(base, path).split(/[\\\\/]/).join(\"/\"), path }));\n}\n\n/**\n * The LIVING path a feature-local topology document addresses.\n *\n * Straight into `architecture/`, with no subdirectory of its own — unlike\n * `architecture/usecases/`, and the difference is the model rather than taste.\n * A `deployment { }` block is part of the fleet's ONE architecture project and\n * is read as such by every loader; filing it under a subdirectory would suggest\n * a second project, which is exactly the reading `subsystems.likec4` and\n * `landscape.likec4` already refuse by sitting at the top level together.\n *\n * `rel` is joined un-split, which `node:path` normalizes. What holds the join is\n * PROVENANCE: every `rel` here came out of an `architectureDocuments` walk, so\n * each component is a directory entry that exists. No caller takes one from argv.\n */\nexport function livingDeploymentPath(docsDir: DocsDir, rel: string): string {\n return join(architectureDir(docsDir), rel);\n}\n\n/**\n * The two things that can go wrong about a topology document's NAME, before\n * anything about its contents is asked.\n *\n * `deployment.doc-reserved` — the name is one loam owns inside `architecture/`.\n * The merge writes straight into that directory, unlike a flow, which lands\n * under `usecases/`, so `<name>` is free to collide with the living map, the\n * generated subsystem views, or the flow tree. `reservedArchitectureName`\n * carries which and why. It is asked FIRST and WITHOUT an existence test,\n * because two of the three fail silently when the target does not exist yet.\n *\n * `deployment.doc-exists` — a topology document this feature introduces that\n * the living `architecture/` already holds.\n *\n * An ERROR and never a warning-that-gates, for `usecase.flow-exists`'s reason:\n * there is no legal reading of it. The merge would replace an authored topology\n * wholesale, and no `--approve` makes that a different act than it is. Two\n * features introducing the same file is the case that makes it load-bearing\n * rather than pedantic — whichever archives first creates the living document,\n * and the second is told before it merges that its own file is the one to\n * delete.\n *\n * The collision is on the FILE, not on what is inside it. Two features may both\n * extend the same living region from two documents of their own and both\n * archive; that is the shape `extend` exists for, and refusing it would make the\n * slot useless for the fleet-wide change it was built for.\n */\nexport async function deploymentDeltaIssues(docsDir: DocsDir, featureDir: FeatureDir): Promise<Issue[]> {\n const docs = await featureDeployments(featureDir);\n const issues: Issue[] = [];\n // The reservation runs FIRST and independently of existence, because two of\n // the three names it refuses need not exist for the merge to be wrong. A\n // topology copied over `subsystems.likec4` is destroyed by the next\n // `loam subsystem sync` and is invisible to every project load until then —\n // the merge would report `created architecture/subsystems.likec4` and nothing\n // downstream would ever say another word about it.\n for (const doc of docs) {\n const reason = reservedArchitectureName(doc.rel);\n if (reason === null) continue;\n issues.push({\n severity: \"error\",\n code: \"deployment.doc-reserved\",\n subject: doc.rel,\n message:\n `features/<FEAT>/deployment/${doc.rel} would merge to architecture/${doc.rel}, and that name is loam's: ` +\n `${reason}. Rename the feature's document — its name is yours to choose, and \\`extend\\` is how it adds to ` +\n \"what a living document already declares.\",\n });\n }\n return issues.concat(\n docs\n .filter((doc) => reservedArchitectureName(doc.rel) === null)\n .filter((doc) => existsSync(livingDeploymentPath(docsDir, doc.rel)))\n .map((doc) => ({\n severity: \"error\" as const,\n code: \"deployment.doc-exists\" as const,\n subject: doc.rel,\n message:\n `architecture/${doc.rel} already exists, so this feature's topology would replace an authored one wholesale — ` +\n \"a feature-local deployment document INTRODUCES topology, it does not rewrite it. \" +\n `Delete features/<FEAT>/deployment/${doc.rel} and edit architecture/${doc.rel} directly in this same change, ` +\n \"where git produces an ordinary conflict if somebody else is editing it too. \" +\n `To ADD to what that file declares, write a document of another name that says \\`extend\\` — ` +\n `\\`${suggest(doc.rel)}\\` — which is what lets two features grow one region without touching each other.`,\n })),\n );\n}\n\n/** What the caller needs to grade this feature's topology against the merge preview. */\nexport interface DeploymentOverlayRequest {\n docsDir: DocsDir;\n featureDir: FeatureDir;\n /** The feature's id — the tag that selects the delta's own additions. */\n featureId: string;\n /**\n * The caller's memoised LikeC4 read, when it has one. A FUNCTION rather than\n * the `FleetContext` itself, for the reason the use-case overlay states: one\n * memoised read does not justify an edge from this package to\n * `core/fleet-context.ts`, and the edge would push it up a DAG level for\n * nothing.\n */\n load?: (path: string) => Promise<LoadedDoc>;\n}\n\n/** Whether this feature's topology could be read at all, against the map its merge would leave. */\nexport type DeploymentOverlay = { kind: \"read\" } | { kind: \"unreadable\"; errors: string[] };\n\n/**\n * Parse this feature's topology documents over the post-merge map.\n *\n * A feature with no `deployment/` directory is `read` at the cost of one walk\n * over a directory that is not there — the axis's per-feature opt-in, and the\n * whole price a fleet that has not adopted it pays. Nothing is staged, nothing\n * is parsed, and no temp tree is made.\n *\n * The verdict is deliberately BINARY. The use-case overlay hands its caller the\n * views it read because the axis has questions about their content; this one\n * has none — a topology document is checked by being parseable against the map\n * the merge will write, and everything else about it is graded by\n * `loam validate --all` once it is living, exactly as the landscape is.\n */\nexport async function readFeatureDeployments(req: DeploymentOverlayRequest): Promise<DeploymentOverlay> {\n const docs = await featureDeployments(req.featureDir);\n if (docs.length === 0) return { kind: \"read\" };\n const staged = await stageMergedProject({\n docsDir: req.docsDir,\n featureDir: req.featureDir,\n featureId: req.featureId,\n documents: docs.map((doc) => ({ rel: doc.rel, path: doc.path })),\n ...(req.load === undefined ? {} : { load: req.load }),\n });\n return staged.kind === \"unreadable\" ? { kind: \"unreadable\", errors: staged.errors } : { kind: \"read\" };\n}\n\n/** A free name near the colliding one, so the fix is a rename the author can copy. */\nfunction suggest(rel: string): string {\n const name = basename(rel).replace(/\\.likec4$/, \"\");\n const dir = rel.slice(0, rel.length - basename(rel).length);\n return `${dir}${name}-extend.likec4`;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"delta.js","sourceRoot":"","sources":["../../../src/core/deployment/delta.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A feature's own topology: `features/<FEAT>/deployment/<name>.likec4`.\n *\n * Until this slot existed a change that stood up a standby cluster had to be\n * written in two places at once — the requirement in the feature, the topology\n * edited straight into `architecture/` — because `delta.likec4` refuses a\n * `deployment { }` block and always will: that document re-declares the\n * landscape's own identifiers and carries its own `specification`, so it cannot\n * be staged beside the map in one LikeC4 project. A document that declares\n * NEITHER can, which is exactly why the use-case axis put its flows in files of\n * their own, and why this mirrors it rather than inventing a shape.\n *\n * WHAT MAKES IT WORK, and it was measured before the axis was written: LikeC4\n * `extend` resolves across documents of one project. A feature writes\n *\n * deployment {\n * extend eu {\n * dcC = datacenter 'DC-C' { k8sC = cluster 'cluster-c' { … } }\n * }\n * eu.dcA.k8sA -> eu.dcC.k8sC 'async replication'\n * }\n *\n * and the living region gains a datacenter with the living file untouched. So\n * the merge is a copy, not a splice, and `unarchive` needs nothing new — a\n * create is undone by deleting, which the snapshot manifest already records for\n * every write in the plan.\n *\n * CREATE-ONLY, for `core/usecases/delta/flows.ts`'s reasons rather than a\n * weaker version of them. A requirement delta merges INTO a living document and\n * two features editing one requirement would overwrite each other silently; a\n * topology document has no such algebra — the file is one block, the merge is a\n * whole-file copy, and there is nothing to merge partially. Rewriting a living\n * one through a feature would be a silent whole-file replacement with no pin to\n * collide on, where editing `architecture/<name>.likec4` directly in the same\n * pull request produces an ordinary git conflict a human resolves.\n *\n * ## Graded against the map its own merge would leave behind\n *\n * `ARCH-LOAM-FEATURE-CORPUS`, and this axis needs it more literally than the\n * use-case one does: a feature that stands a service up in a new cluster\n * declares that service in its `delta.likec4` and instances it here, so the\n * element the topology names exists ONLY post-merge. Read against the living\n * landscape it is unresolved, LikeC4 refuses the whole project, and loam would\n * report a hole against a file whose only fault is arriving with the change\n * that makes it true.\n *\n * The staging is `core/c4/project/staged.ts`, shared with the use-case slot\n * rather than copied — the two would have disagreed first about the merge\n * preview, which is the one thing both refusals rest on.\n */\nimport { existsSync } from \"node:fs\";\nimport { basename, join, relative, resolve } from \"node:path\";\nimport { architectureDir } from \"../c4/project/architecture.js\";\nimport { stageMergedProject } from \"../c4/project/staged.js\";\nimport { type LoadedDoc } from \"../c4/likec4.js\";\nimport type { ExtendingModel } from \"../c4/splice/contract.js\";\nimport { architectureDocuments } from \"../c4/project/documents.js\";\nimport { featureDeploymentDir } from \"../repo/authored/paths.js\";\nimport { reservedArchitectureName } from \"../repo/paths.js\";\nimport type { DocsDir, FeatureDir } from \"../kernel/ids/dirs.js\";\nimport { type Issue } from \"../vocabulary/issue.js\";\n\n/** One topology document a feature carries. */\nexport interface FeatureDeployment {\n /**\n * The path INSIDE the feature's `deployment/` directory, `/`-separated — the\n * id this axis has. Nesting is spelled by the tree, as it is for every other\n * authored tree in the repo.\n */\n rel: string;\n /** The absolute path of the authored file. */\n path: string;\n}\n\n/**\n * The topology documents one feature carries, ordered by path.\n *\n * Zero readdirs when the feature has no `deployment/` directory, which is every\n * feature in a fleet that has not adopted the axis: `architectureDocuments`\n * answers an absent directory with an empty list rather than a throw. That\n * directory's existence is the whole per-feature opt-in.\n */\nexport async function featureDeployments(featureDir: FeatureDir): Promise<FeatureDeployment[]> {\n const dir = featureDeploymentDir(featureDir);\n const paths = await architectureDocuments(dir);\n const base = resolve(dir);\n return paths.map((path) => ({ rel: relative(base, path).split(/[\\\\/]/).join(\"/\"), path }));\n}\n\n/**\n * The LIVING path a feature-local topology document addresses.\n *\n * Straight into `architecture/`, with no subdirectory of its own — unlike\n * `architecture/usecases/`, and the difference is the model rather than taste.\n * A `deployment { }` block is part of the fleet's ONE architecture project and\n * is read as such by every loader; filing it under a subdirectory would suggest\n * a second project, which is exactly the reading `subsystems.likec4` and\n * `landscape.likec4` already refuse by sitting at the top level together.\n *\n * `rel` is joined un-split, which `node:path` normalizes. What holds the join is\n * PROVENANCE: every `rel` here came out of an `architectureDocuments` walk, so\n * each component is a directory entry that exists. No caller takes one from argv.\n */\nexport function livingDeploymentPath(docsDir: DocsDir, rel: string): string {\n return join(architectureDir(docsDir), rel);\n}\n\n/**\n * The two things that can go wrong about a topology document's NAME, before\n * anything about its contents is asked.\n *\n * `deployment.doc-reserved` — the name is one loam owns inside `architecture/`.\n * The merge writes straight into that directory, unlike a flow, which lands\n * under `usecases/`, so `<name>` is free to collide with the living map, the\n * generated subsystem views, or the flow tree. `reservedArchitectureName`\n * carries which and why. It is asked FIRST and WITHOUT an existence test,\n * because two of the three fail silently when the target does not exist yet.\n *\n * `deployment.doc-exists` — a topology document this feature introduces that\n * the living `architecture/` already holds.\n *\n * An ERROR and never a warning-that-gates, for `usecase.flow-exists`'s reason:\n * there is no legal reading of it. The merge would replace an authored topology\n * wholesale, and no `--approve` makes that a different act than it is. Two\n * features introducing the same file is the case that makes it load-bearing\n * rather than pedantic — whichever archives first creates the living document,\n * and the second is told before it merges that its own file is the one to\n * delete.\n *\n * The collision is on the FILE, not on what is inside it. Two features may both\n * extend the same living region from two documents of their own and both\n * archive; that is the shape `extend` exists for, and refusing it would make the\n * slot useless for the fleet-wide change it was built for.\n */\nexport async function deploymentDeltaIssues(docsDir: DocsDir, featureDir: FeatureDir): Promise<Issue[]> {\n const docs = await featureDeployments(featureDir);\n const issues: Issue[] = [];\n // The reservation runs FIRST and independently of existence, because two of\n // the three names it refuses need not exist for the merge to be wrong. A\n // topology copied over `subsystems.likec4` is destroyed by the next\n // `loam subsystem sync` and is invisible to every project load until then —\n // the merge would report `created architecture/subsystems.likec4` and nothing\n // downstream would ever say another word about it.\n for (const doc of docs) {\n const reason = reservedArchitectureName(doc.rel);\n if (reason === null) continue;\n issues.push({\n severity: \"error\",\n code: \"deployment.doc-reserved\",\n subject: doc.rel,\n message:\n `features/<FEAT>/deployment/${doc.rel} would merge to architecture/${doc.rel}, and that name is loam's: ` +\n `${reason}. Rename the feature's document — its name is yours to choose, and \\`extend\\` is how it adds to ` +\n \"what a living document already declares.\",\n });\n }\n return issues.concat(\n docs\n .filter((doc) => reservedArchitectureName(doc.rel) === null)\n .filter((doc) => existsSync(livingDeploymentPath(docsDir, doc.rel)))\n .map((doc) => ({\n severity: \"error\" as const,\n code: \"deployment.doc-exists\" as const,\n subject: doc.rel,\n message:\n `architecture/${doc.rel} already exists, so this feature's topology would replace an authored one wholesale — ` +\n \"a feature-local deployment document INTRODUCES topology, it does not rewrite it. \" +\n `Delete features/<FEAT>/deployment/${doc.rel} and edit architecture/${doc.rel} directly in this same change, ` +\n \"where git produces an ordinary conflict if somebody else is editing it too. \" +\n `To ADD to what that file declares, write a document of another name that says \\`extend\\` — ` +\n `\\`${suggest(doc.rel)}\\` — which is what lets two features grow one region without touching each other.`,\n })),\n );\n}\n\n/** What the caller needs to grade this feature's topology against the merge preview. */\nexport interface DeploymentOverlayRequest {\n docsDir: DocsDir;\n featureDir: FeatureDir;\n /** The feature's id — the tag that selects the delta's own additions. */\n featureId: string;\n /**\n * The caller's memoised LikeC4 read, when it has one. A FUNCTION rather than\n * the `FleetContext` itself, for the reason the use-case overlay states: one\n * memoised read does not justify an edge from this package to\n * `core/fleet-context.ts`, and the edge would push it up a DAG level for\n * nothing.\n */\n load?: (path: string) => Promise<LoadedDoc>;\n /**\n * The fleet's extending models, for the merge preview — the SAME list\n * `loam archive` routes with. A preview computed without them holds interior\n * the archive puts in `services/<…>/model.likec4`, so a topology document\n * naming `<service-fqn>.<container>` reads clean here and against nothing\n * once it is living; `deployment.doc-invalid` is the gate that then fails\n * open (verification 2026-09-04, review F8).\n *\n * A THUNK for `load`'s reason and one more: the early return below is the\n * axis's per-feature opt-in, and a feature with no `deployment/` directory\n * must not pay a fleet enumeration to be told so.\n */\n models?: () => Promise<readonly ExtendingModel[]>;\n}\n\n/** Whether this feature's topology could be read at all, against the map its merge would leave. */\nexport type DeploymentOverlay = { kind: \"read\" } | { kind: \"unreadable\"; errors: string[] };\n\n/**\n * Parse this feature's topology documents over the post-merge map.\n *\n * A feature with no `deployment/` directory is `read` at the cost of one walk\n * over a directory that is not there — the axis's per-feature opt-in, and the\n * whole price a fleet that has not adopted it pays. Nothing is staged, nothing\n * is parsed, and no temp tree is made.\n *\n * The verdict is deliberately BINARY. The use-case overlay hands its caller the\n * views it read because the axis has questions about their content; this one\n * has none — a topology document is checked by being parseable against the map\n * the merge will write, and everything else about it is graded by\n * `loam validate --all` once it is living, exactly as the landscape is.\n */\nexport async function readFeatureDeployments(req: DeploymentOverlayRequest): Promise<DeploymentOverlay> {\n const docs = await featureDeployments(req.featureDir);\n if (docs.length === 0) return { kind: \"read\" };\n // AFTER the early return: the fleet read is paid only by a feature that\n // actually brings a topology document.\n const models = req.models === undefined ? undefined : await req.models();\n const staged = await stageMergedProject({\n docsDir: req.docsDir,\n featureDir: req.featureDir,\n featureId: req.featureId,\n documents: docs.map((doc) => ({ rel: doc.rel, path: doc.path })),\n ...(req.load === undefined ? {} : { load: req.load }),\n ...(models === undefined ? {} : { models }),\n });\n return staged.kind === \"unreadable\" ? { kind: \"unreadable\", errors: staged.errors } : { kind: \"read\" };\n}\n\n/** A free name near the colliding one, so the fix is a rename the author can copy. */\nfunction suggest(rel: string): string {\n const name = basename(rel).replace(/\\.likec4$/, \"\");\n const dir = rel.slice(0, rel.length - basename(rel).length);\n return `${dir}${name}-extend.likec4`;\n}\n"]}
|
|
@@ -9,9 +9,13 @@
|
|
|
9
9
|
* rule, and that is a recorded risk, not an accident: the walk classifies a
|
|
10
10
|
* LIVE filesystem (readdir, symlinks, marker files) and cannot walk a git
|
|
11
11
|
* ref. What keeps the two honest is sharing the artifact-name table
|
|
12
|
-
* (`ARTIFACT_FILES`/`isServiceArtifactName` from `repo/paths.ts`)
|
|
13
|
-
* marker name (`SUBSYSTEM_MARKER`)
|
|
14
|
-
*
|
|
12
|
+
* (`ARTIFACT_FILES`/`isServiceArtifactName` from `repo/paths.ts`), the
|
|
13
|
+
* marker name (`SUBSYSTEM_MARKER`) and the reserved-interior set
|
|
14
|
+
* (`RESERVED_INTERIOR` from `repo/tree/walk.ts`) instead of respelling them, and restating
|
|
15
|
+
* — exactly — the walk's SERVICE branches: (0) a directory named for a
|
|
16
|
+
* service's own interior (`adrs/`, `usecases/`, `ui/`) is classified nowhere,
|
|
17
|
+
* counts as nothing beneath its parent, and hides everything under it, because
|
|
18
|
+
* the walk never descends into one; (1) a directory holding a service
|
|
15
19
|
* artifact is a service, marker present or not (artifacts win); (2) the leaf
|
|
16
20
|
* rule — neither marker nor artifact and nothing beneath (an empty or
|
|
17
21
|
* not-yet-adopted directory, a lone README.md, a lone `.gitkeep` — which is
|
|
@@ -42,6 +46,7 @@ import { openapiFromText } from "../openapi/doc.js";
|
|
|
42
46
|
import { asyncapiFromText } from "../asyncapi/read.js";
|
|
43
47
|
import { ARTIFACT_FILES, isServiceArtifactName } from "../repo/paths.js";
|
|
44
48
|
import { SUBSYSTEM_MARKER } from "../repo/tree/marker.js";
|
|
49
|
+
import { RESERVED_INTERIOR } from "../repo/tree/walk.js";
|
|
45
50
|
import { listBaseTree, showBaseFile } from "./base-git.js";
|
|
46
51
|
/**
|
|
47
52
|
* Classify the base tree's file listing into service directories — the module
|
|
@@ -85,13 +90,24 @@ function classifyBaseDirs(paths) {
|
|
|
85
90
|
const hasMarker = (dir) => fileChildren.get(dir)?.has(SUBSYSTEM_MARKER) === true;
|
|
86
91
|
const hasArtifact = (dir) => [...(fileChildren.get(dir) ?? [])].some((f) => isServiceArtifactName(f, "file")) ||
|
|
87
92
|
[...(dirChildren.get(dir) ?? [])].some((d) => isServiceArtifactName(d, "dir"));
|
|
93
|
+
// The walk's `RESERVED_INTERIOR` rule, restated for a flat listing: a
|
|
94
|
+
// directory named for a service's own interior is never classified, and
|
|
95
|
+
// neither is anything beneath it, because the walk never descends into one.
|
|
96
|
+
// Segment 2 onward, matching where the walk applies it — to a directory's
|
|
97
|
+
// CHILDREN, so a directory literally at `services/usecases/` is still
|
|
98
|
+
// classified there.
|
|
99
|
+
const reserved = (dir) => dir.split("/").slice(2).some((s) => RESERVED_INTERIOR.has(s));
|
|
100
|
+
// …and, for the same reason, one of them is not the subdirectory that keeps
|
|
101
|
+
// its parent off the leaf rule: the walk filters them out of `dirs` before
|
|
102
|
+
// asking whether anything is beneath.
|
|
103
|
+
const classifiableChildren = (dir) => [...(dirChildren.get(dir) ?? [])].some((d) => !RESERVED_INTERIOR.has(d));
|
|
88
104
|
// The walk's leaf rule: neither marker nor artifact and nothing beneath is a
|
|
89
105
|
// service. (A truly EMPTY directory is unrepresentable in a git tree, so at
|
|
90
106
|
// the base this arm means "no visible entry of ours" — files none of which
|
|
91
107
|
// are artifacts, or nothing but dot-named entries, the `loam seed` shape.)
|
|
92
|
-
const isLeafService = (dir) => !hasMarker(dir) && !hasArtifact(dir) && !
|
|
108
|
+
const isLeafService = (dir) => !hasMarker(dir) && !hasArtifact(dir) && !classifiableChildren(dir);
|
|
93
109
|
const candidates = [...new Set([...fileChildren.keys(), ...dirChildren.keys(), ...bare])]
|
|
94
|
-
.filter((dir) => dir !== "services" && dir.startsWith("services/"))
|
|
110
|
+
.filter((dir) => dir !== "services" && dir.startsWith("services/") && !reserved(dir))
|
|
95
111
|
.filter((dir) => hasArtifact(dir) || isLeafService(dir))
|
|
96
112
|
.sort((a, b) => {
|
|
97
113
|
const depth = a.split("/").length - b.split("/").length;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"base-state.js","sourceRoot":"","sources":["../../../src/core/diff/base-state.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The base side of `loam diff`: the fleet as the base commit held it, read\n * entirely out of git's object store (`./base-git.ts`) and parsed with the\n * SAME parsers the living files go through — `parseRequirements`,\n * `openapiFromText`, `asyncapiFromText` — so a base document can never read\n * differently from the identical bytes sitting on disk.\n *\n * CLASSIFICATION is a second, simplified spelling of `repo/tree/walk.ts`'s\n * rule, and that is a recorded risk, not an accident: the walk classifies a\n * LIVE filesystem (readdir, symlinks, marker files) and cannot walk a git\n * ref. What keeps the two honest is sharing the artifact-name table\n * (`ARTIFACT_FILES`/`isServiceArtifactName` from `repo/paths.ts`) and the\n * marker name (`SUBSYSTEM_MARKER`) instead of respelling them, and restating\n * — exactly — the walk's SERVICE branches: (1) a directory holding a service\n * artifact is a service, marker present or not (artifacts win); (2) the leaf\n * rule — neither marker nor artifact and nothing beneath (an empty or\n * not-yet-adopted directory, a lone README.md, a lone `.gitkeep` — which is\n * the whole content of a directory `loam seed` creates) is a service too; (3) nothing\n * inside a PLAIN service is classified again (`examples/openapi.yaml` two\n * levels down is interior, the walk stops there), but a service carrying the\n * marker BESIDE its artifacts is still descended — minus artifact-named\n * children like `adrs/` — so the services stranded beneath it stay\n * enumerated, the walk's marker-misplaced branch. Subsystem and unmarked\n * group directories need no restating: a flat file listing descends through\n * them by construction. What is deliberately NOT restated: the walk's\n * findings (`subsystem.marker-misplaced`, `subsystem.unmarked`,\n * `subsystem.name-collision`) — those are validate's live diagnostics; here\n * a name claimed twice suspends the subject (`ambiguous`) instead. If\n * walk.ts's rule ever changes, this module owes the same change;\n * test/diff.test.ts pins the visible agreement (subsystem move, not-adopted\n * leaf, stranded-under-marker).\n *\n * Deliberate consequence of joining by leaf directory name: a service MOVED\n * between subsystems keeps its id and diffs as UNCHANGED. Subsystem placement\n * is navigation, not identity — SCHEMA's own position, and the reason\n * `subsystem move` never touches a join key.\n */\nimport { isUtf8 } from \"node:buffer\";\nimport { inOrder } from \"../kernel/concurrency.js\";\nimport { parseRequirements } from \"../document/parse.js\";\nimport type { Requirement } from \"../document/spec.js\";\nimport { openapiFromText, type Operation } from \"../openapi/doc.js\";\nimport { asyncapiFromText } from \"../asyncapi/read.js\";\nimport type { EventMessage } from \"../asyncapi/model.js\";\nimport { ARTIFACT_FILES, isServiceArtifactName } from \"../repo/paths.js\";\nimport { SUBSYSTEM_MARKER } from \"../repo/tree/marker.js\";\nimport { listBaseTree, showBaseFile, type ResolvedBase } from \"./base-git.js\";\n\n/**\n * One artifact's state on one side of the diff. `unreadable` is the\n * containment arm: the file exists but could not be read as its kind, so\n * every finding on this axis for this subject is SUSPENDED — \"nobody could\n * look\" must never grade as \"nothing is there\", in either direction.\n */\nexport type AxisState<T> =\n | { kind: \"absent\" }\n | { kind: \"read\"; value: T }\n | { kind: \"unreadable\"; path: string; error: string };\n\n/** The keys of a service state that hold requirement-bearing documents. */\nexport type SpecAxisKey = \"spec\" | \"archSpec\";\n\n/**\n * One service on one side of the diff — the same shape for the base ref and\n * the working tree (`./current-state.ts` builds the other), which is what\n * lets `./semantic.ts` join the two without caring which side came from git.\n */\nexport interface ServiceState {\n id: string;\n /** Docs-relative directory with forward slashes, e.g. `services/billing/payment-service`. */\n dir: string;\n spec: AxisState<Requirement[]>;\n /** Read on both sides for the `Consumes:` join — the outbox requirement lives here as often as in spec.md (core/coherence/events/lookups.ts's lesson). */\n archSpec: AxisState<Requirement[]>;\n /** Operations with `x-loam-remove` markers filtered out: a marker is bookkeeping, never a callable operation. */\n openapi: AxisState<Operation[]>;\n /** Messages with removal markers filtered out, for the same reason. */\n asyncapi: AxisState<EventMessage[]>;\n}\n\nexport interface BaseFleet {\n /** By service id, insertion-sorted by id — deterministic output depends on it. */\n services: Map<string, ServiceState>;\n /**\n * Ids claimed by MORE than one base directory. The live walk grades this\n * `subsystem.name-collision`; here the id's base state is unanswerable, so\n * the subject is carried with every axis suspended rather than one claimant\n * silently winning the join.\n */\n ambiguous: Map<string, string[]>;\n}\n\nexport type BaseFleetRead =\n | { kind: \"ok\"; fleet: BaseFleet }\n | { kind: \"failed\"; detail: string };\n\n/** A classified base service directory and which artifact files the base tree listed in it. */\ninterface BaseDir {\n id: string;\n dir: string;\n files: Set<string>;\n}\n\n/**\n * Classify the base tree's file listing into service directories — the module\n * banner's three restated walk branches, in depth order so every ancestor is\n * decided before anything beneath it.\n */\nfunction classifyBaseDirs(paths: string[]): { dirs: BaseDir[]; ambiguous: Map<string, string[]> } {\n const fileChildren = new Map<string, Set<string>>();\n const dirChildren = new Map<string, Set<string>>();\n // Directories nothing VISIBLE lives in, so neither map above names them.\n const bare = new Set<string>();\n for (const path of paths) {\n const all = path.split(\"/\");\n // Dot-named entries are invisible to the live walk — but the walk still\n // SEES the directory holding them, because readdir discovered the\n // directory itself and only its CONTENTS are filtered (walk.ts's\n // `subdirNames` and `visitDir`). So cut the path at its first dot-named\n // segment rather than discarding the whole path: everything above the cut\n // stays a visible directory, the cut segment and everything beneath it\n // stay invisible. Discarding it outright hid every directory whose only\n // file is dot-named — which is exactly what `loam seed` writes into a\n // service directory it creates (`.gitkeep`, and nothing else), so a seeded\n // service was absent from the base and diffed as `diff.service-added`\n // against the unchanged tree that had just been committed.\n const cut = all.findIndex((s) => s.startsWith(\".\") || s === \"\");\n const segments = cut === -1 ? all : all.slice(0, cut);\n // Only a whole path ends in a file; a cut one is a pure directory chain.\n const fileAt = cut === -1 ? segments.length - 1 : -1;\n for (let i = 1; i < segments.length; i += 1) {\n const dir = segments.slice(0, i).join(\"/\");\n const child = segments[i]!;\n const into = i === fileAt ? fileChildren : dirChildren;\n let set = into.get(dir);\n if (set === undefined) into.set(dir, (set = new Set()));\n set.add(child);\n }\n if (fileAt === -1 && segments.length > 1) bare.add(segments.join(\"/\"));\n }\n const hasMarker = (dir: string): boolean => fileChildren.get(dir)?.has(SUBSYSTEM_MARKER) === true;\n const hasArtifact = (dir: string): boolean =>\n [...(fileChildren.get(dir) ?? [])].some((f) => isServiceArtifactName(f, \"file\")) ||\n [...(dirChildren.get(dir) ?? [])].some((d) => isServiceArtifactName(d, \"dir\"));\n // The walk's leaf rule: neither marker nor artifact and nothing beneath is a\n // service. (A truly EMPTY directory is unrepresentable in a git tree, so at\n // the base this arm means \"no visible entry of ours\" — files none of which\n // are artifacts, or nothing but dot-named entries, the `loam seed` shape.)\n const isLeafService = (dir: string): boolean => !hasMarker(dir) && !hasArtifact(dir) && !dirChildren.has(dir);\n const candidates = [...new Set([...fileChildren.keys(), ...dirChildren.keys(), ...bare])]\n .filter((dir) => dir !== \"services\" && dir.startsWith(\"services/\"))\n .filter((dir) => hasArtifact(dir) || isLeafService(dir))\n .sort((a, b) => {\n const depth = a.split(\"/\").length - b.split(\"/\").length;\n return depth !== 0 ? depth : a < b ? -1 : 1;\n });\n const accepted: BaseDir[] = [];\n const acceptedDirs = new Set<string>();\n // Is this candidate INTERIOR to the nearest service already accepted above\n // it? A plain service swallows everything beneath (the walk stops there); a\n // marker-beside-artifacts service is descended except through its\n // artifact-named children (`adrs/`), so a service stranded under it stays.\n const interior = (segments: string[]): boolean => {\n for (let i = segments.length - 1; i >= 2; i -= 1) {\n const ancestor = segments.slice(0, i).join(\"/\");\n if (!acceptedDirs.has(ancestor)) continue;\n return hasMarker(ancestor) ? isServiceArtifactName(segments[i]!, \"dir\") : true;\n }\n return false;\n };\n for (const dir of candidates) {\n const segments = dir.split(\"/\");\n if (interior(segments)) continue;\n acceptedDirs.add(dir);\n accepted.push({ id: segments[segments.length - 1]!, dir, files: fileChildren.get(dir) ?? new Set() });\n }\n return { dirs: accepted, ambiguous: collisions(accepted) };\n}\n\nfunction collisions(dirs: BaseDir[]): Map<string, string[]> {\n const byId = new Map<string, string[]>();\n for (const d of dirs) byId.set(d.id, [...(byId.get(d.id) ?? []), d.dir]);\n return new Map([...byId].filter(([, claimants]) => claimants.length > 1));\n}\n\n/** One shown base file, or `null` for a file the base tree never listed. */\ntype Fetched = { rel: string; read: Awaited<ReturnType<typeof showBaseFile>> } | null;\n\n/**\n * Lift one fetched file into an axis state through its parser. Absence is the\n * listing's answer, a `show` failure is per-subject containment, and only\n * then do the bytes reach a parser.\n */\nfunction axisFrom<T>(raw: Fetched, parse: (path: string, bytes: Buffer) => AxisState<T>): AxisState<T> {\n if (raw === null) return { kind: \"absent\" };\n if (raw.read.kind === \"failed\") return { kind: \"unreadable\", path: raw.rel, error: raw.read.detail };\n return parse(raw.rel, raw.read.bytes);\n}\n\n/**\n * The same refusal `readOpenapi`/`readAsyncapi`/`decodeDocument` make on the\n * living side: non-UTF-8 bytes would be silently rewritten as U+FFFD by\n * `toString`, and a document nobody wrote must not be graded.\n */\nfunction decodeBase(path: string, bytes: Buffer): { text: string } | { unreadable: AxisState<never> } {\n if (!isUtf8(bytes)) return { unreadable: { kind: \"unreadable\", path, error: \"file is not valid UTF-8\" } };\n return { text: bytes.toString(\"utf8\") };\n}\n\nfunction specState(path: string, bytes: Buffer): AxisState<Requirement[]> {\n const decoded = decodeBase(path, bytes);\n if (\"unreadable\" in decoded) return decoded.unreadable;\n try {\n return { kind: \"read\", value: parseRequirements(decoded.text) };\n } catch (e) {\n return { kind: \"unreadable\", path, error: e instanceof Error ? e.message : String(e) };\n }\n}\n\nfunction openapiState(path: string, bytes: Buffer): AxisState<Operation[]> {\n const decoded = decodeBase(path, bytes);\n if (\"unreadable\" in decoded) return decoded.unreadable;\n const doc = openapiFromText(decoded.text);\n return doc.unreadable\n ? { kind: \"unreadable\", path, error: doc.error ?? \"not a readable OpenAPI document\" }\n : { kind: \"read\", value: doc.ops.filter((op) => !op.remove) };\n}\n\nfunction asyncapiState(path: string, bytes: Buffer): AxisState<EventMessage[]> {\n const decoded = decodeBase(path, bytes);\n if (\"unreadable\" in decoded) return decoded.unreadable;\n const doc = asyncapiFromText(decoded.text);\n return doc.unreadable\n ? { kind: \"unreadable\", path, error: doc.error ?? \"not a readable AsyncAPI document\" }\n : { kind: \"read\", value: doc.messages.filter((m) => m.remove !== true) };\n}\n\n/** One base service's four axes. Sequential INSIDE the service on purpose: the pool below rations children at the service grain, and four spawns per in-flight worker would multiply past the cap it exists to hold. */\nasync function readBaseService(base: ResolvedBase, d: BaseDir): Promise<ServiceState> {\n const fetch = async (file: string): Promise<Fetched> =>\n d.files.has(file) ? { rel: `${d.dir}/${file}`, read: await showBaseFile(base, `${d.dir}/${file}`) } : null;\n return {\n id: d.id,\n dir: d.dir,\n spec: axisFrom(await fetch(ARTIFACT_FILES.spec), specState),\n archSpec: axisFrom(await fetch(ARTIFACT_FILES.archSpec), specState),\n openapi: axisFrom(await fetch(ARTIFACT_FILES.openapi), openapiState),\n asyncapi: axisFrom(await fetch(ARTIFACT_FILES.asyncapi), asyncapiState),\n };\n}\n\n/**\n * The whole base fleet. `failed` only when git cannot LIST the tree — the\n * whole-diff refusal; a single file that cannot be shown or parsed degrades\n * its own subject's axis to `unreadable` and nothing else.\n */\nexport async function readBaseFleet(base: ResolvedBase): Promise<BaseFleetRead> {\n const listing = await listBaseTree(base);\n if (listing.kind === \"failed\") return listing;\n const { dirs, ambiguous } = classifyBaseDirs(listing.paths);\n const sorted = [...dirs].sort((a, b) => (a.id < b.id ? -1 : 1)).filter((d) => !ambiguous.has(d.id));\n // Pooled, ordered reads: bounded children in flight, results by input index,\n // so the same base state always assembles the same fleet.\n const states = await inOrder(sorted, (d) => readBaseService(base, d));\n return { kind: \"ok\", fleet: { services: new Map(states.map((s) => [s.id, s])), ambiguous } };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"base-state.js","sourceRoot":"","sources":["../../../src/core/diff/base-state.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The base side of `loam diff`: the fleet as the base commit held it, read\n * entirely out of git's object store (`./base-git.ts`) and parsed with the\n * SAME parsers the living files go through — `parseRequirements`,\n * `openapiFromText`, `asyncapiFromText` — so a base document can never read\n * differently from the identical bytes sitting on disk.\n *\n * CLASSIFICATION is a second, simplified spelling of `repo/tree/walk.ts`'s\n * rule, and that is a recorded risk, not an accident: the walk classifies a\n * LIVE filesystem (readdir, symlinks, marker files) and cannot walk a git\n * ref. What keeps the two honest is sharing the artifact-name table\n * (`ARTIFACT_FILES`/`isServiceArtifactName` from `repo/paths.ts`), the\n * marker name (`SUBSYSTEM_MARKER`) and the reserved-interior set\n * (`RESERVED_INTERIOR` from `repo/tree/walk.ts`) instead of respelling them, and restating\n * — exactly — the walk's SERVICE branches: (0) a directory named for a\n * service's own interior (`adrs/`, `usecases/`, `ui/`) is classified nowhere,\n * counts as nothing beneath its parent, and hides everything under it, because\n * the walk never descends into one; (1) a directory holding a service\n * artifact is a service, marker present or not (artifacts win); (2) the leaf\n * rule — neither marker nor artifact and nothing beneath (an empty or\n * not-yet-adopted directory, a lone README.md, a lone `.gitkeep` — which is\n * the whole content of a directory `loam seed` creates) is a service too; (3) nothing\n * inside a PLAIN service is classified again (`examples/openapi.yaml` two\n * levels down is interior, the walk stops there), but a service carrying the\n * marker BESIDE its artifacts is still descended — minus artifact-named\n * children like `adrs/` — so the services stranded beneath it stay\n * enumerated, the walk's marker-misplaced branch. Subsystem and unmarked\n * group directories need no restating: a flat file listing descends through\n * them by construction. What is deliberately NOT restated: the walk's\n * findings (`subsystem.marker-misplaced`, `subsystem.unmarked`,\n * `subsystem.name-collision`) — those are validate's live diagnostics; here\n * a name claimed twice suspends the subject (`ambiguous`) instead. If\n * walk.ts's rule ever changes, this module owes the same change;\n * test/diff.test.ts pins the visible agreement (subsystem move, not-adopted\n * leaf, stranded-under-marker).\n *\n * Deliberate consequence of joining by leaf directory name: a service MOVED\n * between subsystems keeps its id and diffs as UNCHANGED. Subsystem placement\n * is navigation, not identity — SCHEMA's own position, and the reason\n * `subsystem move` never touches a join key.\n */\nimport { isUtf8 } from \"node:buffer\";\nimport { inOrder } from \"../kernel/concurrency.js\";\nimport { parseRequirements } from \"../document/parse.js\";\nimport type { Requirement } from \"../document/spec.js\";\nimport { openapiFromText, type Operation } from \"../openapi/doc.js\";\nimport { asyncapiFromText } from \"../asyncapi/read.js\";\nimport type { EventMessage } from \"../asyncapi/model.js\";\nimport { ARTIFACT_FILES, isServiceArtifactName } from \"../repo/paths.js\";\nimport { SUBSYSTEM_MARKER } from \"../repo/tree/marker.js\";\nimport { RESERVED_INTERIOR } from \"../repo/tree/walk.js\";\nimport { listBaseTree, showBaseFile, type ResolvedBase } from \"./base-git.js\";\n\n/**\n * One artifact's state on one side of the diff. `unreadable` is the\n * containment arm: the file exists but could not be read as its kind, so\n * every finding on this axis for this subject is SUSPENDED — \"nobody could\n * look\" must never grade as \"nothing is there\", in either direction.\n */\nexport type AxisState<T> =\n | { kind: \"absent\" }\n | { kind: \"read\"; value: T }\n | { kind: \"unreadable\"; path: string; error: string };\n\n/** The keys of a service state that hold requirement-bearing documents. */\nexport type SpecAxisKey = \"spec\" | \"archSpec\";\n\n/**\n * One service on one side of the diff — the same shape for the base ref and\n * the working tree (`./current-state.ts` builds the other), which is what\n * lets `./semantic.ts` join the two without caring which side came from git.\n */\nexport interface ServiceState {\n id: string;\n /** Docs-relative directory with forward slashes, e.g. `services/billing/payment-service`. */\n dir: string;\n spec: AxisState<Requirement[]>;\n /** Read on both sides for the `Consumes:` join — the outbox requirement lives here as often as in spec.md (core/coherence/events/lookups.ts's lesson). */\n archSpec: AxisState<Requirement[]>;\n /** Operations with `x-loam-remove` markers filtered out: a marker is bookkeeping, never a callable operation. */\n openapi: AxisState<Operation[]>;\n /** Messages with removal markers filtered out, for the same reason. */\n asyncapi: AxisState<EventMessage[]>;\n}\n\nexport interface BaseFleet {\n /** By service id, insertion-sorted by id — deterministic output depends on it. */\n services: Map<string, ServiceState>;\n /**\n * Ids claimed by MORE than one base directory. The live walk grades this\n * `subsystem.name-collision`; here the id's base state is unanswerable, so\n * the subject is carried with every axis suspended rather than one claimant\n * silently winning the join.\n */\n ambiguous: Map<string, string[]>;\n}\n\nexport type BaseFleetRead =\n | { kind: \"ok\"; fleet: BaseFleet }\n | { kind: \"failed\"; detail: string };\n\n/** A classified base service directory and which artifact files the base tree listed in it. */\ninterface BaseDir {\n id: string;\n dir: string;\n files: Set<string>;\n}\n\n/**\n * Classify the base tree's file listing into service directories — the module\n * banner's three restated walk branches, in depth order so every ancestor is\n * decided before anything beneath it.\n */\nfunction classifyBaseDirs(paths: string[]): { dirs: BaseDir[]; ambiguous: Map<string, string[]> } {\n const fileChildren = new Map<string, Set<string>>();\n const dirChildren = new Map<string, Set<string>>();\n // Directories nothing VISIBLE lives in, so neither map above names them.\n const bare = new Set<string>();\n for (const path of paths) {\n const all = path.split(\"/\");\n // Dot-named entries are invisible to the live walk — but the walk still\n // SEES the directory holding them, because readdir discovered the\n // directory itself and only its CONTENTS are filtered (walk.ts's\n // `subdirNames` and `visitDir`). So cut the path at its first dot-named\n // segment rather than discarding the whole path: everything above the cut\n // stays a visible directory, the cut segment and everything beneath it\n // stay invisible. Discarding it outright hid every directory whose only\n // file is dot-named — which is exactly what `loam seed` writes into a\n // service directory it creates (`.gitkeep`, and nothing else), so a seeded\n // service was absent from the base and diffed as `diff.service-added`\n // against the unchanged tree that had just been committed.\n const cut = all.findIndex((s) => s.startsWith(\".\") || s === \"\");\n const segments = cut === -1 ? all : all.slice(0, cut);\n // Only a whole path ends in a file; a cut one is a pure directory chain.\n const fileAt = cut === -1 ? segments.length - 1 : -1;\n for (let i = 1; i < segments.length; i += 1) {\n const dir = segments.slice(0, i).join(\"/\");\n const child = segments[i]!;\n const into = i === fileAt ? fileChildren : dirChildren;\n let set = into.get(dir);\n if (set === undefined) into.set(dir, (set = new Set()));\n set.add(child);\n }\n if (fileAt === -1 && segments.length > 1) bare.add(segments.join(\"/\"));\n }\n const hasMarker = (dir: string): boolean => fileChildren.get(dir)?.has(SUBSYSTEM_MARKER) === true;\n const hasArtifact = (dir: string): boolean =>\n [...(fileChildren.get(dir) ?? [])].some((f) => isServiceArtifactName(f, \"file\")) ||\n [...(dirChildren.get(dir) ?? [])].some((d) => isServiceArtifactName(d, \"dir\"));\n // The walk's `RESERVED_INTERIOR` rule, restated for a flat listing: a\n // directory named for a service's own interior is never classified, and\n // neither is anything beneath it, because the walk never descends into one.\n // Segment 2 onward, matching where the walk applies it — to a directory's\n // CHILDREN, so a directory literally at `services/usecases/` is still\n // classified there.\n const reserved = (dir: string): boolean => dir.split(\"/\").slice(2).some((s) => RESERVED_INTERIOR.has(s));\n // …and, for the same reason, one of them is not the subdirectory that keeps\n // its parent off the leaf rule: the walk filters them out of `dirs` before\n // asking whether anything is beneath.\n const classifiableChildren = (dir: string): boolean =>\n [...(dirChildren.get(dir) ?? [])].some((d) => !RESERVED_INTERIOR.has(d));\n // The walk's leaf rule: neither marker nor artifact and nothing beneath is a\n // service. (A truly EMPTY directory is unrepresentable in a git tree, so at\n // the base this arm means \"no visible entry of ours\" — files none of which\n // are artifacts, or nothing but dot-named entries, the `loam seed` shape.)\n const isLeafService = (dir: string): boolean =>\n !hasMarker(dir) && !hasArtifact(dir) && !classifiableChildren(dir);\n const candidates = [...new Set([...fileChildren.keys(), ...dirChildren.keys(), ...bare])]\n .filter((dir) => dir !== \"services\" && dir.startsWith(\"services/\") && !reserved(dir))\n .filter((dir) => hasArtifact(dir) || isLeafService(dir))\n .sort((a, b) => {\n const depth = a.split(\"/\").length - b.split(\"/\").length;\n return depth !== 0 ? depth : a < b ? -1 : 1;\n });\n const accepted: BaseDir[] = [];\n const acceptedDirs = new Set<string>();\n // Is this candidate INTERIOR to the nearest service already accepted above\n // it? A plain service swallows everything beneath (the walk stops there); a\n // marker-beside-artifacts service is descended except through its\n // artifact-named children (`adrs/`), so a service stranded under it stays.\n const interior = (segments: string[]): boolean => {\n for (let i = segments.length - 1; i >= 2; i -= 1) {\n const ancestor = segments.slice(0, i).join(\"/\");\n if (!acceptedDirs.has(ancestor)) continue;\n return hasMarker(ancestor) ? isServiceArtifactName(segments[i]!, \"dir\") : true;\n }\n return false;\n };\n for (const dir of candidates) {\n const segments = dir.split(\"/\");\n if (interior(segments)) continue;\n acceptedDirs.add(dir);\n accepted.push({ id: segments[segments.length - 1]!, dir, files: fileChildren.get(dir) ?? new Set() });\n }\n return { dirs: accepted, ambiguous: collisions(accepted) };\n}\n\nfunction collisions(dirs: BaseDir[]): Map<string, string[]> {\n const byId = new Map<string, string[]>();\n for (const d of dirs) byId.set(d.id, [...(byId.get(d.id) ?? []), d.dir]);\n return new Map([...byId].filter(([, claimants]) => claimants.length > 1));\n}\n\n/** One shown base file, or `null` for a file the base tree never listed. */\ntype Fetched = { rel: string; read: Awaited<ReturnType<typeof showBaseFile>> } | null;\n\n/**\n * Lift one fetched file into an axis state through its parser. Absence is the\n * listing's answer, a `show` failure is per-subject containment, and only\n * then do the bytes reach a parser.\n */\nfunction axisFrom<T>(raw: Fetched, parse: (path: string, bytes: Buffer) => AxisState<T>): AxisState<T> {\n if (raw === null) return { kind: \"absent\" };\n if (raw.read.kind === \"failed\") return { kind: \"unreadable\", path: raw.rel, error: raw.read.detail };\n return parse(raw.rel, raw.read.bytes);\n}\n\n/**\n * The same refusal `readOpenapi`/`readAsyncapi`/`decodeDocument` make on the\n * living side: non-UTF-8 bytes would be silently rewritten as U+FFFD by\n * `toString`, and a document nobody wrote must not be graded.\n */\nfunction decodeBase(path: string, bytes: Buffer): { text: string } | { unreadable: AxisState<never> } {\n if (!isUtf8(bytes)) return { unreadable: { kind: \"unreadable\", path, error: \"file is not valid UTF-8\" } };\n return { text: bytes.toString(\"utf8\") };\n}\n\nfunction specState(path: string, bytes: Buffer): AxisState<Requirement[]> {\n const decoded = decodeBase(path, bytes);\n if (\"unreadable\" in decoded) return decoded.unreadable;\n try {\n return { kind: \"read\", value: parseRequirements(decoded.text) };\n } catch (e) {\n return { kind: \"unreadable\", path, error: e instanceof Error ? e.message : String(e) };\n }\n}\n\nfunction openapiState(path: string, bytes: Buffer): AxisState<Operation[]> {\n const decoded = decodeBase(path, bytes);\n if (\"unreadable\" in decoded) return decoded.unreadable;\n const doc = openapiFromText(decoded.text);\n return doc.unreadable\n ? { kind: \"unreadable\", path, error: doc.error ?? \"not a readable OpenAPI document\" }\n : { kind: \"read\", value: doc.ops.filter((op) => !op.remove) };\n}\n\nfunction asyncapiState(path: string, bytes: Buffer): AxisState<EventMessage[]> {\n const decoded = decodeBase(path, bytes);\n if (\"unreadable\" in decoded) return decoded.unreadable;\n const doc = asyncapiFromText(decoded.text);\n return doc.unreadable\n ? { kind: \"unreadable\", path, error: doc.error ?? \"not a readable AsyncAPI document\" }\n : { kind: \"read\", value: doc.messages.filter((m) => m.remove !== true) };\n}\n\n/** One base service's four axes. Sequential INSIDE the service on purpose: the pool below rations children at the service grain, and four spawns per in-flight worker would multiply past the cap it exists to hold. */\nasync function readBaseService(base: ResolvedBase, d: BaseDir): Promise<ServiceState> {\n const fetch = async (file: string): Promise<Fetched> =>\n d.files.has(file) ? { rel: `${d.dir}/${file}`, read: await showBaseFile(base, `${d.dir}/${file}`) } : null;\n return {\n id: d.id,\n dir: d.dir,\n spec: axisFrom(await fetch(ARTIFACT_FILES.spec), specState),\n archSpec: axisFrom(await fetch(ARTIFACT_FILES.archSpec), specState),\n openapi: axisFrom(await fetch(ARTIFACT_FILES.openapi), openapiState),\n asyncapi: axisFrom(await fetch(ARTIFACT_FILES.asyncapi), asyncapiState),\n };\n}\n\n/**\n * The whole base fleet. `failed` only when git cannot LIST the tree — the\n * whole-diff refusal; a single file that cannot be shown or parsed degrades\n * its own subject's axis to `unreadable` and nothing else.\n */\nexport async function readBaseFleet(base: ResolvedBase): Promise<BaseFleetRead> {\n const listing = await listBaseTree(base);\n if (listing.kind === \"failed\") return listing;\n const { dirs, ambiguous } = classifyBaseDirs(listing.paths);\n const sorted = [...dirs].sort((a, b) => (a.id < b.id ? -1 : 1)).filter((d) => !ambiguous.has(d.id));\n // Pooled, ordered reads: bounded children in flight, results by input index,\n // so the same base state always assembles the same fleet.\n const states = await inOrder(sorted, (d) => readBaseService(base, d));\n return { kind: \"ok\", fleet: { services: new Map(states.map((s) => [s.id, s])), ambiguous } };\n}\n"]}
|
|
@@ -128,8 +128,17 @@ export function victimIndex(snap) {
|
|
|
128
128
|
const edges = [];
|
|
129
129
|
if (usable !== null && resolve !== null) {
|
|
130
130
|
for (const r of usable.relationships) {
|
|
131
|
-
|
|
131
|
+
// An edge whose SOURCE resolves to the provider as well is the
|
|
132
|
+
// provider's own internal call — the rule `usecase.step-unlinked`
|
|
133
|
+
// already states, that a service owes no operationId to itself — so it
|
|
134
|
+
// is not a consumer of the operation it is losing. Without this the
|
|
135
|
+
// downgrade the message half at :198 has always made was unreachable
|
|
136
|
+
// for any service that draws its own containers: one internal edge
|
|
137
|
+
// carrying the op kept every removal `diff.op-removed-consumed`
|
|
138
|
+
// (verification 2026-09-04, R3).
|
|
139
|
+
if (r.op === op && resolve(r.target) === provider && resolve(r.source) !== provider) {
|
|
132
140
|
edges.push(EDGE(resolve, r));
|
|
141
|
+
}
|
|
133
142
|
}
|
|
134
143
|
}
|
|
135
144
|
// Flows last in each list, deliberately: the edge and the requirement say
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"victims.js","sourceRoot":"","sources":["../../../src/core/diff/victims.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Who, in the CURRENT fleet, still depends on a thing the diff says was\n * removed or deprecated — the joins that turn \"an operation left the\n * contract\" into \"and these named consumers break\".\n *\n * THREE SOURCES answer that, and only the first two are copies of anything.\n *\n * The first two are deliberately a SECOND copy of the consumer join the\n * coherence gate asks, built on the same shared primitives, and both copies\n * say so:\n *\n * - the operation half mirrors `core/coherence/lookups.ts`'s\n * `edgeConsumers`/`requirementConsumers` (landscape edges with\n * `metadata { op }` resolved through `serviceResolver` WITH the enumerated\n * fleet — the container lesson recorded there — plus other services'\n * living `Operations:` lines, spec.md only, exactly as that module scans);\n * - the message half mirrors `core/coherence/events/lookups.ts`'s\n * `messageConsumers` (every `consumes` edge naming the message except one\n * INTO the provider, plus other services' `Consumes:` lines over BOTH spec\n * axes — the outbox requirement lives in arch.spec.md as often as in\n * spec.md, that module's lesson).\n *\n * The copies cannot be one module today: coherence's lookups are closures\n * over a feature's `DeltaScope` and its lazy caches; this one is an index\n * over an already-read fleet. Rule 13 (docs/DESIGN.md) governs — a THIRD\n * copy of this join forces the shared extraction, and whoever changes the\n * victim strings or the scan rules in either module owes the other the same\n * change. The victim string spellings are kept byte-compatible with the\n * coherence findings on purpose, so a reader meets one vocabulary.\n *\n * The THIRD source is not a copy and does not fall under that rule: the fleet's\n * USE CASES, joined in `core/usecases/operations.ts`. A hop of a\n * capability-tagged `dynamic view` that the model attributes to the operation\n * being removed is a business flow that stops working, and saying so — \"step 4\n * of Checkout breaks\" — is the only one of the three that names a consequence a\n * human weighs rather than a document they then have to open. It has its own\n * module because it asks a different question of a different reader (an\n * attribution over the whole `architecture/` PROJECT, not a scan of one\n * landscape file), and because the attributed/contested distinction it turns on\n * belongs beside `attributeStep`, not beside a requirement scan. Coherence has\n * no equivalent to keep in step with, so nothing here owes it a mirror.\n */\nimport { existsSync } from \"node:fs\";\nimport { type LoadedDoc, type Rel } from \"../c4/likec4.js\";\nimport { serviceResolver } from \"../c4/resolve/service.js\";\nimport { landscapePath } from \"../repo/paths.js\";\nimport { readUseCases, type UseCaseScan } from \"../usecases/fleet.js\";\nimport { hopsConsuming, hopsExercising } from \"../usecases/operations.js\";\nimport type { DocsDir } from \"../kernel/ids/dirs.js\";\nimport type { FleetContext } from \"../fleet-context.js\";\nimport type { Requirement } from \"../document/spec.js\";\nimport type { ServiceState } from \"./base-state.js\";\n\n/**\n * One consumer scan's answer, honest about its own coverage. `victims` is\n * positive evidence; `suspended` names the consumers whose relevant document\n * could not be read (or whose identity is ambiguous), because a scan that\n * skipped a file loam REFUSED to read must never let the caller assert\n * \"nobody names it\" — the fail-open shape this type exists to forbid.\n */\nexport interface ConsumerScan {\n victims: string[];\n suspended: string[];\n}\n\n/** The current-state consumer questions `semantic.ts` asks per removal. */\nexport interface VictimIndex {\n /** Current consumers of `provider`'s operation `op`: landscape edges into the provider, then other services' living requirements, then the use-case hops the model attributes to it (contested hops ride as suspensions). */\n opConsumers(provider: string, op: string): Promise<ConsumerScan>;\n /** Current consumers of message `name` a `provider` is dropping: consumes-edges (not into the provider), then other services' `Consumes:` lines over both spec axes, then the use-case hops backed by such an edge. */\n messageConsumers(provider: string, name: string): Promise<ConsumerScan>;\n /** Current services whose asyncapi declares `name` — positive evidence only; a message still declared elsewhere is not orphaned by one producer dropping it. */\n declarers(name: string): string[];\n}\n\n/** The requirements one current service contributes to a scan, per axis rule. */\nfunction reqsOf(state: ServiceState, axes: readonly (\"spec\" | \"archSpec\")[]): Requirement[] {\n return axes.flatMap((axis) => {\n const a = state[axis];\n return a.kind === \"read\" ? a.value : [];\n });\n}\n\nconst EDGE = (resolve: (id: string) => string, r: Rel): string =>\n `edge ${resolve(r.source)} → ${resolve(r.target)}${r.title === undefined ? \"\" : ` (\"${r.title}\")`}`;\n\n/** Entry comparator by map key — deterministic suspension order. */\nconst byFirst = <T>(a: [string, T], b: [string, T]): number => (a[0] < b[0] ? -1 : 1);\n\n/** The landscape half of the index, loaded at most once and only when a removal actually asks. */\ninterface Drawn {\n usable: LoadedDoc | null;\n resolve: ((id: string) => string) | null;\n}\n\nexport function victimIndex(snap: {\n docsDir: DocsDir;\n context: FleetContext;\n current: ReadonlyMap<string, ServiceState>;\n /** Ids the working tree claims twice (current-state.ts) — their documents cannot be scanned, so they ride as suspended consumers. */\n ambiguous: ReadonlyMap<string, string[]>;\n}): VictimIndex {\n // Lazy on purpose, the coherence lookups' own discipline: the landscape is\n // a full LikeC4 workspace spin-up, and a diff that removes nothing must not\n // pay for it. An absent landscape proves nothing; an unreadable one proves\n // nothing either way — `landscape.invalid` is validate's finding to make,\n // and inventing victims out of a parse error would point the reader at the\n // wrong file (core/coherence/lookups.ts's doctrine, verbatim).\n let drawn: Promise<Drawn> | undefined;\n const landscape = (): Promise<Drawn> =>\n (drawn ??= (async (): Promise<Drawn> => {\n const path = landscapePath(snap.docsDir);\n const doc: LoadedDoc | null = existsSync(path) ? await snap.context.loadLikeC4(path) : null;\n const usable = doc !== null && doc.errors.length === 0 ? doc : null;\n // The enumerated fleet rides into the resolver so a landscape that\n // models CONTAINERS stays visible: without `known`, an edge into\n // `payment.api` resolves to a service called \"api\" that has never\n // existed and the join finds nobody — the exact defect DESIGN's\n // serviceResolver-known row records.\n return {\n usable,\n resolve: usable === null ? null : serviceResolver(usable.elements, new Set(snap.current.keys())),\n };\n })());\n\n // Sorted, so victim order never depends on the enumeration's walk order.\n const ids = [...snap.current.keys()].sort();\n\n /**\n * The use-case half, lazy for the same reason and one step further.\n *\n * `readUseCases` carries its own cheap gate — a byte scan for the reserved\n * tag prefix — so a fleet that declares no use case never starts LikeC4 here\n * at all, and a fleet that does pays for the `architecture/` project exactly\n * once per `loam diff`, at the first removal that asks. The fleet set is the\n * same one the landscape resolver above is built from, so the two halves\n * cannot disagree about which service an element stands for.\n */\n let flows: Promise<UseCaseScan> | undefined;\n const useCases = (): Promise<UseCaseScan> =>\n (flows ??= readUseCases({ docsDir: snap.docsDir, known: new Set(snap.current.keys()) }));\n\n /**\n * The requirement half of one scan, `tupleVisible`'s own discipline applied\n * to victims: an unreadable consumer axis contributes a SUSPENSION, never a\n * silent nothing — \"consuming nothing\" and \"loam refused to read the file\"\n * are opposite facts, and collapsing them is how a breaking removal used to\n * grade down to a confident warning.\n */\n const scanRequirements = (unit: { provider: string; axes: readonly (\"spec\" | \"archSpec\")[]; hit: (r: Requirement) => boolean }): ConsumerScan => {\n const out: ConsumerScan = { victims: [], suspended: [] };\n for (const other of ids.filter((id) => id !== unit.provider)) {\n const state = snap.current.get(other)!;\n for (const axis of unit.axes) {\n const a = state[axis];\n if (a.kind === \"unreadable\") {\n out.suspended.push(`${other} — ${a.path} could not be read, so its requirements could not be scanned`);\n continue;\n }\n if (a.kind !== \"read\") continue;\n for (const r of a.value) if (unit.hit(r)) out.victims.push(`${other}'s living requirement '${r.name}'`);\n }\n }\n for (const [id, dirs] of [...snap.ambiguous].sort(byFirst)) {\n if (id !== unit.provider) out.suspended.push(`${id} — claimed by ${dirs.join(\" and \")}, so its documents could not be scanned`);\n }\n return out;\n };\n\n const opConsumers = async (provider: string, op: string): Promise<ConsumerScan> => {\n const { usable, resolve } = await landscape();\n const scan = scanRequirements({ provider, axes: [\"spec\"], hit: (r) => r.operations.includes(op) });\n const edges: string[] = [];\n if (usable !== null && resolve !== null) {\n for (const r of usable.relationships) {\n if (r.op === op && resolve(r.target) === provider) edges.push(EDGE(resolve, r));\n }\n }\n // Flows last in each list, deliberately: the edge and the requirement say\n // WHICH document to open, and the flow says what the reader is about to\n // break — so a `details[]` scanned top-down ends on the consequence.\n const hops = hopsExercising(await useCases(), { provider, op });\n return {\n victims: [...edges, ...scan.victims, ...hops.breaks],\n suspended: [...scan.suspended, ...hops.unsure],\n };\n };\n\n const messageConsumers = async (provider: string, name: string): Promise<ConsumerScan> => {\n const { usable, resolve } = await landscape();\n const scan = scanRequirements({ provider, axes: [\"spec\", \"archSpec\"], hit: (r) => r.consumes.includes(name) });\n const edges: string[] = [];\n if (usable !== null && resolve !== null) {\n for (const r of usable.relationships) {\n // The message join is fleet-global, so every edge naming it counts —\n // except one into the provider itself, whose consumption leaves with\n // the same change (events/lookups.ts's exclusion, verbatim).\n if (r.consumes === name && resolve(r.target) !== provider) edges.push(EDGE(resolve, r));\n }\n }\n const hops = hopsConsuming(await useCases(), { provider, name });\n return { victims: [...edges, ...scan.victims, ...hops], suspended: scan.suspended };\n };\n\n const declarers = (name: string): string[] =>\n ids.filter((id) => {\n const axis = snap.current.get(id)!.asyncapi;\n return axis.kind === \"read\" && axis.value.some((m) => m.name === name);\n });\n\n return { opConsumers, messageConsumers, declarers };\n}\n\n/**\n * One cross-service join: `consumer`'s living requirements name a `token` that\n * `provider`'s contract defines. The same \"who depends on what\" question the\n * index above answers per removal, asked here of a WHOLE state — base or\n * current — so `semantic.ts` can report joins that appeared or went away.\n */\nexport interface ConsumerTuple {\n kind: \"op\" | \"message\";\n consumer: string;\n provider: string;\n token: string;\n}\n\n/** The map key a tuple is joined on between the two states. */\nfunction tupleKey(t: ConsumerTuple): string {\n return `${t.kind}\\0${t.consumer}\\0${t.provider}\\0${t.token}`;\n}\n\n/**\n * Every cross-service join one state holds, keyed for the base/current join.\n * Axis rules mirror the two consumer scans above: `Operations:` from spec.md\n * only, `Consumes:` from both spec axes. Unreadable axes simply contribute\n * nothing here — `semantic.ts` guards the GRADING so a join is never called\n * added or removed off a side nobody could read.\n */\nexport function consumerTuples(states: ReadonlyMap<string, ServiceState>): Map<string, ConsumerTuple> {\n const providersOf = (pick: (s: ServiceState) => string[]): Map<string, string[]> => {\n const map = new Map<string, string[]>();\n for (const id of [...states.keys()].sort()) {\n for (const token of pick(states.get(id)!)) map.set(token, [...(map.get(token) ?? []), id]);\n }\n return map;\n };\n const opProviders = providersOf((s) => (s.openapi.kind === \"read\" ? s.openapi.value.map((op) => op.id) : []));\n const msgProviders = providersOf((s) => (s.asyncapi.kind === \"read\" ? s.asyncapi.value.map((m) => m.name) : []));\n const out = new Map<string, ConsumerTuple>();\n const add = (t: ConsumerTuple): void => {\n out.set(tupleKey(t), t);\n };\n for (const consumer of [...states.keys()].sort()) {\n const state = states.get(consumer)!;\n for (const r of reqsOf(state, [\"spec\"])) {\n for (const op of r.operations) {\n for (const p of opProviders.get(op) ?? []) if (p !== consumer) add({ kind: \"op\", consumer, provider: p, token: op });\n }\n }\n for (const r of reqsOf(state, [\"spec\", \"archSpec\"])) {\n for (const name of r.consumes) {\n for (const p of msgProviders.get(name) ?? []) if (p !== consumer) add({ kind: \"message\", consumer, provider: p, token: name });\n }\n }\n }\n return out;\n}\n\n/** May a tuple be graded off this side? Absence answers; unreadable does not; an ambiguous base identity answers nothing. */\nfunction tupleVisible(states: ReadonlyMap<string, ServiceState>, ambiguous: ReadonlySet<string>, t: ConsumerTuple): boolean {\n if (ambiguous.has(t.consumer) || ambiguous.has(t.provider)) return false;\n const readable = (id: string, axes: (\"spec\" | \"archSpec\" | \"openapi\" | \"asyncapi\")[]): boolean => {\n const state = states.get(id);\n return state === undefined || axes.every((axis) => state[axis].kind !== \"unreadable\");\n };\n return (\n readable(t.consumer, t.kind === \"op\" ? [\"spec\"] : [\"spec\", \"archSpec\"]) &&\n readable(t.provider, [t.kind === \"op\" ? \"openapi\" : \"asyncapi\"])\n );\n}\n\n/** One graded cross-service join change between the two states. */\nexport interface ConsumerChange {\n t: ConsumerTuple;\n change: \"added\" | \"removed\";\n}\n\n/**\n * The joins that appeared or went away between base and current, sorted for\n * deterministic output (provider, kind, token, consumer). The visibility\n * guard is the containment rule: a join is never called \"added\" off a base\n * side nobody could read, nor \"removed\" off an unreadable working file — the\n * suspended axis already carries that fact.\n */\nexport function consumerJoinChanges(input: {\n base: ReadonlyMap<string, ServiceState>;\n baseAmbiguous: ReadonlySet<string>;\n current: ReadonlyMap<string, ServiceState>;\n currentAmbiguous: ReadonlySet<string>;\n}): ConsumerChange[] {\n const baseTuples = consumerTuples(input.base);\n const currentTuples = consumerTuples(input.current);\n const graded: ConsumerChange[] = [];\n for (const [key, t] of currentTuples) {\n if (!baseTuples.has(key) && tupleVisible(input.base, input.baseAmbiguous, t)) graded.push({ t, change: \"added\" });\n }\n for (const [key, t] of baseTuples) {\n if (!currentTuples.has(key) && tupleVisible(input.current, input.currentAmbiguous, t)) graded.push({ t, change: \"removed\" });\n }\n return graded.sort((a, b) => {\n const ka = `${a.t.provider}\\0${a.t.kind}\\0${a.t.token}\\0${a.t.consumer}\\0${a.change}`;\n const kb = `${b.t.provider}\\0${b.t.kind}\\0${b.t.token}\\0${b.t.consumer}\\0${b.change}`;\n return ka < kb ? -1 : 1;\n });\n}\n"]}
|
|
1
|
+
{"version":3,"file":"victims.js","sourceRoot":"","sources":["../../../src/core/diff/victims.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Who, in the CURRENT fleet, still depends on a thing the diff says was\n * removed or deprecated — the joins that turn \"an operation left the\n * contract\" into \"and these named consumers break\".\n *\n * THREE SOURCES answer that, and only the first two are copies of anything.\n *\n * The first two are deliberately a SECOND copy of the consumer join the\n * coherence gate asks, built on the same shared primitives, and both copies\n * say so:\n *\n * - the operation half mirrors `core/coherence/lookups.ts`'s\n * `edgeConsumers`/`requirementConsumers` (landscape edges with\n * `metadata { op }` resolved through `serviceResolver` WITH the enumerated\n * fleet — the container lesson recorded there — plus other services'\n * living `Operations:` lines, spec.md only, exactly as that module scans);\n * - the message half mirrors `core/coherence/events/lookups.ts`'s\n * `messageConsumers` (every `consumes` edge naming the message except one\n * INTO the provider, plus other services' `Consumes:` lines over BOTH spec\n * axes — the outbox requirement lives in arch.spec.md as often as in\n * spec.md, that module's lesson).\n *\n * The copies cannot be one module today: coherence's lookups are closures\n * over a feature's `DeltaScope` and its lazy caches; this one is an index\n * over an already-read fleet. Rule 13 (docs/DESIGN.md) governs — a THIRD\n * copy of this join forces the shared extraction, and whoever changes the\n * victim strings or the scan rules in either module owes the other the same\n * change. The victim string spellings are kept byte-compatible with the\n * coherence findings on purpose, so a reader meets one vocabulary.\n *\n * The THIRD source is not a copy and does not fall under that rule: the fleet's\n * USE CASES, joined in `core/usecases/operations.ts`. A hop of a\n * capability-tagged `dynamic view` that the model attributes to the operation\n * being removed is a business flow that stops working, and saying so — \"step 4\n * of Checkout breaks\" — is the only one of the three that names a consequence a\n * human weighs rather than a document they then have to open. It has its own\n * module because it asks a different question of a different reader (an\n * attribution over the whole `architecture/` PROJECT, not a scan of one\n * landscape file), and because the attributed/contested distinction it turns on\n * belongs beside `attributeStep`, not beside a requirement scan. Coherence has\n * no equivalent to keep in step with, so nothing here owes it a mirror.\n */\nimport { existsSync } from \"node:fs\";\nimport { type LoadedDoc, type Rel } from \"../c4/likec4.js\";\nimport { serviceResolver } from \"../c4/resolve/service.js\";\nimport { landscapePath } from \"../repo/paths.js\";\nimport { readUseCases, type UseCaseScan } from \"../usecases/fleet.js\";\nimport { hopsConsuming, hopsExercising } from \"../usecases/operations.js\";\nimport type { DocsDir } from \"../kernel/ids/dirs.js\";\nimport type { FleetContext } from \"../fleet-context.js\";\nimport type { Requirement } from \"../document/spec.js\";\nimport type { ServiceState } from \"./base-state.js\";\n\n/**\n * One consumer scan's answer, honest about its own coverage. `victims` is\n * positive evidence; `suspended` names the consumers whose relevant document\n * could not be read (or whose identity is ambiguous), because a scan that\n * skipped a file loam REFUSED to read must never let the caller assert\n * \"nobody names it\" — the fail-open shape this type exists to forbid.\n */\nexport interface ConsumerScan {\n victims: string[];\n suspended: string[];\n}\n\n/** The current-state consumer questions `semantic.ts` asks per removal. */\nexport interface VictimIndex {\n /** Current consumers of `provider`'s operation `op`: landscape edges into the provider, then other services' living requirements, then the use-case hops the model attributes to it (contested hops ride as suspensions). */\n opConsumers(provider: string, op: string): Promise<ConsumerScan>;\n /** Current consumers of message `name` a `provider` is dropping: consumes-edges (not into the provider), then other services' `Consumes:` lines over both spec axes, then the use-case hops backed by such an edge. */\n messageConsumers(provider: string, name: string): Promise<ConsumerScan>;\n /** Current services whose asyncapi declares `name` — positive evidence only; a message still declared elsewhere is not orphaned by one producer dropping it. */\n declarers(name: string): string[];\n}\n\n/** The requirements one current service contributes to a scan, per axis rule. */\nfunction reqsOf(state: ServiceState, axes: readonly (\"spec\" | \"archSpec\")[]): Requirement[] {\n return axes.flatMap((axis) => {\n const a = state[axis];\n return a.kind === \"read\" ? a.value : [];\n });\n}\n\nconst EDGE = (resolve: (id: string) => string, r: Rel): string =>\n `edge ${resolve(r.source)} → ${resolve(r.target)}${r.title === undefined ? \"\" : ` (\"${r.title}\")`}`;\n\n/** Entry comparator by map key — deterministic suspension order. */\nconst byFirst = <T>(a: [string, T], b: [string, T]): number => (a[0] < b[0] ? -1 : 1);\n\n/** The landscape half of the index, loaded at most once and only when a removal actually asks. */\ninterface Drawn {\n usable: LoadedDoc | null;\n resolve: ((id: string) => string) | null;\n}\n\nexport function victimIndex(snap: {\n docsDir: DocsDir;\n context: FleetContext;\n current: ReadonlyMap<string, ServiceState>;\n /** Ids the working tree claims twice (current-state.ts) — their documents cannot be scanned, so they ride as suspended consumers. */\n ambiguous: ReadonlyMap<string, string[]>;\n}): VictimIndex {\n // Lazy on purpose, the coherence lookups' own discipline: the landscape is\n // a full LikeC4 workspace spin-up, and a diff that removes nothing must not\n // pay for it. An absent landscape proves nothing; an unreadable one proves\n // nothing either way — `landscape.invalid` is validate's finding to make,\n // and inventing victims out of a parse error would point the reader at the\n // wrong file (core/coherence/lookups.ts's doctrine, verbatim).\n let drawn: Promise<Drawn> | undefined;\n const landscape = (): Promise<Drawn> =>\n (drawn ??= (async (): Promise<Drawn> => {\n const path = landscapePath(snap.docsDir);\n const doc: LoadedDoc | null = existsSync(path) ? await snap.context.loadLikeC4(path) : null;\n const usable = doc !== null && doc.errors.length === 0 ? doc : null;\n // The enumerated fleet rides into the resolver so a landscape that\n // models CONTAINERS stays visible: without `known`, an edge into\n // `payment.api` resolves to a service called \"api\" that has never\n // existed and the join finds nobody — the exact defect DESIGN's\n // serviceResolver-known row records.\n return {\n usable,\n resolve: usable === null ? null : serviceResolver(usable.elements, new Set(snap.current.keys())),\n };\n })());\n\n // Sorted, so victim order never depends on the enumeration's walk order.\n const ids = [...snap.current.keys()].sort();\n\n /**\n * The use-case half, lazy for the same reason and one step further.\n *\n * `readUseCases` carries its own cheap gate — a byte scan for the reserved\n * tag prefix — so a fleet that declares no use case never starts LikeC4 here\n * at all, and a fleet that does pays for the `architecture/` project exactly\n * once per `loam diff`, at the first removal that asks. The fleet set is the\n * same one the landscape resolver above is built from, so the two halves\n * cannot disagree about which service an element stands for.\n */\n let flows: Promise<UseCaseScan> | undefined;\n const useCases = (): Promise<UseCaseScan> =>\n (flows ??= readUseCases({ docsDir: snap.docsDir, known: new Set(snap.current.keys()) }));\n\n /**\n * The requirement half of one scan, `tupleVisible`'s own discipline applied\n * to victims: an unreadable consumer axis contributes a SUSPENSION, never a\n * silent nothing — \"consuming nothing\" and \"loam refused to read the file\"\n * are opposite facts, and collapsing them is how a breaking removal used to\n * grade down to a confident warning.\n */\n const scanRequirements = (unit: { provider: string; axes: readonly (\"spec\" | \"archSpec\")[]; hit: (r: Requirement) => boolean }): ConsumerScan => {\n const out: ConsumerScan = { victims: [], suspended: [] };\n for (const other of ids.filter((id) => id !== unit.provider)) {\n const state = snap.current.get(other)!;\n for (const axis of unit.axes) {\n const a = state[axis];\n if (a.kind === \"unreadable\") {\n out.suspended.push(`${other} — ${a.path} could not be read, so its requirements could not be scanned`);\n continue;\n }\n if (a.kind !== \"read\") continue;\n for (const r of a.value) if (unit.hit(r)) out.victims.push(`${other}'s living requirement '${r.name}'`);\n }\n }\n for (const [id, dirs] of [...snap.ambiguous].sort(byFirst)) {\n if (id !== unit.provider) out.suspended.push(`${id} — claimed by ${dirs.join(\" and \")}, so its documents could not be scanned`);\n }\n return out;\n };\n\n const opConsumers = async (provider: string, op: string): Promise<ConsumerScan> => {\n const { usable, resolve } = await landscape();\n const scan = scanRequirements({ provider, axes: [\"spec\"], hit: (r) => r.operations.includes(op) });\n const edges: string[] = [];\n if (usable !== null && resolve !== null) {\n for (const r of usable.relationships) {\n // An edge whose SOURCE resolves to the provider as well is the\n // provider's own internal call — the rule `usecase.step-unlinked`\n // already states, that a service owes no operationId to itself — so it\n // is not a consumer of the operation it is losing. Without this the\n // downgrade the message half at :198 has always made was unreachable\n // for any service that draws its own containers: one internal edge\n // carrying the op kept every removal `diff.op-removed-consumed`\n // (verification 2026-09-04, R3).\n if (r.op === op && resolve(r.target) === provider && resolve(r.source) !== provider) {\n edges.push(EDGE(resolve, r));\n }\n }\n }\n // Flows last in each list, deliberately: the edge and the requirement say\n // WHICH document to open, and the flow says what the reader is about to\n // break — so a `details[]` scanned top-down ends on the consequence.\n const hops = hopsExercising(await useCases(), { provider, op });\n return {\n victims: [...edges, ...scan.victims, ...hops.breaks],\n suspended: [...scan.suspended, ...hops.unsure],\n };\n };\n\n const messageConsumers = async (provider: string, name: string): Promise<ConsumerScan> => {\n const { usable, resolve } = await landscape();\n const scan = scanRequirements({ provider, axes: [\"spec\", \"archSpec\"], hit: (r) => r.consumes.includes(name) });\n const edges: string[] = [];\n if (usable !== null && resolve !== null) {\n for (const r of usable.relationships) {\n // The message join is fleet-global, so every edge naming it counts —\n // except one into the provider itself, whose consumption leaves with\n // the same change (events/lookups.ts's exclusion, verbatim).\n if (r.consumes === name && resolve(r.target) !== provider) edges.push(EDGE(resolve, r));\n }\n }\n const hops = hopsConsuming(await useCases(), { provider, name });\n return { victims: [...edges, ...scan.victims, ...hops], suspended: scan.suspended };\n };\n\n const declarers = (name: string): string[] =>\n ids.filter((id) => {\n const axis = snap.current.get(id)!.asyncapi;\n return axis.kind === \"read\" && axis.value.some((m) => m.name === name);\n });\n\n return { opConsumers, messageConsumers, declarers };\n}\n\n/**\n * One cross-service join: `consumer`'s living requirements name a `token` that\n * `provider`'s contract defines. The same \"who depends on what\" question the\n * index above answers per removal, asked here of a WHOLE state — base or\n * current — so `semantic.ts` can report joins that appeared or went away.\n */\nexport interface ConsumerTuple {\n kind: \"op\" | \"message\";\n consumer: string;\n provider: string;\n token: string;\n}\n\n/** The map key a tuple is joined on between the two states. */\nfunction tupleKey(t: ConsumerTuple): string {\n return `${t.kind}\\0${t.consumer}\\0${t.provider}\\0${t.token}`;\n}\n\n/**\n * Every cross-service join one state holds, keyed for the base/current join.\n * Axis rules mirror the two consumer scans above: `Operations:` from spec.md\n * only, `Consumes:` from both spec axes. Unreadable axes simply contribute\n * nothing here — `semantic.ts` guards the GRADING so a join is never called\n * added or removed off a side nobody could read.\n */\nexport function consumerTuples(states: ReadonlyMap<string, ServiceState>): Map<string, ConsumerTuple> {\n const providersOf = (pick: (s: ServiceState) => string[]): Map<string, string[]> => {\n const map = new Map<string, string[]>();\n for (const id of [...states.keys()].sort()) {\n for (const token of pick(states.get(id)!)) map.set(token, [...(map.get(token) ?? []), id]);\n }\n return map;\n };\n const opProviders = providersOf((s) => (s.openapi.kind === \"read\" ? s.openapi.value.map((op) => op.id) : []));\n const msgProviders = providersOf((s) => (s.asyncapi.kind === \"read\" ? s.asyncapi.value.map((m) => m.name) : []));\n const out = new Map<string, ConsumerTuple>();\n const add = (t: ConsumerTuple): void => {\n out.set(tupleKey(t), t);\n };\n for (const consumer of [...states.keys()].sort()) {\n const state = states.get(consumer)!;\n for (const r of reqsOf(state, [\"spec\"])) {\n for (const op of r.operations) {\n for (const p of opProviders.get(op) ?? []) if (p !== consumer) add({ kind: \"op\", consumer, provider: p, token: op });\n }\n }\n for (const r of reqsOf(state, [\"spec\", \"archSpec\"])) {\n for (const name of r.consumes) {\n for (const p of msgProviders.get(name) ?? []) if (p !== consumer) add({ kind: \"message\", consumer, provider: p, token: name });\n }\n }\n }\n return out;\n}\n\n/** May a tuple be graded off this side? Absence answers; unreadable does not; an ambiguous base identity answers nothing. */\nfunction tupleVisible(states: ReadonlyMap<string, ServiceState>, ambiguous: ReadonlySet<string>, t: ConsumerTuple): boolean {\n if (ambiguous.has(t.consumer) || ambiguous.has(t.provider)) return false;\n const readable = (id: string, axes: (\"spec\" | \"archSpec\" | \"openapi\" | \"asyncapi\")[]): boolean => {\n const state = states.get(id);\n return state === undefined || axes.every((axis) => state[axis].kind !== \"unreadable\");\n };\n return (\n readable(t.consumer, t.kind === \"op\" ? [\"spec\"] : [\"spec\", \"archSpec\"]) &&\n readable(t.provider, [t.kind === \"op\" ? \"openapi\" : \"asyncapi\"])\n );\n}\n\n/** One graded cross-service join change between the two states. */\nexport interface ConsumerChange {\n t: ConsumerTuple;\n change: \"added\" | \"removed\";\n}\n\n/**\n * The joins that appeared or went away between base and current, sorted for\n * deterministic output (provider, kind, token, consumer). The visibility\n * guard is the containment rule: a join is never called \"added\" off a base\n * side nobody could read, nor \"removed\" off an unreadable working file — the\n * suspended axis already carries that fact.\n */\nexport function consumerJoinChanges(input: {\n base: ReadonlyMap<string, ServiceState>;\n baseAmbiguous: ReadonlySet<string>;\n current: ReadonlyMap<string, ServiceState>;\n currentAmbiguous: ReadonlySet<string>;\n}): ConsumerChange[] {\n const baseTuples = consumerTuples(input.base);\n const currentTuples = consumerTuples(input.current);\n const graded: ConsumerChange[] = [];\n for (const [key, t] of currentTuples) {\n if (!baseTuples.has(key) && tupleVisible(input.base, input.baseAmbiguous, t)) graded.push({ t, change: \"added\" });\n }\n for (const [key, t] of baseTuples) {\n if (!currentTuples.has(key) && tupleVisible(input.current, input.currentAmbiguous, t)) graded.push({ t, change: \"removed\" });\n }\n return graded.sort((a, b) => {\n const ka = `${a.t.provider}\\0${a.t.kind}\\0${a.t.token}\\0${a.t.consumer}\\0${a.change}`;\n const kb = `${b.t.provider}\\0${b.t.kind}\\0${b.t.token}\\0${b.t.consumer}\\0${b.change}`;\n return ka < kb ? -1 : 1;\n });\n}\n"]}
|
package/dist/core/docs.js
CHANGED
|
@@ -14,7 +14,7 @@ import { join, resolve } from "node:path";
|
|
|
14
14
|
import { AGENTS_MD } from "./agent/agents-md.js";
|
|
15
15
|
import { LANDSCAPE_STUB } from "./scaffold/landscape.js";
|
|
16
16
|
import { README_FILENAME, README_MD } from "./scaffold/readme.js";
|
|
17
|
-
import { AGENTS_FILENAME } from "./repo/paths.js";
|
|
17
|
+
import { AGENTS_FILENAME, LIKEC4_PROJECT_FILENAME, LIKEC4_ROOT_PROJECT } from "./repo/paths.js";
|
|
18
18
|
/**
|
|
19
19
|
* Top-level layout of the shared docs repo, and part of its identity rather
|
|
20
20
|
* than a convenience: `services/` is what makes a directory a docs repo —
|
|
@@ -37,33 +37,42 @@ const EMPTY_SUBDIRS = [
|
|
|
37
37
|
* tool other than itself.
|
|
38
38
|
*
|
|
39
39
|
* LikeC4's workspace loader merges EVERY `.likec4` file under a directory tree
|
|
40
|
-
* into one model
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* every
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* were right; only one of them was reading the tree as a workspace.
|
|
40
|
+
* into one model, and whether that is what loam wants depends on the document.
|
|
41
|
+
* A feature's `features/<FEAT>/delta.likec4` is parsed in isolation and declares
|
|
42
|
+
* its own `specification { … }` block, so merging it into the root would report
|
|
43
|
+
* every declaration as a duplicate — measured on loam's own `examples/docs`:
|
|
44
|
+
* `npx likec4 start` reported 16 errors, sixteen duplicate kinds and names,
|
|
45
|
+
* while `loam validate --all` reported none. Both were right; only one of them
|
|
46
|
+
* was reading the tree as a workspace. That directory is therefore excluded
|
|
47
|
+
* here, permanently.
|
|
49
48
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
49
|
+
* `services/**` is NOT, and that is this file's one substantive change since the
|
|
50
|
+
* axis that let a model EXTEND the fleet map. A model written that way declares
|
|
51
|
+
* no kinds of its own and says what is inside an element the map already draws,
|
|
52
|
+
* so the root project is the only place it parses — and excluding it renders the
|
|
53
|
+
* service as a box with nothing inside. A model that STANDS ALONE still has to
|
|
54
|
+
* be excluded, and `loam subsystem sync` is what adds `services/<tree>/**` for
|
|
55
|
+
* each one and removes the entry again when a model migrates. A fresh repo's
|
|
56
|
+
* models are extending, so the scaffold starts with no `services/` entry at all.
|
|
56
57
|
*
|
|
57
58
|
* `**\/node_modules/**` is repeated because naming `exclude` replaces LikeC4's
|
|
58
|
-
* default rather than adding to it.
|
|
59
|
-
*
|
|
60
|
-
*
|
|
59
|
+
* default rather than adding to it.
|
|
60
|
+
*
|
|
61
|
+
* OWNERSHIP, corrected. This file is still the team's — written once by
|
|
62
|
+
* `loam init --create`, never regenerated, and a team that wants a different
|
|
63
|
+
* project layout keeps it. But loam is no longer blind to it: it reads ONE
|
|
64
|
+
* literal fact out of it (`core/c4/root-project/exclude.ts`, `readRootExclude`
|
|
65
|
+
* — the `exclude` list and nothing else), because that list decides whether an
|
|
66
|
+
* extending model is renderable at all, and it MAINTAINS the `services/` entries
|
|
67
|
+
* in it through `loam subsystem sync`. Every other key, and every entry that is
|
|
68
|
+
* not about `services/`, is left exactly as written. The filename and the root
|
|
69
|
+
* project's name are spelled in `repo/paths.ts`, beside every other file loam
|
|
70
|
+
* writes.
|
|
61
71
|
*/
|
|
62
|
-
export const LIKEC4_PROJECT_FILENAME = "likec4.config.json";
|
|
63
72
|
export const LIKEC4_PROJECT_CONFIG = `${JSON.stringify({
|
|
64
|
-
name:
|
|
73
|
+
name: LIKEC4_ROOT_PROJECT,
|
|
65
74
|
title: "Fleet landscape",
|
|
66
|
-
exclude: ["**/node_modules/**", "
|
|
75
|
+
exclude: ["**/node_modules/**", "features/**"],
|
|
67
76
|
}, null, 2)}\n`;
|
|
68
77
|
/**
|
|
69
78
|
* The placeholder that makes an empty scaffolded directory survive a clone.
|
package/dist/core/docs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"docs.js","sourceRoot":"","sources":["../../src/core/docs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACzD,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAElD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,cAAc,EAAE,UAAU,EAAE,UAAU,CAAU,CAAC;AAE9E;;;;;GAKG;AACH,MAAM,aAAa,GAAG;IACpB,CAAC,UAAU,EAAE,UAAU,CAAC;IACxB,CAAC,UAAU,EAAE,UAAU,CAAC;CAChB,CAAC;AAEX;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,oBAAoB,CAAC;AAE5D,MAAM,CAAC,MAAM,qBAAqB,GAAG,GAAG,IAAI,CAAC,SAAS,CACpD;IACE,IAAI,EAAE,OAAO;IACb,KAAK,EAAE,iBAAiB;IACxB,OAAO,EAAE,CAAC,oBAAoB,EAAE,aAAa,EAAE,aAAa,CAAC;CAC9D,EACD,IAAI,EACJ,CAAC,CACF,IAAI,CAAC;AAEN;;;;;;;;GAQG;AACH,MAAM,OAAO,GAAG,EAAE,CAAC;AAEnB;;;;;;;;;GASG;AACH,MAAM,gBAAgB,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;AAO1E,0FAA0F;AAC1F,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,OAAe;IAChD,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9B,MAAM,OAAO,GAAa,EAAE,CAAC;IAE7B,MAAM,KAAK,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAEvC,KAAK,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;QAC/B,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QAC1B,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC;YACnB,MAAM,KAAK,CAAC,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YACpC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;IACH,CAAC;IAED,6EAA6E;IAC7E,6EAA6E;IAC7E,mEAAmE;IACnE,KAAK,MAAM,CAAC,GAAG,EAAE,OAAO,CAAC,IAAI,aAAa,EAAE,EAAE,CAAC;QAC7C,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QAC7B,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YACtB,MAAM,SAAS,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;YACvC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACrB,CAAC;IACH,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;AAC3B,CAAC;AAiBD;;;;;;;;;GASG;AACH,MAAM,UAAU,aAAa,CAAC,IAAI,GAAwB,EAAE;IAC1D,MAAM,QAAQ,GAAG,IAAI,CAAC,iBAAiB,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,iBAAiB,CAAC,OAAO,EAAE,QAAQ,CAAC;IACzG,OAAO;QACL,0EAA0E;QAC1E,oEAAoE;QACpE,CAAC,eAAe,EAAE,SAAS,CAAC;QAC5B,4EAA4E;QAC5E,4CAA4C;QAC5C,CAAC,eAAe,EAAE,SAAS,CAAC;QAC5B,CAAC,IAAI,CAAC,cAAc,EAAE,kBAAkB,CAAC,EAAE,GAAG,QAAQ,GAAG,cAAc,EAAE,CAAC;QAC1E,CAAC,WAAW,EAAE,gBAAgB,CAAC;QAC/B,CAAC,uBAAuB,EAAE,qBAAqB,CAAC;QAChD,sEAAsE;QACtE,oEAAoE;QACpE,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,OAAO,CAAqB,CAAC;KACtF,CAAC;AACJ,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,gBAAgB,CAAC,OAAe;IAC9C,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9B,OAAO;QACL,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QACzC,GAAG,aAAa,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;KACnD,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Scaffolding for the shared docs repo.\n *\n * There is deliberately no manifest. `init` used to write a `loam.docs.json`\n * listing the repo's services; nothing ever read it — `repo.ts` enumerates from\n * the filesystem, because files are the source of truth — and nothing ever\n * updated it, so it named an empty fleet forever. A second list of services is\n * exactly the drift `loam validate` now cross-checks the landscape for; the\n * cheapest way to keep it honest is not to have it.\n */\nimport { mkdir, writeFile } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport { AGENTS_MD } from \"./agent/agents-md.js\";\nimport { LANDSCAPE_STUB } from \"./scaffold/landscape.js\";\nimport { README_FILENAME, README_MD } from \"./scaffold/readme.js\";\nimport { AGENTS_FILENAME } from \"./repo/paths.js\";\n\n/**\n * Top-level layout of the shared docs repo, and part of its identity rather\n * than a convenience: `services/` is what makes a directory a docs repo —\n * `init` will not join one without it, and every enumerating command refuses a\n * `docsDir` that has none. So it is laid down even when nothing is in it yet.\n */\nexport const DOCS_SUBDIRS = [\"architecture\", \"services\", \"features\"] as const;\n\n/**\n * The scaffolded directories that start out empty, and the file that keeps each\n * of them in version control. See {@link GITKEEP}. Spelled as pairs so the\n * marker's name is written once — `listServices` and `listFeatures` enumerate\n * subdirectories only, so a file here is invisible to both.\n */\nconst EMPTY_SUBDIRS = [\n [\"services\", \".gitkeep\"],\n [\"features\", \".gitkeep\"],\n] as const;\n\n/**\n * The LikeC4 project file, and the one thing in the docs repo loam writes for a\n * tool other than itself.\n *\n * LikeC4's workspace loader merges EVERY `.likec4` file under a directory tree\n * into one model. loam's layout is the opposite by design: the landscape,\n * each `services/<id>/model.likec4` and each `features/<FEAT>/delta.likec4` is\n * parsed in isolation (`loadSource`, one file, one throwaway workspace), so\n * every one of them legitimately declares its own `specification { … }` block\n * and re-declares the elements it needs to talk about. Point the real renderer\n * at the repository root and those declarations collide: on loam's own\n * `examples/docs` — four files — `npx likec4 start` reported 16 errors, sixteen\n * duplicate kinds and names, while `loam validate --all` reported none. Both\n * were right; only one of them was reading the tree as a workspace.\n *\n * So the root is declared as one LikeC4 project holding the landscape, and the\n * two directories whose files are meant to be read alone are excluded from it.\n * That makes `npx likec4 start` in the docs repo — the command loam's own docs\n * have always recommended — render the fleet map instead of failing. A service\n * model or a feature delta is still rendered by pointing the renderer at its own\n * directory, which is what parsing them in isolation meant all along.\n *\n * `**\\/node_modules/**` is repeated because naming `exclude` replaces LikeC4's\n * default rather than adding to it. loam reads none of this file: it is written\n * once, never re-read, and a team that wants a different project layout owns it\n * like every other scaffolded file.\n */\nexport const LIKEC4_PROJECT_FILENAME = \"likec4.config.json\";\n\nexport const LIKEC4_PROJECT_CONFIG = `${JSON.stringify(\n {\n name: \"fleet\",\n title: \"Fleet landscape\",\n exclude: [\"**/node_modules/**\", \"services/**\", \"features/**\"],\n },\n null,\n 2,\n)}\\n`;\n\n/**\n * The placeholder that makes an empty scaffolded directory survive a clone.\n *\n * git tracks files, not directories, so `services/` and `features/` — created\n * empty by `loam init --create` — were absent for everyone after the first push:\n * the person who ran init had a green repo, and the second person to clone it\n * got `doctor.services-missing`, a BLOCKER, on a repository nobody had touched.\n * A repo that cannot survive being cloned is not a shared docs repo.\n */\nconst GITKEEP = \"\";\n\n/**\n * The docs repo's own loam.json. It makes the docs repo self-describing: a\n * command run from inside it (or from any directory under it) finds this file\n * first and resolves the fleet to the repo it is standing in, instead of\n * walking out to whatever service repo happens to be above.\n *\n * `\".\"` and not an absolute path for the same reason every other docsDir is\n * stored as written: this file is committed and cloned to machines whose\n * directory layout nobody here can predict.\n */\nconst DOCS_SELF_CONFIG = `${JSON.stringify({ docsDir: \".\" }, null, 2)}\\n`;\n\nexport interface ScaffoldResult {\n root: string;\n created: string[];\n}\n\n/** Idempotently create the docs-repo skeleton. Existing files/dirs are left untouched. */\nexport async function scaffoldDocs(docsDir: string): Promise<ScaffoldResult> {\n const root = resolve(docsDir);\n const created: string[] = [];\n\n await mkdir(root, { recursive: true });\n\n for (const dir of DOCS_SUBDIRS) {\n const p = join(root, dir);\n if (!existsSync(p)) {\n await mkdir(p, { recursive: true });\n created.push(p);\n }\n }\n\n // Never overwritten, all three of them — a team's own house rules, their own\n // map and their own config outrank the template. The order here IS the order\n // `init` probes for what it will skip; keep the two lists in step.\n for (const [rel, content] of docsRepoFiles()) {\n const path = join(root, rel);\n if (!existsSync(path)) {\n await writeFile(path, content, \"utf8\");\n created.push(path);\n }\n }\n\n return { root, created };\n}\n\nexport interface DocsRepoFileOptions {\n /**\n * Comment lines prepended to the landscape stub, for a scaffold with a reason\n * of its own to hand over an empty map — `migrate-openspec` stages a docs repo\n * out of a corpus that HAS no topology, and says so where the migrator reads.\n *\n * A parameter and not a second template on purpose. The rest of the stub —\n * how to declare a service, how to bind an edge to an operationId, why there\n * is no `views` block — is the same advice however the repo came to exist, and\n * the migration used to carry its own shorter copy of it: two files that were\n * both correct on the day they were written and could only diverge after.\n */\n landscapePreamble?: string;\n}\n\n/**\n * The files every docs repo starts with, relative to its root, in creation\n * order. `init` scaffolds them directly; `migrate-openspec` stages the same\n * bytes into its target, because the whole point of migrating into a docs repo\n * is that it be the docs repo `loam init` makes.\n *\n * Exported as paths only (via `plannedDocsFiles`) where `init` reports what it\n * will skip — a duplicate list is exactly the drift that made `created +\n * skipped` disagree with reality.\n */\nexport function docsRepoFiles(opts: DocsRepoFileOptions = {}): Array<[string, string]> {\n const preamble = opts.landscapePreamble === undefined ? \"\" : `${opts.landscapePreamble.trimEnd()}\\n//\\n`;\n return [\n // The one file addressed to a person, first because a forge renders it as\n // the landing page and a reader meets it before anything else here.\n [README_FILENAME, README_MD],\n // The process contract lives with the docs it describes, so an agent handed\n // only the docs repo still knows the cycle.\n [AGENTS_FILENAME, AGENTS_MD],\n [join(\"architecture\", \"landscape.likec4\"), `${preamble}${LANDSCAPE_STUB}`],\n [\"loam.json\", DOCS_SELF_CONFIG],\n [LIKEC4_PROJECT_FILENAME, LIKEC4_PROJECT_CONFIG],\n // The two directories git would otherwise drop on the way to the next\n // clone. `architecture/` needs none — the landscape above is in it.\n ...EMPTY_SUBDIRS.map(([dir, keep]) => [join(dir, keep), GITKEEP] as [string, string]),\n ];\n}\n\n/** Everything `scaffoldDocs(docsDir)` would create, in the order it creates it. */\nexport function plannedDocsFiles(docsDir: string): string[] {\n const root = resolve(docsDir);\n return [\n ...DOCS_SUBDIRS.map((d) => join(root, d)),\n ...docsRepoFiles().map(([rel]) => join(root, rel)),\n ];\n}\n"]}
|
|
1
|
+
{"version":3,"file":"docs.js","sourceRoot":"","sources":["../../src/core/docs.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Scaffolding for the shared docs repo.\n *\n * There is deliberately no manifest. `init` used to write a `loam.docs.json`\n * listing the repo's services; nothing ever read it — `repo.ts` enumerates from\n * the filesystem, because files are the source of truth — and nothing ever\n * updated it, so it named an empty fleet forever. A second list of services is\n * exactly the drift `loam validate` now cross-checks the landscape for; the\n * cheapest way to keep it honest is not to have it.\n */\nimport { mkdir, writeFile } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport { AGENTS_MD } from \"./agent/agents-md.js\";\nimport { LANDSCAPE_STUB } from \"./scaffold/landscape.js\";\nimport { README_FILENAME, README_MD } from \"./scaffold/readme.js\";\nimport { AGENTS_FILENAME, LIKEC4_PROJECT_FILENAME, LIKEC4_ROOT_PROJECT } from \"./repo/paths.js\";\n\n/**\n * Top-level layout of the shared docs repo, and part of its identity rather\n * than a convenience: `services/` is what makes a directory a docs repo —\n * `init` will not join one without it, and every enumerating command refuses a\n * `docsDir` that has none. So it is laid down even when nothing is in it yet.\n */\nexport const DOCS_SUBDIRS = [\"architecture\", \"services\", \"features\"] as const;\n\n/**\n * The scaffolded directories that start out empty, and the file that keeps each\n * of them in version control. See {@link GITKEEP}. Spelled as pairs so the\n * marker's name is written once — `listServices` and `listFeatures` enumerate\n * subdirectories only, so a file here is invisible to both.\n */\nconst EMPTY_SUBDIRS = [\n [\"services\", \".gitkeep\"],\n [\"features\", \".gitkeep\"],\n] as const;\n\n/**\n * The LikeC4 project file, and the one thing in the docs repo loam writes for a\n * tool other than itself.\n *\n * LikeC4's workspace loader merges EVERY `.likec4` file under a directory tree\n * into one model, and whether that is what loam wants depends on the document.\n * A feature's `features/<FEAT>/delta.likec4` is parsed in isolation and declares\n * its own `specification { … }` block, so merging it into the root would report\n * every declaration as a duplicate — measured on loam's own `examples/docs`:\n * `npx likec4 start` reported 16 errors, sixteen duplicate kinds and names,\n * while `loam validate --all` reported none. Both were right; only one of them\n * was reading the tree as a workspace. That directory is therefore excluded\n * here, permanently.\n *\n * `services/**` is NOT, and that is this file's one substantive change since the\n * axis that let a model EXTEND the fleet map. A model written that way declares\n * no kinds of its own and says what is inside an element the map already draws,\n * so the root project is the only place it parses — and excluding it renders the\n * service as a box with nothing inside. A model that STANDS ALONE still has to\n * be excluded, and `loam subsystem sync` is what adds `services/<tree>/**` for\n * each one and removes the entry again when a model migrates. A fresh repo's\n * models are extending, so the scaffold starts with no `services/` entry at all.\n *\n * `**\\/node_modules/**` is repeated because naming `exclude` replaces LikeC4's\n * default rather than adding to it.\n *\n * OWNERSHIP, corrected. This file is still the team's — written once by\n * `loam init --create`, never regenerated, and a team that wants a different\n * project layout keeps it. But loam is no longer blind to it: it reads ONE\n * literal fact out of it (`core/c4/root-project/exclude.ts`, `readRootExclude`\n * — the `exclude` list and nothing else), because that list decides whether an\n * extending model is renderable at all, and it MAINTAINS the `services/` entries\n * in it through `loam subsystem sync`. Every other key, and every entry that is\n * not about `services/`, is left exactly as written. The filename and the root\n * project's name are spelled in `repo/paths.ts`, beside every other file loam\n * writes.\n */\nexport const LIKEC4_PROJECT_CONFIG = `${JSON.stringify(\n {\n name: LIKEC4_ROOT_PROJECT,\n title: \"Fleet landscape\",\n exclude: [\"**/node_modules/**\", \"features/**\"],\n },\n null,\n 2,\n)}\\n`;\n\n/**\n * The placeholder that makes an empty scaffolded directory survive a clone.\n *\n * git tracks files, not directories, so `services/` and `features/` — created\n * empty by `loam init --create` — were absent for everyone after the first push:\n * the person who ran init had a green repo, and the second person to clone it\n * got `doctor.services-missing`, a BLOCKER, on a repository nobody had touched.\n * A repo that cannot survive being cloned is not a shared docs repo.\n */\nconst GITKEEP = \"\";\n\n/**\n * The docs repo's own loam.json. It makes the docs repo self-describing: a\n * command run from inside it (or from any directory under it) finds this file\n * first and resolves the fleet to the repo it is standing in, instead of\n * walking out to whatever service repo happens to be above.\n *\n * `\".\"` and not an absolute path for the same reason every other docsDir is\n * stored as written: this file is committed and cloned to machines whose\n * directory layout nobody here can predict.\n */\nconst DOCS_SELF_CONFIG = `${JSON.stringify({ docsDir: \".\" }, null, 2)}\\n`;\n\nexport interface ScaffoldResult {\n root: string;\n created: string[];\n}\n\n/** Idempotently create the docs-repo skeleton. Existing files/dirs are left untouched. */\nexport async function scaffoldDocs(docsDir: string): Promise<ScaffoldResult> {\n const root = resolve(docsDir);\n const created: string[] = [];\n\n await mkdir(root, { recursive: true });\n\n for (const dir of DOCS_SUBDIRS) {\n const p = join(root, dir);\n if (!existsSync(p)) {\n await mkdir(p, { recursive: true });\n created.push(p);\n }\n }\n\n // Never overwritten, all three of them — a team's own house rules, their own\n // map and their own config outrank the template. The order here IS the order\n // `init` probes for what it will skip; keep the two lists in step.\n for (const [rel, content] of docsRepoFiles()) {\n const path = join(root, rel);\n if (!existsSync(path)) {\n await writeFile(path, content, \"utf8\");\n created.push(path);\n }\n }\n\n return { root, created };\n}\n\nexport interface DocsRepoFileOptions {\n /**\n * Comment lines prepended to the landscape stub, for a scaffold with a reason\n * of its own to hand over an empty map — `migrate-openspec` stages a docs repo\n * out of a corpus that HAS no topology, and says so where the migrator reads.\n *\n * A parameter and not a second template on purpose. The rest of the stub —\n * how to declare a service, how to bind an edge to an operationId, why there\n * is no `views` block — is the same advice however the repo came to exist, and\n * the migration used to carry its own shorter copy of it: two files that were\n * both correct on the day they were written and could only diverge after.\n */\n landscapePreamble?: string;\n}\n\n/**\n * The files every docs repo starts with, relative to its root, in creation\n * order. `init` scaffolds them directly; `migrate-openspec` stages the same\n * bytes into its target, because the whole point of migrating into a docs repo\n * is that it be the docs repo `loam init` makes.\n *\n * Exported as paths only (via `plannedDocsFiles`) where `init` reports what it\n * will skip — a duplicate list is exactly the drift that made `created +\n * skipped` disagree with reality.\n */\nexport function docsRepoFiles(opts: DocsRepoFileOptions = {}): Array<[string, string]> {\n const preamble = opts.landscapePreamble === undefined ? \"\" : `${opts.landscapePreamble.trimEnd()}\\n//\\n`;\n return [\n // The one file addressed to a person, first because a forge renders it as\n // the landing page and a reader meets it before anything else here.\n [README_FILENAME, README_MD],\n // The process contract lives with the docs it describes, so an agent handed\n // only the docs repo still knows the cycle.\n [AGENTS_FILENAME, AGENTS_MD],\n [join(\"architecture\", \"landscape.likec4\"), `${preamble}${LANDSCAPE_STUB}`],\n [\"loam.json\", DOCS_SELF_CONFIG],\n [LIKEC4_PROJECT_FILENAME, LIKEC4_PROJECT_CONFIG],\n // The two directories git would otherwise drop on the way to the next\n // clone. `architecture/` needs none — the landscape above is in it.\n ...EMPTY_SUBDIRS.map(([dir, keep]) => [join(dir, keep), GITKEEP] as [string, string]),\n ];\n}\n\n/** Everything `scaffoldDocs(docsDir)` would create, in the order it creates it. */\nexport function plannedDocsFiles(docsDir: string): string[] {\n const root = resolve(docsDir);\n return [\n ...DOCS_SUBDIRS.map((d) => join(root, d)),\n ...docsRepoFiles().map(([rel]) => join(root, rel)),\n ];\n}\n"]}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import { access, readFile } from "node:fs/promises";
|
|
16
16
|
import { dirname } from "node:path";
|
|
17
17
|
import { configPath, parseConfig } from "../envelope/config.js";
|
|
18
|
-
import { loadFile } from "../c4/likec4.js";
|
|
18
|
+
import { errorText, loadFile } from "../c4/likec4.js";
|
|
19
19
|
import { conflictMarkerLines } from "../conflict-markers.js";
|
|
20
20
|
import {} from "./report.js";
|
|
21
21
|
/**
|
|
@@ -113,8 +113,12 @@ export async function inspectLandscape(path, findings) {
|
|
|
113
113
|
findings.push({
|
|
114
114
|
severity: "blocker",
|
|
115
115
|
code: "doctor.landscape-invalid",
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
// Through `errorText`, the one spelling of a diagnostic loam prints. This
|
|
117
|
+
// site hand-rolled its own and passed the LSP range's line through raw, so
|
|
118
|
+
// `doctor` named the line ABOVE the fault and disagreed with `validate
|
|
119
|
+
// --all` about the same error on the same tree — the last formatter the
|
|
120
|
+
// 1-based consolidation missed (verification 2026-09-04).
|
|
121
|
+
message: `architecture/landscape.likec4 does not parse: ${errorText(first)}` +
|
|
118
122
|
(doc.errors.length > 1 ? ` — and ${doc.errors.length - 1} more` : ""),
|
|
119
123
|
fix: `Fix ${path}; every fleet-wide check is blind until it parses.`,
|
|
120
124
|
});
|