@ybotok/loam 0.2.0-alpha.1 → 0.2.0-alpha.4
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 +165 -0
- package/README.md +6 -6
- package/ROADMAP.md +155 -18
- package/SCHEMA.md +33 -1
- package/dist/commands/archive/plan/gate.js +18 -12
- package/dist/commands/archive/plan/gate.js.map +1 -1
- package/dist/commands/archive/plan/landscape.js +19 -0
- package/dist/commands/archive/plan/landscape.js.map +1 -1
- package/dist/commands/archive/run.js +48 -3
- package/dist/commands/archive/run.js.map +1 -1
- package/dist/commands/seed/plan.js +1 -1
- package/dist/commands/seed/plan.js.map +1 -1
- package/dist/commands/subsystem/sync.js +1 -1
- package/dist/commands/subsystem/sync.js.map +1 -1
- package/dist/commands/subsystem/txn/txn.js +1 -1
- package/dist/commands/subsystem/txn/txn.js.map +1 -1
- package/dist/commands/subsystem/txn/views.js +23 -11
- package/dist/commands/subsystem/txn/views.js.map +1 -1
- package/dist/commands/validate/arch-coverage.js +11 -1
- package/dist/commands/validate/arch-coverage.js.map +1 -1
- package/dist/commands/validate/fleet/obligations.js +43 -9
- package/dist/commands/validate/fleet/obligations.js.map +1 -1
- package/dist/commands/validate/fleet/usecases/steps.js +3 -3
- package/dist/commands/validate/fleet/usecases/steps.js.map +1 -1
- package/dist/commands/validate/fleet/views/ids.js +6 -1
- package/dist/commands/validate/fleet/views/ids.js.map +1 -1
- package/dist/commands/validate/fleet/views/stale.js +15 -39
- package/dist/commands/validate/fleet/views/stale.js.map +1 -1
- package/dist/commands/validate/service/service.js +1 -0
- package/dist/commands/validate/service/service.js.map +1 -1
- package/dist/commands/validate/service/specs.js +33 -1
- package/dist/commands/validate/service/specs.js.map +1 -1
- package/dist/core/agent/agents-md/map/subsystem.js +8 -1
- package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
- package/dist/core/agent/agents-md.js +4 -0
- package/dist/core/agent/agents-md.js.map +1 -1
- package/dist/core/agent/workflows/check.js +3 -0
- package/dist/core/agent/workflows/check.js.map +1 -1
- package/dist/core/brief/unchecked.js +1 -0
- package/dist/core/brief/unchecked.js.map +1 -1
- package/dist/core/c4/arch.js +72 -180
- package/dist/core/c4/arch.js.map +1 -1
- package/dist/core/c4/likec4.js +2 -0
- package/dist/core/c4/likec4.js.map +1 -1
- package/dist/core/c4/parsed/deployment.js +158 -0
- package/dist/core/c4/parsed/deployment.js.map +1 -0
- package/dist/core/c4/project/load.js +4 -0
- package/dist/core/c4/project/load.js.map +1 -1
- package/dist/core/c4/project/staged.js +220 -0
- package/dist/core/c4/project/staged.js.map +1 -0
- package/dist/core/c4/resolve/service.js +46 -0
- package/dist/core/c4/resolve/service.js.map +1 -1
- package/dist/core/c4/resolve/steps.js +144 -0
- package/dist/core/c4/resolve/steps.js.map +1 -0
- package/dist/core/c4/splice/delta-blocks.js +15 -2
- package/dist/core/c4/splice/delta-blocks.js.map +1 -1
- package/dist/core/c4/workspace.js +2 -0
- package/dist/core/c4/workspace.js.map +1 -1
- package/dist/core/coherence/coherence.js +36 -0
- package/dist/core/coherence/coherence.js.map +1 -1
- package/dist/core/deployment/delta.js +188 -0
- package/dist/core/deployment/delta.js.map +1 -0
- package/dist/core/deployment/objects.js +145 -0
- package/dist/core/deployment/objects.js.map +1 -0
- package/dist/core/pack/living.js +19 -0
- package/dist/core/pack/living.js.map +1 -1
- package/dist/core/repo/authored/paths.js +18 -0
- package/dist/core/repo/authored/paths.js.map +1 -1
- package/dist/core/repo/paths.js +44 -3
- package/dist/core/repo/paths.js.map +1 -1
- package/dist/core/repo/tree/render/body.js +219 -0
- package/dist/core/repo/tree/render/body.js.map +1 -0
- package/dist/core/repo/tree/render/stale.js +26 -0
- package/dist/core/repo/tree/render/stale.js.map +1 -0
- package/dist/core/repo/tree/render/text.js +84 -0
- package/dist/core/repo/tree/render/text.js.map +1 -0
- package/dist/core/repo/tree/{views.js → render/views.js} +38 -34
- package/dist/core/repo/tree/render/views.js.map +1 -0
- package/dist/core/status/feature/artifacts.js +29 -4
- package/dist/core/status/feature/artifacts.js.map +1 -1
- package/dist/core/status/fleet/fleet.js +11 -1
- package/dist/core/status/fleet/fleet.js.map +1 -1
- package/dist/core/usecases/delta/overlay.js +28 -191
- package/dist/core/usecases/delta/overlay.js.map +1 -1
- package/dist/core/usecases/fleet.js.map +1 -1
- package/dist/core/usecases/operations.js +2 -2
- package/dist/core/usecases/operations.js.map +1 -1
- package/dist/core/vocabulary/issue.js +3 -0
- package/dist/core/vocabulary/issue.js.map +1 -1
- package/examples/docs/architecture/subsystems.likec4 +4 -0
- package/package.json +1 -1
- package/dist/core/repo/tree/views.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.js","sourceRoot":"","sources":["../../../../src/core/c4/resolve/service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,eAAe,EAAwB,MAAM,6BAA6B,CAAC;AACpF,OAAO,EAAa,MAAM,cAAc,CAAC;AAEzC;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAAC,CAAO;IACpC,OAAO,CAAC,CAAC,OAAO,IAAI,eAAe,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;AAC/C,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAS,WAAW,CAAC,EAAU;IAC7B,MAAM,GAAG,GAAG,CAAC,EAAE,CAAC,CAAC;IACjB,KAAK,IAAI,GAAG,GAAG,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,GAAG,KAAK,CAAC,CAAC,EAAE,GAAG,GAAG,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE,GAAG,GAAG,CAAC,CAAC,EAAE,CAAC;QACnF,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;IAC7B,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,eAAe,CAC7B,QAAgB,EAChB,KAA2B;IAE3B,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IACrD,MAAM,IAAI,GAAG,IAAI,GAAG,EAA2B,CAAC;IAChD,OAAO,CAAC,EAAU,EAAmB,EAAE;QACrC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACzB,IAAI,GAAG,KAAK,SAAS;YAAE,OAAO,GAAG,CAAC;QAClC,IAAI,MAAmC,CAAC;QACxC,KAAK,MAAM,SAAS,IAAI,WAAW,CAAC,EAAE,CAAC,EAAE,CAAC;YACxC,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YAC9B,IAAI,CAAC,EAAE,OAAO,KAAK,SAAS,EAAE,CAAC;gBAC7B,MAAM,GAAG,CAAC,CAAC,OAAO,CAAC;gBACnB,MAAM;YACR,CAAC;QACH,CAAC;QACD,IAAI,MAAM,KAAK,SAAS,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YAChD,KAAK,MAAM,SAAS,IAAI,WAAW,CAAC,EAAE,CAAC,EAAE,CAAC;gBACxC,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;gBAC9B,IAAI,CAAC,KAAK,SAAS,IAAI,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC;oBAC1C,MAAM,GAAG,eAAe,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;oBAClC,MAAM;gBACR,CAAC;YACH,CAAC;QACH,CAAC;QACD,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAC1B,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,EAAE,CAAC,CAAC;QAC7D,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;QACrB,OAAO,MAAM,CAAC;IAChB,CAAC,CAAC;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,SAAS,CAAC,QAAgB,EAAE,EAAU,EAAE,KAA2B;IACjF,OAAO,eAAe,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;AAC9C,CAAC","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"]}
|
|
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"]}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which declared relationship backs one hop of a use case, and what it says the
|
|
3
|
+
* operation is.
|
|
4
|
+
*
|
|
5
|
+
* A `dynamic view`'s hop says "web talks to orders". Answering it is the same
|
|
6
|
+
* shape of join `Covers:` performs for an edge entry one module over — same two
|
|
7
|
+
* tiers, same cached `serviceResolver` (`./service.ts`'s `resolverFor`, which
|
|
8
|
+
* both axes share), same "an endpoint names an element or the service it stands
|
|
9
|
+
* for" rule.
|
|
10
|
+
*
|
|
11
|
+
* It lived in `../arch.ts` while that file was the only home the join had, and
|
|
12
|
+
* moved here when the Covers grammar grew a fourth entry form and the file
|
|
13
|
+
* reached its line limit. The move changed nothing: the seam was already named
|
|
14
|
+
* in that file's own header, and this is the package the resolution it depends
|
|
15
|
+
* on lives in.
|
|
16
|
+
*
|
|
17
|
+
* It does NOT live beside the view reader in `../parsed/dynamic-views.ts`, and
|
|
18
|
+
* that is a rule rather than a preference: a second copy of this join is the
|
|
19
|
+
* copy that drifts, and that module may read `$data` and nothing else
|
|
20
|
+
* (docs/DESIGN.md rule 26) — it has no business holding the model join.
|
|
21
|
+
*/
|
|
22
|
+
import {} from "../likec4.js";
|
|
23
|
+
import { resolverFor } from "./service.js";
|
|
24
|
+
/**
|
|
25
|
+
* The pair a step's backing relationship is looked up under. This is the half
|
|
26
|
+
* that mis-grades the whole fleet if it is wrong, because a reply arrow is the
|
|
27
|
+
* commonest step in any sequence diagram.
|
|
28
|
+
*
|
|
29
|
+
* MEASURED at the `likec4@1.59.2` pin, and `test/likec4-view-shape.test.ts` holds
|
|
30
|
+
* the raw form: a reply written `a <- b 'reply'` parses as
|
|
31
|
+
* `{source: "b", target: "a", isBackward: true}` — LikeC4 has ALREADY reversed
|
|
32
|
+
* the endpoints and set the flag — while the forward `b -> a` yields that same
|
|
33
|
+
* pair with no `isBackward` key at all (ABSENT, not false). So the endpoints as
|
|
34
|
+
* parsed describe the direction the MESSAGE travels, and the flag records which
|
|
35
|
+
* arrow the author typed.
|
|
36
|
+
*
|
|
37
|
+
* loam attributes a reply to the CALL it answers, so the reversal is undone
|
|
38
|
+
* here. A landscape declares `web -> orders` because that edge is the
|
|
39
|
+
* dependency; a mirror `orders -> web` for each response is not something a
|
|
40
|
+
* fleet writes down. Reading a reply hop under the message's own direction would
|
|
41
|
+
* report every return hop against such a landscape as `unbacked`, and where a
|
|
42
|
+
* fleet does draw the mirror edge it would file the two halves of one call under
|
|
43
|
+
* two operations — the call under `op 'createOrder'`, its reply under none.
|
|
44
|
+
*
|
|
45
|
+
* That is deliberately NOT what LikeC4's own computed stage does, and the
|
|
46
|
+
* difference is measured rather than assumed: that stage resolves a backward
|
|
47
|
+
* step's `relations` from the UNFLIPPED pair, so at this pin a `web <- orders`
|
|
48
|
+
* step computes to `relations: []` when only `web -> orders` exists, and to the
|
|
49
|
+
* mirror edge when only `orders -> web` does. It is answering which arrow to
|
|
50
|
+
* draw; loam is answering which operation the hop exercises, and reads what a
|
|
51
|
+
* view DECLARES rather than what it shows — which is why loam never reaches that
|
|
52
|
+
* stage at all (docs/DESIGN.md rule 26, enforced by `scripts/arch-check.mjs`).
|
|
53
|
+
* The consequence to know before changing this: where
|
|
54
|
+
* a fleet declares BOTH directions, the mirror edge is not what backs the reply
|
|
55
|
+
* here — the call is, and the mirror edge backs only a hop drawn forward along
|
|
56
|
+
* it.
|
|
57
|
+
*/
|
|
58
|
+
function callPair(step) {
|
|
59
|
+
return step.isBackward ? { from: step.target, to: step.source } : { from: step.source, to: step.target };
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The distinct `op` values across candidates, first-seen order preserved.
|
|
63
|
+
*
|
|
64
|
+
* Absent is a VALUE here, not a gap to skip: a relationship carrying no `op` and
|
|
65
|
+
* one carrying `createOrder` disagree about the operation just as surely as two
|
|
66
|
+
* different ops do, and skipping the absent one would silently promote that
|
|
67
|
+
* disagreement to a confident answer. `includes` rather than a `Set` because a
|
|
68
|
+
* candidate list is the handful of relationships between one pair of endpoints,
|
|
69
|
+
* and it keeps `undefined` comparing the way the rest of this file expects.
|
|
70
|
+
*/
|
|
71
|
+
function distinctOps(rels) {
|
|
72
|
+
const out = [];
|
|
73
|
+
for (const r of rels)
|
|
74
|
+
if (!out.includes(r.op))
|
|
75
|
+
out.push(r.op);
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The verdict one tier's candidates imply, or `undefined` when that tier found
|
|
80
|
+
* nothing and is therefore not the tier that answers.
|
|
81
|
+
*
|
|
82
|
+
* The count that decides is the DISTINCT-OP count, never the candidate count.
|
|
83
|
+
* That distinction is what makes the service tier safe: once a service is drawn
|
|
84
|
+
* as containers, `a.api -> b` and `a.worker -> b` both carrying
|
|
85
|
+
* `op 'createOrder'` are two relationships and one operation, and a verdict read
|
|
86
|
+
* off `rels.length` would convict a perfectly consistent model of contesting
|
|
87
|
+
* itself.
|
|
88
|
+
*/
|
|
89
|
+
function verdictOn(pair, tier, rels) {
|
|
90
|
+
if (rels.length === 0)
|
|
91
|
+
return undefined;
|
|
92
|
+
const ops = distinctOps(rels);
|
|
93
|
+
if (ops.length > 1)
|
|
94
|
+
return { ...pair, verdict: "contested", tier, rels, ops };
|
|
95
|
+
// Length is exactly one here, so the single distinct value is the answer —
|
|
96
|
+
// and `undefined` is a legitimate answer: every candidate agreed that this
|
|
97
|
+
// hop names no operation.
|
|
98
|
+
const [only] = ops;
|
|
99
|
+
return { ...pair, verdict: "attributed", tier, rels, ...(only === undefined ? {} : { op: only }) };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Relationships whose ENDPOINTS resolve to the same two services as the pair —
|
|
103
|
+
* the fallback tier, and the reason a step drawn between two services matches an
|
|
104
|
+
* edge drawn between two containers.
|
|
105
|
+
*
|
|
106
|
+
* Through the module's cached `resolverFor`, not a fresh `serviceResolver`: this
|
|
107
|
+
* runs once per step of every use case in the fleet, and building the id map per
|
|
108
|
+
* step is the cost that comment above `RESOLVERS` exists to prevent.
|
|
109
|
+
*/
|
|
110
|
+
function resolvedTier(pair, scope) {
|
|
111
|
+
const resolve = resolverFor(scope.elements, scope.known);
|
|
112
|
+
const from = resolve(pair.from);
|
|
113
|
+
const to = resolve(pair.to);
|
|
114
|
+
return scope.relationships.filter((r) => resolve(r.source) === from && resolve(r.target) === to);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Which relationship in the model backs one hop of a use case, and what it says
|
|
118
|
+
* the operation is.
|
|
119
|
+
*
|
|
120
|
+
* Two tiers, and the second runs ONLY when the first is empty. That ordering is
|
|
121
|
+
* load-bearing rather than an optimisation: the service tier necessarily
|
|
122
|
+
* re-finds every exact match — an endpoint that matches literally resolves to
|
|
123
|
+
* the same service trivially — so running it unconditionally would let a
|
|
124
|
+
* container-level edge with a different `op` contest a step the model already
|
|
125
|
+
* answers exactly, turning an `attributed` verdict into a `contested` one on a
|
|
126
|
+
* document nobody changed.
|
|
127
|
+
*
|
|
128
|
+
* Relationship `kind` is deliberately not part of the match, and that is
|
|
129
|
+
* measured too: at this pin `a -[http]-> b` without a `relationship http` in the
|
|
130
|
+
* `specification` block is "Could not resolve reference to RelationshipKind
|
|
131
|
+
* named 'http'" — a parse error, which under loam's standing rule means no model
|
|
132
|
+
* at all — and `test/helpers/harness.ts`'s own `LANDSCAPE` declares no
|
|
133
|
+
* relationship kinds. So narrowing by kind would mean a fix message asking an
|
|
134
|
+
* author to write a line their document does not parse.
|
|
135
|
+
*/
|
|
136
|
+
export function attributeStep(step, scope) {
|
|
137
|
+
const pair = callPair(step);
|
|
138
|
+
const exact = scope.relationships.filter((r) => r.source === pair.from && r.target === pair.to);
|
|
139
|
+
const declared = verdictOn(pair, 1, exact);
|
|
140
|
+
if (declared !== undefined)
|
|
141
|
+
return declared;
|
|
142
|
+
return verdictOn(pair, 2, resolvedTier(pair, scope)) ?? { ...pair, verdict: "unbacked" };
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=steps.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"steps.js","sourceRoot":"","sources":["../../../../src/core/c4/resolve/steps.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Which declared relationship backs one hop of a use case, and what it says the\n * operation is.\n *\n * A `dynamic view`'s hop says \"web talks to orders\". Answering it is the same\n * shape of join `Covers:` performs for an edge entry one module over — same two\n * tiers, same cached `serviceResolver` (`./service.ts`'s `resolverFor`, which\n * both axes share), same \"an endpoint names an element or the service it stands\n * for\" rule.\n *\n * It lived in `../arch.ts` while that file was the only home the join had, and\n * moved here when the Covers grammar grew a fourth entry form and the file\n * reached its line limit. The move changed nothing: the seam was already named\n * in that file's own header, and this is the package the resolution it depends\n * on lives in.\n *\n * It does NOT live beside the view reader in `../parsed/dynamic-views.ts`, and\n * that is a rule rather than a preference: a second copy of this join is the\n * copy that drifts, and that module may read `$data` and nothing else\n * (docs/DESIGN.md rule 26) — it has no business holding the model join.\n */\nimport { type Elem, type Rel } from \"../likec4.js\";\nimport { resolverFor } from \"./service.js\";\nimport type { ParsedStep } from \"../parsed/dynamic-views.js\";\n\n/**\n * The three fields an attribution reads off a step, and no more.\n *\n * Narrower than `ParsedStep` on purpose. Attribution must not depend on the\n * ordinal, the title or the notes — a hop is graded by where it points, never by\n * what the author called it — and a signature that says so is also a signature a\n * test can satisfy with a literal instead of a parsed document. Derived from\n * `ParsedStep` rather than restated, so a rename upstream is a compile error\n * here instead of a second definition that quietly disagrees.\n */\nexport type StepEndpoints = Pick<ParsedStep, \"source\" | \"target\" | \"isBackward\">;\n\n/**\n * What a step is attributed against: the model in view, plus the enumerated\n * fleet where the caller has it.\n *\n * A deliberate subset of `CoverageScope` rather than a reuse of it — an\n * attribution never reads `health`, and demanding one would make every caller\n * fabricate an empty `HealthIds` to ask a question that has nothing to do with\n * alerts. A `CoverageScope` is still structurally assignable here, so a caller\n * already holding the fuller scope passes it through unchanged.\n */\nexport interface StepScope {\n elements: Elem[];\n relationships: Rel[];\n /** As `CoverageScope.known`, for the same reason: one set per run, shared. */\n known?: ReadonlySet<string>;\n}\n\n/** The oriented pair a verdict is about — see `callPair` for which way round. */\ninterface CallPair {\n from: string;\n to: string;\n}\n\n/**\n * What the model says about one hop of a use case.\n *\n * Three variants rather than one record with optional fields, because the fields\n * are not independent: `op` is meaningful only when exactly one operation was\n * implied, `ops` only when more than one was, and an unbacked step has neither\n * and no tier either. Tagged, those states stop being constructible.\n *\n * - `attributed` — the candidates on the matched tier agree, so the hop\n * exercises exactly one operation. `op` is absent when that one agreed\n * operation is *no* operation: the relationships back the hop but none of\n * them carries `metadata { op '…' }`.\n * - `contested` — the candidates disagree, so loam cannot say which operation\n * the hop exercises. `ops` lists the distinct values in candidate order,\n * `undefined` standing for the candidate that declared none.\n * - `unbacked` — nothing in the model backs the hop on either tier. The pair is\n * still carried, because a message about it has to be able to name the edge\n * the author would have to draw.\n *\n * `rels` carries every candidate on the matched tier, not just the first: an\n * `attributed` verdict over two relationships is the normal shape once a service\n * is drawn as containers, and a caller pinning evidence needs all of them.\n */\nexport type StepAttribution =\n | (CallPair & { verdict: \"attributed\"; tier: 1 | 2; rels: readonly Rel[]; op?: string })\n | (CallPair & { verdict: \"contested\"; tier: 1 | 2; rels: readonly Rel[]; ops: readonly (string | undefined)[] })\n | (CallPair & { verdict: \"unbacked\" });\n\n/**\n * The pair a step's backing relationship is looked up under. This is the half\n * that mis-grades the whole fleet if it is wrong, because a reply arrow is the\n * commonest step in any sequence diagram.\n *\n * MEASURED at the `likec4@1.59.2` pin, and `test/likec4-view-shape.test.ts` holds\n * the raw form: a reply written `a <- b 'reply'` parses as\n * `{source: \"b\", target: \"a\", isBackward: true}` — LikeC4 has ALREADY reversed\n * the endpoints and set the flag — while the forward `b -> a` yields that same\n * pair with no `isBackward` key at all (ABSENT, not false). So the endpoints as\n * parsed describe the direction the MESSAGE travels, and the flag records which\n * arrow the author typed.\n *\n * loam attributes a reply to the CALL it answers, so the reversal is undone\n * here. A landscape declares `web -> orders` because that edge is the\n * dependency; a mirror `orders -> web` for each response is not something a\n * fleet writes down. Reading a reply hop under the message's own direction would\n * report every return hop against such a landscape as `unbacked`, and where a\n * fleet does draw the mirror edge it would file the two halves of one call under\n * two operations — the call under `op 'createOrder'`, its reply under none.\n *\n * That is deliberately NOT what LikeC4's own computed stage does, and the\n * difference is measured rather than assumed: that stage resolves a backward\n * step's `relations` from the UNFLIPPED pair, so at this pin a `web <- orders`\n * step computes to `relations: []` when only `web -> orders` exists, and to the\n * mirror edge when only `orders -> web` does. It is answering which arrow to\n * draw; loam is answering which operation the hop exercises, and reads what a\n * view DECLARES rather than what it shows — which is why loam never reaches that\n * stage at all (docs/DESIGN.md rule 26, enforced by `scripts/arch-check.mjs`).\n * The consequence to know before changing this: where\n * a fleet declares BOTH directions, the mirror edge is not what backs the reply\n * here — the call is, and the mirror edge backs only a hop drawn forward along\n * it.\n */\nfunction callPair(step: StepEndpoints): CallPair {\n return step.isBackward ? { from: step.target, to: step.source } : { from: step.source, to: step.target };\n}\n\n/**\n * The distinct `op` values across candidates, first-seen order preserved.\n *\n * Absent is a VALUE here, not a gap to skip: a relationship carrying no `op` and\n * one carrying `createOrder` disagree about the operation just as surely as two\n * different ops do, and skipping the absent one would silently promote that\n * disagreement to a confident answer. `includes` rather than a `Set` because a\n * candidate list is the handful of relationships between one pair of endpoints,\n * and it keeps `undefined` comparing the way the rest of this file expects.\n */\nfunction distinctOps(rels: readonly Rel[]): (string | undefined)[] {\n const out: (string | undefined)[] = [];\n for (const r of rels) if (!out.includes(r.op)) out.push(r.op);\n return out;\n}\n\n/**\n * The verdict one tier's candidates imply, or `undefined` when that tier found\n * nothing and is therefore not the tier that answers.\n *\n * The count that decides is the DISTINCT-OP count, never the candidate count.\n * That distinction is what makes the service tier safe: once a service is drawn\n * as containers, `a.api -> b` and `a.worker -> b` both carrying\n * `op 'createOrder'` are two relationships and one operation, and a verdict read\n * off `rels.length` would convict a perfectly consistent model of contesting\n * itself.\n */\nfunction verdictOn(pair: CallPair, tier: 1 | 2, rels: readonly Rel[]): StepAttribution | undefined {\n if (rels.length === 0) return undefined;\n const ops = distinctOps(rels);\n if (ops.length > 1) return { ...pair, verdict: \"contested\", tier, rels, ops };\n // Length is exactly one here, so the single distinct value is the answer —\n // and `undefined` is a legitimate answer: every candidate agreed that this\n // hop names no operation.\n const [only] = ops;\n return { ...pair, verdict: \"attributed\", tier, rels, ...(only === undefined ? {} : { op: only }) };\n}\n\n/**\n * Relationships whose ENDPOINTS resolve to the same two services as the pair —\n * the fallback tier, and the reason a step drawn between two services matches an\n * edge drawn between two containers.\n *\n * Through the module's cached `resolverFor`, not a fresh `serviceResolver`: this\n * runs once per step of every use case in the fleet, and building the id map per\n * step is the cost that comment above `RESOLVERS` exists to prevent.\n */\nfunction resolvedTier(pair: CallPair, scope: StepScope): Rel[] {\n const resolve = resolverFor(scope.elements, scope.known);\n const from = resolve(pair.from);\n const to = resolve(pair.to);\n return scope.relationships.filter((r) => resolve(r.source) === from && resolve(r.target) === to);\n}\n\n/**\n * Which relationship in the model backs one hop of a use case, and what it says\n * the operation is.\n *\n * Two tiers, and the second runs ONLY when the first is empty. That ordering is\n * load-bearing rather than an optimisation: the service tier necessarily\n * re-finds every exact match — an endpoint that matches literally resolves to\n * the same service trivially — so running it unconditionally would let a\n * container-level edge with a different `op` contest a step the model already\n * answers exactly, turning an `attributed` verdict into a `contested` one on a\n * document nobody changed.\n *\n * Relationship `kind` is deliberately not part of the match, and that is\n * measured too: at this pin `a -[http]-> b` without a `relationship http` in the\n * `specification` block is \"Could not resolve reference to RelationshipKind\n * named 'http'\" — a parse error, which under loam's standing rule means no model\n * at all — and `test/helpers/harness.ts`'s own `LANDSCAPE` declares no\n * relationship kinds. So narrowing by kind would mean a fix message asking an\n * author to write a line their document does not parse.\n */\nexport function attributeStep(step: StepEndpoints, scope: StepScope): StepAttribution {\n const pair = callPair(step);\n const exact = scope.relationships.filter((r) => r.source === pair.from && r.target === pair.to);\n const declared = verdictOn(pair, 1, exact);\n if (declared !== undefined) return declared;\n return verdictOn(pair, 2, resolvedTier(pair, scope)) ?? { ...pair, verdict: \"unbacked\" };\n}\n"]}
|
|
@@ -74,6 +74,10 @@ function refuse(block, detail, home) {
|
|
|
74
74
|
}
|
|
75
75
|
/** Where the living documents keep what this delta cannot carry. */
|
|
76
76
|
const LANDSCAPE_HOME = "architecture/landscape.likec4, where the living documents keep it,";
|
|
77
|
+
/** Where a FEATURE keeps TOPOLOGY — the third whole-file slot, added with the deployment axis. */
|
|
78
|
+
const DEPLOYMENT_HOME = "features/<FEAT>/deployment/<name>.likec4 — a document of its own, which `loam archive` copies into " +
|
|
79
|
+
"architecture/ and `loam unarchive` takes back; say `extend` in it to add to a region the living map " +
|
|
80
|
+
"already declares —";
|
|
77
81
|
/** Where a FEATURE keeps a flow — a slot of its own, not the living tree. See the header. */
|
|
78
82
|
const FLOW_HOME = "features/<FEAT>/usecases/<name>.likec4 — a views-only document of its own, which `loam archive` copies into " +
|
|
79
83
|
"architecture/usecases/ and `loam unarchive` takes back —";
|
|
@@ -104,7 +108,16 @@ export function assertMergeableDelta(deltaText) {
|
|
|
104
108
|
continue;
|
|
105
109
|
const block = m[1];
|
|
106
110
|
if (block !== "views") {
|
|
107
|
-
refuse(`\`${block} { }\` block`, `a top-level \`${block} { }\` block`,
|
|
111
|
+
refuse(`\`${block} { }\` block`, `a top-level \`${block} { }\` block`,
|
|
112
|
+
// The refusal is unchanged and always will be — this document
|
|
113
|
+
// re-declares the landscape's identifiers and carries its own
|
|
114
|
+
// `specification`, so it cannot be staged beside the map in one project.
|
|
115
|
+
// What changed is where it sends the author: a feature introducing
|
|
116
|
+
// topology WITH the change that makes it true now has a slot of its
|
|
117
|
+
// own, and pointing at the living landscape was the advice that made
|
|
118
|
+
// the axis's headline case a two-pull-request job. `global { }` has no
|
|
119
|
+
// such slot and keeps the living home.
|
|
120
|
+
block === "deployment" ? DEPLOYMENT_HOME : LANDSCAPE_HOME);
|
|
108
121
|
}
|
|
109
122
|
const open = m.index + m[0].length - 1;
|
|
110
123
|
const close = matchBrace(code, open);
|
|
@@ -119,7 +132,7 @@ export function assertMergeableDelta(deltaText) {
|
|
|
119
132
|
// an author to the living landscape for one they are introducing WITH
|
|
120
133
|
// this change was the advice that made the axis's headline case a
|
|
121
134
|
// two-pull-request job.
|
|
122
|
-
kind === "dynamic" ? FLOW_HOME :
|
|
135
|
+
kind === "dynamic" ? FLOW_HOME : DEPLOYMENT_HOME);
|
|
123
136
|
}
|
|
124
137
|
}
|
|
125
138
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"delta-blocks.js","sourceRoot":"","sources":["../../../../src/core/c4/splice/delta-blocks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG;AACH,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC3D,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAErD;;;;GAIG;AACH,MAAM,eAAe,GAAG,mCAAmC,CAAC;AAE5D,2FAA2F;AAC3F,MAAM,aAAa,GAAG,iCAAiC,CAAC;AAExD,SAAS,MAAM,CAAC,KAAa,EAAE,MAAc,EAAE,IAAY;IACzD,MAAM,IAAI,oBAAoB,CAC5B,yBAAyB,MAAM,wEAAwE;QACrG,sEAAsE,KAAK,QAAQ;QACnF,GAAG,IAAI,0BAA0B,CACpC,CAAC;AACJ,CAAC;AAED,oEAAoE;AACpE,MAAM,cAAc,GAAG,oEAAoE,CAAC;AAE5F,6FAA6F;AAC7F,MAAM,SAAS,GACb,8GAA8G;IAC9G,0DAA0D,CAAC;AAE7D;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,oBAAoB,CAAC,SAAiB;IACpD,MAAM,EAAE,IAAI,EAAE,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC;IACvC,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,eAAe,CAAC,EAAE,CAAC;QAC/C,OAAO,MAAM,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,IAAI,CAAC,EAAE,CAAC;YACrC,IAAI,IAAI,CAAC,MAAM,CAAC,KAAK,GAAG;gBAAE,KAAK,IAAI,CAAC,CAAC;iBAChC,IAAI,IAAI,CAAC,MAAM,CAAC,KAAK,GAAG;gBAAE,KAAK,IAAI,CAAC,CAAC;QAC5C,CAAC;QACD,IAAI,KAAK,KAAK,CAAC;YAAE,SAAS;QAC1B,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,CAAE,CAAC;QACpB,IAAI,KAAK,KAAK,OAAO,EAAE,CAAC;YACtB,MAAM,CAAC,KAAK,KAAK,cAAc,EAAE,iBAAiB,KAAK,cAAc,EAAE,cAAc,CAAC,CAAC;QACzF,CAAC;QACD,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;QACvC,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACrC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,EAAE,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QACvE,MAAM,QAAQ,GAAG,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC3C,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;YACtB,MAAM,IAAI,GAAG,QAAQ,CAAC,CAAC,CAAE,CAAC;YAC1B,MAAM,CACJ,KAAK,IAAI,SAAS,EAClB,OAAO,IAAI,0FAA0F;gBACnG,sFAAsF;YACxF,yEAAyE;YACzE,wEAAwE;YACxE,sEAAsE;YACtE,kEAAkE;YAClE,wBAAwB;YACxB,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,cAAc,CAChD,CAAC;QACJ,CAAC;IACH,CAAC;AACH,CAAC","sourcesContent":["/**\n * What a delta may declare that the landscape merge cannot carry.\n *\n * `planLandscapeMerge` splices out of the delta's `model { }` block and nothing\n * else. That is correct — the living landscape is a model plus the views its own\n * authors chose — but it used to be SILENT: a delta declaring `deployment { }`\n * or a `dynamic view` archived at exit 0, reported `+N element(s), +M\n * relationship(s)` exactly as usual, and the block was simply gone. The merge's\n * own parse net cannot catch it either, because a landscape missing a deployment\n * model is perfectly legal — there is nothing to fail on. Nobody finds out until\n * somebody goes looking for a diagram that was archived months ago.\n *\n * So the loss is made a refusal, which is the same doctrine as the merge's other\n * mechanical refusals (an unlocatable declaration, two statements on one line):\n * the input cannot be spliced as authored, `--approve` deliberately does not\n * override it — that flag overrides loam's JUDGMENT about coherence, never its\n * ability to carry an axis — and nothing is written.\n *\n * ## Why views are graded and not simply banned\n *\n * `loam new` scaffolds a `views { view <feat> { include * } }` block into every\n * delta.likec4 (commands/new/templates.ts), so refusing the block outright would\n * refuse loam's own scaffold on its first archive. It is there to be RENDERED\n * while the feature is in flight, and it is meant not to travel.\n *\n * The line drawn here is recoverability, and it is the only line that survives\n * the scaffold:\n *\n * - A static `view` is a rendering OF the model. Once the merge lands, the\n * whole model is in landscape.likec4, so the author can restate the view\n * there at any time from information that still exists. Dropping it loses a\n * convenience.\n * - A `dynamic view`'s ordered steps, and a `deployment { }` block's topology,\n * exist in NO other document the fleet keeps. When the merge drops those,\n * the information is gone. That is what gets refused. Rule 26 sharpens the\n * stakes rather than the rule: a `dynamic view`'s steps are now something\n * loam READS, so dropping one silently would discard checked content, not\n * merely a convenience.\n *\n * A residual is accepted knowingly: a delta carrying a SECOND static view still\n * loses it without a word. Refusing that would mean telling an author with two\n * preview views to delete one, which is a false refusal on a harmless file, and\n * the loss there is recoverable by the argument above.\n *\n * ## The dynamic-view refusal is unchanged; its ADVICE moved\n *\n * A `dynamic view` still cannot ride inside `delta.likec4`, and the reason is\n * mechanical rather than a policy about where flows belong: this document\n * re-declares the landscape's own identifiers to anchor its new edges and\n * carries its own `specification` block because LikeC4 parses it standalone, so\n * it cannot be staged beside the map in one project — a duplicate specification\n * is an error blamed on both files, and a re-declared element is a duplicate id.\n * What changed is where the refusal sends the author. It used to say\n * `architecture/landscape.likec4`, which was the only honest answer while the\n * axis had no feature-delta path: a flow could not ship with the change that\n * made it true. `features/<FEAT>/usecases/<name>.likec4` is that path\n * (`core/usecases/delta/flows.ts`), and it is a views-only document — which is\n * exactly why it stages beside the map when this one cannot.\n */\nimport { maskSource, matchBrace } from \"../source-mask.js\";\nimport { LandscapeSpliceError } from \"./contract.js\";\n\n/**\n * Top-level blocks a delta can declare. `specification` and `model` are absent\n * on purpose: the specification is re-declared in every delta because LikeC4\n * parses each one standalone, and the model is the thing being spliced.\n */\nconst TOP_LEVEL_BLOCK = /\\b(deployment|global|views)\\s*\\{/g;\n\n/** A `dynamic view` / `deployment view` inside a views block — the irrecoverable kinds. */\nconst AUTHORED_VIEW = /\\b(dynamic|deployment)\\s+view\\b/;\n\nfunction refuse(block: string, detail: string, home: string): never {\n throw new LandscapeSpliceError(\n `delta.likec4 declares ${detail}, and the landscape merge splices only what is inside \\`model { }\\` — ` +\n `archiving would drop it silently, so nothing was written. Move the ${block} into ` +\n `${home} and re-run the archive.`,\n );\n}\n\n/** Where the living documents keep what this delta cannot carry. */\nconst LANDSCAPE_HOME = \"architecture/landscape.likec4, where the living documents keep it,\";\n\n/** Where a FEATURE keeps a flow — a slot of its own, not the living tree. See the header. */\nconst FLOW_HOME =\n \"features/<FEAT>/usecases/<name>.likec4 — a views-only document of its own, which `loam archive` copies into \" +\n \"architecture/usecases/ and `loam unarchive` takes back —\";\n\n/**\n * Refuse a delta whose non-model blocks the merge would discard.\n *\n * Runs over MASKED source, so a `deployment {` written inside a comment or a\n * description string cannot trigger it — the same masking discipline\n * `scanModel` splices under, and for the same reason: this refuses an archive,\n * and a refusal a comment can provoke is worse than the silence it replaced.\n *\n * Brace depth is tracked in one forward pass rather than recounted per match:\n * only a block at depth 0 is a top-level declaration, and `deployment` at depth\n * 1 is an ordinary `specification { deploymentNode … }` or a view predicate.\n */\nexport function assertMergeableDelta(deltaText: string): void {\n const { code } = maskSource(deltaText);\n let depth = 0;\n let cursor = 0;\n for (const m of code.matchAll(TOP_LEVEL_BLOCK)) {\n for (; cursor < m.index; cursor += 1) {\n if (code[cursor] === \"{\") depth += 1;\n else if (code[cursor] === \"}\") depth -= 1;\n }\n if (depth !== 0) continue;\n const block = m[1]!;\n if (block !== \"views\") {\n refuse(`\\`${block} { }\\` block`, `a top-level \\`${block} { }\\` block`, LANDSCAPE_HOME);\n }\n const open = m.index + m[0].length - 1;\n const close = matchBrace(code, open);\n const inner = code.slice(open + 1, close === -1 ? code.length : close);\n const authored = AUTHORED_VIEW.exec(inner);\n if (authored !== null) {\n const kind = authored[1]!;\n refuse(\n `\\`${kind} view\\``,\n `a \\`${kind} view\\` in its \\`views { }\\` block — a view of that kind describes ordering or topology ` +\n `recorded in no other document, unlike the static preview view \\`loam new\\` scaffolds`,\n // A deployment view is topology and belongs in the living map; a dynamic\n // view is a flow, and a flow now has a feature slot of its own. Sending\n // an author to the living landscape for one they are introducing WITH\n // this change was the advice that made the axis's headline case a\n // two-pull-request job.\n kind === \"dynamic\" ? FLOW_HOME : LANDSCAPE_HOME,\n );\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"delta-blocks.js","sourceRoot":"","sources":["../../../../src/core/c4/splice/delta-blocks.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What a delta may declare that the landscape merge cannot carry.\n *\n * `planLandscapeMerge` splices out of the delta's `model { }` block and nothing\n * else. That is correct — the living landscape is a model plus the views its own\n * authors chose — but it used to be SILENT: a delta declaring `deployment { }`\n * or a `dynamic view` archived at exit 0, reported `+N element(s), +M\n * relationship(s)` exactly as usual, and the block was simply gone. The merge's\n * own parse net cannot catch it either, because a landscape missing a deployment\n * model is perfectly legal — there is nothing to fail on. Nobody finds out until\n * somebody goes looking for a diagram that was archived months ago.\n *\n * So the loss is made a refusal, which is the same doctrine as the merge's other\n * mechanical refusals (an unlocatable declaration, two statements on one line):\n * the input cannot be spliced as authored, `--approve` deliberately does not\n * override it — that flag overrides loam's JUDGMENT about coherence, never its\n * ability to carry an axis — and nothing is written.\n *\n * ## Why views are graded and not simply banned\n *\n * `loam new` scaffolds a `views { view <feat> { include * } }` block into every\n * delta.likec4 (commands/new/templates.ts), so refusing the block outright would\n * refuse loam's own scaffold on its first archive. It is there to be RENDERED\n * while the feature is in flight, and it is meant not to travel.\n *\n * The line drawn here is recoverability, and it is the only line that survives\n * the scaffold:\n *\n * - A static `view` is a rendering OF the model. Once the merge lands, the\n * whole model is in landscape.likec4, so the author can restate the view\n * there at any time from information that still exists. Dropping it loses a\n * convenience.\n * - A `dynamic view`'s ordered steps, and a `deployment { }` block's topology,\n * exist in NO other document the fleet keeps. When the merge drops those,\n * the information is gone. That is what gets refused. Rule 26 sharpens the\n * stakes rather than the rule: a `dynamic view`'s steps are now something\n * loam READS, so dropping one silently would discard checked content, not\n * merely a convenience.\n *\n * A residual is accepted knowingly: a delta carrying a SECOND static view still\n * loses it without a word. Refusing that would mean telling an author with two\n * preview views to delete one, which is a false refusal on a harmless file, and\n * the loss there is recoverable by the argument above.\n *\n * ## The dynamic-view refusal is unchanged; its ADVICE moved\n *\n * A `dynamic view` still cannot ride inside `delta.likec4`, and the reason is\n * mechanical rather than a policy about where flows belong: this document\n * re-declares the landscape's own identifiers to anchor its new edges and\n * carries its own `specification` block because LikeC4 parses it standalone, so\n * it cannot be staged beside the map in one project — a duplicate specification\n * is an error blamed on both files, and a re-declared element is a duplicate id.\n * What changed is where the refusal sends the author. It used to say\n * `architecture/landscape.likec4`, which was the only honest answer while the\n * axis had no feature-delta path: a flow could not ship with the change that\n * made it true. `features/<FEAT>/usecases/<name>.likec4` is that path\n * (`core/usecases/delta/flows.ts`), and it is a views-only document — which is\n * exactly why it stages beside the map when this one cannot.\n */\nimport { maskSource, matchBrace } from \"../source-mask.js\";\nimport { LandscapeSpliceError } from \"./contract.js\";\n\n/**\n * Top-level blocks a delta can declare. `specification` and `model` are absent\n * on purpose: the specification is re-declared in every delta because LikeC4\n * parses each one standalone, and the model is the thing being spliced.\n */\nconst TOP_LEVEL_BLOCK = /\\b(deployment|global|views)\\s*\\{/g;\n\n/** A `dynamic view` / `deployment view` inside a views block — the irrecoverable kinds. */\nconst AUTHORED_VIEW = /\\b(dynamic|deployment)\\s+view\\b/;\n\nfunction refuse(block: string, detail: string, home: string): never {\n throw new LandscapeSpliceError(\n `delta.likec4 declares ${detail}, and the landscape merge splices only what is inside \\`model { }\\` — ` +\n `archiving would drop it silently, so nothing was written. Move the ${block} into ` +\n `${home} and re-run the archive.`,\n );\n}\n\n/** Where the living documents keep what this delta cannot carry. */\nconst LANDSCAPE_HOME = \"architecture/landscape.likec4, where the living documents keep it,\";\n\n/** Where a FEATURE keeps TOPOLOGY — the third whole-file slot, added with the deployment axis. */\nconst DEPLOYMENT_HOME =\n \"features/<FEAT>/deployment/<name>.likec4 — a document of its own, which `loam archive` copies into \" +\n \"architecture/ and `loam unarchive` takes back; say `extend` in it to add to a region the living map \" +\n \"already declares —\";\n\n/** Where a FEATURE keeps a flow — a slot of its own, not the living tree. See the header. */\nconst FLOW_HOME =\n \"features/<FEAT>/usecases/<name>.likec4 — a views-only document of its own, which `loam archive` copies into \" +\n \"architecture/usecases/ and `loam unarchive` takes back —\";\n\n/**\n * Refuse a delta whose non-model blocks the merge would discard.\n *\n * Runs over MASKED source, so a `deployment {` written inside a comment or a\n * description string cannot trigger it — the same masking discipline\n * `scanModel` splices under, and for the same reason: this refuses an archive,\n * and a refusal a comment can provoke is worse than the silence it replaced.\n *\n * Brace depth is tracked in one forward pass rather than recounted per match:\n * only a block at depth 0 is a top-level declaration, and `deployment` at depth\n * 1 is an ordinary `specification { deploymentNode … }` or a view predicate.\n */\nexport function assertMergeableDelta(deltaText: string): void {\n const { code } = maskSource(deltaText);\n let depth = 0;\n let cursor = 0;\n for (const m of code.matchAll(TOP_LEVEL_BLOCK)) {\n for (; cursor < m.index; cursor += 1) {\n if (code[cursor] === \"{\") depth += 1;\n else if (code[cursor] === \"}\") depth -= 1;\n }\n if (depth !== 0) continue;\n const block = m[1]!;\n if (block !== \"views\") {\n refuse(\n `\\`${block} { }\\` block`,\n `a top-level \\`${block} { }\\` block`,\n // The refusal is unchanged and always will be — this document\n // re-declares the landscape's identifiers and carries its own\n // `specification`, so it cannot be staged beside the map in one project.\n // What changed is where it sends the author: a feature introducing\n // topology WITH the change that makes it true now has a slot of its\n // own, and pointing at the living landscape was the advice that made\n // the axis's headline case a two-pull-request job. `global { }` has no\n // such slot and keeps the living home.\n block === \"deployment\" ? DEPLOYMENT_HOME : LANDSCAPE_HOME,\n );\n }\n const open = m.index + m[0].length - 1;\n const close = matchBrace(code, open);\n const inner = code.slice(open + 1, close === -1 ? code.length : close);\n const authored = AUTHORED_VIEW.exec(inner);\n if (authored !== null) {\n const kind = authored[1]!;\n refuse(\n `\\`${kind} view\\``,\n `a \\`${kind} view\\` in its \\`views { }\\` block — a view of that kind describes ordering or topology ` +\n `recorded in no other document, unlike the static preview view \\`loam new\\` scaffolds`,\n // A deployment view is topology and belongs in the living map; a dynamic\n // view is a flow, and a flow now has a feature slot of its own. Sending\n // an author to the living landscape for one they are introducing WITH\n // this change was the advice that made the axis's headline case a\n // two-pull-request job.\n kind === \"dynamic\" ? FLOW_HOME : DEPLOYMENT_HOME,\n );\n }\n }\n}\n"]}
|
|
@@ -45,6 +45,7 @@ import { flattenModel } from "./likec4.js";
|
|
|
45
45
|
import { readDynamicViews } from "./parsed/dynamic-views.js";
|
|
46
46
|
import { readViewIds } from "./parsed/view-ids.js";
|
|
47
47
|
import { readSpecification } from "./parsed/specification.js";
|
|
48
|
+
import { readDeployment } from "./parsed/deployment.js";
|
|
48
49
|
/**
|
|
49
50
|
* Stage every readable document as its own single-file project. A copy failure
|
|
50
51
|
* removes the half-made folder and drops the path — an empty project must not
|
|
@@ -152,6 +153,7 @@ export async function loadBatch(paths) {
|
|
|
152
153
|
specification: readSpecification(model.specification),
|
|
153
154
|
views: readDynamicViews(model),
|
|
154
155
|
viewIds: readViewIds(model),
|
|
156
|
+
deployment: readDeployment(model),
|
|
155
157
|
...flattenModel(model),
|
|
156
158
|
});
|
|
157
159
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace.js","sourceRoot":"","sources":["../../../src/core/c4/workspace.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Batch loader: many self-contained `.likec4` documents through ONE\n * LikeC4/Langium workspace.\n *\n * `loadFile` pays a fresh workspace per call — ~100ms even warm — which is what\n * made `validate --all` O(documents) workspace spins (13.7s median over the\n * 120-service benchmark fixture, docs/BENCHMARKS.md). This module copies every\n * document into a mkdtemp workspace as one single-file PROJECT each (a\n * `likec4.config.json` per folder), calls `LikeC4.fromWorkspace` once, splits\n * `getErrors()` per document, and flattens each clean project through the same\n * `flattenModel` the per-file path uses. Parity — byte-identical error\n * messages/lines and identical elements/relationships — is pinned by\n * test/likec4-batch-parity.test.ts, which is also the tripwire for the exact\n * likec4 version pin: multi-project workspaces are public API, but this use of\n * them is not documented, so an upgrade must fail that suite before it lands.\n *\n * Isolation is load-bearing, not cosmetic: `fromSource` gives each document a\n * workspace of its own, so an author-written `import ... from 'name'` can never\n * resolve. In a shared workspace it COULD — a sibling project is one lucky name\n * away — so every folder carries one crypto-random per-invocation hex token in\n * its project name. A document cannot import what it cannot name, and the\n * parity suite pins that a guessed sibling name errors identically in both\n * modes.\n *\n * Callers own the failure story: a document that cannot be staged (unreadable,\n * missing) is silently DROPPED from the result so the caller's ordinary\n * per-path load reproduces today's error, and a batch-infrastructure failure\n * (tmpdir denied — the sandboxed-runner class ROADMAP documents) rejects the\n * whole call for the caller to catch and degrade. FleetContext.prefetchLikeC4\n * does exactly that: findings can never change because a workspace could not be\n * created, only the speed can.\n *\n * This file took core/c4/ to exactly five files, and the seam that note\n * predicted is now open: `./project/load.ts` loads a DIRECTORY as one project,\n * which is the opposite isolation contract and belongs beside this one rather\n * than inside it.\n */\nimport { randomBytes } from \"node:crypto\";\nimport { inOrder } from \"../kernel/concurrency.js\";\nimport { copyFile, mkdir, mkdtemp, rm, writeFile } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { basename, join, resolve } from \"node:path\";\nimport { LikeC4 } from \"likec4\";\nimport { flattenModel, type LikeC4Error, type LoadedDoc, type ReadableModel } from \"./likec4.js\";\nimport { readDynamicViews } from \"./parsed/dynamic-views.js\";\nimport { readViewIds } from \"./parsed/view-ids.js\";\nimport { readSpecification } from \"./parsed/specification.js\";\n\n/** A staged document: the real path it came from, and its workspace project. */\ninterface StagedDoc {\n path: string;\n project: string;\n}\n\n/**\n * Stage every readable document as its own single-file project. A copy failure\n * removes the half-made folder and drops the path — an empty project must not\n * reach the workspace scan, and the caller's per-path load owns the error. The\n * copy is `copyFile` (raw bytes), not read/decode/write: BOM, CRLF and even\n * non-UTF-8 bytes reach LikeC4's own reader exactly as they would from the\n * original file on disk.\n */\nasync function stage(root: string, targets: string[]): Promise<StagedDoc[]> {\n const token = randomBytes(8).toString(\"hex\");\n // Concurrent, capped, in input order (kernel/concurrency's contract): the\n // per-document folders share nothing, and on a network-mounted docs repo\n // the sequential form paid three round-trips per document. Nulls mark the\n // drops so indices — and with them the p<i> project names — stay aligned.\n const results = await inOrder([...targets.entries()], async ([i, path]) => {\n const project = `p${i}_${token}`;\n const folder = join(root, project);\n try {\n await mkdir(folder);\n await writeFile(join(folder, \"likec4.config.json\"), JSON.stringify({ name: project }), \"utf8\");\n await copyFile(path, join(folder, basename(path)));\n return { path, project };\n } catch {\n await rm(folder, { recursive: true, force: true });\n return null;\n }\n });\n return results.filter((r): r is StagedDoc => r !== null);\n}\n\n/**\n * `getErrors()` split per document, each error's `sourceFsPath` rewritten from\n * the workspace copy to the document's real absolute path.\n *\n * Grouping keys on the unique `p<i>_<token>` path SEGMENT, never on a\n * tmp-root prefix compare: darwin hands back realpathed workspace paths\n * (`/var/...` becomes `/private/var/...`) in some environments and the literal\n * mkdtemp path in others, and a prefix compare silently attributes every error\n * to nobody in exactly one of those two worlds. The segment survives both.\n */\nfunction groupErrors(likec4: LikeC4, staged: StagedDoc[]): Map<string, LikeC4Error[]> {\n const pathOf = new Map(staged.map((doc) => [doc.project, doc.path]));\n const grouped = new Map<string, LikeC4Error[]>();\n for (const err of likec4.getErrors()) {\n const project = err.sourceFsPath.split(/[\\\\/]/).find((segment) => pathOf.has(segment));\n if (project === undefined) {\n // An error this walk cannot attribute would otherwise be DROPPED — and a\n // dropped error grades its document clean, which is failing open in the\n // fleet gate. Unreachable at the pinned likec4 version (every corpus\n // error carries its project folder in sourceFsPath), so the one place a\n // dependency upgrade could break the attribution is the one place that\n // must reject the whole batch: the caller degrades to per-path loads,\n // where the error reappears.\n throw new Error(`unattributable LikeC4 error in batch workspace: ${err.message}`);\n }\n const list = grouped.get(project) ?? [];\n list.push({ ...err, sourceFsPath: pathOf.get(project) });\n grouped.set(project, list);\n }\n return grouped;\n}\n\n/**\n * Load and validate many self-contained `.likec4` documents in one workspace.\n * Returns `resolve(path)` → LoadedDoc; paths are deduped through the same\n * resolution, and a path that could not be staged is absent from the result.\n *\n * A document with errors keeps `loadSource`'s exact rule — empty elements and\n * relationships, WITHOUT calling `parsedModel`: the parsed stage succeeds even\n * over unresolved references, so the \"errors mean no model\" boundary is loam's\n * to apply, not LikeC4's. A clean document is flattened from\n * `parsedModel(project)` — parsed, never computed: the computed stage RESOLVES\n * every view against the model — which loam never wants, because it reads a\n * view's DECLARATIONS and never a view's resolved contents (docs/DESIGN.md\n * rule 26) — and which is superlinear in edge count (see loadSource).\n */\nexport async function loadBatch(paths: string[]): Promise<Map<string, LoadedDoc>> {\n const targets = [...new Set(paths.map((path) => resolve(path)))];\n const out = new Map<string, LoadedDoc>();\n if (targets.length === 0) return out;\n const root = await mkdtemp(join(tmpdir(), \"loam-c4-\"));\n try {\n const staged = await stage(root, targets);\n if (staged.length === 0) return out;\n // One workspace for the whole batch — the entire point. The logger is\n // disabled exactly as in loadSource: diagnostics are returned per document,\n // not written out-of-band to stderr (core never prints).\n const likec4 = await LikeC4.fromWorkspace(root, { logger: false });\n try {\n const grouped = groupErrors(likec4, staged);\n for (const { path, project } of staged) {\n const errors = grouped.get(project) ?? [];\n if (errors.length > 0) {\n out.set(path, { errors, elements: [], relationships: [] });\n } else {\n const model = (await likec4.parsedModel(project)) as ReadableModel;\n // The specification travels with the batch loader too, and must: this\n // is the loader `validate --all` reads the landscape through, so a\n // kind-tag check wired only into `loadSource` would be a check the\n // fleet gate — the one run it exists for — never actually ran.\n out.set(path, {\n errors,\n specification: readSpecification(model.specification),\n views: readDynamicViews(model),\n viewIds: readViewIds(model),\n ...flattenModel(model),\n });\n }\n }\n } finally {\n await likec4.dispose();\n }\n return out;\n } finally {\n // The workspace is this invocation's alone; nothing else may observe it.\n // A kill between mkdtemp and here can strand a loam-c4-* dir in OS tmp —\n // never in the docs repo — which docs/DESIGN.md records as accepted.\n await rm(root, { recursive: true, force: true });\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"workspace.js","sourceRoot":"","sources":["../../../src/core/c4/workspace.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Batch loader: many self-contained `.likec4` documents through ONE\n * LikeC4/Langium workspace.\n *\n * `loadFile` pays a fresh workspace per call — ~100ms even warm — which is what\n * made `validate --all` O(documents) workspace spins (13.7s median over the\n * 120-service benchmark fixture, docs/BENCHMARKS.md). This module copies every\n * document into a mkdtemp workspace as one single-file PROJECT each (a\n * `likec4.config.json` per folder), calls `LikeC4.fromWorkspace` once, splits\n * `getErrors()` per document, and flattens each clean project through the same\n * `flattenModel` the per-file path uses. Parity — byte-identical error\n * messages/lines and identical elements/relationships — is pinned by\n * test/likec4-batch-parity.test.ts, which is also the tripwire for the exact\n * likec4 version pin: multi-project workspaces are public API, but this use of\n * them is not documented, so an upgrade must fail that suite before it lands.\n *\n * Isolation is load-bearing, not cosmetic: `fromSource` gives each document a\n * workspace of its own, so an author-written `import ... from 'name'` can never\n * resolve. In a shared workspace it COULD — a sibling project is one lucky name\n * away — so every folder carries one crypto-random per-invocation hex token in\n * its project name. A document cannot import what it cannot name, and the\n * parity suite pins that a guessed sibling name errors identically in both\n * modes.\n *\n * Callers own the failure story: a document that cannot be staged (unreadable,\n * missing) is silently DROPPED from the result so the caller's ordinary\n * per-path load reproduces today's error, and a batch-infrastructure failure\n * (tmpdir denied — the sandboxed-runner class ROADMAP documents) rejects the\n * whole call for the caller to catch and degrade. FleetContext.prefetchLikeC4\n * does exactly that: findings can never change because a workspace could not be\n * created, only the speed can.\n *\n * This file took core/c4/ to exactly five files, and the seam that note\n * predicted is now open: `./project/load.ts` loads a DIRECTORY as one project,\n * which is the opposite isolation contract and belongs beside this one rather\n * than inside it.\n */\nimport { randomBytes } from \"node:crypto\";\nimport { inOrder } from \"../kernel/concurrency.js\";\nimport { copyFile, mkdir, mkdtemp, rm, writeFile } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { basename, join, resolve } from \"node:path\";\nimport { LikeC4 } from \"likec4\";\nimport { flattenModel, type LikeC4Error, type LoadedDoc, type ReadableModel } from \"./likec4.js\";\nimport { readDynamicViews } from \"./parsed/dynamic-views.js\";\nimport { readViewIds } from \"./parsed/view-ids.js\";\nimport { readSpecification } from \"./parsed/specification.js\";\nimport { readDeployment } from \"./parsed/deployment.js\";\n\n/** A staged document: the real path it came from, and its workspace project. */\ninterface StagedDoc {\n path: string;\n project: string;\n}\n\n/**\n * Stage every readable document as its own single-file project. A copy failure\n * removes the half-made folder and drops the path — an empty project must not\n * reach the workspace scan, and the caller's per-path load owns the error. The\n * copy is `copyFile` (raw bytes), not read/decode/write: BOM, CRLF and even\n * non-UTF-8 bytes reach LikeC4's own reader exactly as they would from the\n * original file on disk.\n */\nasync function stage(root: string, targets: string[]): Promise<StagedDoc[]> {\n const token = randomBytes(8).toString(\"hex\");\n // Concurrent, capped, in input order (kernel/concurrency's contract): the\n // per-document folders share nothing, and on a network-mounted docs repo\n // the sequential form paid three round-trips per document. Nulls mark the\n // drops so indices — and with them the p<i> project names — stay aligned.\n const results = await inOrder([...targets.entries()], async ([i, path]) => {\n const project = `p${i}_${token}`;\n const folder = join(root, project);\n try {\n await mkdir(folder);\n await writeFile(join(folder, \"likec4.config.json\"), JSON.stringify({ name: project }), \"utf8\");\n await copyFile(path, join(folder, basename(path)));\n return { path, project };\n } catch {\n await rm(folder, { recursive: true, force: true });\n return null;\n }\n });\n return results.filter((r): r is StagedDoc => r !== null);\n}\n\n/**\n * `getErrors()` split per document, each error's `sourceFsPath` rewritten from\n * the workspace copy to the document's real absolute path.\n *\n * Grouping keys on the unique `p<i>_<token>` path SEGMENT, never on a\n * tmp-root prefix compare: darwin hands back realpathed workspace paths\n * (`/var/...` becomes `/private/var/...`) in some environments and the literal\n * mkdtemp path in others, and a prefix compare silently attributes every error\n * to nobody in exactly one of those two worlds. The segment survives both.\n */\nfunction groupErrors(likec4: LikeC4, staged: StagedDoc[]): Map<string, LikeC4Error[]> {\n const pathOf = new Map(staged.map((doc) => [doc.project, doc.path]));\n const grouped = new Map<string, LikeC4Error[]>();\n for (const err of likec4.getErrors()) {\n const project = err.sourceFsPath.split(/[\\\\/]/).find((segment) => pathOf.has(segment));\n if (project === undefined) {\n // An error this walk cannot attribute would otherwise be DROPPED — and a\n // dropped error grades its document clean, which is failing open in the\n // fleet gate. Unreachable at the pinned likec4 version (every corpus\n // error carries its project folder in sourceFsPath), so the one place a\n // dependency upgrade could break the attribution is the one place that\n // must reject the whole batch: the caller degrades to per-path loads,\n // where the error reappears.\n throw new Error(`unattributable LikeC4 error in batch workspace: ${err.message}`);\n }\n const list = grouped.get(project) ?? [];\n list.push({ ...err, sourceFsPath: pathOf.get(project) });\n grouped.set(project, list);\n }\n return grouped;\n}\n\n/**\n * Load and validate many self-contained `.likec4` documents in one workspace.\n * Returns `resolve(path)` → LoadedDoc; paths are deduped through the same\n * resolution, and a path that could not be staged is absent from the result.\n *\n * A document with errors keeps `loadSource`'s exact rule — empty elements and\n * relationships, WITHOUT calling `parsedModel`: the parsed stage succeeds even\n * over unresolved references, so the \"errors mean no model\" boundary is loam's\n * to apply, not LikeC4's. A clean document is flattened from\n * `parsedModel(project)` — parsed, never computed: the computed stage RESOLVES\n * every view against the model — which loam never wants, because it reads a\n * view's DECLARATIONS and never a view's resolved contents (docs/DESIGN.md\n * rule 26) — and which is superlinear in edge count (see loadSource).\n */\nexport async function loadBatch(paths: string[]): Promise<Map<string, LoadedDoc>> {\n const targets = [...new Set(paths.map((path) => resolve(path)))];\n const out = new Map<string, LoadedDoc>();\n if (targets.length === 0) return out;\n const root = await mkdtemp(join(tmpdir(), \"loam-c4-\"));\n try {\n const staged = await stage(root, targets);\n if (staged.length === 0) return out;\n // One workspace for the whole batch — the entire point. The logger is\n // disabled exactly as in loadSource: diagnostics are returned per document,\n // not written out-of-band to stderr (core never prints).\n const likec4 = await LikeC4.fromWorkspace(root, { logger: false });\n try {\n const grouped = groupErrors(likec4, staged);\n for (const { path, project } of staged) {\n const errors = grouped.get(project) ?? [];\n if (errors.length > 0) {\n out.set(path, { errors, elements: [], relationships: [] });\n } else {\n const model = (await likec4.parsedModel(project)) as ReadableModel;\n // The specification travels with the batch loader too, and must: this\n // is the loader `validate --all` reads the landscape through, so a\n // kind-tag check wired only into `loadSource` would be a check the\n // fleet gate — the one run it exists for — never actually ran.\n out.set(path, {\n errors,\n specification: readSpecification(model.specification),\n views: readDynamicViews(model),\n viewIds: readViewIds(model),\n deployment: readDeployment(model),\n ...flattenModel(model),\n });\n }\n }\n } finally {\n await likec4.dispose();\n }\n return out;\n } finally {\n // The workspace is this invocation's alone; nothing else may observe it.\n // A kill between mkdtemp and here can strand a loam-c4-* dir in OS tmp —\n // never in the docs repo — which docs/DESIGN.md records as accepted.\n await rm(root, { recursive: true, force: true });\n }\n}\n"]}
|
|
@@ -13,6 +13,7 @@ import { eventCoherence } from "./events/events.js";
|
|
|
13
13
|
import { authoringIssues } from "./authoring/scaffold.js";
|
|
14
14
|
import { glossaryDeltaIssues } from "../glossary/delta.js";
|
|
15
15
|
import { flowDeltaIssues } from "../usecases/delta/flows.js";
|
|
16
|
+
import { deploymentDeltaIssues, readFeatureDeployments } from "../deployment/delta.js";
|
|
16
17
|
export async function featureCoherence(request) {
|
|
17
18
|
const { docsDir, featureDir, featureId, preloadedDelta, context } = request;
|
|
18
19
|
const scope = { docsDir, featureDir, featureId };
|
|
@@ -32,6 +33,41 @@ export async function featureCoherence(request) {
|
|
|
32
33
|
// already holds must be caught before the copy, not after it has replaced an
|
|
33
34
|
// authored hop sequence.
|
|
34
35
|
issues.push(...(await flowDeltaIssues(docsDir, featureDir)));
|
|
36
|
+
issues.push(...(await deploymentDeltaIssues(docsDir, featureDir)));
|
|
37
|
+
// And the topology's second question, which the collision check above cannot
|
|
38
|
+
// ask: does the document READ against the map this feature's own merge would
|
|
39
|
+
// leave behind. It matters more literally here than one axis over — a feature
|
|
40
|
+
// that stands a service up in a new cluster declares that service in its
|
|
41
|
+
// `delta.likec4` and instances it in the topology, so the element the document
|
|
42
|
+
// names exists only post-merge — and without it the archive copies an
|
|
43
|
+
// unreadable document into `architecture/` for the next reader's
|
|
44
|
+
// `loam validate --all` to fail on, against a file they did not write.
|
|
45
|
+
const topology = await readFeatureDeployments({
|
|
46
|
+
docsDir,
|
|
47
|
+
featureDir,
|
|
48
|
+
featureId,
|
|
49
|
+
// A function rather than the context itself, for the reason the use-case
|
|
50
|
+
// overlay states: one memoised read does not justify an edge from
|
|
51
|
+
// `core/deployment/` to `fleet-context`.
|
|
52
|
+
...(context === undefined ? {} : { load: (path) => context.loadLikeC4(path) }),
|
|
53
|
+
});
|
|
54
|
+
if (topology.kind === "unreadable") {
|
|
55
|
+
issues.push({
|
|
56
|
+
severity: "error",
|
|
57
|
+
code: "deployment.doc-invalid",
|
|
58
|
+
subject: featureId,
|
|
59
|
+
// The opening sentence does not say whose file is at fault, on the
|
|
60
|
+
// `usecase.flow-invalid` message's reasoning: the corpus is the feature's
|
|
61
|
+
// `deployment/` PLUS the living `architecture/` documents it extends, so
|
|
62
|
+
// the failing path can be a living one — a landscape that no longer
|
|
63
|
+
// parses, a topology somebody else wrote. Every message carries its own
|
|
64
|
+
// path, so a reader can place it.
|
|
65
|
+
message: `this feature's deployment document(s) could not be read against the map its own merge would leave behind, ` +
|
|
66
|
+
`so the archive would land an architecture/ nothing has checked. The corpus is the feature's deployment/ AND ` +
|
|
67
|
+
`the living architecture/ documents it extends, so the fault may be in either — each message names its file. ` +
|
|
68
|
+
`${topology.errors.length} problem(s): ${topology.errors.join("; ")}`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
35
71
|
// --- C4 delta ---
|
|
36
72
|
let elements = [];
|
|
37
73
|
let taggedEls = [];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"coherence.js","sourceRoot":"","sources":["../../../src/core/coherence/coherence.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { loadFile, type Elem, type LoadedDoc, type Rel } from \"../c4/likec4.js\";\nimport { elementService, serviceResolver } from \"../c4/resolve/service.js\";\nimport { serviceIdProblem } from \"../kernel/ids/service.js\";\nimport { deltaShapeIssues } from \"../delta/delta.js\";\nimport type { Issue } from \"../vocabulary/issue.js\";\nimport { featurePaths } from \"../repo/paths.js\";\nimport { featureSpecServices } from \"../repo/repo.js\";\nimport { enumeratedServiceIds, enumeratedServiceIndex } from \"../repo/service-target.js\";\nimport { serviceOperationIds } from \"../openapi/doc.js\";\nimport type { FleetContext } from \"../fleet-context.js\";\nimport { declaredByService, type DeltaScope } from \"./declared.js\";\nimport { coherenceLookups } from \"./lookups.js\";\nimport { eventCoherence } from \"./events/events.js\";\nimport { authoringIssues } from \"./authoring/scaffold.js\";\nimport { glossaryDeltaIssues } from \"../glossary/delta.js\";\nimport { flowDeltaIssues } from \"../usecases/delta/flows.js\";\nimport type { DocsDir, FeatureDir } from \"../kernel/ids/dirs.js\";\n\n\n/**\n * Cross-axis consistency for a feature: do C4 (architecture), requirements (behaviour),\n * and OpenAPI (contract) agree? Errors are hard (would corrupt the living docs on archive);\n * warnings surface softer misalignments.\n *\n * `preloadedDelta` is the feature's already-parsed delta.likec4, when the caller\n * has it — loading a LikeC4 document spins up a fresh Langium workspace, the\n * dominant per-feature cost, and validate/archive have both parsed the same file\n * moments earlier. Standalone calls omit it and nothing changes: the file is\n * loaded here, exactly as before.\n */\n\n/** How a caller names the feature being graded, and what it already has in hand. */\nexport interface CoherenceRequest {\n docsDir: DocsDir;\n featureDir: FeatureDir;\n featureId: string;\n /** The delta LikeC4 document, when the caller already parsed it. */\n preloadedDelta?: LoadedDoc;\n context?: FleetContext;\n}\n\nexport async function featureCoherence(request: CoherenceRequest): Promise<Issue[]> {\n const { docsDir, featureDir, featureId, preloadedDelta, context } = request;\n const scope: DeltaScope = { docsDir, featureDir, featureId };\n // Delta shape first: a diff that does not apply to the living spec explains\n // everything downstream, and it is the one breach that is silent without a check.\n const issues: Issue[] = await deltaShapeIssues(docsDir, featureDir, featureId, context);\n\n // The vocabulary axis, and it is asked first because it is cheap and\n // independent: one `existsSync` for a feature with no glossary/, and nothing\n // downstream reads its answer. A term this feature introduces that the living\n // glossary already defines is the one thing that can go wrong on a create-only\n // merge, and it must be caught before the copy, not after.\n issues.push(...(await glossaryDeltaIssues(docsDir, featureDir)));\n\n // The use-case axis, and it is asked here for the vocabulary axis's reasons\n // exactly: one walk over a directory that is usually absent, nothing\n // downstream reads its answer, and a create-only merge has one thing that can\n // go wrong. A flow this feature introduces that the living `architecture/`\n // already holds must be caught before the copy, not after it has replaced an\n // authored hop sequence.\n issues.push(...(await flowDeltaIssues(docsDir, featureDir)));\n\n // --- C4 delta ---\n let elements: Elem[] = [];\n let taggedEls: Elem[] = [];\n let taggedRels: Rel[] = [];\n const deltaPath = featurePaths(featureDir).delta;\n if (existsSync(deltaPath)) {\n const res = preloadedDelta ?? (context === undefined ? await loadFile(deltaPath) : await context.loadLikeC4(deltaPath));\n if (res.errors.length > 0) {\n // An unreadable architecture axis can prove nothing — it must never count as coherent.\n issues.push({\n severity: \"error\",\n code: \"delta.invalid\",\n message: `delta.likec4 has ${res.errors.length} parse error(s) — architecture axis unreadable (run \\`loam validate --feature ${featureId}\\`)`,\n });\n } else {\n elements = res.elements;\n taggedEls = res.elements.filter((e) => e.tags.includes(featureId));\n taggedRels = res.relationships.filter((r) => r.tags.includes(featureId));\n // Every rule below filters by the feature tag, so a delta whose author\n // forgot the tags entirely passes every check and archives while merging\n // nothing. Whole-file only: once ANYTHING is tagged, deciding that an\n // untagged edge \"looks intended\" would be guessing.\n if (\n (res.elements.length > 0 || res.relationships.length > 0) &&\n taggedEls.length === 0 &&\n taggedRels.length === 0\n ) {\n issues.push({\n severity: \"error\",\n code: \"delta.nothing-tagged\",\n message: `delta.likec4 declares elements/relationships but none are tagged #${featureId}; loam cannot see untagged changes`,\n });\n }\n }\n }\n // --- per-service specs (requirement operations) + openapi deltas ---\n const svcNames = await featureSpecServices(featureDir, context);\n\n // Every axis below joins on the service id, so endpoints resolve through the\n // element's `metadata { service }` binding — never through what the box is\n // called. The enumerated fleet — `services/` plus this feature's own\n // `specs/` directories, the same union `enumeratedTarget` probes below —\n // rides into the resolver so a delta that models CONTAINERS joins correctly:\n // without it, an edge into `payment.api` resolves to a service called \"api\"\n // that has never existed, and every check below grades the edge against an\n // absent service instead of the one that owns the container. An unenumerable\n // services/ falls back to the feature's own names — every living probe would\n // have found nothing there anyway.\n const fleetIds = await enumeratedServiceIds(docsDir, context);\n const svcOf = serviceResolver(elements, new Set<string>([...fleetIds, ...svcNames]));\n\n // Which elements the landscape merge would SPLICE: the tagged ones and\n // everything nested inside their blocks. Defined here because two checks\n // share it — the scaffold gate just below, and the binding-grammar check\n // near the end of this function — and the merge carries a tagged element's\n // authored block over byte for byte, children included, so a scope narrower\n // than the splice lets a nested child's text ride through unread.\n const taggedIds = taggedEls.map((el) => el.id);\n const ridesWithTag = (el: Elem): boolean =>\n el.tags.includes(featureId) || taggedIds.some((id) => el.id.startsWith(`${id}.`));\n\n // The scaffold gate: placeholder text and an unwritten intent are legal\n // documents whose merge would publish content nobody authored — the exact\n // template strings `loam new` wrote (authoring/scaffold.ts owns the list).\n issues.push(\n ...(await authoringIssues({ featureDir, featureId, splicedEls: elements.filter(ridesWithTag), svcNames, context })),\n );\n const {\n reqOps, removingOps, featureApiOps, unreadableApis,\n } = await declaredByService(scope, svcNames, issues, context);\n const {\n governedByLivingSpec, edgeConsumers, requirementConsumers, definedElsewhere,\n deprecatedInLiving, undeprecatedByFeature,\n } = coherenceLookups(scope, context);\n\n for (const [svc, retiring] of removingOps) {\n for (const op of retiring) {\n const consumers = [...(await edgeConsumers(svc, op)), ...(await requirementConsumers(svc, op))];\n if (consumers.length === 0) continue;\n issues.push({\n severity: \"error\",\n code: \"openapi.remove-op-consumed\",\n subject: svc,\n message: `${svc}: this feature retires '${op}', but the living fleet still consumes it — ${consumers.join(\"; \")}. Retire the consumer in the same feature (drop the edge from architecture/landscape.likec4, or the requirement that names it), or archive with --approve to break it deliberately.`,\n });\n }\n }\n\n // E1: Spec -> API — every operation a requirement governs must exist in that service's OpenAPI.\n for (const [svc, ops] of reqOps) {\n // The union below reads the feature's own contract; unreadable means the\n // op may well be defined in the file nobody could open, and `op-undefined`\n // would point the author at the requirement when the truth is the YAML.\n // `openapi.invalid` (declared.ts) already gates; one breach, one finding.\n if (unreadableApis.has(svc)) continue;\n const available = await serviceOperationIds(docsDir, svc, featureDir, context);\n for (const op of ops) {\n if (available.includes(op)) continue;\n const other = await definedElsewhere(svc, op);\n if (other !== undefined) {\n issues.push({ severity: \"warn\", code: \"spec-api.op-pending\", message: `requirement in ${svc} governs '${op}', defined by in-flight ${other} — archive it first` });\n continue;\n }\n issues.push({ severity: \"error\", code: \"spec-api.op-undefined\", message: `requirement in ${svc} governs '${op}', not defined in ${svc}'s OpenAPI` });\n }\n }\n\n // The declared→enumerated bridge for edge TARGETS. `svcOf` answers with\n // whatever the document wrote — `metadata { service '../../x' }` parses in\n // LikeC4 without one error — and every living lookup below joins its answer\n // into `services/<target>/` or `specs/<target>/`. So a target resolves\n // through the enumerations first (the fleet's `services/` plus this\n // feature's own `specs/`, where a service it introduces lives), and a name\n // neither knows skips the lookups — no ops, not governed, not deprecated —\n // grading exactly as an absent service, spelled with the declared name.\n const enumeratedTarget = enumeratedServiceIndex(docsDir, svcNames, context);\n\n // E2 / W1 / W4: C4 edges vs API + requirements.\n for (const r of taggedRels) {\n if (r.op === undefined) {\n if ((r.title ?? \"\").toLowerCase().startsWith(\"call\")) {\n issues.push({ severity: \"warn\", code: \"c4.op-link-missing\", message: `edge ${svcOf(r.source)} → ${svcOf(r.target)} (\"${r.title}\") has no operation link (metadata { op })` });\n }\n continue;\n }\n const target = svcOf(r.target);\n const pathable = await enumeratedTarget(target);\n // Same suspension as E1, but only for the verdicts that READ a contract:\n // does the op exist, is it being removed, is it deprecated (whose\n // un-deprecation escape reads the feature contract too). Each of those\n // would be an opinion about a file nobody could open. The governance\n // question below is deliberately NOT suspended — it joins the edge against\n // requirement documents alone, so the broken YAML could never change its\n // answer, and silencing it deferred a real warning for no reason.\n const contractUnreadable = pathable !== undefined && unreadableApis.has(pathable);\n const available =\n pathable === undefined || contractUnreadable\n ? []\n : await serviceOperationIds(docsDir, pathable, featureDir, context);\n if (contractUnreadable) {\n // openapi.invalid (declared.ts) already gates; one breach, one finding.\n } else if (pathable !== undefined && removingOps.get(pathable)?.has(r.op) === true) {\n issues.push({ severity: \"error\", code: \"c4-api.op-removing\", message: `${svcOf(r.source)} builds new consumption on '${r.op}', which this feature removes from ${target}` });\n } else if (!available.includes(r.op)) {\n const other = await definedElsewhere(target, r.op);\n if (other !== undefined) {\n issues.push({ severity: \"warn\", code: \"c4-api.op-pending\", message: `${svcOf(r.source)} calls '${r.op}' on ${target}, defined by in-flight ${other} — archive it first` });\n } else {\n issues.push({ severity: \"error\", code: \"c4-api.op-undefined\", message: `${svcOf(r.source)} calls '${r.op}' on ${target}, but ${target}'s OpenAPI does not define it (contract broken)` });\n }\n }\n // Governed by THIS target's requirements, not by anybody's. An operationId\n // is unique only inside one contract, so the fleet-wide set this used to\n // ask meant a `getStatus` governed in service A silenced the question for\n // service B's entirely unrelated `getStatus` — the warning went quiet for\n // the edge that needed it, and only in repos where some other service\n // happened to reuse the name. The living half on the same line has always\n // joined per service (`governedByLivingSpec(target, …)`); this is the delta\n // half asking it the same way.\n if (pathable === undefined || (!(reqOps.get(pathable) ?? []).includes(r.op) && !(await governedByLivingSpec(pathable, r.op)))) {\n issues.push({ severity: \"warn\", code: \"c4.op-ungoverned\", message: `'${r.op}' is called by ${svcOf(r.source)} but no requirement governs it` });\n }\n // Lifecycle: this NEW tagged edge builds consumption on an operation the\n // living provider contract already marks deprecated. Advisory, never an\n // archive gate — the edge is legal and the contract holds — but new\n // consumption of a dying op deserves an eye before it ships. Quiet when\n // the feature's own openapi delta drops the flag: that state is the fix\n // in progress, not new consumption of a dying op.\n if (!contractUnreadable && pathable !== undefined && (await deprecatedInLiving(pathable, r.op)) && !(await undeprecatedByFeature(pathable, r.op))) {\n issues.push({ severity: \"warn\", code: \"c4-api.op-deprecated\", message: `${svcOf(r.source)} builds new consumption on '${r.op}', which ${target}'s living OpenAPI marks deprecated — prefer the replacement operation` });\n }\n }\n\n // W2: API -> C4 — every operation the feature adds should be consumed by some edge.\n const consumed = new Set(taggedRels.map((r) => r.op).filter((o): o is string => o !== undefined));\n for (const op of featureApiOps) {\n if (!consumed.has(op)) {\n issues.push({ severity: \"warn\", code: \"api.op-unconsumed\", message: `operation '${op}' is added but no architecture edge consumes it (provider-only or unmodeled)` });\n }\n }\n\n // W3: a new service should carry a requirement delta.\n for (const e of taggedEls) {\n const svc: string = elementService(e);\n if (e.kind === \"softwareSystem\" && !svcNames.some((n) => n === svc)) {\n issues.push({ severity: \"warn\", code: \"service.no-requirement-delta\", message: `new service ${svc} has no requirement delta under specs/` });\n }\n }\n\n // An explicit `metadata { service '<id>' }` binding, held to the same grammar\n // every `--service` flag is — on the tagged elements AND everything nested\n // inside their blocks. The scope is the splice's, not the tag's: the\n // landscape merge carries a tagged element's authored block over byte for\n // byte, children included (landscape-merge.ts's rides() exists exactly so a\n // child travels inside its parent's text), so an untagged container's\n // binding reaches the living map as surely as its tagged parent's — and\n // checking only the tagged elements themselves let it ride through unread.\n // The binding is the one name the archive TRUSTS: the merge splices it into\n // the living map verbatim, and the new-service scan probes\n // `services/<binding>/` with it — where `metadata { service '../outside-svc' }`,\n // which LikeC4 parses without a murmur, collapses the probe right out of\n // services/, and the spliced map fails the very next `validate --all`\n // (`landscape.binding-unknown`). Explicit bindings only, on purpose: a prose\n // title with no binding is legal C4, and a title becomes a path only through\n // `specs/<svc>/`, whose own grammar check (`delta.service-id-invalid`)\n // guards that route. `ridesWithTag` itself is defined up beside the scaffold\n // gate, which shares the splice's scope for the same reason.\n for (const e of elements.filter(ridesWithTag)) {\n if (e.service === undefined) continue;\n const problem = serviceIdProblem(e.service, \"metadata { service } binding\");\n if (problem === null) continue;\n issues.push({\n severity: \"error\",\n code: \"c4.service-binding-invalid\",\n subject: e.service,\n message:\n `'${e.title}' (${e.id}) — ${problem} ` +\n `\\`loam archive\\` would splice '${e.service}' into the living landscape.likec4 exactly as written, ` +\n `and probe services/${e.service}/ with it. Bind the element to the services/<id>/ directory it means.`,\n });\n }\n\n // The event axis — the same three-way agreement the operation checks above\n // grade, joined on the message name instead of the operationId: the\n // feature's asyncapi deltas (pins, markers, justification), the tagged\n // edges' publishes/consumes metadata, and the requirement deltas'\n // Publishes:/Consumes: lines (./events/events.ts owns the walk).\n issues.push(...(await eventCoherence({ scope, svcNames, taggedRels, svcOf, context })));\n\n return issues;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"coherence.js","sourceRoot":"","sources":["../../../src/core/coherence/coherence.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { loadFile, type Elem, type LoadedDoc, type Rel } from \"../c4/likec4.js\";\nimport { elementService, serviceResolver } from \"../c4/resolve/service.js\";\nimport { serviceIdProblem } from \"../kernel/ids/service.js\";\nimport { deltaShapeIssues } from \"../delta/delta.js\";\nimport type { Issue } from \"../vocabulary/issue.js\";\nimport { featurePaths } from \"../repo/paths.js\";\nimport { featureSpecServices } from \"../repo/repo.js\";\nimport { enumeratedServiceIds, enumeratedServiceIndex } from \"../repo/service-target.js\";\nimport { serviceOperationIds } from \"../openapi/doc.js\";\nimport type { FleetContext } from \"../fleet-context.js\";\nimport { declaredByService, type DeltaScope } from \"./declared.js\";\nimport { coherenceLookups } from \"./lookups.js\";\nimport { eventCoherence } from \"./events/events.js\";\nimport { authoringIssues } from \"./authoring/scaffold.js\";\nimport { glossaryDeltaIssues } from \"../glossary/delta.js\";\nimport { flowDeltaIssues } from \"../usecases/delta/flows.js\";\nimport { deploymentDeltaIssues, readFeatureDeployments } from \"../deployment/delta.js\";\nimport type { DocsDir, FeatureDir } from \"../kernel/ids/dirs.js\";\n\n\n/**\n * Cross-axis consistency for a feature: do C4 (architecture), requirements (behaviour),\n * and OpenAPI (contract) agree? Errors are hard (would corrupt the living docs on archive);\n * warnings surface softer misalignments.\n *\n * `preloadedDelta` is the feature's already-parsed delta.likec4, when the caller\n * has it — loading a LikeC4 document spins up a fresh Langium workspace, the\n * dominant per-feature cost, and validate/archive have both parsed the same file\n * moments earlier. Standalone calls omit it and nothing changes: the file is\n * loaded here, exactly as before.\n */\n\n/** How a caller names the feature being graded, and what it already has in hand. */\nexport interface CoherenceRequest {\n docsDir: DocsDir;\n featureDir: FeatureDir;\n featureId: string;\n /** The delta LikeC4 document, when the caller already parsed it. */\n preloadedDelta?: LoadedDoc;\n context?: FleetContext;\n}\n\nexport async function featureCoherence(request: CoherenceRequest): Promise<Issue[]> {\n const { docsDir, featureDir, featureId, preloadedDelta, context } = request;\n const scope: DeltaScope = { docsDir, featureDir, featureId };\n // Delta shape first: a diff that does not apply to the living spec explains\n // everything downstream, and it is the one breach that is silent without a check.\n const issues: Issue[] = await deltaShapeIssues(docsDir, featureDir, featureId, context);\n\n // The vocabulary axis, and it is asked first because it is cheap and\n // independent: one `existsSync` for a feature with no glossary/, and nothing\n // downstream reads its answer. A term this feature introduces that the living\n // glossary already defines is the one thing that can go wrong on a create-only\n // merge, and it must be caught before the copy, not after.\n issues.push(...(await glossaryDeltaIssues(docsDir, featureDir)));\n\n // The use-case axis, and it is asked here for the vocabulary axis's reasons\n // exactly: one walk over a directory that is usually absent, nothing\n // downstream reads its answer, and a create-only merge has one thing that can\n // go wrong. A flow this feature introduces that the living `architecture/`\n // already holds must be caught before the copy, not after it has replaced an\n // authored hop sequence.\n issues.push(...(await flowDeltaIssues(docsDir, featureDir)));\n issues.push(...(await deploymentDeltaIssues(docsDir, featureDir)));\n\n // And the topology's second question, which the collision check above cannot\n // ask: does the document READ against the map this feature's own merge would\n // leave behind. It matters more literally here than one axis over — a feature\n // that stands a service up in a new cluster declares that service in its\n // `delta.likec4` and instances it in the topology, so the element the document\n // names exists only post-merge — and without it the archive copies an\n // unreadable document into `architecture/` for the next reader's\n // `loam validate --all` to fail on, against a file they did not write.\n const topology = await readFeatureDeployments({\n docsDir,\n featureDir,\n featureId,\n // A function rather than the context itself, for the reason the use-case\n // overlay states: one memoised read does not justify an edge from\n // `core/deployment/` to `fleet-context`.\n ...(context === undefined ? {} : { load: (path: string) => context.loadLikeC4(path) }),\n });\n if (topology.kind === \"unreadable\") {\n issues.push({\n severity: \"error\",\n code: \"deployment.doc-invalid\",\n subject: featureId,\n // The opening sentence does not say whose file is at fault, on the\n // `usecase.flow-invalid` message's reasoning: the corpus is the feature's\n // `deployment/` PLUS the living `architecture/` documents it extends, so\n // the failing path can be a living one — a landscape that no longer\n // parses, a topology somebody else wrote. Every message carries its own\n // path, so a reader can place it.\n message:\n `this feature's deployment document(s) could not be read against the map its own merge would leave behind, ` +\n `so the archive would land an architecture/ nothing has checked. The corpus is the feature's deployment/ AND ` +\n `the living architecture/ documents it extends, so the fault may be in either — each message names its file. ` +\n `${topology.errors.length} problem(s): ${topology.errors.join(\"; \")}`,\n });\n }\n\n // --- C4 delta ---\n let elements: Elem[] = [];\n let taggedEls: Elem[] = [];\n let taggedRels: Rel[] = [];\n const deltaPath = featurePaths(featureDir).delta;\n if (existsSync(deltaPath)) {\n const res = preloadedDelta ?? (context === undefined ? await loadFile(deltaPath) : await context.loadLikeC4(deltaPath));\n if (res.errors.length > 0) {\n // An unreadable architecture axis can prove nothing — it must never count as coherent.\n issues.push({\n severity: \"error\",\n code: \"delta.invalid\",\n message: `delta.likec4 has ${res.errors.length} parse error(s) — architecture axis unreadable (run \\`loam validate --feature ${featureId}\\`)`,\n });\n } else {\n elements = res.elements;\n taggedEls = res.elements.filter((e) => e.tags.includes(featureId));\n taggedRels = res.relationships.filter((r) => r.tags.includes(featureId));\n // Every rule below filters by the feature tag, so a delta whose author\n // forgot the tags entirely passes every check and archives while merging\n // nothing. Whole-file only: once ANYTHING is tagged, deciding that an\n // untagged edge \"looks intended\" would be guessing.\n if (\n (res.elements.length > 0 || res.relationships.length > 0) &&\n taggedEls.length === 0 &&\n taggedRels.length === 0\n ) {\n issues.push({\n severity: \"error\",\n code: \"delta.nothing-tagged\",\n message: `delta.likec4 declares elements/relationships but none are tagged #${featureId}; loam cannot see untagged changes`,\n });\n }\n }\n }\n // --- per-service specs (requirement operations) + openapi deltas ---\n const svcNames = await featureSpecServices(featureDir, context);\n\n // Every axis below joins on the service id, so endpoints resolve through the\n // element's `metadata { service }` binding — never through what the box is\n // called. The enumerated fleet — `services/` plus this feature's own\n // `specs/` directories, the same union `enumeratedTarget` probes below —\n // rides into the resolver so a delta that models CONTAINERS joins correctly:\n // without it, an edge into `payment.api` resolves to a service called \"api\"\n // that has never existed, and every check below grades the edge against an\n // absent service instead of the one that owns the container. An unenumerable\n // services/ falls back to the feature's own names — every living probe would\n // have found nothing there anyway.\n const fleetIds = await enumeratedServiceIds(docsDir, context);\n const svcOf = serviceResolver(elements, new Set<string>([...fleetIds, ...svcNames]));\n\n // Which elements the landscape merge would SPLICE: the tagged ones and\n // everything nested inside their blocks. Defined here because two checks\n // share it — the scaffold gate just below, and the binding-grammar check\n // near the end of this function — and the merge carries a tagged element's\n // authored block over byte for byte, children included, so a scope narrower\n // than the splice lets a nested child's text ride through unread.\n const taggedIds = taggedEls.map((el) => el.id);\n const ridesWithTag = (el: Elem): boolean =>\n el.tags.includes(featureId) || taggedIds.some((id) => el.id.startsWith(`${id}.`));\n\n // The scaffold gate: placeholder text and an unwritten intent are legal\n // documents whose merge would publish content nobody authored — the exact\n // template strings `loam new` wrote (authoring/scaffold.ts owns the list).\n issues.push(\n ...(await authoringIssues({ featureDir, featureId, splicedEls: elements.filter(ridesWithTag), svcNames, context })),\n );\n const {\n reqOps, removingOps, featureApiOps, unreadableApis,\n } = await declaredByService(scope, svcNames, issues, context);\n const {\n governedByLivingSpec, edgeConsumers, requirementConsumers, definedElsewhere,\n deprecatedInLiving, undeprecatedByFeature,\n } = coherenceLookups(scope, context);\n\n for (const [svc, retiring] of removingOps) {\n for (const op of retiring) {\n const consumers = [...(await edgeConsumers(svc, op)), ...(await requirementConsumers(svc, op))];\n if (consumers.length === 0) continue;\n issues.push({\n severity: \"error\",\n code: \"openapi.remove-op-consumed\",\n subject: svc,\n message: `${svc}: this feature retires '${op}', but the living fleet still consumes it — ${consumers.join(\"; \")}. Retire the consumer in the same feature (drop the edge from architecture/landscape.likec4, or the requirement that names it), or archive with --approve to break it deliberately.`,\n });\n }\n }\n\n // E1: Spec -> API — every operation a requirement governs must exist in that service's OpenAPI.\n for (const [svc, ops] of reqOps) {\n // The union below reads the feature's own contract; unreadable means the\n // op may well be defined in the file nobody could open, and `op-undefined`\n // would point the author at the requirement when the truth is the YAML.\n // `openapi.invalid` (declared.ts) already gates; one breach, one finding.\n if (unreadableApis.has(svc)) continue;\n const available = await serviceOperationIds(docsDir, svc, featureDir, context);\n for (const op of ops) {\n if (available.includes(op)) continue;\n const other = await definedElsewhere(svc, op);\n if (other !== undefined) {\n issues.push({ severity: \"warn\", code: \"spec-api.op-pending\", message: `requirement in ${svc} governs '${op}', defined by in-flight ${other} — archive it first` });\n continue;\n }\n issues.push({ severity: \"error\", code: \"spec-api.op-undefined\", message: `requirement in ${svc} governs '${op}', not defined in ${svc}'s OpenAPI` });\n }\n }\n\n // The declared→enumerated bridge for edge TARGETS. `svcOf` answers with\n // whatever the document wrote — `metadata { service '../../x' }` parses in\n // LikeC4 without one error — and every living lookup below joins its answer\n // into `services/<target>/` or `specs/<target>/`. So a target resolves\n // through the enumerations first (the fleet's `services/` plus this\n // feature's own `specs/`, where a service it introduces lives), and a name\n // neither knows skips the lookups — no ops, not governed, not deprecated —\n // grading exactly as an absent service, spelled with the declared name.\n const enumeratedTarget = enumeratedServiceIndex(docsDir, svcNames, context);\n\n // E2 / W1 / W4: C4 edges vs API + requirements.\n for (const r of taggedRels) {\n if (r.op === undefined) {\n if ((r.title ?? \"\").toLowerCase().startsWith(\"call\")) {\n issues.push({ severity: \"warn\", code: \"c4.op-link-missing\", message: `edge ${svcOf(r.source)} → ${svcOf(r.target)} (\"${r.title}\") has no operation link (metadata { op })` });\n }\n continue;\n }\n const target = svcOf(r.target);\n const pathable = await enumeratedTarget(target);\n // Same suspension as E1, but only for the verdicts that READ a contract:\n // does the op exist, is it being removed, is it deprecated (whose\n // un-deprecation escape reads the feature contract too). Each of those\n // would be an opinion about a file nobody could open. The governance\n // question below is deliberately NOT suspended — it joins the edge against\n // requirement documents alone, so the broken YAML could never change its\n // answer, and silencing it deferred a real warning for no reason.\n const contractUnreadable = pathable !== undefined && unreadableApis.has(pathable);\n const available =\n pathable === undefined || contractUnreadable\n ? []\n : await serviceOperationIds(docsDir, pathable, featureDir, context);\n if (contractUnreadable) {\n // openapi.invalid (declared.ts) already gates; one breach, one finding.\n } else if (pathable !== undefined && removingOps.get(pathable)?.has(r.op) === true) {\n issues.push({ severity: \"error\", code: \"c4-api.op-removing\", message: `${svcOf(r.source)} builds new consumption on '${r.op}', which this feature removes from ${target}` });\n } else if (!available.includes(r.op)) {\n const other = await definedElsewhere(target, r.op);\n if (other !== undefined) {\n issues.push({ severity: \"warn\", code: \"c4-api.op-pending\", message: `${svcOf(r.source)} calls '${r.op}' on ${target}, defined by in-flight ${other} — archive it first` });\n } else {\n issues.push({ severity: \"error\", code: \"c4-api.op-undefined\", message: `${svcOf(r.source)} calls '${r.op}' on ${target}, but ${target}'s OpenAPI does not define it (contract broken)` });\n }\n }\n // Governed by THIS target's requirements, not by anybody's. An operationId\n // is unique only inside one contract, so the fleet-wide set this used to\n // ask meant a `getStatus` governed in service A silenced the question for\n // service B's entirely unrelated `getStatus` — the warning went quiet for\n // the edge that needed it, and only in repos where some other service\n // happened to reuse the name. The living half on the same line has always\n // joined per service (`governedByLivingSpec(target, …)`); this is the delta\n // half asking it the same way.\n if (pathable === undefined || (!(reqOps.get(pathable) ?? []).includes(r.op) && !(await governedByLivingSpec(pathable, r.op)))) {\n issues.push({ severity: \"warn\", code: \"c4.op-ungoverned\", message: `'${r.op}' is called by ${svcOf(r.source)} but no requirement governs it` });\n }\n // Lifecycle: this NEW tagged edge builds consumption on an operation the\n // living provider contract already marks deprecated. Advisory, never an\n // archive gate — the edge is legal and the contract holds — but new\n // consumption of a dying op deserves an eye before it ships. Quiet when\n // the feature's own openapi delta drops the flag: that state is the fix\n // in progress, not new consumption of a dying op.\n if (!contractUnreadable && pathable !== undefined && (await deprecatedInLiving(pathable, r.op)) && !(await undeprecatedByFeature(pathable, r.op))) {\n issues.push({ severity: \"warn\", code: \"c4-api.op-deprecated\", message: `${svcOf(r.source)} builds new consumption on '${r.op}', which ${target}'s living OpenAPI marks deprecated — prefer the replacement operation` });\n }\n }\n\n // W2: API -> C4 — every operation the feature adds should be consumed by some edge.\n const consumed = new Set(taggedRels.map((r) => r.op).filter((o): o is string => o !== undefined));\n for (const op of featureApiOps) {\n if (!consumed.has(op)) {\n issues.push({ severity: \"warn\", code: \"api.op-unconsumed\", message: `operation '${op}' is added but no architecture edge consumes it (provider-only or unmodeled)` });\n }\n }\n\n // W3: a new service should carry a requirement delta.\n for (const e of taggedEls) {\n const svc: string = elementService(e);\n if (e.kind === \"softwareSystem\" && !svcNames.some((n) => n === svc)) {\n issues.push({ severity: \"warn\", code: \"service.no-requirement-delta\", message: `new service ${svc} has no requirement delta under specs/` });\n }\n }\n\n // An explicit `metadata { service '<id>' }` binding, held to the same grammar\n // every `--service` flag is — on the tagged elements AND everything nested\n // inside their blocks. The scope is the splice's, not the tag's: the\n // landscape merge carries a tagged element's authored block over byte for\n // byte, children included (landscape-merge.ts's rides() exists exactly so a\n // child travels inside its parent's text), so an untagged container's\n // binding reaches the living map as surely as its tagged parent's — and\n // checking only the tagged elements themselves let it ride through unread.\n // The binding is the one name the archive TRUSTS: the merge splices it into\n // the living map verbatim, and the new-service scan probes\n // `services/<binding>/` with it — where `metadata { service '../outside-svc' }`,\n // which LikeC4 parses without a murmur, collapses the probe right out of\n // services/, and the spliced map fails the very next `validate --all`\n // (`landscape.binding-unknown`). Explicit bindings only, on purpose: a prose\n // title with no binding is legal C4, and a title becomes a path only through\n // `specs/<svc>/`, whose own grammar check (`delta.service-id-invalid`)\n // guards that route. `ridesWithTag` itself is defined up beside the scaffold\n // gate, which shares the splice's scope for the same reason.\n for (const e of elements.filter(ridesWithTag)) {\n if (e.service === undefined) continue;\n const problem = serviceIdProblem(e.service, \"metadata { service } binding\");\n if (problem === null) continue;\n issues.push({\n severity: \"error\",\n code: \"c4.service-binding-invalid\",\n subject: e.service,\n message:\n `'${e.title}' (${e.id}) — ${problem} ` +\n `\\`loam archive\\` would splice '${e.service}' into the living landscape.likec4 exactly as written, ` +\n `and probe services/${e.service}/ with it. Bind the element to the services/<id>/ directory it means.`,\n });\n }\n\n // The event axis — the same three-way agreement the operation checks above\n // grade, joined on the message name instead of the operationId: the\n // feature's asyncapi deltas (pins, markers, justification), the tagged\n // edges' publishes/consumes metadata, and the requirement deltas'\n // Publishes:/Consumes: lines (./events/events.ts owns the walk).\n issues.push(...(await eventCoherence({ scope, svcNames, taggedRels, svcOf, context })));\n\n return issues;\n}\n"]}
|