@ybotok/loam 0.2.0-alpha.5 → 0.2.0-alpha.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1063 -0
- package/MIGRATING-from-OpenSpec.md +7 -4
- package/QUICKSTART.md +8 -3
- package/README.md +13 -7
- package/ROADMAP.md +285 -11
- package/SCHEMA.md +566 -45
- package/WORKFLOW.md +23 -10
- package/dist/commands/adopt/render.js +47 -8
- package/dist/commands/adopt/render.js.map +1 -1
- package/dist/commands/archive/plan/landscape.js +45 -1
- package/dist/commands/archive/plan/landscape.js.map +1 -1
- package/dist/commands/archive/plan/model/extending.js +85 -0
- package/dist/commands/archive/plan/model/extending.js.map +1 -0
- package/dist/commands/archive/run.js +53 -23
- package/dist/commands/archive/run.js.map +1 -1
- package/dist/commands/context/print.js +11 -1
- package/dist/commands/context/print.js.map +1 -1
- package/dist/commands/doctor.js +46 -0
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/explore.js +12 -2
- package/dist/commands/explore.js.map +1 -1
- package/dist/commands/list/views.js +5 -1
- package/dist/commands/list/views.js.map +1 -1
- package/dist/commands/seed/commit.js +34 -1
- package/dist/commands/seed/commit.js.map +1 -1
- package/dist/commands/seed/plan.js +158 -13
- package/dist/commands/seed/plan.js.map +1 -1
- package/dist/commands/seed/seed.js +7 -2
- package/dist/commands/seed/seed.js.map +1 -1
- package/dist/commands/show/feature.js +2 -1
- package/dist/commands/show/feature.js.map +1 -1
- package/dist/commands/show/marks.js +3 -5
- package/dist/commands/show/marks.js.map +1 -1
- package/dist/commands/show/service.js +39 -18
- package/dist/commands/show/service.js.map +1 -1
- package/dist/commands/subsystem/edit.js +4 -2
- package/dist/commands/subsystem/edit.js.map +1 -1
- package/dist/commands/subsystem/move.js +55 -17
- package/dist/commands/subsystem/move.js.map +1 -1
- package/dist/commands/subsystem/sync.js +238 -87
- package/dist/commands/subsystem/sync.js.map +1 -1
- package/dist/commands/subsystem/txn/exclude.js +132 -0
- package/dist/commands/subsystem/txn/exclude.js.map +1 -0
- package/dist/commands/subsystem/txn/report.js +174 -0
- package/dist/commands/subsystem/txn/report.js.map +1 -0
- package/dist/commands/subsystem/txn/txn.js +126 -22
- package/dist/commands/subsystem/txn/txn.js.map +1 -1
- package/dist/commands/subsystem/txn/views.js +159 -19
- package/dist/commands/subsystem/txn/views.js.map +1 -1
- package/dist/commands/validate/arch-coverage.js +16 -9
- package/dist/commands/validate/arch-coverage.js.map +1 -1
- package/dist/commands/validate/checks/fleet-shape.js +30 -77
- package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
- package/dist/commands/validate/checks/health-deps.js +9 -6
- package/dist/commands/validate/checks/health-deps.js.map +1 -1
- package/dist/commands/validate/checks/vocabulary.js +4 -19
- package/dist/commands/validate/checks/vocabulary.js.map +1 -1
- package/dist/commands/validate/feature.js +1 -1
- package/dist/commands/validate/feature.js.map +1 -1
- package/dist/commands/validate/fleet/census.js +37 -13
- package/dist/commands/validate/fleet/census.js.map +1 -1
- package/dist/commands/validate/fleet/landscape.js +131 -120
- package/dist/commands/validate/fleet/landscape.js.map +1 -1
- package/dist/commands/validate/fleet/load.js +66 -0
- package/dist/commands/validate/fleet/load.js.map +1 -1
- package/dist/commands/validate/fleet/map/attest.js +102 -0
- package/dist/commands/validate/fleet/map/attest.js.map +1 -0
- package/dist/commands/validate/fleet/map/bindings.js +109 -0
- package/dist/commands/validate/fleet/map/bindings.js.map +1 -0
- package/dist/commands/validate/fleet/map/consumers.js +219 -0
- package/dist/commands/validate/fleet/map/consumers.js.map +1 -0
- package/dist/commands/validate/fleet/map/isolation.js +116 -0
- package/dist/commands/validate/fleet/map/isolation.js.map +1 -0
- package/dist/commands/validate/fleet/map/usecases.js +56 -0
- package/dist/commands/validate/fleet/map/usecases.js.map +1 -0
- package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -1
- package/dist/commands/validate/fleet/usecases/steps.js +49 -17
- package/dist/commands/validate/fleet/usecases/steps.js.map +1 -1
- package/dist/commands/validate/fleet/views/fleet-project.js +217 -0
- package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -0
- package/dist/commands/validate/fleet/views/ids.js +45 -15
- package/dist/commands/validate/fleet/views/ids.js.map +1 -1
- package/dist/commands/validate/fleet/views/projects.js +258 -70
- package/dist/commands/validate/fleet/views/projects.js.map +1 -1
- package/dist/commands/validate/fleet/views/root-documents.js +99 -0
- package/dist/commands/validate/fleet/views/root-documents.js.map +1 -0
- package/dist/commands/validate/fleet/views/stale.js +32 -15
- package/dist/commands/validate/fleet/views/stale.js.map +1 -1
- package/dist/commands/validate/service/model/diverged.js +226 -0
- package/dist/commands/validate/service/model/diverged.js.map +1 -0
- package/dist/commands/validate/service/model/grade.js +163 -0
- package/dist/commands/validate/service/model/grade.js.map +1 -0
- package/dist/commands/validate/service/service.js +110 -34
- package/dist/commands/validate/service/service.js.map +1 -1
- package/dist/commands/validate/service/specs.js +39 -5
- package/dist/commands/validate/service/specs.js.map +1 -1
- package/dist/commands/validate/service/spine.js +41 -3
- package/dist/commands/validate/service/spine.js.map +1 -1
- package/dist/commands/validate/service/usecases/tags.js +113 -0
- package/dist/commands/validate/service/usecases/tags.js.map +1 -0
- package/dist/commands/validate/service/usecases/usecases.js +209 -0
- package/dist/commands/validate/service/usecases/usecases.js.map +1 -0
- package/dist/commands/validate/validate.js +10 -24
- package/dist/commands/validate/validate.js.map +1 -1
- package/dist/core/agent/agents-md/artifacts.js +1 -0
- package/dist/core/agent/agents-md/artifacts.js.map +1 -1
- package/dist/core/agent/agents-md/command-map.js +19 -5
- package/dist/core/agent/agents-md/command-map.js.map +1 -1
- package/dist/core/agent/agents-md/cycle.js +24 -9
- package/dist/core/agent/agents-md/cycle.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/context.js +4 -3
- package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -1
- package/dist/core/agent/agents-md/map/subsystem.js +70 -17
- package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
- package/dist/core/agent/agents-md/map/usecases.js +45 -3
- package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
- package/dist/core/agent/agents-md/refusals.js +5 -3
- package/dist/core/agent/agents-md/refusals.js.map +1 -1
- package/dist/core/agent/agents-md.js +26 -7
- package/dist/core/agent/agents-md.js.map +1 -1
- package/dist/core/agent/support/report.js +39 -5
- package/dist/core/agent/support/report.js.map +1 -1
- package/dist/core/agent/workflows/adopt.js +123 -14
- package/dist/core/agent/workflows/adopt.js.map +1 -1
- package/dist/core/agent/workflows/check.js +34 -20
- package/dist/core/agent/workflows/check.js.map +1 -1
- package/dist/core/agent/workflows/closing.js +3 -1
- package/dist/core/agent/workflows/closing.js.map +1 -1
- package/dist/core/agent/workflows/reference/spine.js +12 -6
- package/dist/core/agent/workflows/reference/spine.js.map +1 -1
- package/dist/core/brief/brief.js +88 -19
- package/dist/core/brief/brief.js.map +1 -1
- package/dist/core/brief/checks.js +133 -7
- package/dist/core/brief/checks.js.map +1 -1
- package/dist/core/brief/landscape.js +212 -129
- package/dist/core/brief/landscape.js.map +1 -1
- package/dist/core/brief/map/extend.js +81 -0
- package/dist/core/brief/map/extend.js.map +1 -0
- package/dist/core/brief/map/owed.js +284 -0
- package/dist/core/brief/map/owed.js.map +1 -0
- package/dist/core/brief/targets.js +94 -29
- package/dist/core/brief/targets.js.map +1 -1
- package/dist/core/brief/unchecked.js +13 -3
- package/dist/core/brief/unchecked.js.map +1 -1
- package/dist/core/c4/likec4.js +23 -1
- package/dist/core/c4/likec4.js.map +1 -1
- package/dist/core/c4/parsed/deployment.js +5 -3
- package/dist/core/c4/parsed/deployment.js.map +1 -1
- package/dist/core/c4/parsed/view-ids.js +39 -1
- package/dist/core/c4/parsed/view-ids.js.map +1 -1
- package/dist/core/c4/prefetch/fleet.js +142 -0
- package/dist/core/c4/prefetch/fleet.js.map +1 -0
- package/dist/core/c4/project/architecture.js +77 -7
- package/dist/core/c4/project/architecture.js.map +1 -1
- package/dist/core/c4/project/batch.js +140 -0
- package/dist/core/c4/project/batch.js.map +1 -0
- package/dist/core/c4/project/documents.js +12 -6
- package/dist/core/c4/project/documents.js.map +1 -1
- package/dist/core/c4/project/load.js +85 -42
- package/dist/core/c4/project/load.js.map +1 -1
- package/dist/core/c4/project/staged.js +53 -15
- package/dist/core/c4/project/staged.js.map +1 -1
- package/dist/core/c4/resolve/attested.js +0 -0
- package/dist/core/c4/resolve/attested.js.map +1 -0
- package/dist/core/c4/resolve/service.js +8 -17
- package/dist/core/c4/resolve/service.js.map +1 -1
- package/dist/core/c4/resolve/steps.js +18 -0
- package/dist/core/c4/resolve/steps.js.map +1 -1
- package/dist/core/c4/root-project/exclude.js +352 -0
- package/dist/core/c4/root-project/exclude.js.map +1 -0
- package/dist/core/c4/service-model/documents.js +47 -0
- package/dist/core/c4/service-model/documents.js.map +1 -0
- package/dist/core/c4/service-model/fleet/extending.js +82 -0
- package/dist/core/c4/service-model/fleet/extending.js.map +1 -0
- package/dist/core/c4/service-model/load.js +165 -0
- package/dist/core/c4/service-model/load.js.map +1 -0
- package/dist/core/c4/service-model/renderer.js +128 -0
- package/dist/core/c4/service-model/renderer.js.map +1 -0
- package/dist/core/c4/service-model/shape.js +126 -0
- package/dist/core/c4/service-model/shape.js.map +1 -0
- package/dist/core/c4/service-model/slice.js +188 -0
- package/dist/core/c4/service-model/slice.js.map +1 -0
- package/dist/core/c4/source-scan.js +26 -0
- package/dist/core/c4/source-scan.js.map +1 -1
- package/dist/core/c4/splice/authored-source.js +60 -13
- package/dist/core/c4/splice/authored-source.js.map +1 -1
- package/dist/core/c4/splice/contract.js.map +1 -1
- package/dist/core/c4/splice/identity/declared.js +87 -0
- package/dist/core/c4/splice/identity/declared.js.map +1 -0
- package/dist/core/c4/splice/identity/edges.js +17 -0
- package/dist/core/c4/splice/identity/edges.js.map +1 -1
- package/dist/core/c4/splice/identity/existing.js +80 -0
- package/dist/core/c4/splice/identity/existing.js.map +1 -0
- package/dist/core/c4/splice/identity/interior.js +80 -0
- package/dist/core/c4/splice/identity/interior.js.map +1 -0
- package/dist/core/c4/splice/identity/owner.js +50 -0
- package/dist/core/c4/splice/identity/owner.js.map +1 -0
- package/dist/core/c4/splice/landscape-merge.js +154 -100
- package/dist/core/c4/splice/landscape-merge.js.map +1 -1
- package/dist/core/c4/splice/model/merge.js +140 -0
- package/dist/core/c4/splice/model/merge.js.map +1 -0
- package/dist/core/c4/splice/placement.js +14 -4
- package/dist/core/c4/splice/placement.js.map +1 -1
- package/dist/core/c4/workspace.js +2 -1
- package/dist/core/c4/workspace.js.map +1 -1
- package/dist/core/coherence/coherence.js +7 -0
- package/dist/core/coherence/coherence.js.map +1 -1
- package/dist/core/delta/delta.js +9 -0
- package/dist/core/delta/delta.js.map +1 -1
- package/dist/core/deployment/delta.js +16 -10
- package/dist/core/deployment/delta.js.map +1 -1
- package/dist/core/diff/base-state.js +21 -5
- package/dist/core/diff/base-state.js.map +1 -1
- package/dist/core/diff/victims.js +10 -1
- package/dist/core/diff/victims.js.map +1 -1
- package/dist/core/docs.js +29 -24
- package/dist/core/docs.js.map +1 -1
- package/dist/core/doctor/config.js +7 -3
- package/dist/core/doctor/config.js.map +1 -1
- package/dist/core/doctor/doctor.js +36 -8
- package/dist/core/doctor/doctor.js.map +1 -1
- package/dist/core/doctor/report.js.map +1 -1
- package/dist/core/doctor/reports/scan.js +242 -0
- package/dist/core/doctor/reports/scan.js.map +1 -0
- package/dist/core/explain/refusals.js +1 -1
- package/dist/core/explain/refusals.js.map +1 -1
- package/dist/core/explore/explore.js +44 -9
- package/dist/core/explore/explore.js.map +1 -1
- package/dist/core/fleet-context.js +47 -0
- package/dist/core/fleet-context.js.map +1 -1
- package/dist/core/gate/checks.js +5 -1
- package/dist/core/gate/checks.js.map +1 -1
- package/dist/core/gate/partners.js +65 -26
- package/dist/core/gate/partners.js.map +1 -1
- package/dist/core/kernel/ids/fqn/ancestors.js +65 -0
- package/dist/core/kernel/ids/fqn/ancestors.js.map +1 -0
- package/dist/core/pack/living.js +47 -13
- package/dist/core/pack/living.js.map +1 -1
- package/dist/core/pack/pack.js +9 -6
- package/dist/core/pack/pack.js.map +1 -1
- package/dist/core/projection/arch-slice.js +2 -2
- package/dist/core/projection/arch-slice.js.map +1 -1
- package/dist/core/repo/paths.js +45 -38
- package/dist/core/repo/paths.js.map +1 -1
- package/dist/core/repo/tree/render/body.js +45 -6
- package/dist/core/repo/tree/render/body.js.map +1 -1
- package/dist/core/repo/tree/render/projects.js +93 -35
- package/dist/core/repo/tree/render/projects.js.map +1 -1
- package/dist/core/repo/tree/render/service/header.js +125 -0
- package/dist/core/repo/tree/render/service/header.js.map +1 -0
- package/dist/core/repo/tree/render/service/peers.js +28 -0
- package/dist/core/repo/tree/render/service/peers.js.map +1 -0
- package/dist/core/repo/tree/render/service/view-id.js +67 -0
- package/dist/core/repo/tree/render/service/view-id.js.map +1 -0
- package/dist/core/repo/tree/render/service/views.js +78 -0
- package/dist/core/repo/tree/render/service/views.js.map +1 -0
- package/dist/core/repo/tree/render/stale.js +20 -4
- package/dist/core/repo/tree/render/stale.js.map +1 -1
- package/dist/core/repo/tree/render/views.js +149 -32
- package/dist/core/repo/tree/render/views.js.map +1 -1
- package/dist/core/repo/tree/walk.js +45 -2
- package/dist/core/repo/tree/walk.js.map +1 -1
- package/dist/core/scaffold/landscape.js +219 -9
- package/dist/core/scaffold/landscape.js.map +1 -1
- package/dist/core/staging/writes.js.map +1 -1
- package/dist/core/usecases/delta/claims.js +1 -0
- package/dist/core/usecases/delta/claims.js.map +1 -1
- package/dist/core/usecases/delta/overlay.js +4 -0
- package/dist/core/usecases/delta/overlay.js.map +1 -1
- package/dist/core/usecases/fleet.js +18 -6
- package/dist/core/usecases/fleet.js.map +1 -1
- package/dist/core/usecases/operations.js +7 -0
- package/dist/core/usecases/operations.js.map +1 -1
- package/dist/core/usecases/service/exclude.js +119 -0
- package/dist/core/usecases/service/exclude.js.map +1 -0
- package/dist/core/usecases/service/flows.js +161 -0
- package/dist/core/usecases/service/flows.js.map +1 -0
- package/dist/core/verify/checklist.js +42 -10
- package/dist/core/verify/checklist.js.map +1 -1
- package/dist/core/vocabulary/issue.js.map +1 -1
- package/dist/core/vocabulary/maturity.js +51 -1
- package/dist/core/vocabulary/maturity.js.map +1 -1
- package/examples/README.md +30 -8
- package/examples/docs/architecture/landscape.likec4 +47 -0
- package/examples/docs/architecture/services.likec4 +70 -0
- package/examples/docs/architecture/subsystems.likec4 +3 -0
- package/examples/docs/architecture/usecases/order-notification.likec4 +5 -2
- package/examples/docs/likec4.config.json +0 -1
- package/examples/docs/services/checkout-web/model.likec4 +39 -56
- package/examples/docs/services/order-service/arch.spec.md +13 -6
- package/examples/docs/services/order-service/model.likec4 +58 -70
- package/examples/docs/services/payment-service/arch.spec.md +7 -4
- package/examples/docs/services/payment-service/model.likec4 +38 -39
- package/examples/docs/services/platform/identity-service/arch.spec.md +7 -4
- package/examples/docs/services/platform/identity-service/model.likec4 +35 -37
- package/examples/docs/services/platform/notification-service/health.yaml +8 -3
- package/examples/docs/services/platform/notification-service/model.likec4 +43 -64
- package/package.json +1 -1
- package/examples/docs/services/checkout-web/likec4.config.json +0 -4
- package/examples/docs/services/order-service/likec4.config.json +0 -4
- package/examples/docs/services/payment-service/likec4.config.json +0 -4
- package/examples/docs/services/platform/identity-service/likec4.config.json +0 -4
- package/examples/docs/services/platform/notification-service/likec4.config.json +0 -4
|
@@ -19,20 +19,28 @@ import { inOrder } from "../../kernel/concurrency.js";
|
|
|
19
19
|
import { flattenModel } from "../likec4.js";
|
|
20
20
|
import { readDynamicViews } from "../parsed/dynamic-views.js";
|
|
21
21
|
import { readSpecification } from "../parsed/specification.js";
|
|
22
|
-
import { readViewIds } from "../parsed/view-ids.js";
|
|
22
|
+
import { readGlobalStyleIds, readViewIds } from "../parsed/view-ids.js";
|
|
23
23
|
import { readDeployment, NO_DEPLOYMENT } from "../parsed/deployment.js";
|
|
24
24
|
/**
|
|
25
|
-
* Stage a set of documents as ONE project
|
|
26
|
-
* `base`.
|
|
25
|
+
* Stage a set of documents as ONE project named `project`, mirroring their
|
|
26
|
+
* paths relative to `base`.
|
|
27
27
|
*
|
|
28
28
|
* The relative structure is preserved rather than flattened to basenames, and
|
|
29
29
|
* that is load-bearing twice over: two use cases named `checkout.likec4` in
|
|
30
30
|
* different subdirectories would overwrite each other on copy, and LikeC4
|
|
31
31
|
* reports a view's `sourcePath` relative to the project root — so flattening
|
|
32
32
|
* would make every finding name a file the author cannot find.
|
|
33
|
+
*
|
|
34
|
+
* The NAME arrives from the caller rather than being minted in here, and that
|
|
35
|
+
* is what lets `./batch.ts` reuse this staging verbatim: many projects in one
|
|
36
|
+
* workspace need the folder name twice over — `parsedModel(<name>)` asks for
|
|
37
|
+
* that project's model, and error attribution keys on the folder SEGMENT.
|
|
38
|
+
* `loadProject` below has one project and passes a name it never looks at
|
|
39
|
+
* again; both spellings carry a crypto-random per-invocation token for
|
|
40
|
+
* `../workspace.ts`'s reason (an author-written `import` must not be able to
|
|
41
|
+
* name a sibling project).
|
|
33
42
|
*/
|
|
34
|
-
async function stageProject(root, base, targets) {
|
|
35
|
-
const project = `arch_${randomBytes(8).toString("hex")}`;
|
|
43
|
+
export async function stageProject(root, project, base, targets) {
|
|
36
44
|
const folder = join(root, project);
|
|
37
45
|
await mkdir(folder, { recursive: true });
|
|
38
46
|
await writeFile(join(folder, "likec4.config.json"), JSON.stringify({ name: project }), "utf8");
|
|
@@ -80,44 +88,30 @@ async function stageProject(root, base, targets) {
|
|
|
80
88
|
* landscape — but loam's standing rule is that errors mean no model, and
|
|
81
89
|
* here the model is the project's. `clean` says which case the caller is in.
|
|
82
90
|
*
|
|
83
|
-
* The
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
91
|
+
* The documents loam GENERATES under `architecture/` must NOT be in `targets`
|
|
92
|
+
* — there are two of them now (`subsystems.likec4` and `services.likec4`, both
|
|
93
|
+
* written by `loam subsystem sync`), and `repo/paths.ts`'s
|
|
94
|
+
* `GENERATED_ARCHITECTURE_FILES` is the one table that names them. docs/DESIGN.md
|
|
95
|
+
* rule 26 keeps each a byte compare, and a stale one whose `include` names a
|
|
96
|
+
* removed element would otherwise be a parse error that blanks the whole fleet
|
|
97
|
+
* map. `./architecture.ts` holds the join from that table to a docsDir, so no
|
|
98
|
+
* caller has to remember the list.
|
|
87
99
|
*/
|
|
88
100
|
export async function loadProject(base, paths) {
|
|
89
|
-
const empty = (errors, clean) => ({
|
|
90
|
-
errors,
|
|
91
|
-
clean,
|
|
92
|
-
elements: [],
|
|
93
|
-
relationships: [],
|
|
94
|
-
views: [],
|
|
95
|
-
viewIds: [],
|
|
96
|
-
deployment: NO_DEPLOYMENT,
|
|
97
|
-
});
|
|
98
101
|
const targets = [...new Set(paths.map((path) => resolve(path)))];
|
|
99
102
|
if (targets.length === 0)
|
|
100
|
-
return
|
|
103
|
+
return emptyProject(new Map(), true);
|
|
101
104
|
const root = await mkdtemp(join(tmpdir(), "loam-c4-"));
|
|
102
105
|
try {
|
|
103
|
-
const byRel = await stageProject(root, resolve(base), targets);
|
|
106
|
+
const byRel = await stageProject(root, `arch_${randomBytes(8).toString("hex")}`, resolve(base), targets);
|
|
104
107
|
if (byRel.size === 0)
|
|
105
|
-
return
|
|
108
|
+
return emptyProject(new Map(), true);
|
|
106
109
|
const likec4 = await LikeC4.fromWorkspace(root, { logger: false });
|
|
107
110
|
try {
|
|
108
|
-
const errors =
|
|
111
|
+
const errors = groupErrorsByRel(likec4.getErrors(), byRel);
|
|
109
112
|
if (errors.size > 0)
|
|
110
|
-
return
|
|
111
|
-
|
|
112
|
-
return {
|
|
113
|
-
errors,
|
|
114
|
-
clean: true,
|
|
115
|
-
specification: readSpecification(model.specification),
|
|
116
|
-
views: readDynamicViews(model),
|
|
117
|
-
viewIds: readViewIds(model),
|
|
118
|
-
deployment: readDeployment(model),
|
|
119
|
-
...flattenModel(model),
|
|
120
|
-
};
|
|
113
|
+
return emptyProject(errors, false);
|
|
114
|
+
return cleanProjectDoc((await likec4.parsedModel()));
|
|
121
115
|
}
|
|
122
116
|
finally {
|
|
123
117
|
await likec4.dispose();
|
|
@@ -128,20 +122,68 @@ export async function loadProject(base, paths) {
|
|
|
128
122
|
}
|
|
129
123
|
}
|
|
130
124
|
/**
|
|
131
|
-
* `
|
|
125
|
+
* A `ProjectDoc` carrying NO model — the answer for a project that did not
|
|
126
|
+
* parse (`clean: false`) and for one there was nothing to load (`clean: true`,
|
|
127
|
+
* empty errors).
|
|
128
|
+
*
|
|
129
|
+
* Exported for `./batch.ts`, which answers about many projects at once and
|
|
130
|
+
* must answer each of them in exactly this shape: a second literal there would
|
|
131
|
+
* be a second chance to spell `deployment` as `undefined` and turn "this
|
|
132
|
+
* project declares no topology" into a crash in every reader.
|
|
133
|
+
*/
|
|
134
|
+
export function emptyProject(errors, clean) {
|
|
135
|
+
return {
|
|
136
|
+
errors,
|
|
137
|
+
clean,
|
|
138
|
+
elements: [],
|
|
139
|
+
relationships: [],
|
|
140
|
+
views: [],
|
|
141
|
+
viewIds: [],
|
|
142
|
+
globalStyles: [],
|
|
143
|
+
deployment: NO_DEPLOYMENT,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Everything loam reads out of a project that parsed, from LikeC4's parsed
|
|
148
|
+
* stage — parsed, never computed, for the reason `../likec4.ts` records.
|
|
149
|
+
*
|
|
150
|
+
* Exported beside `emptyProject` and for the same reason: `./batch.ts` builds
|
|
151
|
+
* one of these per clean project, and an adapter wired into one loader and not
|
|
152
|
+
* the other reads as an absence rather than an error — the failure shape
|
|
153
|
+
* test/likec4-batch-parity.test.ts was written to catch.
|
|
154
|
+
*/
|
|
155
|
+
export function cleanProjectDoc(model) {
|
|
156
|
+
return {
|
|
157
|
+
errors: new Map(),
|
|
158
|
+
clean: true,
|
|
159
|
+
specification: readSpecification(model.specification),
|
|
160
|
+
views: readDynamicViews(model),
|
|
161
|
+
viewIds: readViewIds(model),
|
|
162
|
+
globalStyles: readGlobalStyleIds(model),
|
|
163
|
+
deployment: readDeployment(model),
|
|
164
|
+
...flattenModel(model),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Diagnostics split per DOCUMENT inside one project, each error's
|
|
132
169
|
* `sourceFsPath` rewritten to the document's real absolute path.
|
|
133
170
|
*
|
|
134
|
-
* `groupErrors`
|
|
135
|
-
* here — every document shares one project. This keys on the staged
|
|
136
|
-
* path instead, longest first so `usecases/checkout.likec4` is matched
|
|
137
|
-
* hypothetical `checkout.likec4` at the root. An error nobody can
|
|
138
|
-
* throws for the same reason it does there: a dropped error grades
|
|
139
|
-
* clean, which is failing open in the fleet gate.
|
|
171
|
+
* `../workspace.ts`'s `groupErrors` keys on the project folder segment, which
|
|
172
|
+
* cannot work here — every document shares one project. This keys on the staged
|
|
173
|
+
* relative path instead, longest first so `usecases/checkout.likec4` is matched
|
|
174
|
+
* before a hypothetical `checkout.likec4` at the root. An error nobody can
|
|
175
|
+
* attribute throws for the same reason it does there: a dropped error grades
|
|
176
|
+
* its document clean, which is failing open in the fleet gate.
|
|
177
|
+
*
|
|
178
|
+
* It takes the diagnostics rather than the workspace so `./batch.ts` can hand
|
|
179
|
+
* it ONE project's share of a multi-project `getErrors()` — the two questions
|
|
180
|
+
* compose (which project, then which document) exactly because neither is
|
|
181
|
+
* baked in here.
|
|
140
182
|
*/
|
|
141
|
-
function
|
|
183
|
+
export function groupErrorsByRel(errors, byRel) {
|
|
142
184
|
const rels = [...byRel.keys()].sort((a, b) => b.length - a.length);
|
|
143
185
|
const grouped = new Map();
|
|
144
|
-
for (const err of
|
|
186
|
+
for (const err of errors) {
|
|
145
187
|
const spelled = err.sourceFsPath.split(/[\\/]/).join("/");
|
|
146
188
|
const rel = rels.find((candidate) => spelled.endsWith(`/${candidate}`) || spelled === candidate);
|
|
147
189
|
if (rel === undefined) {
|
|
@@ -179,6 +221,7 @@ export function asLoadedDoc(doc) {
|
|
|
179
221
|
...(doc.specification === undefined ? {} : { specification: doc.specification }),
|
|
180
222
|
views: doc.views,
|
|
181
223
|
viewIds: doc.viewIds,
|
|
224
|
+
globalStyles: doc.globalStyles,
|
|
182
225
|
deployment: doc.deployment,
|
|
183
226
|
elements: doc.elements,
|
|
184
227
|
relationships: doc.relationships,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"load.js","sourceRoot":"","sources":["../../../../src/core/c4/project/load.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * One directory of `.likec4` documents, loaded as ONE LikeC4 project — the way\n * the renderer already reads `architecture/`.\n *\n * A package of its own rather than more of `../workspace.ts`, and the seam is\n * the isolation rule, not the line count. That module stages every document as\n * its own single-file project ON PURPOSE, so an author-written `import` can\n * never resolve against a sibling; this one does the opposite deliberately, for\n * the set of documents that are meant to see each other. Two loaders with\n * opposite contracts should not share a file: the next reader has to be able to\n * tell at a glance which one they are looking at.\n */\nimport { randomBytes } from \"node:crypto\";\nimport { copyFile, mkdir, mkdtemp, rm, writeFile } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { dirname, join, relative, resolve } from \"node:path\";\nimport { LikeC4 } from \"likec4\";\nimport { inOrder } from \"../../kernel/concurrency.js\";\nimport { flattenModel, type Elem, type LikeC4Error, type LoadedDoc, type ReadableModel, type Rel } from \"../likec4.js\";\nimport { readDynamicViews, type ParsedView } from \"../parsed/dynamic-views.js\";\nimport { readSpecification, type DocSpecification } from \"../parsed/specification.js\";\nimport { readViewIds, type ViewIdClaim } from \"../parsed/view-ids.js\";\nimport { readDeployment, NO_DEPLOYMENT, type DeploymentModel } from \"../parsed/deployment.js\";\n\n/** A directory of `.likec4` documents loaded as one LikeC4 project. */\nexport interface ProjectDoc {\n /** Errors by the REAL absolute path of the document that raised them. */\n errors: Map<string, LikeC4Error[]>;\n /** True when no document in the project raised one — loam's \"errors mean no model\" gate. */\n clean: boolean;\n elements: Elem[];\n relationships: Rel[];\n specification?: DocSpecification;\n /** Every dynamic view the project declares, each carrying the file it is written in. */\n views: ParsedView[];\n /** Every authored view id the project claims, each with its file. */\n viewIds: ViewIdClaim[];\n /** The project's deployment model — empty when it declares none, and when it did not parse. */\n deployment: DeploymentModel;\n}\n\n/**\n * Stage a set of documents as ONE project, mirroring their paths relative to\n * `base`.\n *\n * The relative structure is preserved rather than flattened to basenames, and\n * that is load-bearing twice over: two use cases named `checkout.likec4` in\n * different subdirectories would overwrite each other on copy, and LikeC4\n * reports a view's `sourcePath` relative to the project root — so flattening\n * would make every finding name a file the author cannot find.\n */\nasync function stageProject(root: string, base: string, targets: string[]): Promise<Map<string, string>> {\n const project = `arch_${randomBytes(8).toString(\"hex\")}`;\n const folder = join(root, project);\n await mkdir(folder, { recursive: true });\n await writeFile(join(folder, \"likec4.config.json\"), JSON.stringify({ name: project }), \"utf8\");\n // rel -> real path, so an error carrying the staged path can be handed back\n // spelled the way the author wrote it.\n const byRel = new Map<string, string>();\n await inOrder(targets, async (path) => {\n const rel = relative(base, path).split(/[\\\\/]/).join(\"/\");\n const dest = join(folder, ...rel.split(\"/\"));\n try {\n await mkdir(dirname(dest), { recursive: true });\n await copyFile(path, dest);\n byRel.set(rel, path);\n } catch {\n // Dropped, exactly as `stage` drops one: the caller's ordinary per-path\n // load owns the error, and a project missing one document still answers\n // about the rest.\n }\n return null;\n });\n return byRel;\n}\n\n/**\n * Load a directory of `.likec4` documents as ONE LikeC4 project — the way the\n * renderer already reads `architecture/`, and the only way a document that\n * declares views over ANOTHER file's elements can be read at all.\n *\n * Measured at the 1.59.2 pin: a views-only `usecases/checkout.likec4` parsed\n * alone is five errors (unresolved tag, unresolved elements); the same file\n * beside its landscape in one project is zero, and each view comes back with\n * its own `sourcePath`. That is the whole reason this function exists — a use\n * case lives in a file of its own, and `loadBatch`'s one-project-per-document\n * isolation makes such a file unreadable by construction.\n *\n * Two rules the caller must know, because both invert what holds elsewhere:\n *\n * - **Exactly one document in the set declares the `specification` block.**\n * Every `.likec4` file loam parses alone must declare its own; inside one\n * project a second declaration is a duplicate error blamed on BOTH files\n * (measured).\n * - **Errors are per document, but the MODEL is all-or-nothing.** A typo in\n * one file is attributed to that file — so the finding names it, which is\n * better than today, where a use case in the landscape would blame the\n * landscape — but loam's standing rule is that errors mean no model, and\n * here the model is the project's. `clean` says which case the caller is in.\n *\n * The generated `architecture/subsystems.likec4` must NOT be in `targets`:\n * docs/DESIGN.md rule 26 keeps it a byte compare, and a stale one whose\n * `include` names a removed element would otherwise be a parse error that\n * blanks the whole fleet map.\n */\nexport async function loadProject(base: string, paths: string[]): Promise<ProjectDoc> {\n const empty = (errors: Map<string, LikeC4Error[]>, clean: boolean): ProjectDoc => ({\n errors,\n clean,\n elements: [],\n relationships: [],\n views: [],\n viewIds: [],\n deployment: NO_DEPLOYMENT,\n });\n const targets = [...new Set(paths.map((path) => resolve(path)))];\n if (targets.length === 0) return empty(new Map(), true);\n const root = await mkdtemp(join(tmpdir(), \"loam-c4-\"));\n try {\n const byRel = await stageProject(root, resolve(base), targets);\n if (byRel.size === 0) return empty(new Map(), true);\n const likec4 = await LikeC4.fromWorkspace(root, { logger: false });\n try {\n const errors = groupProjectErrors(likec4, byRel);\n if (errors.size > 0) return empty(errors, false);\n const model = (await likec4.parsedModel()) as ReadableModel;\n return {\n errors,\n clean: true,\n specification: readSpecification(model.specification),\n views: readDynamicViews(model),\n viewIds: readViewIds(model),\n deployment: readDeployment(model),\n ...flattenModel(model),\n };\n } finally {\n await likec4.dispose();\n }\n } finally {\n await rm(root, { recursive: true, force: true });\n }\n}\n\n/**\n * `getErrors()` split per DOCUMENT inside one project, each error's\n * `sourceFsPath` rewritten to the document's real absolute path.\n *\n * `groupErrors` above keys on the project folder segment, which cannot work\n * here — every document shares one project. This keys on the staged relative\n * path instead, longest first so `usecases/checkout.likec4` is matched before a\n * hypothetical `checkout.likec4` at the root. An error nobody can attribute\n * throws for the same reason it does there: a dropped error grades its document\n * clean, which is failing open in the fleet gate.\n */\nfunction groupProjectErrors(likec4: LikeC4, byRel: Map<string, string>): Map<string, LikeC4Error[]> {\n const rels = [...byRel.keys()].sort((a, b) => b.length - a.length);\n const grouped = new Map<string, LikeC4Error[]>();\n for (const err of likec4.getErrors()) {\n const spelled = err.sourceFsPath.split(/[\\\\/]/).join(\"/\");\n const rel = rels.find((candidate) => spelled.endsWith(`/${candidate}`) || spelled === candidate);\n if (rel === undefined) {\n throw new Error(`unattributable LikeC4 error in project workspace: ${err.message}`);\n }\n const real = byRel.get(rel)!;\n const list = grouped.get(real) ?? [];\n list.push({ ...err, sourceFsPath: real });\n grouped.set(real, list);\n }\n return grouped;\n}\n\n/**\n * A `ProjectDoc` in the shape every existing landscape reader already takes.\n *\n * The per-file error map flattens into one list, which loses nothing: each\n * error still carries the real path of the document that raised it, so a\n * finding can name the file even though the list is flat. Sorted by that path\n * so a project with two broken documents reports them in a stable order —\n * `getErrors()` returns whatever order the workspace scan produced.\n *\n * The model travels only when the project is clean, which is loam's standing\n * rule (`errors mean no model`) applied at project altitude: a use-case file\n * with an unresolved element leaves the landscape unusable, and pretending\n * otherwise would grade a fleet against half a map.\n */\nexport function asLoadedDoc(doc: ProjectDoc): LoadedDoc {\n const errors = [...doc.errors]\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n .flatMap(([, list]) => list);\n if (!doc.clean) return { errors, elements: [], relationships: [] };\n return {\n errors,\n ...(doc.specification === undefined ? {} : { specification: doc.specification }),\n views: doc.views,\n viewIds: doc.viewIds,\n deployment: doc.deployment,\n elements: doc.elements,\n relationships: doc.relationships,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"load.js","sourceRoot":"","sources":["../../../../src/core/c4/project/load.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * One directory of `.likec4` documents, loaded as ONE LikeC4 project — the way\n * the renderer already reads `architecture/`.\n *\n * A package of its own rather than more of `../workspace.ts`, and the seam is\n * the isolation rule, not the line count. That module stages every document as\n * its own single-file project ON PURPOSE, so an author-written `import` can\n * never resolve against a sibling; this one does the opposite deliberately, for\n * the set of documents that are meant to see each other. Two loaders with\n * opposite contracts should not share a file: the next reader has to be able to\n * tell at a glance which one they are looking at.\n */\nimport { randomBytes } from \"node:crypto\";\nimport { copyFile, mkdir, mkdtemp, rm, writeFile } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { dirname, join, relative, resolve } from \"node:path\";\nimport { LikeC4 } from \"likec4\";\nimport { inOrder } from \"../../kernel/concurrency.js\";\nimport { flattenModel, type Elem, type LikeC4Error, type LoadedDoc, type ReadableModel, type Rel } from \"../likec4.js\";\nimport { readDynamicViews, type ParsedView } from \"../parsed/dynamic-views.js\";\nimport { readSpecification, type DocSpecification } from \"../parsed/specification.js\";\nimport { readGlobalStyleIds, readViewIds, type ViewIdClaim } from \"../parsed/view-ids.js\";\nimport { readDeployment, NO_DEPLOYMENT, type DeploymentModel } from \"../parsed/deployment.js\";\n\n/**\n * A diagnostic exactly as a WORKSPACE hands it back, where `sourceFsPath` is\n * always present — unlike loam's own `LikeC4Error`, whose field is optional\n * because the several literals standing in for an unread file have no path to\n * put there. Named so the attribution below can take diagnostics rather than\n * the workspace object, without a non-null assertion per error.\n */\ntype WorkspaceError = LikeC4Error & { sourceFsPath: string };\n\n/** A directory of `.likec4` documents loaded as one LikeC4 project. */\nexport interface ProjectDoc {\n /** Errors by the REAL absolute path of the document that raised them. */\n errors: Map<string, LikeC4Error[]>;\n /** True when no document in the project raised one — loam's \"errors mean no model\" gate. */\n clean: boolean;\n elements: Elem[];\n relationships: Rel[];\n specification?: DocSpecification;\n /** Every dynamic view the project declares, each carrying the file it is written in. */\n views: ParsedView[];\n /** Every authored view id the project claims, each with its file. */\n viewIds: ViewIdClaim[];\n /**\n * Every global style id declared anywhere in the project, sorted — the\n * renderer's table, so a palette kept in `usecases/style.likec4` counts\n * exactly as one in the landscape does. Empty when none, and when it did\n * not parse.\n */\n globalStyles: string[];\n /** The project's deployment model — empty when it declares none, and when it did not parse. */\n deployment: DeploymentModel;\n}\n\n/**\n * Stage a set of documents as ONE project named `project`, mirroring their\n * paths relative to `base`.\n *\n * The relative structure is preserved rather than flattened to basenames, and\n * that is load-bearing twice over: two use cases named `checkout.likec4` in\n * different subdirectories would overwrite each other on copy, and LikeC4\n * reports a view's `sourcePath` relative to the project root — so flattening\n * would make every finding name a file the author cannot find.\n *\n * The NAME arrives from the caller rather than being minted in here, and that\n * is what lets `./batch.ts` reuse this staging verbatim: many projects in one\n * workspace need the folder name twice over — `parsedModel(<name>)` asks for\n * that project's model, and error attribution keys on the folder SEGMENT.\n * `loadProject` below has one project and passes a name it never looks at\n * again; both spellings carry a crypto-random per-invocation token for\n * `../workspace.ts`'s reason (an author-written `import` must not be able to\n * name a sibling project).\n */\nexport async function stageProject(\n root: string,\n project: string,\n base: string,\n targets: string[],\n): Promise<Map<string, string>> {\n const folder = join(root, project);\n await mkdir(folder, { recursive: true });\n await writeFile(join(folder, \"likec4.config.json\"), JSON.stringify({ name: project }), \"utf8\");\n // rel -> real path, so an error carrying the staged path can be handed back\n // spelled the way the author wrote it.\n const byRel = new Map<string, string>();\n await inOrder(targets, async (path) => {\n const rel = relative(base, path).split(/[\\\\/]/).join(\"/\");\n const dest = join(folder, ...rel.split(\"/\"));\n try {\n await mkdir(dirname(dest), { recursive: true });\n await copyFile(path, dest);\n byRel.set(rel, path);\n } catch {\n // Dropped, exactly as `stage` drops one: the caller's ordinary per-path\n // load owns the error, and a project missing one document still answers\n // about the rest.\n }\n return null;\n });\n return byRel;\n}\n\n/**\n * Load a directory of `.likec4` documents as ONE LikeC4 project — the way the\n * renderer already reads `architecture/`, and the only way a document that\n * declares views over ANOTHER file's elements can be read at all.\n *\n * Measured at the 1.59.2 pin: a views-only `usecases/checkout.likec4` parsed\n * alone is five errors (unresolved tag, unresolved elements); the same file\n * beside its landscape in one project is zero, and each view comes back with\n * its own `sourcePath`. That is the whole reason this function exists — a use\n * case lives in a file of its own, and `loadBatch`'s one-project-per-document\n * isolation makes such a file unreadable by construction.\n *\n * Two rules the caller must know, because both invert what holds elsewhere:\n *\n * - **Exactly one document in the set declares the `specification` block.**\n * Every `.likec4` file loam parses alone must declare its own; inside one\n * project a second declaration is a duplicate error blamed on BOTH files\n * (measured).\n * - **Errors are per document, but the MODEL is all-or-nothing.** A typo in\n * one file is attributed to that file — so the finding names it, which is\n * better than today, where a use case in the landscape would blame the\n * landscape — but loam's standing rule is that errors mean no model, and\n * here the model is the project's. `clean` says which case the caller is in.\n *\n * The documents loam GENERATES under `architecture/` must NOT be in `targets`\n * — there are two of them now (`subsystems.likec4` and `services.likec4`, both\n * written by `loam subsystem sync`), and `repo/paths.ts`'s\n * `GENERATED_ARCHITECTURE_FILES` is the one table that names them. docs/DESIGN.md\n * rule 26 keeps each a byte compare, and a stale one whose `include` names a\n * removed element would otherwise be a parse error that blanks the whole fleet\n * map. `./architecture.ts` holds the join from that table to a docsDir, so no\n * caller has to remember the list.\n */\nexport async function loadProject(base: string, paths: string[]): Promise<ProjectDoc> {\n const targets = [...new Set(paths.map((path) => resolve(path)))];\n if (targets.length === 0) return emptyProject(new Map(), true);\n const root = await mkdtemp(join(tmpdir(), \"loam-c4-\"));\n try {\n const byRel = await stageProject(root, `arch_${randomBytes(8).toString(\"hex\")}`, resolve(base), targets);\n if (byRel.size === 0) return emptyProject(new Map(), true);\n const likec4 = await LikeC4.fromWorkspace(root, { logger: false });\n try {\n const errors = groupErrorsByRel(likec4.getErrors(), byRel);\n if (errors.size > 0) return emptyProject(errors, false);\n return cleanProjectDoc((await likec4.parsedModel()) as ReadableModel);\n } finally {\n await likec4.dispose();\n }\n } finally {\n await rm(root, { recursive: true, force: true });\n }\n}\n\n/**\n * A `ProjectDoc` carrying NO model — the answer for a project that did not\n * parse (`clean: false`) and for one there was nothing to load (`clean: true`,\n * empty errors).\n *\n * Exported for `./batch.ts`, which answers about many projects at once and\n * must answer each of them in exactly this shape: a second literal there would\n * be a second chance to spell `deployment` as `undefined` and turn \"this\n * project declares no topology\" into a crash in every reader.\n */\nexport function emptyProject(errors: Map<string, LikeC4Error[]>, clean: boolean): ProjectDoc {\n return {\n errors,\n clean,\n elements: [],\n relationships: [],\n views: [],\n viewIds: [],\n globalStyles: [],\n deployment: NO_DEPLOYMENT,\n };\n}\n\n/**\n * Everything loam reads out of a project that parsed, from LikeC4's parsed\n * stage — parsed, never computed, for the reason `../likec4.ts` records.\n *\n * Exported beside `emptyProject` and for the same reason: `./batch.ts` builds\n * one of these per clean project, and an adapter wired into one loader and not\n * the other reads as an absence rather than an error — the failure shape\n * test/likec4-batch-parity.test.ts was written to catch.\n */\nexport function cleanProjectDoc(model: ReadableModel): ProjectDoc {\n return {\n errors: new Map(),\n clean: true,\n specification: readSpecification(model.specification),\n views: readDynamicViews(model),\n viewIds: readViewIds(model),\n globalStyles: readGlobalStyleIds(model),\n deployment: readDeployment(model),\n ...flattenModel(model),\n };\n}\n\n/**\n * Diagnostics split per DOCUMENT inside one project, each error's\n * `sourceFsPath` rewritten to the document's real absolute path.\n *\n * `../workspace.ts`'s `groupErrors` keys on the project folder segment, which\n * cannot work here — every document shares one project. This keys on the staged\n * relative path instead, longest first so `usecases/checkout.likec4` is matched\n * before a hypothetical `checkout.likec4` at the root. An error nobody can\n * attribute throws for the same reason it does there: a dropped error grades\n * its document clean, which is failing open in the fleet gate.\n *\n * It takes the diagnostics rather than the workspace so `./batch.ts` can hand\n * it ONE project's share of a multi-project `getErrors()` — the two questions\n * compose (which project, then which document) exactly because neither is\n * baked in here.\n */\nexport function groupErrorsByRel(\n errors: readonly WorkspaceError[],\n byRel: Map<string, string>,\n): Map<string, LikeC4Error[]> {\n const rels = [...byRel.keys()].sort((a, b) => b.length - a.length);\n const grouped = new Map<string, LikeC4Error[]>();\n for (const err of errors) {\n const spelled = err.sourceFsPath.split(/[\\\\/]/).join(\"/\");\n const rel = rels.find((candidate) => spelled.endsWith(`/${candidate}`) || spelled === candidate);\n if (rel === undefined) {\n throw new Error(`unattributable LikeC4 error in project workspace: ${err.message}`);\n }\n const real = byRel.get(rel)!;\n const list = grouped.get(real) ?? [];\n list.push({ ...err, sourceFsPath: real });\n grouped.set(real, list);\n }\n return grouped;\n}\n\n/**\n * A `ProjectDoc` in the shape every existing landscape reader already takes.\n *\n * The per-file error map flattens into one list, which loses nothing: each\n * error still carries the real path of the document that raised it, so a\n * finding can name the file even though the list is flat. Sorted by that path\n * so a project with two broken documents reports them in a stable order —\n * `getErrors()` returns whatever order the workspace scan produced.\n *\n * The model travels only when the project is clean, which is loam's standing\n * rule (`errors mean no model`) applied at project altitude: a use-case file\n * with an unresolved element leaves the landscape unusable, and pretending\n * otherwise would grade a fleet against half a map.\n */\nexport function asLoadedDoc(doc: ProjectDoc): LoadedDoc {\n const errors = [...doc.errors]\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n .flatMap(([, list]) => list);\n if (!doc.clean) return { errors, elements: [], relationships: [] };\n return {\n errors,\n ...(doc.specification === undefined ? {} : { specification: doc.specification }),\n views: doc.views,\n viewIds: doc.viewIds,\n globalStyles: doc.globalStyles,\n deployment: doc.deployment,\n elements: doc.elements,\n relationships: doc.relationships,\n };\n}\n"]}
|
|
@@ -52,12 +52,12 @@ import { existsSync } from "node:fs";
|
|
|
52
52
|
import { copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
53
53
|
import { tmpdir } from "node:os";
|
|
54
54
|
import { dirname, join, relative, resolve } from "node:path";
|
|
55
|
-
import { loadFile } from "../likec4.js";
|
|
55
|
+
import { errorText, loadFile } from "../likec4.js";
|
|
56
56
|
import { planLandscapeMerge } from "../splice/landscape-merge.js";
|
|
57
57
|
import { inOrder } from "../../kernel/concurrency.js";
|
|
58
58
|
import { decodeDocument } from "../../kernel/document-bytes.js";
|
|
59
|
-
import { featurePaths, landscapePath
|
|
60
|
-
import { architectureDir } from "./architecture.js";
|
|
59
|
+
import { featurePaths, landscapePath } from "../../repo/paths.js";
|
|
60
|
+
import { architectureDir, generatedArchitectureDocuments } from "./architecture.js";
|
|
61
61
|
import { architectureDocuments } from "./documents.js";
|
|
62
62
|
import { asLoadedDoc, loadProject } from "./load.js";
|
|
63
63
|
/**
|
|
@@ -74,7 +74,11 @@ export async function stageMergedProject(req) {
|
|
|
74
74
|
if (merged.kind === "failed")
|
|
75
75
|
return { kind: "unreadable", errors: [merged.reason] };
|
|
76
76
|
const arch = architectureDir(req.docsDir);
|
|
77
|
-
|
|
77
|
+
// BOTH generated documents come out, through the one list every project read
|
|
78
|
+
// shares: a views-only file does not parse standalone, so staging one that
|
|
79
|
+
// `sync` has not caught up with would blank the merge preview this whole
|
|
80
|
+
// module exists to parse (`./architecture.ts` states the rule).
|
|
81
|
+
const living = await architectureDocuments(arch, generatedArchitectureDocuments(req.docsDir));
|
|
78
82
|
const root = await mkdtemp(join(tmpdir(), "loam-staged-"));
|
|
79
83
|
try {
|
|
80
84
|
// Staged rel -> the path the AUTHOR wrote, so every message that leaves
|
|
@@ -120,9 +124,18 @@ export async function stageMergedProject(req) {
|
|
|
120
124
|
// still travels: a hole loam cannot place is still a hole, and dropping
|
|
121
125
|
// it here would grade the feature clean on the strength of a missing
|
|
122
126
|
// field.
|
|
127
|
+
//
|
|
128
|
+
// DOCS-RELATIVE, `/`-separated, and carrying the line — the spelling the
|
|
129
|
+
// SERVICE arm of `usecase.flow-invalid` uses on purpose. These strings are
|
|
130
|
+
// the feature arm's whole payload (text, `--json` and the `loam archive`
|
|
131
|
+
// block message), and `real()` alone answered with an absolute Windows path
|
|
132
|
+
// and no line at all: `C:\Users\…\docs\features\FEAT-1\usecases\x.likec4:
|
|
133
|
+
// <message>` in a finding a person is asked to act on (verification
|
|
134
|
+
// 2026-09-04, W7). A path outside the docs root — which nothing here
|
|
135
|
+
// produces, but `relative` would spell `..\..` — keeps its absolute form.
|
|
123
136
|
return {
|
|
124
137
|
kind: "unreadable",
|
|
125
|
-
errors: doc.errors.map((e) =>
|
|
138
|
+
errors: doc.errors.map((e) => e.sourceFsPath === undefined ? errorText(e) : `${spellReal(req.docsDir, real(e.sourceFsPath))}: ${errorText(e)}`),
|
|
126
139
|
};
|
|
127
140
|
}
|
|
128
141
|
return { kind: "read", doc, real, mine };
|
|
@@ -153,16 +166,30 @@ export async function stageMergedProject(req) {
|
|
|
153
166
|
* there and allowed here would be a document graded against a map the merge
|
|
154
167
|
* never writes.
|
|
155
168
|
*
|
|
156
|
-
* THE PARITY IS THE MERGE, NOT THE WHOLE GATE, and the
|
|
157
|
-
* naming rather than leaving for a reader to discover.
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
169
|
+
* THE PARITY IS THE MERGE, NOT THE WHOLE GATE, and the two places the sides
|
|
170
|
+
* differ are worth naming rather than leaving for a reader to discover.
|
|
171
|
+
*
|
|
172
|
+
* The first changes no verdict. `planLandscape` runs one check BEFORE it
|
|
173
|
+
* computes the merge and this does not: a delta declaring the feature tag on a
|
|
174
|
+
* specification KIND is refused there, because every element of that kind
|
|
175
|
+
* inherits the tag and the merge would splice the whole document. Such a delta
|
|
176
|
+
* reaches `newEls` here unfiltered, so the preview this grades against is one
|
|
177
|
+
* the archive would never write — but that archive refuses either way, before
|
|
178
|
+
* anything is written.
|
|
179
|
+
*
|
|
180
|
+
* The second changed every verdict about a fleet with extending models, and is
|
|
181
|
+
* why `models` is on the request at all. `planLandscape` names the fleet's
|
|
182
|
+
* extending models, so a nested addition is ROUTED into the model that owns
|
|
183
|
+
* that service's interior and never reaches the map; a request that omits them
|
|
184
|
+
* previews a map holding interior the archive will put somewhere else. A
|
|
185
|
+
* feature-local `usecases/<x>.likec4` whose hop names `<service-fqn>.<container>`
|
|
186
|
+
* then resolved in the preview, passed `usecase.flow-invalid` — the gate whose
|
|
187
|
+
* own message is "the archive would land an architecture/ nothing has checked",
|
|
188
|
+
* and which `--approve` cannot override — and was copied verbatim into
|
|
189
|
+
* `architecture/`, where it resolves against nothing and takes the whole fleet
|
|
190
|
+
* map down (verification 2026-09-04, review C; SCHEMA names that outcome as the
|
|
191
|
+
* thing that must not happen). A caller that leaves `models` out is asking for
|
|
192
|
+
* the pre-routing preview, and gets exactly the answers it got before.
|
|
166
193
|
*
|
|
167
194
|
* A throw is `failed` rather than propagated. Every `LandscapeSpliceError` this
|
|
168
195
|
* can raise is one `planLandscape` raises again — with its own message, its own
|
|
@@ -189,6 +216,7 @@ async function mergedLandscape(req) {
|
|
|
189
216
|
newEls: delta.elements.filter((e) => e.tags.includes(req.featureId)),
|
|
190
217
|
newRels: delta.relationships.filter((r) => r.tags.includes(req.featureId)),
|
|
191
218
|
featureId: req.featureId,
|
|
219
|
+
...(req.models === undefined ? {} : { models: req.models }),
|
|
192
220
|
});
|
|
193
221
|
return plan.content === null ? { kind: "living" } : { kind: "merged", content: plan.content };
|
|
194
222
|
}
|
|
@@ -204,6 +232,16 @@ async function text(path) {
|
|
|
204
232
|
function spell(path) {
|
|
205
233
|
return path.split(/[\\/]/).join("/");
|
|
206
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* An authored path as a FINDING spells it: relative to the docs root,
|
|
237
|
+
* `/`-separated. A path that does not sit under the docs root keeps its absolute
|
|
238
|
+
* form — `relative` would answer with `..` segments, and a finding naming a
|
|
239
|
+
* document outside the repository is more honest as the absolute path it is.
|
|
240
|
+
*/
|
|
241
|
+
function spellReal(docsDir, path) {
|
|
242
|
+
const rel = relative(docsDir, path);
|
|
243
|
+
return rel.length > 0 && !rel.startsWith("..") ? spell(rel) : spell(path);
|
|
244
|
+
}
|
|
207
245
|
/** Copy one authored document into the staged tree, remembering where it came from. */
|
|
208
246
|
async function stage(root, rel, path, authored) {
|
|
209
247
|
const dest = join(root, ...rel.split("/"));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"staged.js","sourceRoot":"","sources":["../../../../src/core/c4/project/staged.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACrF,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7D,OAAO,EAAE,QAAQ,EAAkB,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAClE,OAAO,EAAE,OAAO,EAAE,MAAM,6BAA6B,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAEhE,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AACtF,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAwCrD;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,GAAyB;IAChE,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,GAAG,CAAC,CAAC;IAC1C,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ;QAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;IAErF,MAAM,IAAI,GAAG,eAAe,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAC1C,MAAM,MAAM,GAAG,MAAM,qBAAqB,CAAC,IAAI,EAAE,CAAC,kBAAkB,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IACpF,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,cAAc,CAAC,CAAC,CAAC;IAC3D,IAAI,CAAC;QACH,wEAAwE;QACxE,wEAAwE;QACxE,qDAAqD;QACrD,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAC;QAC3C,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC;QACtD,MAAM,OAAO,CAAC,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;YACnC,sEAAsE;YACtE,6DAA6D;YAC7D,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,KAAK,SAAS;gBAAE,OAAO,IAAI,CAAC;YACzE,MAAM,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;YAC/D,OAAO,IAAI,CAAC;QACd,CAAC,CAAC,CAAC;QACH,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC7B,MAAM,GAAG,GAAG,KAAK,CAAC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC;YAC7C,MAAM,KAAK,CAAC,IAAI,EAAE,GAAG,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;YACvC,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;QAC/B,CAAC;QACD,yEAAyE;QACzE,2EAA2E;QAC3E,6DAA6D;QAC7D,0EAA0E;QAC1E,yEAAyE;QACzE,mBAAmB;QACnB,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;QAC/B,KAAK,MAAM,QAAQ,IAAI,GAAG,CAAC,SAAS,EAAE,CAAC;YACrC,MAAM,KAAK,CAAC,IAAI,EAAE,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;YACzD,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QACzB,CAAC;QACD,MAAM,GAAG,GAAG,WAAW,CAAC,MAAM,WAAW,CAAC,IAAI,EAAE,MAAM,qBAAqB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACpF,0EAA0E;QAC1E,8DAA8D;QAC9D,kEAAkE;QAClE,uEAAuE;QACvE,0EAA0E;QAC1E,wEAAwE;QACxE,yEAAyE;QACzE,MAAM,IAAI,GAAG,CAAC,IAAY,EAAU,EAAE,CACpC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;QACjF,IAAI,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC1B,uEAAuE;YACvE,wEAAwE;YACxE,qEAAqE;YACrE,SAAS;YACT,OAAO;gBACL,IAAI,EAAE,YAAY;gBAClB,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;aACpH,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IAC3C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,0EAA0E;QAC1E,2EAA2E;QAC3E,2EAA2E;QAC3E,wEAAwE;QACxE,0EAA0E;QAC1E,yEAAyE;QACzE,2EAA2E;QAC3E,qEAAqE;QACrE,+BAA+B;QAC/B,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;IAC5F,CAAC;YAAS,CAAC;QACT,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACnD,CAAC;AACH,CAAC;AAWD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,KAAK,UAAU,eAAe,CAAC,GAAyB;IACtD,MAAM,SAAS,GAAG,YAAY,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,KAAK,CAAC;IACrD,MAAM,SAAS,GAAG,aAAa,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAC7C,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;IAChF,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,IAAI,QAAQ,CAAC,CAAC,SAAS,CAAC,CAAC;IACtD,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC5B,OAAO;YACL,IAAI,EAAE,QAAQ;YACd,MAAM,EAAE,oBAAoB,KAAK,CAAC,MAAM,CAAC,MAAM,gGAAgG;SAChJ,CAAC;IACJ,CAAC;IACD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,kBAAkB,CAAC;YACpC,aAAa,EAAE,MAAM,IAAI,CAAC,SAAS,CAAC;YACpC,SAAS,EAAE,MAAM,IAAI,CAAC,SAAS,CAAC;YAChC,aAAa,EAAE,KAAK,CAAC,QAAQ;YAC7B,MAAM,EAAE,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YACpE,OAAO,EAAE,KAAK,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YAC1E,SAAS,EAAE,GAAG,CAAC,SAAS;SACzB,CAAC,CAAC;QACH,OAAO,IAAI,CAAC,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC;IAChG,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;IACtF,CAAC;AACH,CAAC;AAED,iGAAiG;AACjG,KAAK,UAAU,IAAI,CAAC,IAAY;IAC9B,OAAO,cAAc,CAAC,MAAM,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,CAAC;AACpD,CAAC;AAED,yFAAyF;AACzF,SAAS,KAAK,CAAC,IAAY;IACzB,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACvC,CAAC;AAED,uFAAuF;AACvF,KAAK,UAAU,KAAK,CAAC,IAAY,EAAE,GAAW,EAAE,IAAY,EAAE,QAA6B;IACzF,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IAC3C,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAChD,MAAM,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAC3B,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;AACnC,CAAC;AAED,wFAAwF;AACxF,KAAK,UAAU,KAAK,CAAC,IAAY,EAAE,GAAW,EAAE,OAAe;IAC7D,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IAC3C,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAChD,MAAM,SAAS,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;AACzC,CAAC","sourcesContent":["/**\n * The `architecture/` project AS THIS FEATURE'S MERGE WOULD LEAVE IT, staged\n * and parsed, with a feature's own documents standing beside it.\n *\n * WHY THE POST-MERGE MAP AND NOT THE LIVING ONE. A document a feature brings\n * names elements, and a feature that adds a service adds the element its own\n * document names. Graded against the LIVING landscape that name is unresolved —\n * LikeC4 refuses the document, the project blanks, and loam reports a hole\n * against a file whose only fault is arriving with the change that makes it\n * true. Graded against the merge it resolves exactly when the merge lands the\n * element it names, which is `ARCH-LOAM-FEATURE-CORPUS`: inside a feature\n * window, the corpus is feature ∪ living.\n *\n * IT IS ALSO THE CHECK. The merge preview is the only reading under which \"a\n * document naming an element the merge does not land\" is answerable at all, and\n * that refusal is why it runs before `loam archive` writes rather than after:\n * without it the document is copied into `architecture/` and the NEXT reader's\n * `loam validate --all` carries the failure, against a file they did not write.\n *\n * ## Why it lives here rather than in one axis\n *\n * It was `core/usecases/delta/overlay.ts`'s private machinery while the\n * use-case slot was the only feature-local `.likec4` document. The deployment\n * slot is the second, and a second copy of this staging is the copy that\n * drifts — the two would disagree first about the merge preview, which is the\n * one thing both refusals rest on. So the staging moved down to the package\n * that owns the project loader and the merge, and each axis keeps only its own\n * INTERPRETATION of the parse: which views are the feature's, or which\n * documents failed.\n *\n * ## Why it stages its own tree\n *\n * `loadProject` copies documents from disk, keyed on their path relative to a\n * base — so it can be handed neither a landscape that exists only in memory nor\n * a document living outside `architecture/`, and both are exactly what this\n * needs. Teaching it to take content would put a second, rarely-exercised\n * staging rule inside a loader whose docstring already carries two rules that\n * invert what holds elsewhere. Staging here costs one copy of a handful of\n * small files and keeps that loader's contract at one.\n *\n * THE PRICE OF THAT CHOICE IS PATHS, and it is paid here rather than left to\n * the caller: every path LikeC4 reports — a parse error's `sourceFsPath`, a\n * view's `sourcePath` — names a temp tree, and a finding naming a temp\n * directory is a finding nobody can act on. `real()` maps them back to the file\n * the author wrote, and the temp tree is gone before this returns.\n *\n * FAIL CLOSED, and not silently. Every failure is the `unreadable` arm carrying\n * LikeC4's own messages: a document this cannot read is never one that grades\n * clean.\n */\nimport { existsSync } from \"node:fs\";\nimport { copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { dirname, join, relative, resolve } from \"node:path\";\nimport { loadFile, type LoadedDoc } from \"../likec4.js\";\nimport { planLandscapeMerge } from \"../splice/landscape-merge.js\";\nimport { inOrder } from \"../../kernel/concurrency.js\";\nimport { decodeDocument } from \"../../kernel/document-bytes.js\";\nimport type { DocsDir, FeatureDir } from \"../../kernel/ids/dirs.js\";\nimport { featurePaths, landscapePath, subsystemViewsPath } from \"../../repo/paths.js\";\nimport { architectureDir } from \"./architecture.js\";\nimport { architectureDocuments } from \"./documents.js\";\nimport { asLoadedDoc, loadProject } from \"./load.js\";\n\n/** One document a feature brings, and where it would live under `architecture/`. */\nexport interface StagedDocument {\n /** Its path relative to `architecture/`, `/`-separated — where the merge would write it. */\n rel: string;\n /** The absolute path of the authored file. */\n path: string;\n}\n\nexport interface StagedProjectRequest {\n docsDir: DocsDir;\n featureDir: FeatureDir;\n /** The feature's id — the tag that selects the delta's own additions. */\n featureId: string;\n /** The feature's own documents, staged LAST so they win over a living twin. */\n documents: readonly StagedDocument[];\n /**\n * The caller's memoised LikeC4 read, when it has one. Only `delta.likec4`\n * goes through it, and only to compute the merge preview — but a caller\n * inside `featureCoherence` has already loaded that exact document, and\n * loading one spins a fresh Langium workspace, which\n * `core/coherence/coherence.ts` calls the dominant per-feature cost.\n */\n load?: (path: string) => Promise<LoadedDoc>;\n}\n\n/** The staged parse, or the honest refusal to say. */\nexport type StagedProject =\n | {\n kind: \"read\";\n /** The whole staged project, parsed. */\n doc: LoadedDoc;\n /** A path LikeC4 reported, spelled as the author wrote it. */\n real: (path: string) => string;\n /** The staged rels that are THIS feature's documents — everything else is living. */\n mine: ReadonlySet<string>;\n }\n | { kind: \"unreadable\"; errors: string[] };\n\n/**\n * Stage the living `architecture/` with the merge preview in place of the\n * landscape, add the feature's own documents, and parse the result.\n *\n * A feature bringing no documents is not this function's business: both callers\n * return their own empty answer before reaching it, at the cost of one walk\n * over a directory that is not there. That walk is the axis's per-feature\n * opt-in and the whole price a fleet that has not adopted it pays.\n */\nexport async function stageMergedProject(req: StagedProjectRequest): Promise<StagedProject> {\n const merged = await mergedLandscape(req);\n if (merged.kind === \"failed\") return { kind: \"unreadable\", errors: [merged.reason] };\n\n const arch = architectureDir(req.docsDir);\n const living = await architectureDocuments(arch, [subsystemViewsPath(req.docsDir)]);\n const root = await mkdtemp(join(tmpdir(), \"loam-staged-\"));\n try {\n // Staged rel -> the path the AUTHOR wrote, so every message that leaves\n // here names a file they can open. Built as the tree is staged, because\n // that is the one moment both spellings are in hand.\n const authored = new Map<string, string>();\n const landscape = resolve(landscapePath(req.docsDir));\n await inOrder(living, async (path) => {\n // The living landscape is staged from the merge preview when there is\n // one; copying it as well would declare every element twice.\n if (merged.kind === \"merged\" && resolve(path) === landscape) return null;\n await stage(root, spell(relative(arch, path)), path, authored);\n return null;\n });\n if (merged.kind === \"merged\") {\n const rel = spell(relative(arch, landscape));\n await write(root, rel, merged.content);\n authored.set(rel, landscape);\n }\n // The feature's own documents LAST, so one whose living twin exists wins\n // the staging rather than colliding with it. That case is already an error\n // the gate refuses before any merge — `usecase.flow-exists`,\n // `deployment.doc-exists` — and grading the feature's version is the more\n // useful of the two answers to give an author who is about to be told to\n // delete the file.\n const mine = new Set<string>();\n for (const document of req.documents) {\n await stage(root, document.rel, document.path, authored);\n mine.add(document.rel);\n }\n const doc = asLoadedDoc(await loadProject(root, await architectureDocuments(root)));\n // TWO SPELLINGS REACH HERE and both must map, which is the one thing this\n // helper cannot be allowed to get half right. A parse error's\n // `sourceFsPath` is an ABSOLUTE path into the temp tree; a view's\n // `sourcePath` is already the project-RELATIVE rel LikeC4 reports. The\n // rel-first lookup answers the second, and the relative() computation the\n // first. Handling only one leaves the other silently returning the temp\n // spelling, which is a finding naming a directory that no longer exists.\n const real = (path: string): string =>\n authored.get(spell(path)) ?? authored.get(spell(relative(root, path))) ?? path;\n if (doc.errors.length > 0) {\n // An error naming no document is LikeC4 declining to attribute one. It\n // still travels: a hole loam cannot place is still a hole, and dropping\n // it here would grade the feature clean on the strength of a missing\n // field.\n return {\n kind: \"unreadable\",\n errors: doc.errors.map((e) => (e.sourceFsPath === undefined ? e.message : `${real(e.sourceFsPath)}: ${e.message}`)),\n };\n }\n return { kind: \"read\", doc, real, mine };\n } catch (err) {\n // The `unreadable` arm is a PROMISE, not a description of the happy path.\n // Two things under here throw rather than return: `loadProject` refuses an\n // error it cannot attribute to a document, and every `mkdir`/`copyFile` in\n // the staging above can fail on a full disk or a denied temp directory.\n // Callers run inside `loam validate --feature` and the archive gate — and\n // the one behaviour a check added to those may never have is failing the\n // run it was added to. So a throw becomes the same could-not-look answer a\n // parse error already produces, which each caller turns into its own\n // refusal that writes nothing.\n return { kind: \"unreadable\", errors: [err instanceof Error ? err.message : String(err)] };\n } finally {\n await rm(root, { recursive: true, force: true });\n }\n}\n\n/** What the landscape looks like after this feature's C4 delta is spliced in. */\ntype MergedLandscape =\n /** The delta adds nothing, or there is none — the living landscape stands. */\n | { kind: \"living\" }\n /** The merge preview, as authored bytes. */\n | { kind: \"merged\"; content: string }\n /** The merge could not be computed; `planLandscape` owns the refusal and says why. */\n | { kind: \"failed\"; reason: string };\n\n/**\n * The merge preview, computed with the SAME function `loam archive` commits\n * with.\n *\n * Reusing `planLandscapeMerge` rather than approximating it is what makes this\n * an answer about the archive rather than a second opinion beside it: an\n * element placed differently, a tag stripped differently, or a splice refused\n * there and allowed here would be a document graded against a map the merge\n * never writes.\n *\n * THE PARITY IS THE MERGE, NOT THE WHOLE GATE, and the difference is worth\n * naming rather than leaving for a reader to discover. `planLandscape` runs one\n * check BEFORE it computes the merge and this does not: a delta declaring the\n * feature tag on a specification KIND is refused there, because every element\n * of that kind inherits the tag and the merge would splice the whole document.\n * Such a delta reaches `newEls` here unfiltered, so the preview this grades\n * against is one the archive would never write. It changes no verdict — that\n * archive refuses either way, before anything is written — and it is recorded\n * because the next reader to widen either side needs to know the two are not\n * the same gate.\n *\n * A throw is `failed` rather than propagated. Every `LandscapeSpliceError` this\n * can raise is one `planLandscape` raises again — with its own message, its own\n * refusal code, and before anything is written — so throwing here would only\n * decide which of two identical refusals an author reads first.\n */\nasync function mergedLandscape(req: StagedProjectRequest): Promise<MergedLandscape> {\n const deltaPath = featurePaths(req.featureDir).delta;\n const landscape = landscapePath(req.docsDir);\n if (!existsSync(deltaPath) || !existsSync(landscape)) return { kind: \"living\" };\n const delta = await (req.load ?? loadFile)(deltaPath);\n if (delta.errors.length > 0) {\n return {\n kind: \"failed\",\n reason: `delta.likec4 has ${delta.errors.length} parse error(s) — a feature's own documents cannot be graded against a map that does not parse`,\n };\n }\n try {\n const plan = await planLandscapeMerge({\n landscapeText: await text(landscape),\n deltaText: await text(deltaPath),\n deltaElements: delta.elements,\n newEls: delta.elements.filter((e) => e.tags.includes(req.featureId)),\n newRels: delta.relationships.filter((r) => r.tags.includes(req.featureId)),\n featureId: req.featureId,\n });\n return plan.content === null ? { kind: \"living\" } : { kind: \"merged\", content: plan.content };\n } catch (err) {\n return { kind: \"failed\", reason: err instanceof Error ? err.message : String(err) };\n }\n}\n\n/** Read a document loam is about to hand LikeC4, refusing the encodings that decode as empty. */\nasync function text(path: string): Promise<string> {\n return decodeDocument(await readFile(path), path);\n}\n\n/** A path spelled the way a staged rel is spelled — `/`-separated, on every platform. */\nfunction spell(path: string): string {\n return path.split(/[\\\\/]/).join(\"/\");\n}\n\n/** Copy one authored document into the staged tree, remembering where it came from. */\nasync function stage(root: string, rel: string, path: string, authored: Map<string, string>): Promise<void> {\n const dest = join(root, ...rel.split(\"/\"));\n await mkdir(dirname(dest), { recursive: true });\n await copyFile(path, dest);\n authored.set(rel, resolve(path));\n}\n\n/** Write one document into the staged tree from bytes loam holds rather than a file. */\nasync function write(root: string, rel: string, content: string): Promise<void> {\n const dest = join(root, ...rel.split(\"/\"));\n await mkdir(dirname(dest), { recursive: true });\n await writeFile(dest, content, \"utf8\");\n}\n"]}
|
|
1
|
+
{"version":3,"file":"staged.js","sourceRoot":"","sources":["../../../../src/core/c4/project/staged.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The `architecture/` project AS THIS FEATURE'S MERGE WOULD LEAVE IT, staged\n * and parsed, with a feature's own documents standing beside it.\n *\n * WHY THE POST-MERGE MAP AND NOT THE LIVING ONE. A document a feature brings\n * names elements, and a feature that adds a service adds the element its own\n * document names. Graded against the LIVING landscape that name is unresolved —\n * LikeC4 refuses the document, the project blanks, and loam reports a hole\n * against a file whose only fault is arriving with the change that makes it\n * true. Graded against the merge it resolves exactly when the merge lands the\n * element it names, which is `ARCH-LOAM-FEATURE-CORPUS`: inside a feature\n * window, the corpus is feature ∪ living.\n *\n * IT IS ALSO THE CHECK. The merge preview is the only reading under which \"a\n * document naming an element the merge does not land\" is answerable at all, and\n * that refusal is why it runs before `loam archive` writes rather than after:\n * without it the document is copied into `architecture/` and the NEXT reader's\n * `loam validate --all` carries the failure, against a file they did not write.\n *\n * ## Why it lives here rather than in one axis\n *\n * It was `core/usecases/delta/overlay.ts`'s private machinery while the\n * use-case slot was the only feature-local `.likec4` document. The deployment\n * slot is the second, and a second copy of this staging is the copy that\n * drifts — the two would disagree first about the merge preview, which is the\n * one thing both refusals rest on. So the staging moved down to the package\n * that owns the project loader and the merge, and each axis keeps only its own\n * INTERPRETATION of the parse: which views are the feature's, or which\n * documents failed.\n *\n * ## Why it stages its own tree\n *\n * `loadProject` copies documents from disk, keyed on their path relative to a\n * base — so it can be handed neither a landscape that exists only in memory nor\n * a document living outside `architecture/`, and both are exactly what this\n * needs. Teaching it to take content would put a second, rarely-exercised\n * staging rule inside a loader whose docstring already carries two rules that\n * invert what holds elsewhere. Staging here costs one copy of a handful of\n * small files and keeps that loader's contract at one.\n *\n * THE PRICE OF THAT CHOICE IS PATHS, and it is paid here rather than left to\n * the caller: every path LikeC4 reports — a parse error's `sourceFsPath`, a\n * view's `sourcePath` — names a temp tree, and a finding naming a temp\n * directory is a finding nobody can act on. `real()` maps them back to the file\n * the author wrote, and the temp tree is gone before this returns.\n *\n * FAIL CLOSED, and not silently. Every failure is the `unreadable` arm carrying\n * LikeC4's own messages: a document this cannot read is never one that grades\n * clean.\n */\nimport { existsSync } from \"node:fs\";\nimport { copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { dirname, join, relative, resolve } from \"node:path\";\nimport { errorText, loadFile, type LoadedDoc } from \"../likec4.js\";\nimport type { ExtendingModel } from \"../splice/contract.js\";\nimport { planLandscapeMerge } from \"../splice/landscape-merge.js\";\nimport { inOrder } from \"../../kernel/concurrency.js\";\nimport { decodeDocument } from \"../../kernel/document-bytes.js\";\nimport type { DocsDir, FeatureDir } from \"../../kernel/ids/dirs.js\";\nimport { featurePaths, landscapePath } from \"../../repo/paths.js\";\nimport { architectureDir, generatedArchitectureDocuments } from \"./architecture.js\";\nimport { architectureDocuments } from \"./documents.js\";\nimport { asLoadedDoc, loadProject } from \"./load.js\";\n\n/** One document a feature brings, and where it would live under `architecture/`. */\nexport interface StagedDocument {\n /** Its path relative to `architecture/`, `/`-separated — where the merge would write it. */\n rel: string;\n /** The absolute path of the authored file. */\n path: string;\n}\n\nexport interface StagedProjectRequest {\n docsDir: DocsDir;\n featureDir: FeatureDir;\n /** The feature's id — the tag that selects the delta's own additions. */\n featureId: string;\n /** The feature's own documents, staged LAST so they win over a living twin. */\n documents: readonly StagedDocument[];\n /**\n * The caller's memoised LikeC4 read, when it has one. Only `delta.likec4`\n * goes through it, and only to compute the merge preview — but a caller\n * inside `featureCoherence` has already loaded that exact document, and\n * loading one spins a fresh Langium workspace, which\n * `core/coherence/coherence.ts` calls the dominant per-feature cost.\n */\n load?: (path: string) => Promise<LoadedDoc>;\n /**\n * The fleet's extending models — the SAME list `loam archive` merges with.\n * The merge routes a nested addition into the model that owns that service's\n * interior, so a preview computed without this list holds interior the\n * archive will put somewhere else, and every gate resting on the preview\n * grades a map that will never exist (verification 2026-09-04, review C).\n *\n * Optional because it must be INJECTED rather than read here: the shape scan\n * that decides which models extend the map lives in\n * `../service-model/shape.ts`, and that package imports this one — reading it\n * from here would be a package cycle. The reader is\n * `../service-model/fleet/extending.ts`, one level below that scan, and both\n * shipped callers fill this in from it: `core/delta/delta.ts` for the\n * use-case overlay, `core/coherence/coherence.ts` for the deployment one.\n * Omitted, the preview is the pre-routing one — the behaviour every caller\n * had before this field existed, and what a fleet whose models all stand\n * alone gets either way.\n */\n models?: readonly ExtendingModel[];\n}\n\n/** The staged parse, or the honest refusal to say. */\nexport type StagedProject =\n | {\n kind: \"read\";\n /** The whole staged project, parsed. */\n doc: LoadedDoc;\n /** A path LikeC4 reported, spelled as the author wrote it. */\n real: (path: string) => string;\n /** The staged rels that are THIS feature's documents — everything else is living. */\n mine: ReadonlySet<string>;\n }\n | { kind: \"unreadable\"; errors: string[] };\n\n/**\n * Stage the living `architecture/` with the merge preview in place of the\n * landscape, add the feature's own documents, and parse the result.\n *\n * A feature bringing no documents is not this function's business: both callers\n * return their own empty answer before reaching it, at the cost of one walk\n * over a directory that is not there. That walk is the axis's per-feature\n * opt-in and the whole price a fleet that has not adopted it pays.\n */\nexport async function stageMergedProject(req: StagedProjectRequest): Promise<StagedProject> {\n const merged = await mergedLandscape(req);\n if (merged.kind === \"failed\") return { kind: \"unreadable\", errors: [merged.reason] };\n\n const arch = architectureDir(req.docsDir);\n // BOTH generated documents come out, through the one list every project read\n // shares: a views-only file does not parse standalone, so staging one that\n // `sync` has not caught up with would blank the merge preview this whole\n // module exists to parse (`./architecture.ts` states the rule).\n const living = await architectureDocuments(arch, generatedArchitectureDocuments(req.docsDir));\n const root = await mkdtemp(join(tmpdir(), \"loam-staged-\"));\n try {\n // Staged rel -> the path the AUTHOR wrote, so every message that leaves\n // here names a file they can open. Built as the tree is staged, because\n // that is the one moment both spellings are in hand.\n const authored = new Map<string, string>();\n const landscape = resolve(landscapePath(req.docsDir));\n await inOrder(living, async (path) => {\n // The living landscape is staged from the merge preview when there is\n // one; copying it as well would declare every element twice.\n if (merged.kind === \"merged\" && resolve(path) === landscape) return null;\n await stage(root, spell(relative(arch, path)), path, authored);\n return null;\n });\n if (merged.kind === \"merged\") {\n const rel = spell(relative(arch, landscape));\n await write(root, rel, merged.content);\n authored.set(rel, landscape);\n }\n // The feature's own documents LAST, so one whose living twin exists wins\n // the staging rather than colliding with it. That case is already an error\n // the gate refuses before any merge — `usecase.flow-exists`,\n // `deployment.doc-exists` — and grading the feature's version is the more\n // useful of the two answers to give an author who is about to be told to\n // delete the file.\n const mine = new Set<string>();\n for (const document of req.documents) {\n await stage(root, document.rel, document.path, authored);\n mine.add(document.rel);\n }\n const doc = asLoadedDoc(await loadProject(root, await architectureDocuments(root)));\n // TWO SPELLINGS REACH HERE and both must map, which is the one thing this\n // helper cannot be allowed to get half right. A parse error's\n // `sourceFsPath` is an ABSOLUTE path into the temp tree; a view's\n // `sourcePath` is already the project-RELATIVE rel LikeC4 reports. The\n // rel-first lookup answers the second, and the relative() computation the\n // first. Handling only one leaves the other silently returning the temp\n // spelling, which is a finding naming a directory that no longer exists.\n const real = (path: string): string =>\n authored.get(spell(path)) ?? authored.get(spell(relative(root, path))) ?? path;\n if (doc.errors.length > 0) {\n // An error naming no document is LikeC4 declining to attribute one. It\n // still travels: a hole loam cannot place is still a hole, and dropping\n // it here would grade the feature clean on the strength of a missing\n // field.\n //\n // DOCS-RELATIVE, `/`-separated, and carrying the line — the spelling the\n // SERVICE arm of `usecase.flow-invalid` uses on purpose. These strings are\n // the feature arm's whole payload (text, `--json` and the `loam archive`\n // block message), and `real()` alone answered with an absolute Windows path\n // and no line at all: `C:\\Users\\…\\docs\\features\\FEAT-1\\usecases\\x.likec4:\n // <message>` in a finding a person is asked to act on (verification\n // 2026-09-04, W7). A path outside the docs root — which nothing here\n // produces, but `relative` would spell `..\\..` — keeps its absolute form.\n return {\n kind: \"unreadable\",\n errors: doc.errors.map((e) =>\n e.sourceFsPath === undefined ? errorText(e) : `${spellReal(req.docsDir, real(e.sourceFsPath))}: ${errorText(e)}`,\n ),\n };\n }\n return { kind: \"read\", doc, real, mine };\n } catch (err) {\n // The `unreadable` arm is a PROMISE, not a description of the happy path.\n // Two things under here throw rather than return: `loadProject` refuses an\n // error it cannot attribute to a document, and every `mkdir`/`copyFile` in\n // the staging above can fail on a full disk or a denied temp directory.\n // Callers run inside `loam validate --feature` and the archive gate — and\n // the one behaviour a check added to those may never have is failing the\n // run it was added to. So a throw becomes the same could-not-look answer a\n // parse error already produces, which each caller turns into its own\n // refusal that writes nothing.\n return { kind: \"unreadable\", errors: [err instanceof Error ? err.message : String(err)] };\n } finally {\n await rm(root, { recursive: true, force: true });\n }\n}\n\n/** What the landscape looks like after this feature's C4 delta is spliced in. */\ntype MergedLandscape =\n /** The delta adds nothing, or there is none — the living landscape stands. */\n | { kind: \"living\" }\n /** The merge preview, as authored bytes. */\n | { kind: \"merged\"; content: string }\n /** The merge could not be computed; `planLandscape` owns the refusal and says why. */\n | { kind: \"failed\"; reason: string };\n\n/**\n * The merge preview, computed with the SAME function `loam archive` commits\n * with.\n *\n * Reusing `planLandscapeMerge` rather than approximating it is what makes this\n * an answer about the archive rather than a second opinion beside it: an\n * element placed differently, a tag stripped differently, or a splice refused\n * there and allowed here would be a document graded against a map the merge\n * never writes.\n *\n * THE PARITY IS THE MERGE, NOT THE WHOLE GATE, and the two places the sides\n * differ are worth naming rather than leaving for a reader to discover.\n *\n * The first changes no verdict. `planLandscape` runs one check BEFORE it\n * computes the merge and this does not: a delta declaring the feature tag on a\n * specification KIND is refused there, because every element of that kind\n * inherits the tag and the merge would splice the whole document. Such a delta\n * reaches `newEls` here unfiltered, so the preview this grades against is one\n * the archive would never write — but that archive refuses either way, before\n * anything is written.\n *\n * The second changed every verdict about a fleet with extending models, and is\n * why `models` is on the request at all. `planLandscape` names the fleet's\n * extending models, so a nested addition is ROUTED into the model that owns\n * that service's interior and never reaches the map; a request that omits them\n * previews a map holding interior the archive will put somewhere else. A\n * feature-local `usecases/<x>.likec4` whose hop names `<service-fqn>.<container>`\n * then resolved in the preview, passed `usecase.flow-invalid` — the gate whose\n * own message is \"the archive would land an architecture/ nothing has checked\",\n * and which `--approve` cannot override — and was copied verbatim into\n * `architecture/`, where it resolves against nothing and takes the whole fleet\n * map down (verification 2026-09-04, review C; SCHEMA names that outcome as the\n * thing that must not happen). A caller that leaves `models` out is asking for\n * the pre-routing preview, and gets exactly the answers it got before.\n *\n * A throw is `failed` rather than propagated. Every `LandscapeSpliceError` this\n * can raise is one `planLandscape` raises again — with its own message, its own\n * refusal code, and before anything is written — so throwing here would only\n * decide which of two identical refusals an author reads first.\n */\nasync function mergedLandscape(req: StagedProjectRequest): Promise<MergedLandscape> {\n const deltaPath = featurePaths(req.featureDir).delta;\n const landscape = landscapePath(req.docsDir);\n if (!existsSync(deltaPath) || !existsSync(landscape)) return { kind: \"living\" };\n const delta = await (req.load ?? loadFile)(deltaPath);\n if (delta.errors.length > 0) {\n return {\n kind: \"failed\",\n reason: `delta.likec4 has ${delta.errors.length} parse error(s) — a feature's own documents cannot be graded against a map that does not parse`,\n };\n }\n try {\n const plan = await planLandscapeMerge({\n landscapeText: await text(landscape),\n deltaText: await text(deltaPath),\n deltaElements: delta.elements,\n newEls: delta.elements.filter((e) => e.tags.includes(req.featureId)),\n newRels: delta.relationships.filter((r) => r.tags.includes(req.featureId)),\n featureId: req.featureId,\n ...(req.models === undefined ? {} : { models: req.models }),\n });\n return plan.content === null ? { kind: \"living\" } : { kind: \"merged\", content: plan.content };\n } catch (err) {\n return { kind: \"failed\", reason: err instanceof Error ? err.message : String(err) };\n }\n}\n\n/** Read a document loam is about to hand LikeC4, refusing the encodings that decode as empty. */\nasync function text(path: string): Promise<string> {\n return decodeDocument(await readFile(path), path);\n}\n\n/** A path spelled the way a staged rel is spelled — `/`-separated, on every platform. */\nfunction spell(path: string): string {\n return path.split(/[\\\\/]/).join(\"/\");\n}\n\n/**\n * An authored path as a FINDING spells it: relative to the docs root,\n * `/`-separated. A path that does not sit under the docs root keeps its absolute\n * form — `relative` would answer with `..` segments, and a finding naming a\n * document outside the repository is more honest as the absolute path it is.\n */\nfunction spellReal(docsDir: DocsDir, path: string): string {\n const rel = relative(docsDir, path);\n return rel.length > 0 && !rel.startsWith(\"..\") ? spell(rel) : spell(path);\n}\n\n/** Copy one authored document into the staged tree, remembering where it came from. */\nasync function stage(root: string, rel: string, path: string, authored: Map<string, string>): Promise<void> {\n const dest = join(root, ...rel.split(\"/\"));\n await mkdir(dirname(dest), { recursive: true });\n await copyFile(path, dest);\n authored.set(rel, resolve(path));\n}\n\n/** Write one document into the staged tree from bytes loam holds rather than a file. */\nasync function write(root: string, rel: string, content: string): Promise<void> {\n const dest = join(root, ...rel.split(\"/\"));\n await mkdir(dirname(dest), { recursive: true });\n await writeFile(dest, content, \"utf8\");\n}\n"]}
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attested.js","sourceRoot":"","sources":["../../../../src/core/c4/resolve/attested.ts"],"names":[],"mappings":"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","sourcesContent":["/**\r\n * The calls a service's OWN `model.likec4` attests across its boundary.\r\n *\r\n * Two readers derive this list and must derive the same one: the adopt brief\r\n * (core/brief/landscape.ts reads it; core/brief/map/owed.ts names these calls\r\n * as the edges the fleet map still owes a drawn-but-edgeless service), and the fleet check\r\n * `landscape.service-isolated` (commands/validate/fleet/map/isolation.ts),\r\n * whose whole evidence gate is \"this list is non-empty\". A second spelling of\r\n * \"which relationships cross the boundary\" is how the brief would name an edge\r\n * the check then never asks for, or the reverse.\r\n *\r\n * It sits in this package because it is a question about the RESOLVED model —\r\n * which endpoints belong to the service — and `serviceResolver` beside it is\r\n * the one answer every join in loam gives to that. A relationship whose two\r\n * endpoints both resolve to the service (`api -> db`) is the service's own\r\n * internals; one where neither does (two context elements) is somebody else's\r\n * call. Only a relationship with EXACTLY one endpoint inside the boundary is\r\n * attested here, collapsed to the service side.\r\n *\r\n * What is deliberately NOT done: matching the counterpart to a landscape\r\n * element. One word names two different things in two documents often enough\r\n * — the reporting fleet's `orchestrator` was a container in the service model\r\n * and a campaign orchestrator on the map — that the join is the agent's to\r\n * make by hand. The counterpart is handed over exactly as the model spells\r\n * it, plus its title where the model declares the element.\r\n */\r\nimport { type Elem, type Rel } from \"../likec4.js\";\r\nimport { serviceResolver } from \"./service.js\";\r\n\r\nexport interface AttestedCall {\r\n /** `out` when the service is the source; `in` when it is the target. */\r\n direction: \"out\" | \"in\";\r\n /** The foreign endpoint's id as model.likec4 spells it — never matched to a landscape element. */\r\n counterpartId: string;\r\n /** The foreign element's title when the model declares it; the id otherwise. */\r\n counterpart: string;\r\n title?: string;\r\n op?: string;\r\n publishes?: string;\r\n consumes?: string;\r\n}\r\n\r\n/** The slice of a loaded document this derivation reads. */\r\nexport interface AttestingModel {\r\n elements: Elem[];\r\n relationships: Rel[];\r\n}\r\n\r\n/**\r\n * Every call the model declares with exactly one endpoint resolving to\r\n * `service`, de-duplicated and sorted.\r\n *\r\n * `known` is the enumerated fleet, for the reason every resolver call carries\r\n * it: a container of this service resolves to the service, and a stand-in\r\n * element titled like another REAL service resolves to that service rather\r\n * than to itself. The dedupe key is (direction, counterpart, op, publishes,\r\n * consumes) so two containers calling one system's one operation are one\r\n * attested call — the map draws edges between services, and that is one edge.\r\n */\r\nexport function attestedCalls(model: AttestingModel, service: string, known: ReadonlySet<string>): AttestedCall[] {\r\n const resolve = serviceResolver(model.elements, known);\r\n const byId = new Map(model.elements.map((e) => [e.id, e]));\r\n const out = new Map<string, AttestedCall>();\r\n for (const r of model.relationships) {\r\n const from = resolve(r.source) === service;\r\n const to = resolve(r.target) === service;\r\n if (from === to) continue;\r\n const foreign = from ? r.target : r.source;\r\n const call: AttestedCall = {\r\n direction: from ? \"out\" : \"in\",\r\n counterpartId: foreign,\r\n counterpart: byId.get(foreign)?.title ?? foreign,\r\n ...(r.title === undefined ? {} : { title: r.title }),\r\n ...(r.op === undefined ? {} : { op: r.op }),\r\n ...(r.publishes === undefined ? {} : { publishes: r.publishes }),\r\n ...(r.consumes === undefined ? {} : { consumes: r.consumes }),\r\n };\r\n const key = [call.direction, foreign, r.op ?? \"\", r.publishes ?? \"\", r.consumes ?? \"\"].join(\"\u0000\");\r\n if (!out.has(key)) out.set(key, call);\r\n }\r\n // Keyed sort, so the order is a function of the calls and never of the\r\n // order the author wrote them in — the brief's text and the finding's\r\n // message are compared byte for byte by tests and by readers.\r\n return [...out.entries()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)).map(([, call]) => call);\r\n}\r\n\r\n/**\r\n * One call as prose: `-> stripe (op 'authorize')`, `<- kafka (consumes\r\n * 'order.placed')`, `-> ledger` when the edge carries no join. Shared by the\r\n * brief's instruction and the fleet finding's message so the two surfaces\r\n * spell the same call the same way — a reader who meets it in `adopt` and\r\n * again in `validate --all` must recognise it as one thing.\r\n */\r\nexport function spellCall(call: AttestedCall): string {\r\n const arrow = call.direction === \"out\" ? \"->\" : \"<-\";\r\n const join =\r\n call.op !== undefined\r\n ? ` (op '${call.op}')`\r\n : call.publishes !== undefined\r\n ? ` (publishes '${call.publishes}')`\r\n : call.consumes !== undefined\r\n ? ` (consumes '${call.consumes}')`\r\n : \"\";\r\n return `${arrow} ${call.counterpart}${join}`;\r\n}\r\n\r\n/**\r\n * The edge form(s) the map owes for these calls, on the element that already\r\n * resolves to the service: `<id> -> <callee> …` when the service is the\r\n * caller, `<caller> -> <id> …` when it is the callee, both when the calls run\r\n * both ways. Direction is the part a template gets wrong silently: the repair\r\n * sentence used to spell the outbound form alone, so an agent following it\r\n * for an inbound call drew the dependency backwards — and because `op` must\r\n * be an operationId the TARGET's openapi.yaml defines, the reversed edge then\r\n * earned `spine.op-undefined` against the wrong service. Shared by the\r\n * brief's instruction and the fleet finding's message for the reason\r\n * `spellCall` is. With no call at all the outbound form stands in, so a\r\n * caller never receives an empty list.\r\n */\r\nexport function edgeTemplates(id: string, calls: readonly AttestedCall[]): string[] {\r\n const outbound = calls.some((c) => c.direction === \"out\");\r\n const inbound = calls.some((c) => c.direction === \"in\");\r\n const forms: string[] = [];\r\n if (outbound || !inbound) forms.push(`${id} -> <callee> 'Calls <op>' { metadata { op '<op>' } }`);\r\n if (inbound) forms.push(`<caller> -> ${id} 'Calls <op>' { metadata { op '<op>' } }`);\r\n return forms;\r\n}\r\n"]}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* The resolution order below is the whole of it, and its last rung is the one
|
|
15
15
|
* that lies: read that comment before adding a caller.
|
|
16
16
|
*/
|
|
17
|
+
import { ancestorIds } from "../../kernel/ids/fqn/ancestors.js";
|
|
17
18
|
import { declaredService } from "../../kernel/ids/service.js";
|
|
18
19
|
import {} from "../likec4.js";
|
|
19
20
|
/**
|
|
@@ -28,24 +29,14 @@ import {} from "../likec4.js";
|
|
|
28
29
|
export function elementService(e) {
|
|
29
30
|
return e.service ?? declaredService(e.title);
|
|
30
31
|
}
|
|
31
|
-
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* what made those edges invisible: the spine check silently skipped them, and
|
|
39
|
-
* the no-openapi grace treated a service with a dozen inbound container calls as
|
|
40
|
-
* one nobody calls at all.
|
|
32
|
+
/*
|
|
33
|
+
* `ancestorIds` — every id an endpoint may be filed under, nearest first:
|
|
34
|
+
* `a.b.c`, `a.b`, `a` — now lives in `core/kernel/ids/fqn/ancestors.ts`, with
|
|
35
|
+
* the reason it exists at all (a landscape may draw a service's containers, and
|
|
36
|
+
* an edge into one is still an edge into the service). It moved because five
|
|
37
|
+
* modules were each walking that chain privately; the kernel is where a leaf
|
|
38
|
+
* helper goes when the modules that want it are heavier than it is.
|
|
41
39
|
*/
|
|
42
|
-
function ancestorIds(id) {
|
|
43
|
-
const out = [id];
|
|
44
|
-
for (let dot = id.lastIndexOf("."); dot !== -1; dot = id.lastIndexOf(".", dot - 1)) {
|
|
45
|
-
out.push(id.slice(0, dot));
|
|
46
|
-
}
|
|
47
|
-
return out;
|
|
48
|
-
}
|
|
49
40
|
/**
|
|
50
41
|
* A memoized `id -> service` resolver over one document.
|
|
51
42
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.js","sourceRoot":"","sources":["../../../../src/core/c4/resolve/service.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Which `services/<id>/` directory a modelled element belongs to.\n *\n * A distinct subject from loading a document, and a distinct phase: the\n * loader beside this package (`../likec4.ts`) turns LikeC4's parse output into\n * loam's neutral `Elem`/`Rel` records, and everything here runs AFTER that, over\n * those records, answering one question the model itself never states outright.\n * It is the join every fleet-wide check rests on — the landscape↔services\n * reconciliation, the spine's edge grouping, `Covers:` resolution, health.yaml's\n * dependency matching — which is why `docs/DESIGN.md`'s Open-decisions table\n * audits its call sites as a subject of their own rather than as a detail of the\n * loader.\n *\n * The resolution order below is the whole of it, and its last rung is the one\n * that lies: read that comment before adding a caller.\n */\nimport { declaredService, type DeclaredService } from \"../../kernel/ids/service.js\";\nimport { type Elem } from \"../likec4.js\";\n\n/**\n * The service directory an element stands for: an explicit\n * `metadata { service '<id>' }` binding wins, the title is the fallback.\n *\n * The fallback is what every docs repo written before the binding existed relies\n * on, and it is also the trap the binding exists to close: matching on the title\n * means renaming a box in a diagram silently unlinks it from its service, and\n * every check that joined the two just stops finding anything.\n */\nexport function elementService(e: Elem): DeclaredService {\n return e.service ?? declaredService(e.title);\n}\n\n/**\n * Every id an endpoint may be filed under, nearest first: `a.b.c`, `a.b`, `a`.\n *\n * A landscape does not have to draw a service as one opaque box. The moment\n * somebody models its containers — `paymentService.api`, `paymentService.worker`\n * — an edge drawn INTO a container is still an edge into the service, and every\n * join that groups by service has to know that. Resolving only the exact id is\n * what made those edges invisible: the spine check silently skipped them, and\n * the no-openapi grace treated a service with a dozen inbound container calls as\n * one nobody calls at all.\n */\nfunction ancestorIds(id: string): string[] {\n const out = [id];\n for (let dot = id.lastIndexOf(\".\"); dot !== -1; dot = id.lastIndexOf(\".\", dot - 1)) {\n out.push(id.slice(0, dot));\n }\n return out;\n}\n\n/**\n * A memoized `id -> service` resolver over one document.\n *\n * Resolution order, and why it is this order:\n *\n * 1. the nearest ancestor (the id itself first) carrying an explicit\n * `metadata { service '<id>' }` — a binding is a claim somebody wrote down,\n * so it outranks every guess;\n * 2. the nearest ancestor whose title names a REAL `services/<id>/` directory —\n * positive evidence from the filesystem, available only to callers that\n * hand in `known`;\n * 3. the element's own title, and finally the raw id — today's fallback, kept\n * LAST because it is the one that lies: it happily resolves\n * `paymentService.api` to \"api\", a service that has never existed.\n *\n * Built once per document and shared, because it is called inside loops over\n * every relationship and the walk up the ancestor chain is not free.\n */\nexport function serviceResolver(\n elements: Elem[],\n known?: ReadonlySet<string>,\n): (id: string) => DeclaredService {\n const byId = new Map(elements.map((e) => [e.id, e]));\n const memo = new Map<string, DeclaredService>();\n return (id: string): DeclaredService => {\n const hit = memo.get(id);\n if (hit !== undefined) return hit;\n let answer: DeclaredService | undefined;\n for (const candidate of ancestorIds(id)) {\n const e = byId.get(candidate);\n if (e?.service !== undefined) {\n answer = e.service;\n break;\n }\n }\n if (answer === undefined && known !== undefined) {\n for (const candidate of ancestorIds(id)) {\n const e = byId.get(candidate);\n if (e !== undefined && known.has(e.title)) {\n answer = declaredService(e.title);\n break;\n }\n }\n }\n if (answer === undefined) {\n const self = byId.get(id);\n answer = self ? elementService(self) : declaredService(id);\n }\n memo.set(id, answer);\n return answer;\n };\n}\n\n/**\n * The service a relationship endpoint belongs to. An id that names no element\n * resolves to itself, so a partial document degrades to the id rather than\n * throwing.\n *\n * `known` is the set of service directories that actually exist; pass it\n * wherever the caller has enumerated `services/`, so an edge into a modelled\n * container resolves to the service that owns it instead of to the container's\n * own title. Callers with a single document and no repository in hand omit it\n * and keep the pre-existing behaviour.\n */\nexport function serviceOf(elements: Elem[], id: string, known?: ReadonlySet<string>): DeclaredService {\n return serviceResolver(elements, known)(id);\n}\n\n/**\n * One `id -> service` resolver per document, not one per lookup.\n *\n * `serviceOf` builds a fresh id map on every call, and this axis calls it twice\n * for every relationship in scope — inside loops that already run over every\n * relationship a service can see. `serviceResolver`'s own doc says it is \"built\n * once per document and shared\"; this is where both axes that need it keep\n * that promise — `../arch.ts`'s `coversEdge` and `./steps.ts`'s `resolvedTier`,\n * each exported and each handed only the element array.\n *\n * A module-level cache is normally the wrong shape here, because a command runs\n * against whatever directory it was invoked in. This one is safe: the key is the\n * per-invocation `Elem[]` a parse produced, its value is a pure function of that\n * key alone, and nothing in it is derived from the working directory — so two\n * runs can never see each other's answer, and the entry dies with the document.\n * Nothing mutates an `Elem[]` after parse.\n *\n * Two tiers because the fleet set is part of the answer: the same document\n * resolves differently with and without `known`, so a cache keyed on the\n * elements alone handed a with-fleet caller the without-fleet resolver.\n * Callers build ONE set per run and pass the same instance through, which is\n * what lets the inner WeakMap key on the set's identity.\n */\nconst RESOLVERS = new WeakMap<Elem[], (id: string) => string>();\nconst FLEET_RESOLVERS = new WeakMap<Elem[], WeakMap<ReadonlySet<string>, (id: string) => string>>();\n\nexport function resolverFor(elements: Elem[], known?: ReadonlySet<string>): (id: string) => string {\n if (known === undefined) {\n const cached = RESOLVERS.get(elements);\n if (cached !== undefined) return cached;\n const resolver = serviceResolver(elements);\n RESOLVERS.set(elements, resolver);\n return resolver;\n }\n let perSet = FLEET_RESOLVERS.get(elements);\n if (perSet === undefined) {\n perSet = new WeakMap();\n FLEET_RESOLVERS.set(elements, perSet);\n }\n const cached = perSet.get(known);\n if (cached !== undefined) return cached;\n const resolver = serviceResolver(elements, known);\n perSet.set(known, resolver);\n return resolver;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"service.js","sourceRoot":"","sources":["../../../../src/core/c4/resolve/service.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Which `services/<id>/` directory a modelled element belongs to.\n *\n * A distinct subject from loading a document, and a distinct phase: the\n * loader beside this package (`../likec4.ts`) turns LikeC4's parse output into\n * loam's neutral `Elem`/`Rel` records, and everything here runs AFTER that, over\n * those records, answering one question the model itself never states outright.\n * It is the join every fleet-wide check rests on — the landscape↔services\n * reconciliation, the spine's edge grouping, `Covers:` resolution, health.yaml's\n * dependency matching — which is why `docs/DESIGN.md`'s Open-decisions table\n * audits its call sites as a subject of their own rather than as a detail of the\n * loader.\n *\n * The resolution order below is the whole of it, and its last rung is the one\n * that lies: read that comment before adding a caller.\n */\nimport { ancestorIds } from \"../../kernel/ids/fqn/ancestors.js\";\nimport { declaredService, type DeclaredService } from \"../../kernel/ids/service.js\";\nimport { type Elem } from \"../likec4.js\";\n\n/**\n * The service directory an element stands for: an explicit\n * `metadata { service '<id>' }` binding wins, the title is the fallback.\n *\n * The fallback is what every docs repo written before the binding existed relies\n * on, and it is also the trap the binding exists to close: matching on the title\n * means renaming a box in a diagram silently unlinks it from its service, and\n * every check that joined the two just stops finding anything.\n */\nexport function elementService(e: Elem): DeclaredService {\n return e.service ?? declaredService(e.title);\n}\n\n/*\n * `ancestorIds` — every id an endpoint may be filed under, nearest first:\n * `a.b.c`, `a.b`, `a` — now lives in `core/kernel/ids/fqn/ancestors.ts`, with\n * the reason it exists at all (a landscape may draw a service's containers, and\n * an edge into one is still an edge into the service). It moved because five\n * modules were each walking that chain privately; the kernel is where a leaf\n * helper goes when the modules that want it are heavier than it is.\n */\n\n/**\n * A memoized `id -> service` resolver over one document.\n *\n * Resolution order, and why it is this order:\n *\n * 1. the nearest ancestor (the id itself first) carrying an explicit\n * `metadata { service '<id>' }` — a binding is a claim somebody wrote down,\n * so it outranks every guess;\n * 2. the nearest ancestor whose title names a REAL `services/<id>/` directory —\n * positive evidence from the filesystem, available only to callers that\n * hand in `known`;\n * 3. the element's own title, and finally the raw id — today's fallback, kept\n * LAST because it is the one that lies: it happily resolves\n * `paymentService.api` to \"api\", a service that has never existed.\n *\n * Built once per document and shared, because it is called inside loops over\n * every relationship and the walk up the ancestor chain is not free.\n */\nexport function serviceResolver(\n elements: Elem[],\n known?: ReadonlySet<string>,\n): (id: string) => DeclaredService {\n const byId = new Map(elements.map((e) => [e.id, e]));\n const memo = new Map<string, DeclaredService>();\n return (id: string): DeclaredService => {\n const hit = memo.get(id);\n if (hit !== undefined) return hit;\n let answer: DeclaredService | undefined;\n for (const candidate of ancestorIds(id)) {\n const e = byId.get(candidate);\n if (e?.service !== undefined) {\n answer = e.service;\n break;\n }\n }\n if (answer === undefined && known !== undefined) {\n for (const candidate of ancestorIds(id)) {\n const e = byId.get(candidate);\n if (e !== undefined && known.has(e.title)) {\n answer = declaredService(e.title);\n break;\n }\n }\n }\n if (answer === undefined) {\n const self = byId.get(id);\n answer = self ? elementService(self) : declaredService(id);\n }\n memo.set(id, answer);\n return answer;\n };\n}\n\n/**\n * The service a relationship endpoint belongs to. An id that names no element\n * resolves to itself, so a partial document degrades to the id rather than\n * throwing.\n *\n * `known` is the set of service directories that actually exist; pass it\n * wherever the caller has enumerated `services/`, so an edge into a modelled\n * container resolves to the service that owns it instead of to the container's\n * own title. Callers with a single document and no repository in hand omit it\n * and keep the pre-existing behaviour.\n */\nexport function serviceOf(elements: Elem[], id: string, known?: ReadonlySet<string>): DeclaredService {\n return serviceResolver(elements, known)(id);\n}\n\n/**\n * One `id -> service` resolver per document, not one per lookup.\n *\n * `serviceOf` builds a fresh id map on every call, and this axis calls it twice\n * for every relationship in scope — inside loops that already run over every\n * relationship a service can see. `serviceResolver`'s own doc says it is \"built\n * once per document and shared\"; this is where both axes that need it keep\n * that promise — `../arch.ts`'s `coversEdge` and `./steps.ts`'s `resolvedTier`,\n * each exported and each handed only the element array.\n *\n * A module-level cache is normally the wrong shape here, because a command runs\n * against whatever directory it was invoked in. This one is safe: the key is the\n * per-invocation `Elem[]` a parse produced, its value is a pure function of that\n * key alone, and nothing in it is derived from the working directory — so two\n * runs can never see each other's answer, and the entry dies with the document.\n * Nothing mutates an `Elem[]` after parse.\n *\n * Two tiers because the fleet set is part of the answer: the same document\n * resolves differently with and without `known`, so a cache keyed on the\n * elements alone handed a with-fleet caller the without-fleet resolver.\n * Callers build ONE set per run and pass the same instance through, which is\n * what lets the inner WeakMap key on the set's identity.\n */\nconst RESOLVERS = new WeakMap<Elem[], (id: string) => string>();\nconst FLEET_RESOLVERS = new WeakMap<Elem[], WeakMap<ReadonlySet<string>, (id: string) => string>>();\n\nexport function resolverFor(elements: Elem[], known?: ReadonlySet<string>): (id: string) => string {\n if (known === undefined) {\n const cached = RESOLVERS.get(elements);\n if (cached !== undefined) return cached;\n const resolver = serviceResolver(elements);\n RESOLVERS.set(elements, resolver);\n return resolver;\n }\n let perSet = FLEET_RESOLVERS.get(elements);\n if (perSet === undefined) {\n perSet = new WeakMap();\n FLEET_RESOLVERS.set(elements, perSet);\n }\n const cached = perSet.get(known);\n if (cached !== undefined) return cached;\n const resolver = serviceResolver(elements, known);\n perSet.set(known, resolver);\n return resolver;\n}\n"]}
|