@ybotok/loam 0.2.0-alpha.1 → 0.2.0-alpha.2

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.
Files changed (65) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/README.md +6 -6
  3. package/ROADMAP.md +123 -18
  4. package/dist/commands/archive/plan/gate.js +18 -12
  5. package/dist/commands/archive/plan/gate.js.map +1 -1
  6. package/dist/commands/archive/plan/landscape.js +19 -0
  7. package/dist/commands/archive/plan/landscape.js.map +1 -1
  8. package/dist/commands/archive/run.js +2 -1
  9. package/dist/commands/archive/run.js.map +1 -1
  10. package/dist/commands/validate/arch-coverage.js +11 -1
  11. package/dist/commands/validate/arch-coverage.js.map +1 -1
  12. package/dist/commands/validate/fleet/obligations.js +43 -9
  13. package/dist/commands/validate/fleet/obligations.js.map +1 -1
  14. package/dist/commands/validate/fleet/usecases/steps.js +3 -3
  15. package/dist/commands/validate/fleet/usecases/steps.js.map +1 -1
  16. package/dist/commands/validate/service/service.js +1 -0
  17. package/dist/commands/validate/service/service.js.map +1 -1
  18. package/dist/commands/validate/service/specs.js +33 -1
  19. package/dist/commands/validate/service/specs.js.map +1 -1
  20. package/dist/core/agent/workflows/check.js +3 -0
  21. package/dist/core/agent/workflows/check.js.map +1 -1
  22. package/dist/core/brief/unchecked.js +1 -0
  23. package/dist/core/brief/unchecked.js.map +1 -1
  24. package/dist/core/c4/arch.js +72 -180
  25. package/dist/core/c4/arch.js.map +1 -1
  26. package/dist/core/c4/likec4.js +2 -0
  27. package/dist/core/c4/likec4.js.map +1 -1
  28. package/dist/core/c4/parsed/deployment.js +158 -0
  29. package/dist/core/c4/parsed/deployment.js.map +1 -0
  30. package/dist/core/c4/project/load.js +4 -0
  31. package/dist/core/c4/project/load.js.map +1 -1
  32. package/dist/core/c4/project/staged.js +220 -0
  33. package/dist/core/c4/project/staged.js.map +1 -0
  34. package/dist/core/c4/resolve/service.js +46 -0
  35. package/dist/core/c4/resolve/service.js.map +1 -1
  36. package/dist/core/c4/resolve/steps.js +144 -0
  37. package/dist/core/c4/resolve/steps.js.map +1 -0
  38. package/dist/core/c4/splice/delta-blocks.js +15 -2
  39. package/dist/core/c4/splice/delta-blocks.js.map +1 -1
  40. package/dist/core/c4/workspace.js +2 -0
  41. package/dist/core/c4/workspace.js.map +1 -1
  42. package/dist/core/coherence/coherence.js +36 -0
  43. package/dist/core/coherence/coherence.js.map +1 -1
  44. package/dist/core/deployment/delta.js +188 -0
  45. package/dist/core/deployment/delta.js.map +1 -0
  46. package/dist/core/deployment/objects.js +145 -0
  47. package/dist/core/deployment/objects.js.map +1 -0
  48. package/dist/core/pack/living.js +19 -0
  49. package/dist/core/pack/living.js.map +1 -1
  50. package/dist/core/repo/authored/paths.js +18 -0
  51. package/dist/core/repo/authored/paths.js.map +1 -1
  52. package/dist/core/repo/paths.js +43 -2
  53. package/dist/core/repo/paths.js.map +1 -1
  54. package/dist/core/status/feature/artifacts.js +29 -4
  55. package/dist/core/status/feature/artifacts.js.map +1 -1
  56. package/dist/core/status/fleet/fleet.js +11 -1
  57. package/dist/core/status/fleet/fleet.js.map +1 -1
  58. package/dist/core/usecases/delta/overlay.js +28 -191
  59. package/dist/core/usecases/delta/overlay.js.map +1 -1
  60. package/dist/core/usecases/fleet.js.map +1 -1
  61. package/dist/core/usecases/operations.js +2 -2
  62. package/dist/core/usecases/operations.js.map +1 -1
  63. package/dist/core/vocabulary/issue.js +3 -0
  64. package/dist/core/vocabulary/issue.js.map +1 -1
  65. package/package.json +1 -1
@@ -0,0 +1,188 @@
1
+ /**
2
+ * A feature's own topology: `features/<FEAT>/deployment/<name>.likec4`.
3
+ *
4
+ * Until this slot existed a change that stood up a standby cluster had to be
5
+ * written in two places at once — the requirement in the feature, the topology
6
+ * edited straight into `architecture/` — because `delta.likec4` refuses a
7
+ * `deployment { }` block and always will: that document re-declares the
8
+ * landscape's own identifiers and carries its own `specification`, so it cannot
9
+ * be staged beside the map in one LikeC4 project. A document that declares
10
+ * NEITHER can, which is exactly why the use-case axis put its flows in files of
11
+ * their own, and why this mirrors it rather than inventing a shape.
12
+ *
13
+ * WHAT MAKES IT WORK, and it was measured before the axis was written: LikeC4
14
+ * `extend` resolves across documents of one project. A feature writes
15
+ *
16
+ * deployment {
17
+ * extend eu {
18
+ * dcC = datacenter 'DC-C' { k8sC = cluster 'cluster-c' { … } }
19
+ * }
20
+ * eu.dcA.k8sA -> eu.dcC.k8sC 'async replication'
21
+ * }
22
+ *
23
+ * and the living region gains a datacenter with the living file untouched. So
24
+ * the merge is a copy, not a splice, and `unarchive` needs nothing new — a
25
+ * create is undone by deleting, which the snapshot manifest already records for
26
+ * every write in the plan.
27
+ *
28
+ * CREATE-ONLY, for `core/usecases/delta/flows.ts`'s reasons rather than a
29
+ * weaker version of them. A requirement delta merges INTO a living document and
30
+ * two features editing one requirement would overwrite each other silently; a
31
+ * topology document has no such algebra — the file is one block, the merge is a
32
+ * whole-file copy, and there is nothing to merge partially. Rewriting a living
33
+ * one through a feature would be a silent whole-file replacement with no pin to
34
+ * collide on, where editing `architecture/<name>.likec4` directly in the same
35
+ * pull request produces an ordinary git conflict a human resolves.
36
+ *
37
+ * ## Graded against the map its own merge would leave behind
38
+ *
39
+ * `ARCH-LOAM-FEATURE-CORPUS`, and this axis needs it more literally than the
40
+ * use-case one does: a feature that stands a service up in a new cluster
41
+ * declares that service in its `delta.likec4` and instances it here, so the
42
+ * element the topology names exists ONLY post-merge. Read against the living
43
+ * landscape it is unresolved, LikeC4 refuses the whole project, and loam would
44
+ * report a hole against a file whose only fault is arriving with the change
45
+ * that makes it true.
46
+ *
47
+ * The staging is `core/c4/project/staged.ts`, shared with the use-case slot
48
+ * rather than copied — the two would have disagreed first about the merge
49
+ * preview, which is the one thing both refusals rest on.
50
+ */
51
+ import { existsSync } from "node:fs";
52
+ import { basename, join, relative, resolve } from "node:path";
53
+ import { architectureDir } from "../c4/project/architecture.js";
54
+ import { stageMergedProject } from "../c4/project/staged.js";
55
+ import {} from "../c4/likec4.js";
56
+ import { architectureDocuments } from "../c4/project/documents.js";
57
+ import { featureDeploymentDir } from "../repo/authored/paths.js";
58
+ import { reservedArchitectureName } from "../repo/paths.js";
59
+ import {} from "../vocabulary/issue.js";
60
+ /**
61
+ * The topology documents one feature carries, ordered by path.
62
+ *
63
+ * Zero readdirs when the feature has no `deployment/` directory, which is every
64
+ * feature in a fleet that has not adopted the axis: `architectureDocuments`
65
+ * answers an absent directory with an empty list rather than a throw. That
66
+ * directory's existence is the whole per-feature opt-in.
67
+ */
68
+ export async function featureDeployments(featureDir) {
69
+ const dir = featureDeploymentDir(featureDir);
70
+ const paths = await architectureDocuments(dir);
71
+ const base = resolve(dir);
72
+ return paths.map((path) => ({ rel: relative(base, path).split(/[\\/]/).join("/"), path }));
73
+ }
74
+ /**
75
+ * The LIVING path a feature-local topology document addresses.
76
+ *
77
+ * Straight into `architecture/`, with no subdirectory of its own — unlike
78
+ * `architecture/usecases/`, and the difference is the model rather than taste.
79
+ * A `deployment { }` block is part of the fleet's ONE architecture project and
80
+ * is read as such by every loader; filing it under a subdirectory would suggest
81
+ * a second project, which is exactly the reading `subsystems.likec4` and
82
+ * `landscape.likec4` already refuse by sitting at the top level together.
83
+ *
84
+ * `rel` is joined un-split, which `node:path` normalizes. What holds the join is
85
+ * PROVENANCE: every `rel` here came out of an `architectureDocuments` walk, so
86
+ * each component is a directory entry that exists. No caller takes one from argv.
87
+ */
88
+ export function livingDeploymentPath(docsDir, rel) {
89
+ return join(architectureDir(docsDir), rel);
90
+ }
91
+ /**
92
+ * The two things that can go wrong about a topology document's NAME, before
93
+ * anything about its contents is asked.
94
+ *
95
+ * `deployment.doc-reserved` — the name is one loam owns inside `architecture/`.
96
+ * The merge writes straight into that directory, unlike a flow, which lands
97
+ * under `usecases/`, so `<name>` is free to collide with the living map, the
98
+ * generated subsystem views, or the flow tree. `reservedArchitectureName`
99
+ * carries which and why. It is asked FIRST and WITHOUT an existence test,
100
+ * because two of the three fail silently when the target does not exist yet.
101
+ *
102
+ * `deployment.doc-exists` — a topology document this feature introduces that
103
+ * the living `architecture/` already holds.
104
+ *
105
+ * An ERROR and never a warning-that-gates, for `usecase.flow-exists`'s reason:
106
+ * there is no legal reading of it. The merge would replace an authored topology
107
+ * wholesale, and no `--approve` makes that a different act than it is. Two
108
+ * features introducing the same file is the case that makes it load-bearing
109
+ * rather than pedantic — whichever archives first creates the living document,
110
+ * and the second is told before it merges that its own file is the one to
111
+ * delete.
112
+ *
113
+ * The collision is on the FILE, not on what is inside it. Two features may both
114
+ * extend the same living region from two documents of their own and both
115
+ * archive; that is the shape `extend` exists for, and refusing it would make the
116
+ * slot useless for the fleet-wide change it was built for.
117
+ */
118
+ export async function deploymentDeltaIssues(docsDir, featureDir) {
119
+ const docs = await featureDeployments(featureDir);
120
+ const issues = [];
121
+ // The reservation runs FIRST and independently of existence, because two of
122
+ // the three names it refuses need not exist for the merge to be wrong. A
123
+ // topology copied over `subsystems.likec4` is destroyed by the next
124
+ // `loam subsystem sync` and is invisible to every project load until then —
125
+ // the merge would report `created architecture/subsystems.likec4` and nothing
126
+ // downstream would ever say another word about it.
127
+ for (const doc of docs) {
128
+ const reason = reservedArchitectureName(doc.rel);
129
+ if (reason === null)
130
+ continue;
131
+ issues.push({
132
+ severity: "error",
133
+ code: "deployment.doc-reserved",
134
+ subject: doc.rel,
135
+ message: `features/<FEAT>/deployment/${doc.rel} would merge to architecture/${doc.rel}, and that name is loam's: ` +
136
+ `${reason}. Rename the feature's document — its name is yours to choose, and \`extend\` is how it adds to ` +
137
+ "what a living document already declares.",
138
+ });
139
+ }
140
+ return issues.concat(docs
141
+ .filter((doc) => reservedArchitectureName(doc.rel) === null)
142
+ .filter((doc) => existsSync(livingDeploymentPath(docsDir, doc.rel)))
143
+ .map((doc) => ({
144
+ severity: "error",
145
+ code: "deployment.doc-exists",
146
+ subject: doc.rel,
147
+ message: `architecture/${doc.rel} already exists, so this feature's topology would replace an authored one wholesale — ` +
148
+ "a feature-local deployment document INTRODUCES topology, it does not rewrite it. " +
149
+ `Delete features/<FEAT>/deployment/${doc.rel} and edit architecture/${doc.rel} directly in this same change, ` +
150
+ "where git produces an ordinary conflict if somebody else is editing it too. " +
151
+ `To ADD to what that file declares, write a document of another name that says \`extend\` — ` +
152
+ `\`${suggest(doc.rel)}\` — which is what lets two features grow one region without touching each other.`,
153
+ })));
154
+ }
155
+ /**
156
+ * Parse this feature's topology documents over the post-merge map.
157
+ *
158
+ * A feature with no `deployment/` directory is `read` at the cost of one walk
159
+ * over a directory that is not there — the axis's per-feature opt-in, and the
160
+ * whole price a fleet that has not adopted it pays. Nothing is staged, nothing
161
+ * is parsed, and no temp tree is made.
162
+ *
163
+ * The verdict is deliberately BINARY. The use-case overlay hands its caller the
164
+ * views it read because the axis has questions about their content; this one
165
+ * has none — a topology document is checked by being parseable against the map
166
+ * the merge will write, and everything else about it is graded by
167
+ * `loam validate --all` once it is living, exactly as the landscape is.
168
+ */
169
+ export async function readFeatureDeployments(req) {
170
+ const docs = await featureDeployments(req.featureDir);
171
+ if (docs.length === 0)
172
+ return { kind: "read" };
173
+ const staged = await stageMergedProject({
174
+ docsDir: req.docsDir,
175
+ featureDir: req.featureDir,
176
+ featureId: req.featureId,
177
+ documents: docs.map((doc) => ({ rel: doc.rel, path: doc.path })),
178
+ ...(req.load === undefined ? {} : { load: req.load }),
179
+ });
180
+ return staged.kind === "unreadable" ? { kind: "unreadable", errors: staged.errors } : { kind: "read" };
181
+ }
182
+ /** A free name near the colliding one, so the fix is a rename the author can copy. */
183
+ function suggest(rel) {
184
+ const name = basename(rel).replace(/\.likec4$/, "");
185
+ const dir = rel.slice(0, rel.length - basename(rel).length);
186
+ return `${dir}${name}-extend.likec4`;
187
+ }
188
+ //# sourceMappingURL=delta.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delta.js","sourceRoot":"","sources":["../../../src/core/deployment/delta.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A feature's own topology: `features/<FEAT>/deployment/<name>.likec4`.\n *\n * Until this slot existed a change that stood up a standby cluster had to be\n * written in two places at once — the requirement in the feature, the topology\n * edited straight into `architecture/` — because `delta.likec4` refuses a\n * `deployment { }` block and always will: that document re-declares the\n * landscape's own identifiers and carries its own `specification`, so it cannot\n * be staged beside the map in one LikeC4 project. A document that declares\n * NEITHER can, which is exactly why the use-case axis put its flows in files of\n * their own, and why this mirrors it rather than inventing a shape.\n *\n * WHAT MAKES IT WORK, and it was measured before the axis was written: LikeC4\n * `extend` resolves across documents of one project. A feature writes\n *\n * deployment {\n * extend eu {\n * dcC = datacenter 'DC-C' { k8sC = cluster 'cluster-c' { … } }\n * }\n * eu.dcA.k8sA -> eu.dcC.k8sC 'async replication'\n * }\n *\n * and the living region gains a datacenter with the living file untouched. So\n * the merge is a copy, not a splice, and `unarchive` needs nothing new — a\n * create is undone by deleting, which the snapshot manifest already records for\n * every write in the plan.\n *\n * CREATE-ONLY, for `core/usecases/delta/flows.ts`'s reasons rather than a\n * weaker version of them. A requirement delta merges INTO a living document and\n * two features editing one requirement would overwrite each other silently; a\n * topology document has no such algebra — the file is one block, the merge is a\n * whole-file copy, and there is nothing to merge partially. Rewriting a living\n * one through a feature would be a silent whole-file replacement with no pin to\n * collide on, where editing `architecture/<name>.likec4` directly in the same\n * pull request produces an ordinary git conflict a human resolves.\n *\n * ## Graded against the map its own merge would leave behind\n *\n * `ARCH-LOAM-FEATURE-CORPUS`, and this axis needs it more literally than the\n * use-case one does: a feature that stands a service up in a new cluster\n * declares that service in its `delta.likec4` and instances it here, so the\n * element the topology names exists ONLY post-merge. Read against the living\n * landscape it is unresolved, LikeC4 refuses the whole project, and loam would\n * report a hole against a file whose only fault is arriving with the change\n * that makes it true.\n *\n * The staging is `core/c4/project/staged.ts`, shared with the use-case slot\n * rather than copied — the two would have disagreed first about the merge\n * preview, which is the one thing both refusals rest on.\n */\nimport { existsSync } from \"node:fs\";\nimport { basename, join, relative, resolve } from \"node:path\";\nimport { architectureDir } from \"../c4/project/architecture.js\";\nimport { stageMergedProject } from \"../c4/project/staged.js\";\nimport { type LoadedDoc } from \"../c4/likec4.js\";\nimport { architectureDocuments } from \"../c4/project/documents.js\";\nimport { featureDeploymentDir } from \"../repo/authored/paths.js\";\nimport { reservedArchitectureName } from \"../repo/paths.js\";\nimport type { DocsDir, FeatureDir } from \"../kernel/ids/dirs.js\";\nimport { type Issue } from \"../vocabulary/issue.js\";\n\n/** One topology document a feature carries. */\nexport interface FeatureDeployment {\n /**\n * The path INSIDE the feature's `deployment/` directory, `/`-separated — the\n * id this axis has. Nesting is spelled by the tree, as it is for every other\n * authored tree in the repo.\n */\n rel: string;\n /** The absolute path of the authored file. */\n path: string;\n}\n\n/**\n * The topology documents one feature carries, ordered by path.\n *\n * Zero readdirs when the feature has no `deployment/` directory, which is every\n * feature in a fleet that has not adopted the axis: `architectureDocuments`\n * answers an absent directory with an empty list rather than a throw. That\n * directory's existence is the whole per-feature opt-in.\n */\nexport async function featureDeployments(featureDir: FeatureDir): Promise<FeatureDeployment[]> {\n const dir = featureDeploymentDir(featureDir);\n const paths = await architectureDocuments(dir);\n const base = resolve(dir);\n return paths.map((path) => ({ rel: relative(base, path).split(/[\\\\/]/).join(\"/\"), path }));\n}\n\n/**\n * The LIVING path a feature-local topology document addresses.\n *\n * Straight into `architecture/`, with no subdirectory of its own — unlike\n * `architecture/usecases/`, and the difference is the model rather than taste.\n * A `deployment { }` block is part of the fleet's ONE architecture project and\n * is read as such by every loader; filing it under a subdirectory would suggest\n * a second project, which is exactly the reading `subsystems.likec4` and\n * `landscape.likec4` already refuse by sitting at the top level together.\n *\n * `rel` is joined un-split, which `node:path` normalizes. What holds the join is\n * PROVENANCE: every `rel` here came out of an `architectureDocuments` walk, so\n * each component is a directory entry that exists. No caller takes one from argv.\n */\nexport function livingDeploymentPath(docsDir: DocsDir, rel: string): string {\n return join(architectureDir(docsDir), rel);\n}\n\n/**\n * The two things that can go wrong about a topology document's NAME, before\n * anything about its contents is asked.\n *\n * `deployment.doc-reserved` — the name is one loam owns inside `architecture/`.\n * The merge writes straight into that directory, unlike a flow, which lands\n * under `usecases/`, so `<name>` is free to collide with the living map, the\n * generated subsystem views, or the flow tree. `reservedArchitectureName`\n * carries which and why. It is asked FIRST and WITHOUT an existence test,\n * because two of the three fail silently when the target does not exist yet.\n *\n * `deployment.doc-exists` — a topology document this feature introduces that\n * the living `architecture/` already holds.\n *\n * An ERROR and never a warning-that-gates, for `usecase.flow-exists`'s reason:\n * there is no legal reading of it. The merge would replace an authored topology\n * wholesale, and no `--approve` makes that a different act than it is. Two\n * features introducing the same file is the case that makes it load-bearing\n * rather than pedantic — whichever archives first creates the living document,\n * and the second is told before it merges that its own file is the one to\n * delete.\n *\n * The collision is on the FILE, not on what is inside it. Two features may both\n * extend the same living region from two documents of their own and both\n * archive; that is the shape `extend` exists for, and refusing it would make the\n * slot useless for the fleet-wide change it was built for.\n */\nexport async function deploymentDeltaIssues(docsDir: DocsDir, featureDir: FeatureDir): Promise<Issue[]> {\n const docs = await featureDeployments(featureDir);\n const issues: Issue[] = [];\n // The reservation runs FIRST and independently of existence, because two of\n // the three names it refuses need not exist for the merge to be wrong. A\n // topology copied over `subsystems.likec4` is destroyed by the next\n // `loam subsystem sync` and is invisible to every project load until then —\n // the merge would report `created architecture/subsystems.likec4` and nothing\n // downstream would ever say another word about it.\n for (const doc of docs) {\n const reason = reservedArchitectureName(doc.rel);\n if (reason === null) continue;\n issues.push({\n severity: \"error\",\n code: \"deployment.doc-reserved\",\n subject: doc.rel,\n message:\n `features/<FEAT>/deployment/${doc.rel} would merge to architecture/${doc.rel}, and that name is loam's: ` +\n `${reason}. Rename the feature's document — its name is yours to choose, and \\`extend\\` is how it adds to ` +\n \"what a living document already declares.\",\n });\n }\n return issues.concat(\n docs\n .filter((doc) => reservedArchitectureName(doc.rel) === null)\n .filter((doc) => existsSync(livingDeploymentPath(docsDir, doc.rel)))\n .map((doc) => ({\n severity: \"error\" as const,\n code: \"deployment.doc-exists\" as const,\n subject: doc.rel,\n message:\n `architecture/${doc.rel} already exists, so this feature's topology would replace an authored one wholesale — ` +\n \"a feature-local deployment document INTRODUCES topology, it does not rewrite it. \" +\n `Delete features/<FEAT>/deployment/${doc.rel} and edit architecture/${doc.rel} directly in this same change, ` +\n \"where git produces an ordinary conflict if somebody else is editing it too. \" +\n `To ADD to what that file declares, write a document of another name that says \\`extend\\` — ` +\n `\\`${suggest(doc.rel)}\\` — which is what lets two features grow one region without touching each other.`,\n })),\n );\n}\n\n/** What the caller needs to grade this feature's topology against the merge preview. */\nexport interface DeploymentOverlayRequest {\n docsDir: DocsDir;\n featureDir: FeatureDir;\n /** The feature's id — the tag that selects the delta's own additions. */\n featureId: string;\n /**\n * The caller's memoised LikeC4 read, when it has one. A FUNCTION rather than\n * the `FleetContext` itself, for the reason the use-case overlay states: one\n * memoised read does not justify an edge from this package to\n * `core/fleet-context.ts`, and the edge would push it up a DAG level for\n * nothing.\n */\n load?: (path: string) => Promise<LoadedDoc>;\n}\n\n/** Whether this feature's topology could be read at all, against the map its merge would leave. */\nexport type DeploymentOverlay = { kind: \"read\" } | { kind: \"unreadable\"; errors: string[] };\n\n/**\n * Parse this feature's topology documents over the post-merge map.\n *\n * A feature with no `deployment/` directory is `read` at the cost of one walk\n * over a directory that is not there — the axis's per-feature opt-in, and the\n * whole price a fleet that has not adopted it pays. Nothing is staged, nothing\n * is parsed, and no temp tree is made.\n *\n * The verdict is deliberately BINARY. The use-case overlay hands its caller the\n * views it read because the axis has questions about their content; this one\n * has none — a topology document is checked by being parseable against the map\n * the merge will write, and everything else about it is graded by\n * `loam validate --all` once it is living, exactly as the landscape is.\n */\nexport async function readFeatureDeployments(req: DeploymentOverlayRequest): Promise<DeploymentOverlay> {\n const docs = await featureDeployments(req.featureDir);\n if (docs.length === 0) return { kind: \"read\" };\n const staged = await stageMergedProject({\n docsDir: req.docsDir,\n featureDir: req.featureDir,\n featureId: req.featureId,\n documents: docs.map((doc) => ({ rel: doc.rel, path: doc.path })),\n ...(req.load === undefined ? {} : { load: req.load }),\n });\n return staged.kind === \"unreadable\" ? { kind: \"unreadable\", errors: staged.errors } : { kind: \"read\" };\n}\n\n/** A free name near the colliding one, so the fix is a rename the author can copy. */\nfunction suggest(rel: string): string {\n const name = basename(rel).replace(/\\.likec4$/, \"\");\n const dir = rel.slice(0, rel.length - basename(rel).length);\n return `${dir}${name}-extend.likec4`;\n}\n"]}
@@ -0,0 +1,145 @@
1
+ /**
2
+ * The deployment axis: which objects on the fleet's TOPOLOGY carry a rule, and
3
+ * who owns each of them.
4
+ *
5
+ * A `#obl-` tag is a claim about the object it sits on, and the object's model
6
+ * is not a property any reader can see. Before this module the same undeclared
7
+ * tag was `obligation.unknown` on a container and SILENCE on a datacenter,
8
+ * because `validate/fleet/obligations.ts` walked the logical model only — the
9
+ * fail-open case where a rule an architect placed does not exist for any check,
10
+ * and a reviewer has no way to tell that from a rule that passed. This is the
11
+ * derivation that closes it (FEAT-1's ARCH-LOAM-DEPLOY-TAGGED).
12
+ *
13
+ * ## The `where` string is the `Covers:` line
14
+ *
15
+ * Every finding about a tagged object tells its reader to write
16
+ * `Covers: <where>`, so `where` is spelled as the grammar reads it —
17
+ * `node:eu.dcA`, `node:a.db -> node:b.db` — and never as a bare id. A message
18
+ * that names an object one way and asks for a line spelled another way sends
19
+ * the reader round the loop twice.
20
+ *
21
+ * ## Who owns a datacenter
22
+ *
23
+ * Nobody, and the answer is `undefined` rather than a guess. An INSTANCE is
24
+ * owned by the service its logical element belongs to, which is a real join
25
+ * through the landscape's own binding; an edge between two instances of one
26
+ * service is owned by that service; a region, a datacenter or a cluster is
27
+ * owned by whoever runs the platform, and loam has no such concept. The finding
28
+ * changes its sentence rather than naming a service that did not ask for the
29
+ * work — "write it in payment-service's arch.spec.md" is actionable, and
30
+ * "write it in <the wrong team>'s" is worse than no attribution at all.
31
+ *
32
+ * This package is one module today and gains its second with the feature-local
33
+ * `features/<FEAT>/deployment/` slot, which is the step of the axis that needs
34
+ * `repo` and `kernel`. Nothing here reads a path.
35
+ */
36
+ import {} from "../c4/likec4.js";
37
+ import {} from "../c4/parsed/deployment.js";
38
+ import { serviceResolver } from "../c4/resolve/service.js";
39
+ /**
40
+ * Every `#obl-` tag on the deployment model, one entry per (obligation, object)
41
+ * pair — the same shape, and the same bare-prefix tolerance, the logical walk
42
+ * has: a bare `#obl-` slugs to the empty string and is KEPT, because the prefix
43
+ * is the author's opt-in however little follows it, and dropping it would leave
44
+ * an object that asked to be graded silently ungraded.
45
+ */
46
+ export function taggedDeployment(deployment, elements, known, prefix) {
47
+ const owner = instanceOwners(deployment, elements, known);
48
+ const out = [];
49
+ const push = (tags, rest) => {
50
+ for (const tag of tags.filter((t) => t.startsWith(prefix))) {
51
+ out.push({ obligation: tag.slice(prefix.length), ...rest });
52
+ }
53
+ };
54
+ // Nodes first, then instances, then edges — the order the model declares them
55
+ // in, so a fleet report reads top-down the way the document does.
56
+ for (const node of deployment.nodes)
57
+ push(node.tags, { where: `node:${node.id}`, id: node.id });
58
+ for (const instance of deployment.instances) {
59
+ const service = owner.get(instance.id);
60
+ push(instance.tags, {
61
+ where: `node:${instance.id}`,
62
+ id: instance.id,
63
+ ...(service === undefined ? {} : { service }),
64
+ });
65
+ }
66
+ for (const edge of deployment.relationships) {
67
+ // An edge is owned only when BOTH ends agree. A replication edge between
68
+ // two instances of one service belongs to that service; one crossing
69
+ // services belongs to neither, and picking the target — the rule the
70
+ // logical walk uses, where the arrow means "calls" — says nothing here,
71
+ // because a deployment edge is as often symmetric as directed.
72
+ const from = owner.get(edge.source);
73
+ const to = owner.get(edge.target);
74
+ const service = from !== undefined && from === to ? from : undefined;
75
+ push(edge.tags, {
76
+ where: `node:${edge.source} -> node:${edge.target}`,
77
+ edge,
78
+ ...(service === undefined ? {} : { service }),
79
+ });
80
+ }
81
+ return out;
82
+ }
83
+ /**
84
+ * The topology one service is part of.
85
+ *
86
+ * Built for the CONTEXT PACK, which is the surface whose reader is an agent
87
+ * about to write the implementation. Before this, that pack described a
88
+ * service's calls and contracts and said nothing whatever about where it runs —
89
+ * so a failover was implemented against a map nobody had read, which is the
90
+ * gap FEAT-1's ARCH-LOAM-DEPLOY-UNCHECKED names alongside the sentence about
91
+ * what a green axis does not mean.
92
+ *
93
+ * BOTH ENDS, not only the outbound one. A replication edge is as often read
94
+ * from the standby's side as from the primary's, and a pack that showed only
95
+ * what this service points at would hide the stream it receives.
96
+ */
97
+ export function serviceTopology(deployment, elements, known, service) {
98
+ const owner = instanceOwners(deployment, elements, known);
99
+ const mine = deployment.instances.filter((i) => owner.get(i.id) === service);
100
+ const ids = new Set(mine.map((i) => i.id));
101
+ return {
102
+ instances: mine.map((i) => ({ id: i.id, element: i.element })),
103
+ edges: deployment.relationships
104
+ .filter((r) => ids.has(r.source) || ids.has(r.target))
105
+ .map((r) => ({ source: r.source, target: r.target, ...(r.title === undefined ? {} : { title: r.title }) })),
106
+ };
107
+ }
108
+ /**
109
+ * instance id → the service its logical element belongs to.
110
+ *
111
+ * Through `serviceResolver` and NOT `elementService`, and the difference is a
112
+ * defect this axis shipped once. A deployment instances a CONTAINER far more
113
+ * often than a whole system — `instanceOf orders.cache` is the ordinary way to
114
+ * say where the Redis a service owns actually runs — and `elementService` reads
115
+ * the binding or the TITLE of the element handed to it. A nested container
116
+ * carries no `metadata { service }` of its own, so that read answered "order
117
+ * cache", and `obligation.uncovered` told an author to write the requirement in
118
+ * "order cache's arch.spec.md" — a service that has never existed. The
119
+ * resolver's whole job is the ancestor walk that finds the bound system above
120
+ * it, which is the same join every other check makes about a container.
121
+ *
122
+ * `known` rides in for the reason `coversEdge` takes it: without the enumerated
123
+ * fleet the resolver's second tier — the nearest ancestor whose title names a
124
+ * real `services/<id>/` — cannot run, and a fleet that binds by title rather
125
+ * than by `metadata { service }` resolves to the title alone. It must be the
126
+ * SAME set the caller's own findings resolve with, or the message names one
127
+ * service and the check counts another.
128
+ *
129
+ * An instance of an element the landscape does not declare is left UNOWNED
130
+ * rather than filed under a fabricated name. LikeC4 refuses an unresolved
131
+ * `instanceOf` outright, so that state is reachable only when the caller hands
132
+ * in elements from a narrower document than the one the topology was parsed
133
+ * with.
134
+ */
135
+ function instanceOwners(deployment, elements, known) {
136
+ const ids = new Set(elements.map((e) => e.id));
137
+ const resolve = serviceResolver([...elements], known);
138
+ const out = new Map();
139
+ for (const instance of deployment.instances) {
140
+ if (ids.has(instance.element))
141
+ out.set(instance.id, resolve(instance.element));
142
+ }
143
+ return out;
144
+ }
145
+ //# sourceMappingURL=objects.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"objects.js","sourceRoot":"","sources":["../../../src/core/deployment/objects.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The deployment axis: which objects on the fleet's TOPOLOGY carry a rule, and\n * who owns each of them.\n *\n * A `#obl-` tag is a claim about the object it sits on, and the object's model\n * is not a property any reader can see. Before this module the same undeclared\n * tag was `obligation.unknown` on a container and SILENCE on a datacenter,\n * because `validate/fleet/obligations.ts` walked the logical model only — the\n * fail-open case where a rule an architect placed does not exist for any check,\n * and a reviewer has no way to tell that from a rule that passed. This is the\n * derivation that closes it (FEAT-1's ARCH-LOAM-DEPLOY-TAGGED).\n *\n * ## The `where` string is the `Covers:` line\n *\n * Every finding about a tagged object tells its reader to write\n * `Covers: <where>`, so `where` is spelled as the grammar reads it —\n * `node:eu.dcA`, `node:a.db -> node:b.db` — and never as a bare id. A message\n * that names an object one way and asks for a line spelled another way sends\n * the reader round the loop twice.\n *\n * ## Who owns a datacenter\n *\n * Nobody, and the answer is `undefined` rather than a guess. An INSTANCE is\n * owned by the service its logical element belongs to, which is a real join\n * through the landscape's own binding; an edge between two instances of one\n * service is owned by that service; a region, a datacenter or a cluster is\n * owned by whoever runs the platform, and loam has no such concept. The finding\n * changes its sentence rather than naming a service that did not ask for the\n * work — \"write it in payment-service's arch.spec.md\" is actionable, and\n * \"write it in <the wrong team>'s\" is worse than no attribution at all.\n *\n * This package is one module today and gains its second with the feature-local\n * `features/<FEAT>/deployment/` slot, which is the step of the axis that needs\n * `repo` and `kernel`. Nothing here reads a path.\n */\nimport { type Elem } from \"../c4/likec4.js\";\nimport { type DeploymentModel, type DeployRel } from \"../c4/parsed/deployment.js\";\nimport { serviceResolver } from \"../c4/resolve/service.js\";\n\n/** One deployment object carrying one obligation tag. */\nexport interface TaggedDeployment {\n /** The obligation id — the tag suffix, exactly as the logical walk reads it. */\n obligation: string;\n /**\n * How a `Covers:` line names this object, and how every message spells it:\n * `node:<id>` for a node or an instance, `node:<a> -> node:<b>` for an edge.\n */\n where: string;\n /** The service that owns it, where the topology says. Absent for a node nobody's service runs. */\n service?: string;\n /** The edge, when this object is one — the caller matches `Covers:` against it. */\n edge?: DeployRel;\n /** The node or instance id, when this object is one. */\n id?: string;\n}\n\n/**\n * Every `#obl-` tag on the deployment model, one entry per (obligation, object)\n * pair — the same shape, and the same bare-prefix tolerance, the logical walk\n * has: a bare `#obl-` slugs to the empty string and is KEPT, because the prefix\n * is the author's opt-in however little follows it, and dropping it would leave\n * an object that asked to be graded silently ungraded.\n */\nexport function taggedDeployment(\n deployment: DeploymentModel,\n elements: readonly Elem[],\n known: ReadonlySet<string>,\n prefix: string,\n): TaggedDeployment[] {\n const owner = instanceOwners(deployment, elements, known);\n const out: TaggedDeployment[] = [];\n const push = (tags: readonly string[], rest: Omit<TaggedDeployment, \"obligation\">): void => {\n for (const tag of tags.filter((t) => t.startsWith(prefix))) {\n out.push({ obligation: tag.slice(prefix.length), ...rest });\n }\n };\n\n // Nodes first, then instances, then edges — the order the model declares them\n // in, so a fleet report reads top-down the way the document does.\n for (const node of deployment.nodes) push(node.tags, { where: `node:${node.id}`, id: node.id });\n for (const instance of deployment.instances) {\n const service = owner.get(instance.id);\n push(instance.tags, {\n where: `node:${instance.id}`,\n id: instance.id,\n ...(service === undefined ? {} : { service }),\n });\n }\n for (const edge of deployment.relationships) {\n // An edge is owned only when BOTH ends agree. A replication edge between\n // two instances of one service belongs to that service; one crossing\n // services belongs to neither, and picking the target — the rule the\n // logical walk uses, where the arrow means \"calls\" — says nothing here,\n // because a deployment edge is as often symmetric as directed.\n const from = owner.get(edge.source);\n const to = owner.get(edge.target);\n const service = from !== undefined && from === to ? from : undefined;\n push(edge.tags, {\n where: `node:${edge.source} -> node:${edge.target}`,\n edge,\n ...(service === undefined ? {} : { service }),\n });\n }\n return out;\n}\n\n/** Where one service runs, and what its instances talk to across the topology. */\nexport interface ServiceTopology {\n /** This service's instances: the deployment id, and the element each one deploys. */\n instances: Array<{ id: string; element: string }>;\n /** Deployment edges with at least one end among them — replication, failover, a network hop. */\n edges: Array<{ source: string; target: string; title?: string }>;\n}\n\n/**\n * The topology one service is part of.\n *\n * Built for the CONTEXT PACK, which is the surface whose reader is an agent\n * about to write the implementation. Before this, that pack described a\n * service's calls and contracts and said nothing whatever about where it runs —\n * so a failover was implemented against a map nobody had read, which is the\n * gap FEAT-1's ARCH-LOAM-DEPLOY-UNCHECKED names alongside the sentence about\n * what a green axis does not mean.\n *\n * BOTH ENDS, not only the outbound one. A replication edge is as often read\n * from the standby's side as from the primary's, and a pack that showed only\n * what this service points at would hide the stream it receives.\n */\nexport function serviceTopology(\n deployment: DeploymentModel,\n elements: readonly Elem[],\n known: ReadonlySet<string>,\n service: string,\n): ServiceTopology {\n const owner = instanceOwners(deployment, elements, known);\n const mine = deployment.instances.filter((i) => owner.get(i.id) === service);\n const ids = new Set(mine.map((i) => i.id));\n return {\n instances: mine.map((i) => ({ id: i.id, element: i.element })),\n edges: deployment.relationships\n .filter((r) => ids.has(r.source) || ids.has(r.target))\n .map((r) => ({ source: r.source, target: r.target, ...(r.title === undefined ? {} : { title: r.title }) })),\n };\n}\n\n/**\n * instance id → the service its logical element belongs to.\n *\n * Through `serviceResolver` and NOT `elementService`, and the difference is a\n * defect this axis shipped once. A deployment instances a CONTAINER far more\n * often than a whole system — `instanceOf orders.cache` is the ordinary way to\n * say where the Redis a service owns actually runs — and `elementService` reads\n * the binding or the TITLE of the element handed to it. A nested container\n * carries no `metadata { service }` of its own, so that read answered \"order\n * cache\", and `obligation.uncovered` told an author to write the requirement in\n * \"order cache's arch.spec.md\" — a service that has never existed. The\n * resolver's whole job is the ancestor walk that finds the bound system above\n * it, which is the same join every other check makes about a container.\n *\n * `known` rides in for the reason `coversEdge` takes it: without the enumerated\n * fleet the resolver's second tier — the nearest ancestor whose title names a\n * real `services/<id>/` — cannot run, and a fleet that binds by title rather\n * than by `metadata { service }` resolves to the title alone. It must be the\n * SAME set the caller's own findings resolve with, or the message names one\n * service and the check counts another.\n *\n * An instance of an element the landscape does not declare is left UNOWNED\n * rather than filed under a fabricated name. LikeC4 refuses an unresolved\n * `instanceOf` outright, so that state is reachable only when the caller hands\n * in elements from a narrower document than the one the topology was parsed\n * with.\n */\nfunction instanceOwners(\n deployment: DeploymentModel,\n elements: readonly Elem[],\n known: ReadonlySet<string>,\n): Map<string, string> {\n const ids = new Set(elements.map((e) => e.id));\n const resolve = serviceResolver([...elements], known);\n const out = new Map<string, string>();\n for (const instance of deployment.instances) {\n if (ids.has(instance.element)) out.set(instance.id, resolve(instance.element));\n }\n return out;\n}\n"]}
@@ -18,6 +18,9 @@ import {} from "../document/spec.js";
18
18
  import { landscapeEvidence, maturityGaps, serviceMaturity, } from "../vocabulary/maturity.js";
19
19
  import {} from "../repo/entries.js";
20
20
  import { landscapePath, servicePathsAt } from "../repo/paths.js";
21
+ import { hasDeployment, NO_DEPLOYMENT } from "../c4/parsed/deployment.js";
22
+ import { loadArchitecture } from "../c4/project/architecture.js";
23
+ import { serviceTopology } from "../deployment/objects.js";
21
24
  export function packRequirement(r) {
22
25
  return {
23
26
  kind: r.kind,
@@ -68,6 +71,21 @@ export async function buildLiving(req) {
68
71
  elementIds: elements.map((e) => e.id),
69
72
  svcOf,
70
73
  });
74
+ // Where this service RUNS, from the landscape's own deployment model. The
75
+ // pack's reader is an agent about to write the implementation, and until this
76
+ // slice existed it described a service's calls and contracts while saying
77
+ // nothing whatever about its topology — so a failover got written against a
78
+ // map nobody had read. Empty for every fleet that draws none, which is the
79
+ // axis's opt-in and not a finding.
80
+ //
81
+ // Read through the PROJECT when the landscape file itself declares none, for
82
+ // the reason `validate/service/specs.ts` pays the same load: `architecture/`
83
+ // is one LikeC4 project and a fleet's `deployment { }` block is ordinarily a
84
+ // file beside the landscape, not inside it. A single-file read would report
85
+ // "this service runs nowhere" over a repo that draws it, which is the one
86
+ // answer a briefing must never give.
87
+ const topology = parses ? (land.deployment ?? NO_DEPLOYMENT) : NO_DEPLOYMENT;
88
+ const deployment = serviceTopology(hasDeployment(topology) || !parses ? topology : ((await loadArchitecture(docsDir)).deployment ?? NO_DEPLOYMENT), elements, known, entry.id);
71
89
  const input = { entry, archSpec: existsSync(paths.archSpec), apiExpected };
72
90
  const header = (text) => text === null ? { present: false, malformed: false, data: {}, body: "" } : parseFrontmatter(text);
73
91
  const fm = header(specText);
@@ -89,6 +107,7 @@ export async function buildLiving(req) {
89
107
  },
90
108
  archSpec: { vouch_scope: scopeText(header(archText)) },
91
109
  landscape: { present, parses, modelled, inbound, outbound },
110
+ deployment,
92
111
  requirements: reqs.map(packRequirement),
93
112
  archRequirements: archReqs.map(packRequirement),
94
113
  openapi: { unreadable: api.unreadable, ...(api.error === undefined ? {} : { error: api.error }) },
@@ -1 +1 @@
1
- {"version":3,"file":"living.js","sourceRoot":"","sources":["../../../src/core/pack/living.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The living slice of one service's context pack: its requirements verbatim,\n * its two contracts, its provenance, its maturity, and the fleet edges around\n * it — everything the docs repo says about the service TODAY, before any\n * feature in flight is consulted.\n *\n * Split from `./pack.ts` because it answers about the LIVING documents while\n * `./features.ts` answers about the deltas over them — two phases of one\n * briefing, and the line limit is what asked the question.\n */\nimport { existsSync } from \"node:fs\";\nimport { FleetContext } from \"../fleet-context.js\";\nimport { serviceResolver } from \"../c4/resolve/service.js\";\nimport { repoPath } from \"../envelope/json.js\";\nimport { listField, parseFrontmatter, stringField, type Frontmatter } from \"../document/frontmatter.js\";\nimport { scopeText } from \"../provenance/sample/scope.js\";\nimport { type Requirement } from \"../document/spec.js\";\nimport {\n landscapeEvidence,\n maturityGaps,\n serviceMaturity,\n type LandscapeEdge,\n type Maturity,\n} from \"../vocabulary/maturity.js\";\nimport { type ServiceEntry } from \"../repo/entries.js\";\nimport { landscapePath, servicePathsAt } from \"../repo/paths.js\";\nimport type { DocsDir } from \"../kernel/ids/dirs.js\";\n\n/**\n * A requirement as the pack carries it: the delta brief's `reqJson` shape plus\n * the four join lines (`Requires:`, `Capability:`, `Publishes:`, `Consumes:`)\n * the brief never needed. Verbatim on purpose — the pack is the briefing, and\n * a heading without its body is a table of contents for a file the reader then\n * has to open.\n */\nexport interface PackRequirement {\n kind: Requirement[\"kind\"];\n id?: string;\n name: string;\n text: string;\n operations: string[];\n covers: string[];\n requires: string[];\n capabilities: string[];\n publishes: string[];\n consumes: string[];\n /** Verbatim: the acceptance criteria, and the source for the tests whoever picks this up writes. */\n scenarios: Array<{ name: string; lines: string[] }>;\n}\n\nexport function packRequirement(r: Requirement): PackRequirement {\n return {\n kind: r.kind,\n ...(r.id === undefined ? {} : { id: r.id }),\n name: r.name,\n text: r.text.join(\"\\n\").trim(),\n operations: r.operations,\n covers: r.covers,\n requires: r.requires,\n capabilities: r.capabilities,\n publishes: r.publishes,\n consumes: r.consumes,\n scenarios: r.scenarios.map((s) => ({ name: s.name, lines: s.lines })),\n };\n}\n\nexport interface LivingSlice {\n maturity: Maturity;\n /** What stands between the service and `vouched` — `maturityGaps`, so it matches `list --needs-work`. */\n missing: string[];\n /**\n * `vouch_scope` rides beside `status` because this pack is read by an agent\n * about to work on the service, and a bare `status: verified` over a vouch\n * that read a recorded sample of the document would be read as the whole of\n * it having been checked. Verbatim, null when there is none.\n *\n * spec.md's header ALONE — arch.spec.md carries its own, below.\n */\n frontmatter: {\n status: string | null;\n owner: string | null;\n last_verified: string | null;\n vouch_scope: string | null;\n sources: string[];\n };\n /**\n * arch.spec.md's own `vouch_scope`, under `show --json`'s spelling\n * (`archSpec.vouch_scope`) so the two surfaces name one fact one way.\n *\n * The sample is per FILE: one `loam vouch --sample` run reads a short\n * spec.md in full and stamps a long arch.spec.md sampled, and the pack read\n * only the first header. Every other trust surface — list, show,\n * `vouch --pack`, `validate --all`, status — reported that service as\n * sampled; context printed `verified`, `vouch_scope: null`, and then all of\n * arch.spec.md's requirements verbatim, of which nobody had read half. It is\n * the one surface whose reader is an agent that will act on the licence, and\n * `loam_context` exposes it as an MCP tool besides.\n */\n archSpec: { vouch_scope: string | null };\n /**\n * `present`/`parses` ride beside the edges (explore's shape) so silence is\n * evidence, not absence: an empty edge list under `parses: false` says\n * \"nobody could look\", not \"nobody calls this\".\n */\n landscape: {\n present: boolean;\n parses: boolean;\n modelled: boolean;\n inbound: LandscapeEdge[];\n outbound: LandscapeEdge[];\n };\n requirements: PackRequirement[];\n archRequirements: PackRequirement[];\n /** Whether the living contract could be read at all — the vacuously-green guard every sibling carries. */\n openapi: { unreadable: boolean; error?: string };\n operations: Array<{ id: string; method: string; path: string; governedBy: string[] }>;\n asyncapi: { unreadable: boolean; error?: string };\n messages: Array<{ name: string; slot: string; direction: \"send\" | \"receive\" | null }>;\n /** Free-form prose stays a pointer: unbounded documents do not belong in a bounded pack. */\n pointers: {\n runbook: { present: boolean; path: string };\n health: { present: boolean; path: string };\n adrs: { count: number; path: string };\n };\n}\n\nexport interface LivingRequest {\n docsDir: DocsDir;\n entry: ServiceEntry;\n context: FleetContext;\n}\n\nexport async function buildLiving(req: LivingRequest): Promise<LivingSlice> {\n const { docsDir, entry, context } = req;\n const paths = servicePathsAt(entry.dir);\n\n // Independent reads, fanned out together; each is memoized on the shared\n // context, so anything `./joins.ts` or `./features.ts` re-asks is free.\n const [reqs, archReqs, api, events, specText, archText, land] = await Promise.all([\n entry.has.spec ? context.readRequirements(paths.spec) : [],\n existsSync(paths.archSpec) ? context.readRequirements(paths.archSpec) : [],\n context.readOpenapi(paths.openapi),\n context.readAsyncapi(paths.asyncapi),\n entry.has.spec ? context.readText(paths.spec) : null,\n // Both headers, exactly as `show` reads both (commands/show/service.ts):\n // a memo hit on the same read the requirements above already paid for.\n existsSync(paths.archSpec) ? context.readText(paths.archSpec) : null,\n existsSync(landscapePath(docsDir)) ? context.loadLikeC4(landscapePath(docsDir)) : null,\n ]);\n\n const present = land !== null;\n const parses = land !== null && land.errors.length === 0;\n const elements = parses ? land.elements : [];\n // Edges are filed under the service an element is BOUND to, with the\n // enumerated fleet riding in — the same resolution `show`, `explore` and\n // `validate` use, so no two commands can disagree about who calls whom.\n const known = new Set((await context.listServices(docsDir)).map((s) => s.id));\n const svcOf = serviceResolver(elements, known);\n // The partition, the modelled probe and the apiExpected rule are ONE shared\n // derivation (core/vocabulary/maturity.ts `landscapeEvidence`) — the same\n // one explore's describe uses, so the pack and explore cannot grade the\n // same service differently.\n const { inbound, outbound, modelled, apiExpected } = landscapeEvidence({\n id: entry.id,\n parses,\n relationships: parses ? land.relationships : [],\n elementIds: elements.map((e) => e.id),\n svcOf,\n });\n const input = { entry, archSpec: existsSync(paths.archSpec), apiExpected };\n\n const header = (text: string | null): Frontmatter =>\n text === null ? { present: false, malformed: false, data: {}, body: \"\" } : parseFrontmatter(text);\n const fm: Frontmatter = header(specText);\n\n const governs = (op: string): string[] =>\n reqs.filter((r) => r.operations.includes(op)).map((r) => r.name);\n const sent = new Set(events.sent);\n const received = new Set(events.received);\n\n return {\n maturity: serviceMaturity(input),\n missing: maturityGaps(input),\n frontmatter: {\n status: stringField(fm, \"status\") ?? null,\n owner: stringField(fm, \"owner\") ?? null,\n last_verified: stringField(fm, \"last_verified\") ?? null,\n // Through `scopeText`, not `stringField`: a scope whose value is not\n // text is still a stamped partial read, and this pack is read by an\n // agent that will otherwise take `status: verified` at face value.\n vouch_scope: scopeText(fm),\n sources: listField(fm, \"sources\"),\n },\n archSpec: { vouch_scope: scopeText(header(archText)) },\n landscape: { present, parses, modelled, inbound, outbound },\n requirements: reqs.map(packRequirement),\n archRequirements: archReqs.map(packRequirement),\n openapi: { unreadable: api.unreadable, ...(api.error === undefined ? {} : { error: api.error }) },\n // Document order — a fact of the file, hence of the state. `x-loam-remove`\n // markers are filtered exactly as `show` filters them: they are deletion\n // markers from a delta, not operations anybody can call.\n operations: api.ops\n .filter((o) => !o.remove)\n .map((o) => ({ id: o.id, method: o.method.toUpperCase(), path: o.path, governedBy: governs(o.id) })),\n asyncapi: { unreadable: events.unreadable, ...(events.error === undefined ? {} : { error: events.error }) },\n messages: events.messages\n .filter((m) => m.remove !== true)\n .map((m) => ({\n name: m.name,\n slot: m.slot,\n direction: sent.has(m.name) ? \"send\" : received.has(m.name) ? \"receive\" : null,\n })),\n pointers: {\n runbook: { present: entry.has.runbook, path: repoPath(docsDir, paths.runbook) },\n health: { present: entry.has.health, path: repoPath(docsDir, paths.health) },\n // The enumeration already counted the ADRs; a second readdir here could\n // only disagree with it.\n adrs: { count: entry.adrs, path: repoPath(docsDir, paths.adrsDir) },\n },\n };\n}\n"]}
1
+ {"version":3,"file":"living.js","sourceRoot":"","sources":["../../../src/core/pack/living.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The living slice of one service's context pack: its requirements verbatim,\n * its two contracts, its provenance, its maturity, and the fleet edges around\n * it — everything the docs repo says about the service TODAY, before any\n * feature in flight is consulted.\n *\n * Split from `./pack.ts` because it answers about the LIVING documents while\n * `./features.ts` answers about the deltas over them — two phases of one\n * briefing, and the line limit is what asked the question.\n */\nimport { existsSync } from \"node:fs\";\nimport { FleetContext } from \"../fleet-context.js\";\nimport { serviceResolver } from \"../c4/resolve/service.js\";\nimport { repoPath } from \"../envelope/json.js\";\nimport { listField, parseFrontmatter, stringField, type Frontmatter } from \"../document/frontmatter.js\";\nimport { scopeText } from \"../provenance/sample/scope.js\";\nimport { type Requirement } from \"../document/spec.js\";\nimport {\n landscapeEvidence,\n maturityGaps,\n serviceMaturity,\n type LandscapeEdge,\n type Maturity,\n} from \"../vocabulary/maturity.js\";\nimport { type ServiceEntry } from \"../repo/entries.js\";\nimport { landscapePath, servicePathsAt } from \"../repo/paths.js\";\nimport type { DocsDir } from \"../kernel/ids/dirs.js\";\nimport { hasDeployment, NO_DEPLOYMENT } from \"../c4/parsed/deployment.js\";\nimport { loadArchitecture } from \"../c4/project/architecture.js\";\nimport { serviceTopology, type ServiceTopology } from \"../deployment/objects.js\";\n\n/**\n * A requirement as the pack carries it: the delta brief's `reqJson` shape plus\n * the four join lines (`Requires:`, `Capability:`, `Publishes:`, `Consumes:`)\n * the brief never needed. Verbatim on purpose — the pack is the briefing, and\n * a heading without its body is a table of contents for a file the reader then\n * has to open.\n */\nexport interface PackRequirement {\n kind: Requirement[\"kind\"];\n id?: string;\n name: string;\n text: string;\n operations: string[];\n covers: string[];\n requires: string[];\n capabilities: string[];\n publishes: string[];\n consumes: string[];\n /** Verbatim: the acceptance criteria, and the source for the tests whoever picks this up writes. */\n scenarios: Array<{ name: string; lines: string[] }>;\n}\n\nexport function packRequirement(r: Requirement): PackRequirement {\n return {\n kind: r.kind,\n ...(r.id === undefined ? {} : { id: r.id }),\n name: r.name,\n text: r.text.join(\"\\n\").trim(),\n operations: r.operations,\n covers: r.covers,\n requires: r.requires,\n capabilities: r.capabilities,\n publishes: r.publishes,\n consumes: r.consumes,\n scenarios: r.scenarios.map((s) => ({ name: s.name, lines: s.lines })),\n };\n}\n\nexport interface LivingSlice {\n maturity: Maturity;\n /** What stands between the service and `vouched` — `maturityGaps`, so it matches `list --needs-work`. */\n missing: string[];\n /**\n * `vouch_scope` rides beside `status` because this pack is read by an agent\n * about to work on the service, and a bare `status: verified` over a vouch\n * that read a recorded sample of the document would be read as the whole of\n * it having been checked. Verbatim, null when there is none.\n *\n * spec.md's header ALONE — arch.spec.md carries its own, below.\n */\n frontmatter: {\n status: string | null;\n owner: string | null;\n last_verified: string | null;\n vouch_scope: string | null;\n sources: string[];\n };\n /**\n * arch.spec.md's own `vouch_scope`, under `show --json`'s spelling\n * (`archSpec.vouch_scope`) so the two surfaces name one fact one way.\n *\n * The sample is per FILE: one `loam vouch --sample` run reads a short\n * spec.md in full and stamps a long arch.spec.md sampled, and the pack read\n * only the first header. Every other trust surface — list, show,\n * `vouch --pack`, `validate --all`, status — reported that service as\n * sampled; context printed `verified`, `vouch_scope: null`, and then all of\n * arch.spec.md's requirements verbatim, of which nobody had read half. It is\n * the one surface whose reader is an agent that will act on the licence, and\n * `loam_context` exposes it as an MCP tool besides.\n */\n archSpec: { vouch_scope: string | null };\n /**\n * `present`/`parses` ride beside the edges (explore's shape) so silence is\n * evidence, not absence: an empty edge list under `parses: false` says\n * \"nobody could look\", not \"nobody calls this\".\n */\n landscape: {\n present: boolean;\n parses: boolean;\n modelled: boolean;\n inbound: LandscapeEdge[];\n outbound: LandscapeEdge[];\n };\n /**\n * Where this service runs, off the landscape's `deployment { }` model, and\n * the deployment edges touching it — replication, failover, a network hop.\n *\n * Both ends, not only the outbound one: a replication edge is as often read\n * from the standby's side as from the primary's. Empty for a fleet that draws\n * no topology, and empty is not a finding here any more than it is anywhere\n * else on this axis.\n *\n * WHAT IT IS NOT. A green deployment axis means the documents agree with each\n * other — never that the second cluster exists, is reachable, or holds the\n * data a requirement claims. `core/brief/unchecked.ts` says so beside the\n * sixteen statements that say the same about the rest of the map, and an\n * agent reading this slice is exactly the reader that sentence is for.\n */\n deployment: ServiceTopology;\n requirements: PackRequirement[];\n archRequirements: PackRequirement[];\n /** Whether the living contract could be read at all — the vacuously-green guard every sibling carries. */\n openapi: { unreadable: boolean; error?: string };\n operations: Array<{ id: string; method: string; path: string; governedBy: string[] }>;\n asyncapi: { unreadable: boolean; error?: string };\n messages: Array<{ name: string; slot: string; direction: \"send\" | \"receive\" | null }>;\n /** Free-form prose stays a pointer: unbounded documents do not belong in a bounded pack. */\n pointers: {\n runbook: { present: boolean; path: string };\n health: { present: boolean; path: string };\n adrs: { count: number; path: string };\n };\n}\n\nexport interface LivingRequest {\n docsDir: DocsDir;\n entry: ServiceEntry;\n context: FleetContext;\n}\n\nexport async function buildLiving(req: LivingRequest): Promise<LivingSlice> {\n const { docsDir, entry, context } = req;\n const paths = servicePathsAt(entry.dir);\n\n // Independent reads, fanned out together; each is memoized on the shared\n // context, so anything `./joins.ts` or `./features.ts` re-asks is free.\n const [reqs, archReqs, api, events, specText, archText, land] = await Promise.all([\n entry.has.spec ? context.readRequirements(paths.spec) : [],\n existsSync(paths.archSpec) ? context.readRequirements(paths.archSpec) : [],\n context.readOpenapi(paths.openapi),\n context.readAsyncapi(paths.asyncapi),\n entry.has.spec ? context.readText(paths.spec) : null,\n // Both headers, exactly as `show` reads both (commands/show/service.ts):\n // a memo hit on the same read the requirements above already paid for.\n existsSync(paths.archSpec) ? context.readText(paths.archSpec) : null,\n existsSync(landscapePath(docsDir)) ? context.loadLikeC4(landscapePath(docsDir)) : null,\n ]);\n\n const present = land !== null;\n const parses = land !== null && land.errors.length === 0;\n const elements = parses ? land.elements : [];\n // Edges are filed under the service an element is BOUND to, with the\n // enumerated fleet riding in — the same resolution `show`, `explore` and\n // `validate` use, so no two commands can disagree about who calls whom.\n const known = new Set((await context.listServices(docsDir)).map((s) => s.id));\n const svcOf = serviceResolver(elements, known);\n // The partition, the modelled probe and the apiExpected rule are ONE shared\n // derivation (core/vocabulary/maturity.ts `landscapeEvidence`) — the same\n // one explore's describe uses, so the pack and explore cannot grade the\n // same service differently.\n const { inbound, outbound, modelled, apiExpected } = landscapeEvidence({\n id: entry.id,\n parses,\n relationships: parses ? land.relationships : [],\n elementIds: elements.map((e) => e.id),\n svcOf,\n });\n // Where this service RUNS, from the landscape's own deployment model. The\n // pack's reader is an agent about to write the implementation, and until this\n // slice existed it described a service's calls and contracts while saying\n // nothing whatever about its topology — so a failover got written against a\n // map nobody had read. Empty for every fleet that draws none, which is the\n // axis's opt-in and not a finding.\n //\n // Read through the PROJECT when the landscape file itself declares none, for\n // the reason `validate/service/specs.ts` pays the same load: `architecture/`\n // is one LikeC4 project and a fleet's `deployment { }` block is ordinarily a\n // file beside the landscape, not inside it. A single-file read would report\n // \"this service runs nowhere\" over a repo that draws it, which is the one\n // answer a briefing must never give.\n const topology = parses ? (land.deployment ?? NO_DEPLOYMENT) : NO_DEPLOYMENT;\n const deployment = serviceTopology(\n hasDeployment(topology) || !parses ? topology : ((await loadArchitecture(docsDir)).deployment ?? NO_DEPLOYMENT),\n elements,\n known,\n entry.id,\n );\n\n const input = { entry, archSpec: existsSync(paths.archSpec), apiExpected };\n\n const header = (text: string | null): Frontmatter =>\n text === null ? { present: false, malformed: false, data: {}, body: \"\" } : parseFrontmatter(text);\n const fm: Frontmatter = header(specText);\n\n const governs = (op: string): string[] =>\n reqs.filter((r) => r.operations.includes(op)).map((r) => r.name);\n const sent = new Set(events.sent);\n const received = new Set(events.received);\n\n return {\n maturity: serviceMaturity(input),\n missing: maturityGaps(input),\n frontmatter: {\n status: stringField(fm, \"status\") ?? null,\n owner: stringField(fm, \"owner\") ?? null,\n last_verified: stringField(fm, \"last_verified\") ?? null,\n // Through `scopeText`, not `stringField`: a scope whose value is not\n // text is still a stamped partial read, and this pack is read by an\n // agent that will otherwise take `status: verified` at face value.\n vouch_scope: scopeText(fm),\n sources: listField(fm, \"sources\"),\n },\n archSpec: { vouch_scope: scopeText(header(archText)) },\n landscape: { present, parses, modelled, inbound, outbound },\n deployment,\n requirements: reqs.map(packRequirement),\n archRequirements: archReqs.map(packRequirement),\n openapi: { unreadable: api.unreadable, ...(api.error === undefined ? {} : { error: api.error }) },\n // Document order — a fact of the file, hence of the state. `x-loam-remove`\n // markers are filtered exactly as `show` filters them: they are deletion\n // markers from a delta, not operations anybody can call.\n operations: api.ops\n .filter((o) => !o.remove)\n .map((o) => ({ id: o.id, method: o.method.toUpperCase(), path: o.path, governedBy: governs(o.id) })),\n asyncapi: { unreadable: events.unreadable, ...(events.error === undefined ? {} : { error: events.error }) },\n messages: events.messages\n .filter((m) => m.remove !== true)\n .map((m) => ({\n name: m.name,\n slot: m.slot,\n direction: sent.has(m.name) ? \"send\" : received.has(m.name) ? \"receive\" : null,\n })),\n pointers: {\n runbook: { present: entry.has.runbook, path: repoPath(docsDir, paths.runbook) },\n health: { present: entry.has.health, path: repoPath(docsDir, paths.health) },\n // The enumeration already counted the ADRs; a second readdir here could\n // only disagree with it.\n adrs: { count: entry.adrs, path: repoPath(docsDir, paths.adrsDir) },\n },\n };\n}\n"]}
@@ -189,4 +189,22 @@ export function featureGlossaryDir(featureDir) {
189
189
  export function featureUseCasesDir(featureDir) {
190
190
  return join(featureDir, "usecases");
191
191
  }
192
+ /**
193
+ * `features/<FEAT>/deployment/` — the TOPOLOGY a feature brings.
194
+ *
195
+ * The third document a feature carries whole rather than as a delta, and for
196
+ * the same reason as the two above: a `deployment { }` block is a topology, the
197
+ * merge is a whole-file copy, and there is nothing to merge partially. What
198
+ * makes the slot work at all is that LikeC4 `extend` resolves ACROSS documents
199
+ * of one project — measured before the axis was written — so a feature adds a
200
+ * standby datacenter inside a living region without touching the living file.
201
+ *
202
+ * `delta.likec4` still refuses a `deployment { }` block, and the refusal now
203
+ * names this directory: the C4 delta re-declares the landscape's identifiers
204
+ * and carries its own `specification`, so it cannot be staged beside the map in
205
+ * one project, and a document that declares neither can.
206
+ */
207
+ export function featureDeploymentDir(featureDir) {
208
+ return join(featureDir, "deployment");
209
+ }
192
210
  //# sourceMappingURL=paths.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"paths.js","sourceRoot":"","sources":["../../../../src/core/repo/authored/paths.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAG7C;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAgB;IAChD,OAAO,IAAI,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC;AACvC,CAAC;AAaD;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAW;IAC9C,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,IAAI,CAAC,EAAE,CAAC;AACvD,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,UAAU,qBAAqB,CAAC,OAAgB,EAAE,EAAU;IAChE,OAAO,oBAAoB,CAAC,IAAI,CAAC,iBAAiB,CAAC,OAAO,CAAC,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AAClF,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,0BAA0B,CAAC,UAAsB;IAC/D,OAAO,IAAI,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC;AAC1C,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,UAAU,WAAW,CAAC,OAAgB;IAC1C,OAAO,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;AACnC,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,kBAAkB,CAAC,UAAsB;IACvD,OAAO,IAAI,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;AACtC,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,UAAU,kBAAkB,CAAC,UAAsB;IACvD,OAAO,IAAI,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;AACtC,CAAC","sourcesContent":["/**\n * WHERE THE TWO AUTHORED VOCABULARY TREES LIVE — `capabilities/<id>/` and\n * `glossary/<term>.md`.\n *\n * Split out of `../paths.ts` when that file passed 400 lines, and along the seam\n * that was already there rather than at the line the count happened to land on:\n * everything left in the parent spells the layout loam MAINTAINS — a service's\n * artifacts, a feature's deltas, the generated views — while these two spell the\n * trees a human AUTHORS and loam only reads. They share a rule the parent's\n * paths do not have: the directory IS the list, its existence is the axis's\n * opt-in, and `loam init` scaffolds neither.\n *\n * `ARTIFACT_FILES` is imported back from the parent rather than restated. The\n * capability document is `spec.md` because it is the same grammar read by the\n * same parser as a service's, and a second spelling of that name is exactly the\n * drift the parent's one-table rule exists to prevent.\n */\nimport { join } from \"node:path\";\nimport { ARTIFACT_FILES } from \"../paths.js\";\nimport type { DocsDir, FeatureDir } from \"../../kernel/ids/dirs.js\";\n\n/**\n * The AUTHORED business tree — `capabilities/<id>/`, one directory per\n * capability, each holding the document that describes it.\n *\n * Top-level and not under `architecture/`, because the two altitudes have\n * different authors and different reasons to change: `architecture/` is the\n * architect's — the map, the vocabularies, the use cases — while a capability\n * document is the analyst's, and it must survive every redesign of the fleet\n * that realizes it. A business tree filed inside the architecture tree would\n * say the opposite of what the axis is for.\n *\n * NESTING IS SPELLED BY THE TREE, not inside a name. A capability id keeps its\n * slashes wherever it is written (`core/capabilities/capabilities.ts` preserves\n * the YAML key exactly as a `Capability:` line spells it), so `payments/refunds`\n * lives at `capabilities/payments/refunds/spec.md` and `payments` may be a\n * capability in its own right at `capabilities/payments/spec.md`. A directory is\n * a capability if and only if it holds the document — the same\n * presence-classifies rule `isServiceArtifactName` applies one tree over, and\n * the reason it is safe here too: a group directory that acquires a document\n * becomes a capability visibly, in a diff, rather than by a marker file nobody\n * updated.\n *\n * NOT SCAFFOLDED BY `loam init`, exactly as `architecture/adrs/` is not. git\n * does not carry an empty directory, so a scaffolded one vanishes on the first\n * clone and returns as a diff on the next `init`; and an empty `capabilities/`\n * in a fresh repo reads as an obligation nobody has met, when a fleet that has\n * not adopted the axis owes nothing. The directory's EXISTENCE is the opt-in.\n */\nexport function capabilityDocsDir(docsDir: DocsDir): string {\n return join(docsDir, \"capabilities\");\n}\n\nexport interface CapabilityDocPaths {\n dir: string;\n /**\n * The capability's own document: narrative and requirements in one file.\n * `spec.md`, the same name a service's requirements live under, because it is\n * the same grammar read by the same parser — a second spelling would be a\n * second thing to keep in step for no reader's benefit.\n */\n spec: string;\n}\n\n/**\n * The document paths of a capability whose directory is already KNOWN — the\n * enumeration's `dir`, at whatever depth the walk found it. Same discipline as\n * `servicePathsAt`: joining `capabilities/<id>/` at the root is only true for a\n * flat id, and a nested capability would silently grade as absent through it.\n */\nexport function capabilityDocPathsAt(dir: string): CapabilityDocPaths {\n return { dir, spec: join(dir, ARTIFACT_FILES.spec) };\n}\n\n/**\n * The LIVING document a capability id addresses — `capabilities/` with the id's\n * own nesting spelled back out as directories, so `payments/refunds` resolves to\n * `capabilities/payments/refunds/spec.md`.\n *\n * Spelled here because FIVE readers need it — the delta grade, the archive\n * merge, the conflict-marker scan, the archive gate's strayed-requirement scan\n * and `loam rebase` — and a reader that resolved it differently would disagree\n * about which document a feature is changing. The delta grade is where that costs most: a living path that finds\n * nothing reads as an empty document, so every ADDED looks new and no\n * `Based-On:` is ever compared, and the merge then lands over text nobody\n * re-read.\n *\n * `...id.split(\"/\")` rather than passing the id whole is explicitness, not a\n * fix: `node:path.join` already normalizes an embedded separator, so both\n * spellings resolve identically on every platform. The split is what makes the\n * nesting visible at the one place the rule is written down. The mistake this\n * function actually prevents is the OTHER one — resolving a nested id by its\n * leaf, which silently addresses a different capability.\n *\n * `id` IS caller-controlled path input and carries no brand, for the reason\n * `capabilityDocPathsAt` above takes a bare `dir`: a capability id has no\n * grammar — it is a YAML key and a directory name, constrained nowhere — so\n * there is nothing for a smart constructor to validate. What holds the join is\n * PROVENANCE, and every caller owes it: the id must have come from a\n * `readCapabilityTree` walk, where each component is a `readdir` entry name.\n * An id read out of `architecture/capabilities.yaml` has not, and must not\n * reach here.\n */\nexport function livingCapabilityPaths(docsDir: DocsDir, id: string): CapabilityDocPaths {\n return capabilityDocPathsAt(join(capabilityDocsDir(docsDir), ...id.split(\"/\")));\n}\n\n/**\n * A feature's own capability deltas — `features/<FEAT>/capabilities/<id>/`,\n * the delta tree that merges into `capabilityDocsDir` above.\n *\n * The layout MIRRORS the living one exactly, nesting included: a delta for\n * `payments/refunds` sits at `features/<FEAT>/capabilities/payments/refunds/`,\n * so `readCapabilityTree` walks both sides with one implementation and the\n * merge target is the same id split the same way. The alternative — a flat\n * escaped spelling like `payments%2Frefunds/` — would be a second grammar with\n * its own escaping defects, and a diff nobody could read by eye against\n * `specs/<svc>/spec.md` ↔ `services/<svc>/spec.md` one directory over.\n *\n * The DIRECTORY'S EXISTENCE is this axis's per-feature opt-in, exactly as\n * `capabilityDocsDir`'s is the fleet's: a feature without one pays a single\n * `existsSync` and produces no finding, so a fleet that has not adopted the\n * business axis sees nothing.\n */\nexport function featureCapabilityDeltasDir(featureDir: FeatureDir): string {\n return join(featureDir, \"capabilities\");\n}\n\n/**\n * The domain's own vocabulary — `glossary/<term>.md`, one FILE per term.\n *\n * A FILE, where a capability gets a directory, and the difference is what sits\n * beside the document. A capability's directory is the seam its feature-local\n * deltas and anything later join through; a term has nothing beside it but its\n * definition, and a directory holding one file is ceremony. The general rule\n * both follow is the one `core/capabilities/tree.ts` states: an entry with prose\n * gets a file, an entry without prose stays a line in YAML — and a domain term\n * is prose or it is nothing.\n *\n * NO `glossary.yaml`, for the reason the capability tree gives at length: a\n * single vocabulary file at fleet scale is unworkable, and a second list is the\n * drift `loam init`'s removed `loam.docs.json` was removed for. The DIRECTORY is\n * the list. Unlike the capability axis there is no YAML half to union with —\n * that file predates the tree and carries `description`/`owner` fields prose\n * cannot overwrite, and nothing here has ever had a second side.\n *\n * NESTING IS ALLOWED and spelled by the tree: `glossary/payments/order.md` is\n * the term `payments/order`. A fleet with three hundred terms in one flat\n * directory is the state this prevents, and it costs nothing — the walk is\n * recursive and the id is the path.\n *\n * TOP-LEVEL, not under `architecture/`, and for `capabilityDocsDir`'s reason:\n * the vocabulary is the domain's, and it must survive every redesign of the\n * fleet that speaks it.\n *\n * NOT SCAFFOLDED BY `loam init`, exactly as `architecture/adrs/` and\n * `capabilities/` are not. The directory's existence is the opt-in.\n */\nexport function glossaryDir(docsDir: DocsDir): string {\n return join(docsDir, \"glossary\");\n}\n\n/**\n * A feature's own glossary deltas — `features/<FEAT>/glossary/<term>.md`, the\n * vocabulary axis's answer to `specs/<svc>/spec.md`.\n *\n * A term a feature INTRODUCES ships and unships with it: the archive copies the\n * definition into `glossary/`, and unarchive takes it back out with everything\n * else the feature landed. That is the whole reason this route exists, and it is\n * why it is CREATE-ONLY (`core/glossary/delta.ts` states the rule and the\n * refusal): a definition is prose with no delta algebra to protect, so rewriting\n * a living one belongs in a pull request where git itself produces the conflict.\n *\n * The DIRECTORY'S EXISTENCE is this axis's per-feature opt-in, exactly as\n * `featureCapabilityDeltasDir`'s is the business axis's: a feature without one\n * pays a single `existsSync` and produces nothing.\n */\nexport function featureGlossaryDir(featureDir: FeatureDir): string {\n return join(featureDir, \"glossary\");\n}\n\n/**\n * A feature's own flows — `features/<FEAT>/usecases/<name>.likec4`, the use-case\n * axis's answer to `specs/<svc>/spec.md`.\n *\n * A `dynamic view` is the only realizer a promise that CROSSES services has, and\n * it was the one realizer no feature could carry: every other axis had a delta\n * slot and this one had none, so the business axis's headline case — an analyst\n * adds a cross-service requirement, the architect answers it with a flow — could\n * not ship as one change. The promise had to land first, kept by nothing, and\n * the flow followed in a second pull request.\n *\n * The name mirrors the LIVING layout rather than inventing a second one:\n * `architecture/usecases/` is where a flow already lives, `usecases/` is where a\n * feature writes the one it brings, and the merge is the same path one directory\n * over. Nesting is spelled by the tree, as it is for every other authored tree\n * here.\n *\n * CREATE-ONLY (`core/usecases/delta/flows.ts` states the rule and the refusal),\n * for `featureGlossaryDir`'s reason: a flow is one ordered hop sequence with no\n * delta algebra to protect, so rewriting a living one belongs in a pull request\n * where git itself produces the conflict.\n *\n * The DIRECTORY'S EXISTENCE is this axis's per-feature opt-in, exactly as the\n * two above: a feature without one pays a single walk over a directory that is\n * not there and produces nothing.\n */\nexport function featureUseCasesDir(featureDir: FeatureDir): string {\n return join(featureDir, \"usecases\");\n}\n"]}
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../../../../src/core/repo/authored/paths.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAG7C;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAgB;IAChD,OAAO,IAAI,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC;AACvC,CAAC;AAaD;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAW;IAC9C,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,IAAI,CAAC,EAAE,CAAC;AACvD,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,UAAU,qBAAqB,CAAC,OAAgB,EAAE,EAAU;IAChE,OAAO,oBAAoB,CAAC,IAAI,CAAC,iBAAiB,CAAC,OAAO,CAAC,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AAClF,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,0BAA0B,CAAC,UAAsB;IAC/D,OAAO,IAAI,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC;AAC1C,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,UAAU,WAAW,CAAC,OAAgB;IAC1C,OAAO,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;AACnC,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,kBAAkB,CAAC,UAAsB;IACvD,OAAO,IAAI,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;AACtC,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,UAAU,kBAAkB,CAAC,UAAsB;IACvD,OAAO,IAAI,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;AACtC,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,oBAAoB,CAAC,UAAsB;IACzD,OAAO,IAAI,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC;AACxC,CAAC","sourcesContent":["/**\n * WHERE THE TWO AUTHORED VOCABULARY TREES LIVE — `capabilities/<id>/` and\n * `glossary/<term>.md`.\n *\n * Split out of `../paths.ts` when that file passed 400 lines, and along the seam\n * that was already there rather than at the line the count happened to land on:\n * everything left in the parent spells the layout loam MAINTAINS — a service's\n * artifacts, a feature's deltas, the generated views — while these two spell the\n * trees a human AUTHORS and loam only reads. They share a rule the parent's\n * paths do not have: the directory IS the list, its existence is the axis's\n * opt-in, and `loam init` scaffolds neither.\n *\n * `ARTIFACT_FILES` is imported back from the parent rather than restated. The\n * capability document is `spec.md` because it is the same grammar read by the\n * same parser as a service's, and a second spelling of that name is exactly the\n * drift the parent's one-table rule exists to prevent.\n */\nimport { join } from \"node:path\";\nimport { ARTIFACT_FILES } from \"../paths.js\";\nimport type { DocsDir, FeatureDir } from \"../../kernel/ids/dirs.js\";\n\n/**\n * The AUTHORED business tree — `capabilities/<id>/`, one directory per\n * capability, each holding the document that describes it.\n *\n * Top-level and not under `architecture/`, because the two altitudes have\n * different authors and different reasons to change: `architecture/` is the\n * architect's — the map, the vocabularies, the use cases — while a capability\n * document is the analyst's, and it must survive every redesign of the fleet\n * that realizes it. A business tree filed inside the architecture tree would\n * say the opposite of what the axis is for.\n *\n * NESTING IS SPELLED BY THE TREE, not inside a name. A capability id keeps its\n * slashes wherever it is written (`core/capabilities/capabilities.ts` preserves\n * the YAML key exactly as a `Capability:` line spells it), so `payments/refunds`\n * lives at `capabilities/payments/refunds/spec.md` and `payments` may be a\n * capability in its own right at `capabilities/payments/spec.md`. A directory is\n * a capability if and only if it holds the document — the same\n * presence-classifies rule `isServiceArtifactName` applies one tree over, and\n * the reason it is safe here too: a group directory that acquires a document\n * becomes a capability visibly, in a diff, rather than by a marker file nobody\n * updated.\n *\n * NOT SCAFFOLDED BY `loam init`, exactly as `architecture/adrs/` is not. git\n * does not carry an empty directory, so a scaffolded one vanishes on the first\n * clone and returns as a diff on the next `init`; and an empty `capabilities/`\n * in a fresh repo reads as an obligation nobody has met, when a fleet that has\n * not adopted the axis owes nothing. The directory's EXISTENCE is the opt-in.\n */\nexport function capabilityDocsDir(docsDir: DocsDir): string {\n return join(docsDir, \"capabilities\");\n}\n\nexport interface CapabilityDocPaths {\n dir: string;\n /**\n * The capability's own document: narrative and requirements in one file.\n * `spec.md`, the same name a service's requirements live under, because it is\n * the same grammar read by the same parser — a second spelling would be a\n * second thing to keep in step for no reader's benefit.\n */\n spec: string;\n}\n\n/**\n * The document paths of a capability whose directory is already KNOWN — the\n * enumeration's `dir`, at whatever depth the walk found it. Same discipline as\n * `servicePathsAt`: joining `capabilities/<id>/` at the root is only true for a\n * flat id, and a nested capability would silently grade as absent through it.\n */\nexport function capabilityDocPathsAt(dir: string): CapabilityDocPaths {\n return { dir, spec: join(dir, ARTIFACT_FILES.spec) };\n}\n\n/**\n * The LIVING document a capability id addresses — `capabilities/` with the id's\n * own nesting spelled back out as directories, so `payments/refunds` resolves to\n * `capabilities/payments/refunds/spec.md`.\n *\n * Spelled here because FIVE readers need it — the delta grade, the archive\n * merge, the conflict-marker scan, the archive gate's strayed-requirement scan\n * and `loam rebase` — and a reader that resolved it differently would disagree\n * about which document a feature is changing. The delta grade is where that costs most: a living path that finds\n * nothing reads as an empty document, so every ADDED looks new and no\n * `Based-On:` is ever compared, and the merge then lands over text nobody\n * re-read.\n *\n * `...id.split(\"/\")` rather than passing the id whole is explicitness, not a\n * fix: `node:path.join` already normalizes an embedded separator, so both\n * spellings resolve identically on every platform. The split is what makes the\n * nesting visible at the one place the rule is written down. The mistake this\n * function actually prevents is the OTHER one — resolving a nested id by its\n * leaf, which silently addresses a different capability.\n *\n * `id` IS caller-controlled path input and carries no brand, for the reason\n * `capabilityDocPathsAt` above takes a bare `dir`: a capability id has no\n * grammar — it is a YAML key and a directory name, constrained nowhere — so\n * there is nothing for a smart constructor to validate. What holds the join is\n * PROVENANCE, and every caller owes it: the id must have come from a\n * `readCapabilityTree` walk, where each component is a `readdir` entry name.\n * An id read out of `architecture/capabilities.yaml` has not, and must not\n * reach here.\n */\nexport function livingCapabilityPaths(docsDir: DocsDir, id: string): CapabilityDocPaths {\n return capabilityDocPathsAt(join(capabilityDocsDir(docsDir), ...id.split(\"/\")));\n}\n\n/**\n * A feature's own capability deltas — `features/<FEAT>/capabilities/<id>/`,\n * the delta tree that merges into `capabilityDocsDir` above.\n *\n * The layout MIRRORS the living one exactly, nesting included: a delta for\n * `payments/refunds` sits at `features/<FEAT>/capabilities/payments/refunds/`,\n * so `readCapabilityTree` walks both sides with one implementation and the\n * merge target is the same id split the same way. The alternative — a flat\n * escaped spelling like `payments%2Frefunds/` — would be a second grammar with\n * its own escaping defects, and a diff nobody could read by eye against\n * `specs/<svc>/spec.md` ↔ `services/<svc>/spec.md` one directory over.\n *\n * The DIRECTORY'S EXISTENCE is this axis's per-feature opt-in, exactly as\n * `capabilityDocsDir`'s is the fleet's: a feature without one pays a single\n * `existsSync` and produces no finding, so a fleet that has not adopted the\n * business axis sees nothing.\n */\nexport function featureCapabilityDeltasDir(featureDir: FeatureDir): string {\n return join(featureDir, \"capabilities\");\n}\n\n/**\n * The domain's own vocabulary — `glossary/<term>.md`, one FILE per term.\n *\n * A FILE, where a capability gets a directory, and the difference is what sits\n * beside the document. A capability's directory is the seam its feature-local\n * deltas and anything later join through; a term has nothing beside it but its\n * definition, and a directory holding one file is ceremony. The general rule\n * both follow is the one `core/capabilities/tree.ts` states: an entry with prose\n * gets a file, an entry without prose stays a line in YAML — and a domain term\n * is prose or it is nothing.\n *\n * NO `glossary.yaml`, for the reason the capability tree gives at length: a\n * single vocabulary file at fleet scale is unworkable, and a second list is the\n * drift `loam init`'s removed `loam.docs.json` was removed for. The DIRECTORY is\n * the list. Unlike the capability axis there is no YAML half to union with —\n * that file predates the tree and carries `description`/`owner` fields prose\n * cannot overwrite, and nothing here has ever had a second side.\n *\n * NESTING IS ALLOWED and spelled by the tree: `glossary/payments/order.md` is\n * the term `payments/order`. A fleet with three hundred terms in one flat\n * directory is the state this prevents, and it costs nothing — the walk is\n * recursive and the id is the path.\n *\n * TOP-LEVEL, not under `architecture/`, and for `capabilityDocsDir`'s reason:\n * the vocabulary is the domain's, and it must survive every redesign of the\n * fleet that speaks it.\n *\n * NOT SCAFFOLDED BY `loam init`, exactly as `architecture/adrs/` and\n * `capabilities/` are not. The directory's existence is the opt-in.\n */\nexport function glossaryDir(docsDir: DocsDir): string {\n return join(docsDir, \"glossary\");\n}\n\n/**\n * A feature's own glossary deltas — `features/<FEAT>/glossary/<term>.md`, the\n * vocabulary axis's answer to `specs/<svc>/spec.md`.\n *\n * A term a feature INTRODUCES ships and unships with it: the archive copies the\n * definition into `glossary/`, and unarchive takes it back out with everything\n * else the feature landed. That is the whole reason this route exists, and it is\n * why it is CREATE-ONLY (`core/glossary/delta.ts` states the rule and the\n * refusal): a definition is prose with no delta algebra to protect, so rewriting\n * a living one belongs in a pull request where git itself produces the conflict.\n *\n * The DIRECTORY'S EXISTENCE is this axis's per-feature opt-in, exactly as\n * `featureCapabilityDeltasDir`'s is the business axis's: a feature without one\n * pays a single `existsSync` and produces nothing.\n */\nexport function featureGlossaryDir(featureDir: FeatureDir): string {\n return join(featureDir, \"glossary\");\n}\n\n/**\n * A feature's own flows — `features/<FEAT>/usecases/<name>.likec4`, the use-case\n * axis's answer to `specs/<svc>/spec.md`.\n *\n * A `dynamic view` is the only realizer a promise that CROSSES services has, and\n * it was the one realizer no feature could carry: every other axis had a delta\n * slot and this one had none, so the business axis's headline case — an analyst\n * adds a cross-service requirement, the architect answers it with a flow — could\n * not ship as one change. The promise had to land first, kept by nothing, and\n * the flow followed in a second pull request.\n *\n * The name mirrors the LIVING layout rather than inventing a second one:\n * `architecture/usecases/` is where a flow already lives, `usecases/` is where a\n * feature writes the one it brings, and the merge is the same path one directory\n * over. Nesting is spelled by the tree, as it is for every other authored tree\n * here.\n *\n * CREATE-ONLY (`core/usecases/delta/flows.ts` states the rule and the refusal),\n * for `featureGlossaryDir`'s reason: a flow is one ordered hop sequence with no\n * delta algebra to protect, so rewriting a living one belongs in a pull request\n * where git itself produces the conflict.\n *\n * The DIRECTORY'S EXISTENCE is this axis's per-feature opt-in, exactly as the\n * two above: a feature without one pays a single walk over a directory that is\n * not there and produces nothing.\n */\nexport function featureUseCasesDir(featureDir: FeatureDir): string {\n return join(featureDir, \"usecases\");\n}\n\n/**\n * `features/<FEAT>/deployment/` — the TOPOLOGY a feature brings.\n *\n * The third document a feature carries whole rather than as a delta, and for\n * the same reason as the two above: a `deployment { }` block is a topology, the\n * merge is a whole-file copy, and there is nothing to merge partially. What\n * makes the slot work at all is that LikeC4 `extend` resolves ACROSS documents\n * of one project — measured before the axis was written — so a feature adds a\n * standby datacenter inside a living region without touching the living file.\n *\n * `delta.likec4` still refuses a `deployment { }` block, and the refusal now\n * names this directory: the C4 delta re-declares the landscape's identifiers\n * and carries its own `specification`, so it cannot be staged beside the map in\n * one project, and a document that declares neither can.\n */\nexport function featureDeploymentDir(featureDir: FeatureDir): string {\n return join(featureDir, \"deployment\");\n}\n"]}