@ybotok/loam 0.2.0-alpha.5 → 0.2.0-alpha.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (273) hide show
  1. package/CHANGELOG.md +901 -0
  2. package/MIGRATING-from-OpenSpec.md +7 -4
  3. package/QUICKSTART.md +8 -3
  4. package/README.md +13 -7
  5. package/ROADMAP.md +264 -11
  6. package/SCHEMA.md +439 -38
  7. package/WORKFLOW.md +23 -10
  8. package/dist/commands/adopt/render.js +47 -8
  9. package/dist/commands/adopt/render.js.map +1 -1
  10. package/dist/commands/archive/plan/landscape.js +45 -1
  11. package/dist/commands/archive/plan/landscape.js.map +1 -1
  12. package/dist/commands/archive/plan/model/extending.js +85 -0
  13. package/dist/commands/archive/plan/model/extending.js.map +1 -0
  14. package/dist/commands/archive/run.js +5 -2
  15. package/dist/commands/archive/run.js.map +1 -1
  16. package/dist/commands/context/print.js +11 -1
  17. package/dist/commands/context/print.js.map +1 -1
  18. package/dist/commands/doctor.js +46 -0
  19. package/dist/commands/doctor.js.map +1 -1
  20. package/dist/commands/explore.js +12 -2
  21. package/dist/commands/explore.js.map +1 -1
  22. package/dist/commands/list/views.js +5 -1
  23. package/dist/commands/list/views.js.map +1 -1
  24. package/dist/commands/seed/plan.js +97 -3
  25. package/dist/commands/seed/plan.js.map +1 -1
  26. package/dist/commands/seed/seed.js +7 -2
  27. package/dist/commands/seed/seed.js.map +1 -1
  28. package/dist/commands/show/feature.js +2 -1
  29. package/dist/commands/show/feature.js.map +1 -1
  30. package/dist/commands/show/marks.js +3 -5
  31. package/dist/commands/show/marks.js.map +1 -1
  32. package/dist/commands/show/service.js +39 -18
  33. package/dist/commands/show/service.js.map +1 -1
  34. package/dist/commands/subsystem/sync.js +128 -74
  35. package/dist/commands/subsystem/sync.js.map +1 -1
  36. package/dist/commands/subsystem/txn/exclude.js +132 -0
  37. package/dist/commands/subsystem/txn/exclude.js.map +1 -0
  38. package/dist/commands/subsystem/txn/report.js +124 -0
  39. package/dist/commands/subsystem/txn/report.js.map +1 -0
  40. package/dist/commands/subsystem/txn/txn.js +10 -3
  41. package/dist/commands/subsystem/txn/txn.js.map +1 -1
  42. package/dist/commands/subsystem/txn/views.js +113 -16
  43. package/dist/commands/subsystem/txn/views.js.map +1 -1
  44. package/dist/commands/validate/arch-coverage.js +16 -9
  45. package/dist/commands/validate/arch-coverage.js.map +1 -1
  46. package/dist/commands/validate/checks/fleet-shape.js +30 -77
  47. package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
  48. package/dist/commands/validate/checks/vocabulary.js +4 -19
  49. package/dist/commands/validate/checks/vocabulary.js.map +1 -1
  50. package/dist/commands/validate/feature.js +1 -1
  51. package/dist/commands/validate/feature.js.map +1 -1
  52. package/dist/commands/validate/fleet/census.js +11 -10
  53. package/dist/commands/validate/fleet/census.js.map +1 -1
  54. package/dist/commands/validate/fleet/landscape.js +127 -119
  55. package/dist/commands/validate/fleet/landscape.js.map +1 -1
  56. package/dist/commands/validate/fleet/load.js +66 -0
  57. package/dist/commands/validate/fleet/load.js.map +1 -1
  58. package/dist/commands/validate/fleet/map/attest.js +103 -0
  59. package/dist/commands/validate/fleet/map/attest.js.map +1 -0
  60. package/dist/commands/validate/fleet/map/bindings.js +96 -0
  61. package/dist/commands/validate/fleet/map/bindings.js.map +1 -0
  62. package/dist/commands/validate/fleet/map/consumers.js +298 -0
  63. package/dist/commands/validate/fleet/map/consumers.js.map +1 -0
  64. package/dist/commands/validate/fleet/map/isolation.js +116 -0
  65. package/dist/commands/validate/fleet/map/isolation.js.map +1 -0
  66. package/dist/commands/validate/fleet/map/usecases.js +56 -0
  67. package/dist/commands/validate/fleet/map/usecases.js.map +1 -0
  68. package/dist/commands/validate/fleet/usecases/steps.js +49 -17
  69. package/dist/commands/validate/fleet/usecases/steps.js.map +1 -1
  70. package/dist/commands/validate/fleet/views/fleet-project.js +217 -0
  71. package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -0
  72. package/dist/commands/validate/fleet/views/ids.js +13 -9
  73. package/dist/commands/validate/fleet/views/ids.js.map +1 -1
  74. package/dist/commands/validate/fleet/views/projects.js +258 -70
  75. package/dist/commands/validate/fleet/views/projects.js.map +1 -1
  76. package/dist/commands/validate/fleet/views/root-documents.js +97 -0
  77. package/dist/commands/validate/fleet/views/root-documents.js.map +1 -0
  78. package/dist/commands/validate/fleet/views/stale.js +2 -2
  79. package/dist/commands/validate/fleet/views/stale.js.map +1 -1
  80. package/dist/commands/validate/service/model/diverged.js +226 -0
  81. package/dist/commands/validate/service/model/diverged.js.map +1 -0
  82. package/dist/commands/validate/service/model/grade.js +163 -0
  83. package/dist/commands/validate/service/model/grade.js.map +1 -0
  84. package/dist/commands/validate/service/service.js +110 -34
  85. package/dist/commands/validate/service/service.js.map +1 -1
  86. package/dist/commands/validate/service/specs.js +39 -5
  87. package/dist/commands/validate/service/specs.js.map +1 -1
  88. package/dist/commands/validate/service/spine.js +41 -3
  89. package/dist/commands/validate/service/spine.js.map +1 -1
  90. package/dist/commands/validate/service/usecases/tags.js +113 -0
  91. package/dist/commands/validate/service/usecases/tags.js.map +1 -0
  92. package/dist/commands/validate/service/usecases/usecases.js +209 -0
  93. package/dist/commands/validate/service/usecases/usecases.js.map +1 -0
  94. package/dist/commands/validate/validate.js +10 -24
  95. package/dist/commands/validate/validate.js.map +1 -1
  96. package/dist/core/agent/agents-md/artifacts.js +1 -0
  97. package/dist/core/agent/agents-md/artifacts.js.map +1 -1
  98. package/dist/core/agent/agents-md/command-map.js +18 -4
  99. package/dist/core/agent/agents-md/command-map.js.map +1 -1
  100. package/dist/core/agent/agents-md/cycle.js +24 -9
  101. package/dist/core/agent/agents-md/cycle.js.map +1 -1
  102. package/dist/core/agent/agents-md/map/lenses/context.js +4 -3
  103. package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -1
  104. package/dist/core/agent/agents-md/map/subsystem.js +51 -11
  105. package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
  106. package/dist/core/agent/agents-md/map/usecases.js +45 -3
  107. package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
  108. package/dist/core/agent/agents-md.js +14 -4
  109. package/dist/core/agent/agents-md.js.map +1 -1
  110. package/dist/core/agent/support/report.js +39 -5
  111. package/dist/core/agent/support/report.js.map +1 -1
  112. package/dist/core/agent/workflows/adopt.js +123 -14
  113. package/dist/core/agent/workflows/adopt.js.map +1 -1
  114. package/dist/core/agent/workflows/check.js +35 -20
  115. package/dist/core/agent/workflows/check.js.map +1 -1
  116. package/dist/core/agent/workflows/closing.js +3 -1
  117. package/dist/core/agent/workflows/closing.js.map +1 -1
  118. package/dist/core/brief/brief.js +63 -11
  119. package/dist/core/brief/brief.js.map +1 -1
  120. package/dist/core/brief/checks.js +133 -7
  121. package/dist/core/brief/checks.js.map +1 -1
  122. package/dist/core/brief/landscape.js +212 -129
  123. package/dist/core/brief/landscape.js.map +1 -1
  124. package/dist/core/brief/map/extend.js +81 -0
  125. package/dist/core/brief/map/extend.js.map +1 -0
  126. package/dist/core/brief/map/owed.js +284 -0
  127. package/dist/core/brief/map/owed.js.map +1 -0
  128. package/dist/core/brief/targets.js +30 -29
  129. package/dist/core/brief/targets.js.map +1 -1
  130. package/dist/core/brief/unchecked.js +12 -2
  131. package/dist/core/brief/unchecked.js.map +1 -1
  132. package/dist/core/c4/likec4.js +23 -1
  133. package/dist/core/c4/likec4.js.map +1 -1
  134. package/dist/core/c4/parsed/deployment.js +5 -3
  135. package/dist/core/c4/parsed/deployment.js.map +1 -1
  136. package/dist/core/c4/parsed/view-ids.js +39 -1
  137. package/dist/core/c4/parsed/view-ids.js.map +1 -1
  138. package/dist/core/c4/prefetch/fleet.js +142 -0
  139. package/dist/core/c4/prefetch/fleet.js.map +1 -0
  140. package/dist/core/c4/project/architecture.js +52 -4
  141. package/dist/core/c4/project/architecture.js.map +1 -1
  142. package/dist/core/c4/project/batch.js +140 -0
  143. package/dist/core/c4/project/batch.js.map +1 -0
  144. package/dist/core/c4/project/documents.js +9 -4
  145. package/dist/core/c4/project/documents.js.map +1 -1
  146. package/dist/core/c4/project/load.js +77 -38
  147. package/dist/core/c4/project/load.js.map +1 -1
  148. package/dist/core/c4/project/staged.js +46 -12
  149. package/dist/core/c4/project/staged.js.map +1 -1
  150. package/dist/core/c4/resolve/attested.js +0 -0
  151. package/dist/core/c4/resolve/attested.js.map +1 -0
  152. package/dist/core/c4/resolve/service.js +8 -17
  153. package/dist/core/c4/resolve/service.js.map +1 -1
  154. package/dist/core/c4/resolve/steps.js +18 -0
  155. package/dist/core/c4/resolve/steps.js.map +1 -1
  156. package/dist/core/c4/root-project/exclude.js +352 -0
  157. package/dist/core/c4/root-project/exclude.js.map +1 -0
  158. package/dist/core/c4/service-model/documents.js +47 -0
  159. package/dist/core/c4/service-model/documents.js.map +1 -0
  160. package/dist/core/c4/service-model/fleet/extending.js +82 -0
  161. package/dist/core/c4/service-model/fleet/extending.js.map +1 -0
  162. package/dist/core/c4/service-model/load.js +165 -0
  163. package/dist/core/c4/service-model/load.js.map +1 -0
  164. package/dist/core/c4/service-model/renderer.js +128 -0
  165. package/dist/core/c4/service-model/renderer.js.map +1 -0
  166. package/dist/core/c4/service-model/shape.js +126 -0
  167. package/dist/core/c4/service-model/shape.js.map +1 -0
  168. package/dist/core/c4/service-model/slice.js +188 -0
  169. package/dist/core/c4/service-model/slice.js.map +1 -0
  170. package/dist/core/c4/source-scan.js +26 -0
  171. package/dist/core/c4/source-scan.js.map +1 -1
  172. package/dist/core/c4/splice/authored-source.js +60 -13
  173. package/dist/core/c4/splice/authored-source.js.map +1 -1
  174. package/dist/core/c4/splice/contract.js.map +1 -1
  175. package/dist/core/c4/splice/identity/declared.js +87 -0
  176. package/dist/core/c4/splice/identity/declared.js.map +1 -0
  177. package/dist/core/c4/splice/identity/edges.js +17 -0
  178. package/dist/core/c4/splice/identity/edges.js.map +1 -1
  179. package/dist/core/c4/splice/identity/existing.js +80 -0
  180. package/dist/core/c4/splice/identity/existing.js.map +1 -0
  181. package/dist/core/c4/splice/identity/interior.js +80 -0
  182. package/dist/core/c4/splice/identity/interior.js.map +1 -0
  183. package/dist/core/c4/splice/identity/owner.js +50 -0
  184. package/dist/core/c4/splice/identity/owner.js.map +1 -0
  185. package/dist/core/c4/splice/landscape-merge.js +154 -100
  186. package/dist/core/c4/splice/landscape-merge.js.map +1 -1
  187. package/dist/core/c4/splice/model/merge.js +140 -0
  188. package/dist/core/c4/splice/model/merge.js.map +1 -0
  189. package/dist/core/c4/splice/placement.js +14 -4
  190. package/dist/core/c4/splice/placement.js.map +1 -1
  191. package/dist/core/c4/workspace.js +2 -1
  192. package/dist/core/c4/workspace.js.map +1 -1
  193. package/dist/core/coherence/coherence.js +7 -0
  194. package/dist/core/coherence/coherence.js.map +1 -1
  195. package/dist/core/delta/delta.js +9 -0
  196. package/dist/core/delta/delta.js.map +1 -1
  197. package/dist/core/deployment/delta.js +4 -0
  198. package/dist/core/deployment/delta.js.map +1 -1
  199. package/dist/core/diff/base-state.js +21 -5
  200. package/dist/core/diff/base-state.js.map +1 -1
  201. package/dist/core/diff/victims.js +10 -1
  202. package/dist/core/diff/victims.js.map +1 -1
  203. package/dist/core/docs.js +29 -24
  204. package/dist/core/docs.js.map +1 -1
  205. package/dist/core/doctor/config.js +7 -3
  206. package/dist/core/doctor/config.js.map +1 -1
  207. package/dist/core/doctor/doctor.js +36 -8
  208. package/dist/core/doctor/doctor.js.map +1 -1
  209. package/dist/core/doctor/report.js.map +1 -1
  210. package/dist/core/doctor/reports/scan.js +242 -0
  211. package/dist/core/doctor/reports/scan.js.map +1 -0
  212. package/dist/core/explain/refusals.js +1 -1
  213. package/dist/core/explain/refusals.js.map +1 -1
  214. package/dist/core/explore/explore.js +44 -9
  215. package/dist/core/explore/explore.js.map +1 -1
  216. package/dist/core/fleet-context.js +47 -0
  217. package/dist/core/fleet-context.js.map +1 -1
  218. package/dist/core/gate/partners.js +5 -9
  219. package/dist/core/gate/partners.js.map +1 -1
  220. package/dist/core/kernel/ids/fqn/ancestors.js +65 -0
  221. package/dist/core/kernel/ids/fqn/ancestors.js.map +1 -0
  222. package/dist/core/pack/living.js +47 -13
  223. package/dist/core/pack/living.js.map +1 -1
  224. package/dist/core/pack/pack.js +9 -6
  225. package/dist/core/pack/pack.js.map +1 -1
  226. package/dist/core/projection/arch-slice.js +2 -2
  227. package/dist/core/projection/arch-slice.js.map +1 -1
  228. package/dist/core/repo/paths.js +11 -9
  229. package/dist/core/repo/paths.js.map +1 -1
  230. package/dist/core/repo/tree/render/projects.js +93 -35
  231. package/dist/core/repo/tree/render/projects.js.map +1 -1
  232. package/dist/core/repo/tree/render/stale.js +2 -3
  233. package/dist/core/repo/tree/render/stale.js.map +1 -1
  234. package/dist/core/repo/tree/render/views.js +51 -3
  235. package/dist/core/repo/tree/render/views.js.map +1 -1
  236. package/dist/core/repo/tree/walk.js +45 -2
  237. package/dist/core/repo/tree/walk.js.map +1 -1
  238. package/dist/core/scaffold/landscape.js +219 -9
  239. package/dist/core/scaffold/landscape.js.map +1 -1
  240. package/dist/core/staging/writes.js.map +1 -1
  241. package/dist/core/usecases/delta/claims.js +1 -0
  242. package/dist/core/usecases/delta/claims.js.map +1 -1
  243. package/dist/core/usecases/delta/overlay.js +4 -0
  244. package/dist/core/usecases/delta/overlay.js.map +1 -1
  245. package/dist/core/usecases/fleet.js +18 -6
  246. package/dist/core/usecases/fleet.js.map +1 -1
  247. package/dist/core/usecases/operations.js +7 -0
  248. package/dist/core/usecases/operations.js.map +1 -1
  249. package/dist/core/usecases/service/exclude.js +119 -0
  250. package/dist/core/usecases/service/exclude.js.map +1 -0
  251. package/dist/core/usecases/service/flows.js +161 -0
  252. package/dist/core/usecases/service/flows.js.map +1 -0
  253. package/dist/core/vocabulary/maturity.js +10 -0
  254. package/dist/core/vocabulary/maturity.js.map +1 -1
  255. package/examples/README.md +24 -6
  256. package/examples/docs/architecture/landscape.likec4 +41 -0
  257. package/examples/docs/architecture/usecases/order-notification.likec4 +5 -2
  258. package/examples/docs/likec4.config.json +0 -1
  259. package/examples/docs/services/checkout-web/model.likec4 +39 -56
  260. package/examples/docs/services/order-service/arch.spec.md +13 -6
  261. package/examples/docs/services/order-service/model.likec4 +53 -67
  262. package/examples/docs/services/payment-service/arch.spec.md +7 -4
  263. package/examples/docs/services/payment-service/model.likec4 +32 -35
  264. package/examples/docs/services/platform/identity-service/arch.spec.md +7 -4
  265. package/examples/docs/services/platform/identity-service/model.likec4 +29 -33
  266. package/examples/docs/services/platform/notification-service/health.yaml +8 -3
  267. package/examples/docs/services/platform/notification-service/model.likec4 +39 -59
  268. package/package.json +1 -1
  269. package/examples/docs/services/checkout-web/likec4.config.json +0 -4
  270. package/examples/docs/services/order-service/likec4.config.json +0 -4
  271. package/examples/docs/services/payment-service/likec4.config.json +0 -4
  272. package/examples/docs/services/platform/identity-service/likec4.config.json +0 -4
  273. package/examples/docs/services/platform/notification-service/likec4.config.json +0 -4
@@ -31,6 +31,11 @@ import { coversEntries, coversUnknownFindings } from "./checks/requirements.js";
31
31
  export async function deltaArchCoverage(delta) {
32
32
  const { docsDir, elements, relationships, taggedEls, taggedRels, archDeltas, featureServices, scenarioText, preloadedLand, fleet, } = delta;
33
33
  const findings = [];
34
+ // The read index the service models below are read through. A feature target
35
+ // may arrive without one (`validate --feature` builds no fleet), and it gets
36
+ // its own for `validateService`'s reason: `serviceModel` needs the enumeration
37
+ // and a document memo to answer at all.
38
+ const context = fleet ?? new FleetContext();
34
39
  // The delta's own element→service resolver, built once for the whole feature.
35
40
  // `serviceOf` is a one-shot wrapper that rebuilds its index on every call, and
36
41
  // the two loops below ask it up to five times per tagged edge — so a delta
@@ -171,16 +176,18 @@ export async function deltaArchCoverage(delta) {
171
176
  };
172
177
  const unresolved = coversUnknownFindings(reqs, { where: `${svc}: arch.spec.md`, subject: svc }, scope, health.unreadable);
173
178
  if (unresolved.length > 0) {
174
- const modelPath = (await locateServicePaths(docsDir, svc, fleet)).model;
175
- const model = existsSync(modelPath)
176
- ? fleet === undefined
177
- ? await loadFile(modelPath)
178
- : await fleet.loadLikeC4(modelPath)
179
- : null;
180
- if (model !== null && model.errors.length === 0) {
179
+ // Through the ONE reader of `model.likec4`: a model that EXTENDS the map
180
+ // is not a file this could open, and reading it as one would answer with
181
+ // a pile of parse errors — which this arm reads as "no widening", so
182
+ // every `Covers:` line naming a container of a migrated service would be
183
+ // convicted as a typo. `mapUnreadable` is the same silence for the same
184
+ // reason: nothing may widen a scope out of a document nobody could read.
185
+ const svcPaths = await locateServicePaths(docsDir, svc, fleet);
186
+ const model = existsSync(svcPaths.model) ? await context.serviceModel(docsDir, svcPaths) : null;
187
+ if (model !== null && !model.mapUnreadable && model.doc.errors.length === 0) {
181
188
  scope = {
182
- elements: [...baseElements, ...model.elements],
183
- relationships: [...baseRels, ...model.relationships],
189
+ elements: [...baseElements, ...model.doc.elements],
190
+ relationships: [...baseRels, ...model.doc.relationships],
184
191
  health: health.ids,
185
192
  known,
186
193
  };
@@ -1 +1 @@
1
- {"version":3,"file":"arch-coverage.js","sourceRoot":"","sources":["../../../src/commands/validate/arch-coverage.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Whether the architecture a delta INTRODUCES is covered by a requirement that\n * will actually merge.\n *\n * Three findings, one question asked three ways: `archedge.uncovered` (a\n * scenario names the new edge — heuristic, warn-only), `c4.uncovered` (an\n * arch.spec.md requirement covers the new element or edge — mechanical), and\n * `covers.unknown` (the `Covers:` line resolves to something real). This is\n * where agent-built code cuts its corners — the outbox, the retries, the alerts\n * — because no business scenario was ever going to mention them.\n *\n * Split out of `validateFeature` because it is the only part of the feature\n * target that needs the LIVING landscape: everything else there reads the\n * feature directory alone. Keeping the lazy load and its two exemption sets in\n * one module is what stops \"new\" being decided differently in three places.\n */\nimport { existsSync } from \"node:fs\";\nimport { loadFile, type Elem, type LoadedDoc, type Rel } from \"../../core/c4/likec4.js\";\nimport { elementService, serviceResolver } from \"../../core/c4/resolve/service.js\";\nimport { type PathableService } from \"../../core/kernel/ids/service.js\";\nimport { landscapePath as landscapeFile } from \"../../core/repo/paths.js\";\nimport { locateServicePaths } from \"../../core/repo/service-target.js\";\nimport { enumeratedServiceIds } from \"../../core/repo/service-target.js\";\nimport { type Finding } from \"../../core/vocabulary/report.js\";\nimport { type Requirement } from \"../../core/document/spec.js\";\nimport { coversEdge, coversElement, type CoverageScope } from \"../../core/c4/arch.js\";\nimport { readHealth } from \"../../core/vocabulary/health.js\";\nimport { FleetContext } from \"../../core/fleet-context.js\";\nimport { ACTOR_KINDS, EXTERNAL_TAG } from \"../../core/vocabulary/maturity.js\";\nimport { coversEntries, coversUnknownFindings } from \"./checks/requirements.js\";\nimport type { DocsDir } from \"../../core/kernel/ids/dirs.js\";\n\n/** One feature delta's architecture, and everything needed to decide what in it is new. */\nexport interface DeltaArch {\n docsDir: DocsDir;\n /** Every element the delta declares — the resolver's index, and `coversEdge`'s scope. */\n elements: Elem[];\n /** Every relationship the delta declares. */\n relationships: Rel[];\n /** The delta's own additions: elements and edges carrying the feature id as a tag. */\n taggedEls: Elem[];\n taggedRels: Rel[];\n /** Each addressed service's arch.spec.md delta, in enumeration order. */\n archDeltas: Array<{ service: PathableService; reqs: Requirement[] }>;\n /**\n * Every service the feature's `specs/` addresses — not just the ones with an\n * arch delta. The resolver's fleet set unions these in because a service the\n * feature INTRODUCES has no `services/` directory yet, and without its name\n * an edge into that service's own modelled container resolved to the\n * container's title — a phantom the fleet half alone cannot rule out.\n */\n featureServices: PathableService[];\n /** Every scenario the feature's deltas carry, lowercased — the heuristic's haystack. */\n scenarioText: string;\n /** The living landscape under --all; undefined means \"load it if you need it\", null \"there is none\". */\n preloadedLand?: LoadedDoc | null;\n fleet?: FleetContext;\n}\n\nexport async function deltaArchCoverage(delta: DeltaArch): Promise<Finding[]> {\n const {\n docsDir,\n elements,\n relationships,\n taggedEls,\n taggedRels,\n archDeltas,\n featureServices,\n scenarioText,\n preloadedLand,\n fleet,\n } = delta;\n const findings: Finding[] = [];\n\n // The delta's own element→service resolver, built once for the whole feature.\n // `serviceOf` is a one-shot wrapper that rebuilds its index on every call, and\n // the two loops below ask it up to five times per tagged edge — so a delta\n // over a large model paid for one Map of every element per question asked.\n // The enumerated fleet — `services/` plus the feature's own `specs/` names,\n // where a service it introduces lives — rides into it, and into `baseSvcOf`\n // below, which MUST resolve the same way or the already-living exemption\n // keys never match: so an edge drawn into a modelled container\n // (`payment.api`) grades against the service that owns it instead of a\n // service called \"api\" that has never existed. An unenumerable services/\n // degrades to the feature's own names, exactly what every caller got before\n // the set existed.\n const known: ReadonlySet<string> = new Set<string>([\n ...(await enumeratedServiceIds(docsDir, fleet)),\n ...featureServices,\n ]);\n const svcOf = serviceResolver(elements, known);\n\n // Arch-edge coverage (heuristic, warn-only): each new tagged edge should be named by a scenario.\n for (const r of taggedRels) {\n const target = svcOf(r.target);\n const covered = edgeCovered(target, r.title, scenarioText);\n findings.push({\n severity: covered ? \"ok\" : \"warn\",\n code: covered ? \"archedge.covered\" : \"archedge.uncovered\",\n subject: target,\n message: `${svcOf(r.source)} → ${target} \"${r.title ?? \"\"}\"${covered ? \"\" : \" — no scenario names it\"}`,\n text: { indent: 4, header: \"arch-edge coverage (heuristic):\" },\n });\n }\n\n // The architecture spec axis, feature scope — the mechanical counterpart of\n // the heuristic above. Every NEW tagged element and edge in the delta wants a\n // `Covers:` line in one of the feature's arch.spec.md deltas (c4.uncovered):\n // this is where agent-built code cuts its corners — the outbox, the retries,\n // the alerts — because no business scenario was ever going to mention them.\n // Grouping-only elements take the same exemptions the fleet map applies\n // (person kinds, #external) — one set, in checks/vocabulary.ts, so \"not a\n // service\" cannot mean two things. Warnings, never archive gates; `--strict`\n // escalates.\n //\n // Only requirements the archive will MERGE grant coverage here. In a delta,\n // BASE means \"the living state, quoted\": it merges nothing, emits no\n // .feature, and yields no scenario.tested claim — so a Covers: line under a\n // plain `## Requirements` quote is an obligation that ships nowhere, and\n // counting it silenced c4.uncovered for free. (The service-scope pass keeps\n // the unfiltered call: a LIVING spec is legitimately all BASE.)\n const activeCovers = archDeltas.flatMap(({ reqs }) =>\n coversEntries(reqs.filter((r) => r.kind === \"ADDED\" || r.kind === \"MODIFIED\")),\n );\n\n // What the living landscape ALREADY holds. A delta has to re-declare the\n // elements its new edges attach to, and authors tag those re-declarations\n // along with everything else — so a requirements-only feature that touches an\n // existing service was told to write `Covers:` lines for architecture it is\n // not adding. c4.uncovered is an obligation on NEW architecture; an element\n // the living landscape already resolves is not new, whatever the tag says.\n // Loaded lazily: a delta with nothing tagged never pays for the parse.\n let living: LoadedDoc | null | undefined = preloadedLand;\n const livingLandscape = async (): Promise<LoadedDoc | null> => {\n if (living === undefined) {\n const lp = landscapeFile(docsDir);\n living = existsSync(lp)\n ? fleet === undefined\n ? await loadFile(lp)\n : await fleet.loadLikeC4(lp)\n : null;\n }\n return living;\n };\n const alreadyLiving = async (): Promise<LoadedDoc | null> => {\n if (taggedEls.length === 0 && taggedRels.length === 0) return null;\n const doc = await livingLandscape();\n return doc !== null && doc.errors.length === 0 ? doc : null;\n };\n const base = await alreadyLiving();\n const baseSvcOf = base === null ? null : serviceResolver(base.elements, known);\n const baseIds = new Set(base?.elements.map((e) => e.id) ?? []);\n const baseServices = new Set((base?.elements ?? []).map(elementService));\n // How a service→service pair is keyed, spelled ONCE. The two sides used to\n // join with different separators — a NUL where the set was built, a space\n // where it was queried — so the exemption never matched anything, and every\n // edge a delta re-declares verbatim (which it must, to attach anything to\n // it) was reported as new architecture nobody covers. The join is structural\n // rather than a separator character: the last one was a NUL, and a raw NUL in\n // a template literal makes the source read as binary to `file` and invisible\n // to `grep` — which is how a one-character mismatch survived review.\n const edgeKey = (source: string, target: string): string => JSON.stringify([source, target]);\n const baseEdges = new Set(\n (base?.relationships ?? []).map((r) => edgeKey(baseSvcOf!(r.source), baseSvcOf!(r.target))),\n );\n\n for (const e of taggedEls) {\n if (ACTOR_KINDS.has(e.kind.toLowerCase()) || e.tags.includes(EXTERNAL_TAG)) continue;\n if (baseIds.has(e.id) || baseServices.has(elementService(e))) continue;\n if (activeCovers.some((c) => coversElement(c, e))) continue;\n findings.push({\n severity: \"warn\",\n code: \"c4.uncovered\",\n subject: elementService(e),\n message: `delta adds '${e.title}' (${e.id}) but no arch requirement covers it — add 'Covers: ${e.id}' to a specs/<svc>/arch.spec.md delta, or its architectural obligations ship unchecked`,\n });\n }\n for (const r of taggedRels) {\n if (baseEdges.has(edgeKey(svcOf(r.source), svcOf(r.target)))) continue;\n if (activeCovers.some((c) => coversEdge(c, r, elements, known))) continue;\n findings.push({\n severity: \"warn\",\n code: \"c4.uncovered\",\n subject: svcOf(r.target),\n message: `delta adds edge ${svcOf(r.source)} → ${svcOf(r.target)} (\"${r.title ?? \"\"}\") but no arch requirement covers it — add 'Covers: ${r.source} -> ${r.target}' to a specs/<svc>/arch.spec.md delta`,\n });\n }\n\n // covers.unknown, feature scope. Resolution looks at the delta itself, the\n // living landscape, the service's own model and its health.yaml — a delta's\n // arch requirement may cover an element it adds, one that already exists, or\n // an alert the service declares. The landscape and each model are loaded\n // lazily, and only when an entry fails against what is already in hand: the\n // clean path never pays for a workspace spin.\n if (archDeltas.some(({ reqs }) => coversEntries(reqs).length > 0)) {\n const land = await livingLandscape();\n const landParses = land !== null && land.errors.length === 0 ? land : null;\n const baseElements = [...elements, ...(landParses?.elements ?? [])];\n const baseRels = [...relationships, ...(landParses?.relationships ?? [])];\n // A feature never brings topology through `delta.likec4` — a `deployment { }`\n // block in one is refused by the merge — so the living map is the whole\n // scope for a `node:` entry in a delta, exactly as it is in service scope.\n const baseDeployment = landParses?.deployment;\n for (const { service: svc, reqs } of archDeltas) {\n // An unreadable living health.yaml mutes the alert:/sli: entries here\n // exactly as in service scope — the health.invalid finding itself\n // belongs to the service target, which owns the file's diagnosis.\n const health = await readHealth((await locateServicePaths(docsDir, svc, fleet)).health);\n let scope: CoverageScope = {\n elements: baseElements,\n relationships: baseRels,\n ...(baseDeployment === undefined ? {} : { deployment: baseDeployment }),\n health: health.ids,\n known,\n };\n const unresolved = coversUnknownFindings(reqs, { where: `${svc}: arch.spec.md`, subject: svc }, scope, health.unreadable);\n if (unresolved.length > 0) {\n const modelPath = (await locateServicePaths(docsDir, svc, fleet)).model;\n const model = existsSync(modelPath)\n ? fleet === undefined\n ? await loadFile(modelPath)\n : await fleet.loadLikeC4(modelPath)\n : null;\n if (model !== null && model.errors.length === 0) {\n scope = {\n elements: [...baseElements, ...model.elements],\n relationships: [...baseRels, ...model.relationships],\n health: health.ids,\n known,\n };\n }\n findings.push(...coversUnknownFindings(reqs, { where: `${svc}: arch.spec.md`, subject: svc }, scope, health.unreadable));\n }\n }\n }\n\n return findings;\n}\n\n/** Heuristic: an edge is \"covered\" if a scenario names the target or a keyword from the edge title. */\nfunction edgeCovered(target: string, title: string | undefined, scenarioText: string): boolean {\n if (scenarioText.includes(target.toLowerCase())) return true;\n for (const token of (title ?? \"\").split(/[^A-Za-z0-9]+/)) {\n if (token.length >= 5 && scenarioText.includes(token.toLowerCase())) return true;\n }\n return false;\n}\n"]}
1
+ {"version":3,"file":"arch-coverage.js","sourceRoot":"","sources":["../../../src/commands/validate/arch-coverage.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Whether the architecture a delta INTRODUCES is covered by a requirement that\n * will actually merge.\n *\n * Three findings, one question asked three ways: `archedge.uncovered` (a\n * scenario names the new edge — heuristic, warn-only), `c4.uncovered` (an\n * arch.spec.md requirement covers the new element or edge — mechanical), and\n * `covers.unknown` (the `Covers:` line resolves to something real). This is\n * where agent-built code cuts its corners — the outbox, the retries, the alerts\n * — because no business scenario was ever going to mention them.\n *\n * Split out of `validateFeature` because it is the only part of the feature\n * target that needs the LIVING landscape: everything else there reads the\n * feature directory alone. Keeping the lazy load and its two exemption sets in\n * one module is what stops \"new\" being decided differently in three places.\n */\nimport { existsSync } from \"node:fs\";\nimport { loadFile, type Elem, type LoadedDoc, type Rel } from \"../../core/c4/likec4.js\";\nimport { elementService, serviceResolver } from \"../../core/c4/resolve/service.js\";\nimport { type PathableService } from \"../../core/kernel/ids/service.js\";\nimport { landscapePath as landscapeFile } from \"../../core/repo/paths.js\";\nimport { locateServicePaths } from \"../../core/repo/service-target.js\";\nimport { enumeratedServiceIds } from \"../../core/repo/service-target.js\";\nimport { type Finding } from \"../../core/vocabulary/report.js\";\nimport { type Requirement } from \"../../core/document/spec.js\";\nimport { coversEdge, coversElement, type CoverageScope } from \"../../core/c4/arch.js\";\nimport { readHealth } from \"../../core/vocabulary/health.js\";\nimport { FleetContext } from \"../../core/fleet-context.js\";\nimport { ACTOR_KINDS, EXTERNAL_TAG } from \"../../core/vocabulary/maturity.js\";\nimport { coversEntries, coversUnknownFindings } from \"./checks/requirements.js\";\nimport type { DocsDir } from \"../../core/kernel/ids/dirs.js\";\n\n/** One feature delta's architecture, and everything needed to decide what in it is new. */\nexport interface DeltaArch {\n docsDir: DocsDir;\n /** Every element the delta declares — the resolver's index, and `coversEdge`'s scope. */\n elements: Elem[];\n /** Every relationship the delta declares. */\n relationships: Rel[];\n /** The delta's own additions: elements and edges carrying the feature id as a tag. */\n taggedEls: Elem[];\n taggedRels: Rel[];\n /** Each addressed service's arch.spec.md delta, in enumeration order. */\n archDeltas: Array<{ service: PathableService; reqs: Requirement[] }>;\n /**\n * Every service the feature's `specs/` addresses — not just the ones with an\n * arch delta. The resolver's fleet set unions these in because a service the\n * feature INTRODUCES has no `services/` directory yet, and without its name\n * an edge into that service's own modelled container resolved to the\n * container's title — a phantom the fleet half alone cannot rule out.\n */\n featureServices: PathableService[];\n /** Every scenario the feature's deltas carry, lowercased — the heuristic's haystack. */\n scenarioText: string;\n /** The living landscape under --all; undefined means \"load it if you need it\", null \"there is none\". */\n preloadedLand?: LoadedDoc | null;\n fleet?: FleetContext;\n}\n\nexport async function deltaArchCoverage(delta: DeltaArch): Promise<Finding[]> {\n const {\n docsDir,\n elements,\n relationships,\n taggedEls,\n taggedRels,\n archDeltas,\n featureServices,\n scenarioText,\n preloadedLand,\n fleet,\n } = delta;\n const findings: Finding[] = [];\n // The read index the service models below are read through. A feature target\n // may arrive without one (`validate --feature` builds no fleet), and it gets\n // its own for `validateService`'s reason: `serviceModel` needs the enumeration\n // and a document memo to answer at all.\n const context = fleet ?? new FleetContext();\n\n // The delta's own element→service resolver, built once for the whole feature.\n // `serviceOf` is a one-shot wrapper that rebuilds its index on every call, and\n // the two loops below ask it up to five times per tagged edge — so a delta\n // over a large model paid for one Map of every element per question asked.\n // The enumerated fleet — `services/` plus the feature's own `specs/` names,\n // where a service it introduces lives — rides into it, and into `baseSvcOf`\n // below, which MUST resolve the same way or the already-living exemption\n // keys never match: so an edge drawn into a modelled container\n // (`payment.api`) grades against the service that owns it instead of a\n // service called \"api\" that has never existed. An unenumerable services/\n // degrades to the feature's own names, exactly what every caller got before\n // the set existed.\n const known: ReadonlySet<string> = new Set<string>([\n ...(await enumeratedServiceIds(docsDir, fleet)),\n ...featureServices,\n ]);\n const svcOf = serviceResolver(elements, known);\n\n // Arch-edge coverage (heuristic, warn-only): each new tagged edge should be named by a scenario.\n for (const r of taggedRels) {\n const target = svcOf(r.target);\n const covered = edgeCovered(target, r.title, scenarioText);\n findings.push({\n severity: covered ? \"ok\" : \"warn\",\n code: covered ? \"archedge.covered\" : \"archedge.uncovered\",\n subject: target,\n message: `${svcOf(r.source)} → ${target} \"${r.title ?? \"\"}\"${covered ? \"\" : \" — no scenario names it\"}`,\n text: { indent: 4, header: \"arch-edge coverage (heuristic):\" },\n });\n }\n\n // The architecture spec axis, feature scope — the mechanical counterpart of\n // the heuristic above. Every NEW tagged element and edge in the delta wants a\n // `Covers:` line in one of the feature's arch.spec.md deltas (c4.uncovered):\n // this is where agent-built code cuts its corners — the outbox, the retries,\n // the alerts — because no business scenario was ever going to mention them.\n // Grouping-only elements take the same exemptions the fleet map applies\n // (person kinds, #external) — one set, in checks/vocabulary.ts, so \"not a\n // service\" cannot mean two things. Warnings, never archive gates; `--strict`\n // escalates.\n //\n // Only requirements the archive will MERGE grant coverage here. In a delta,\n // BASE means \"the living state, quoted\": it merges nothing, emits no\n // .feature, and yields no scenario.tested claim — so a Covers: line under a\n // plain `## Requirements` quote is an obligation that ships nowhere, and\n // counting it silenced c4.uncovered for free. (The service-scope pass keeps\n // the unfiltered call: a LIVING spec is legitimately all BASE.)\n const activeCovers = archDeltas.flatMap(({ reqs }) =>\n coversEntries(reqs.filter((r) => r.kind === \"ADDED\" || r.kind === \"MODIFIED\")),\n );\n\n // What the living landscape ALREADY holds. A delta has to re-declare the\n // elements its new edges attach to, and authors tag those re-declarations\n // along with everything else — so a requirements-only feature that touches an\n // existing service was told to write `Covers:` lines for architecture it is\n // not adding. c4.uncovered is an obligation on NEW architecture; an element\n // the living landscape already resolves is not new, whatever the tag says.\n // Loaded lazily: a delta with nothing tagged never pays for the parse.\n let living: LoadedDoc | null | undefined = preloadedLand;\n const livingLandscape = async (): Promise<LoadedDoc | null> => {\n if (living === undefined) {\n const lp = landscapeFile(docsDir);\n living = existsSync(lp)\n ? fleet === undefined\n ? await loadFile(lp)\n : await fleet.loadLikeC4(lp)\n : null;\n }\n return living;\n };\n const alreadyLiving = async (): Promise<LoadedDoc | null> => {\n if (taggedEls.length === 0 && taggedRels.length === 0) return null;\n const doc = await livingLandscape();\n return doc !== null && doc.errors.length === 0 ? doc : null;\n };\n const base = await alreadyLiving();\n const baseSvcOf = base === null ? null : serviceResolver(base.elements, known);\n const baseIds = new Set(base?.elements.map((e) => e.id) ?? []);\n const baseServices = new Set((base?.elements ?? []).map(elementService));\n // How a service→service pair is keyed, spelled ONCE. The two sides used to\n // join with different separators — a NUL where the set was built, a space\n // where it was queried — so the exemption never matched anything, and every\n // edge a delta re-declares verbatim (which it must, to attach anything to\n // it) was reported as new architecture nobody covers. The join is structural\n // rather than a separator character: the last one was a NUL, and a raw NUL in\n // a template literal makes the source read as binary to `file` and invisible\n // to `grep` — which is how a one-character mismatch survived review.\n const edgeKey = (source: string, target: string): string => JSON.stringify([source, target]);\n const baseEdges = new Set(\n (base?.relationships ?? []).map((r) => edgeKey(baseSvcOf!(r.source), baseSvcOf!(r.target))),\n );\n\n for (const e of taggedEls) {\n if (ACTOR_KINDS.has(e.kind.toLowerCase()) || e.tags.includes(EXTERNAL_TAG)) continue;\n if (baseIds.has(e.id) || baseServices.has(elementService(e))) continue;\n if (activeCovers.some((c) => coversElement(c, e))) continue;\n findings.push({\n severity: \"warn\",\n code: \"c4.uncovered\",\n subject: elementService(e),\n message: `delta adds '${e.title}' (${e.id}) but no arch requirement covers it — add 'Covers: ${e.id}' to a specs/<svc>/arch.spec.md delta, or its architectural obligations ship unchecked`,\n });\n }\n for (const r of taggedRels) {\n if (baseEdges.has(edgeKey(svcOf(r.source), svcOf(r.target)))) continue;\n if (activeCovers.some((c) => coversEdge(c, r, elements, known))) continue;\n findings.push({\n severity: \"warn\",\n code: \"c4.uncovered\",\n subject: svcOf(r.target),\n message: `delta adds edge ${svcOf(r.source)} → ${svcOf(r.target)} (\"${r.title ?? \"\"}\") but no arch requirement covers it — add 'Covers: ${r.source} -> ${r.target}' to a specs/<svc>/arch.spec.md delta`,\n });\n }\n\n // covers.unknown, feature scope. Resolution looks at the delta itself, the\n // living landscape, the service's own model and its health.yaml — a delta's\n // arch requirement may cover an element it adds, one that already exists, or\n // an alert the service declares. The landscape and each model are loaded\n // lazily, and only when an entry fails against what is already in hand: the\n // clean path never pays for a workspace spin.\n if (archDeltas.some(({ reqs }) => coversEntries(reqs).length > 0)) {\n const land = await livingLandscape();\n const landParses = land !== null && land.errors.length === 0 ? land : null;\n const baseElements = [...elements, ...(landParses?.elements ?? [])];\n const baseRels = [...relationships, ...(landParses?.relationships ?? [])];\n // A feature never brings topology through `delta.likec4` — a `deployment { }`\n // block in one is refused by the merge — so the living map is the whole\n // scope for a `node:` entry in a delta, exactly as it is in service scope.\n const baseDeployment = landParses?.deployment;\n for (const { service: svc, reqs } of archDeltas) {\n // An unreadable living health.yaml mutes the alert:/sli: entries here\n // exactly as in service scope — the health.invalid finding itself\n // belongs to the service target, which owns the file's diagnosis.\n const health = await readHealth((await locateServicePaths(docsDir, svc, fleet)).health);\n let scope: CoverageScope = {\n elements: baseElements,\n relationships: baseRels,\n ...(baseDeployment === undefined ? {} : { deployment: baseDeployment }),\n health: health.ids,\n known,\n };\n const unresolved = coversUnknownFindings(reqs, { where: `${svc}: arch.spec.md`, subject: svc }, scope, health.unreadable);\n if (unresolved.length > 0) {\n // Through the ONE reader of `model.likec4`: a model that EXTENDS the map\n // is not a file this could open, and reading it as one would answer with\n // a pile of parse errors — which this arm reads as \"no widening\", so\n // every `Covers:` line naming a container of a migrated service would be\n // convicted as a typo. `mapUnreadable` is the same silence for the same\n // reason: nothing may widen a scope out of a document nobody could read.\n const svcPaths = await locateServicePaths(docsDir, svc, fleet);\n const model = existsSync(svcPaths.model) ? await context.serviceModel(docsDir, svcPaths) : null;\n if (model !== null && !model.mapUnreadable && model.doc.errors.length === 0) {\n scope = {\n elements: [...baseElements, ...model.doc.elements],\n relationships: [...baseRels, ...model.doc.relationships],\n health: health.ids,\n known,\n };\n }\n findings.push(...coversUnknownFindings(reqs, { where: `${svc}: arch.spec.md`, subject: svc }, scope, health.unreadable));\n }\n }\n }\n\n return findings;\n}\n\n/** Heuristic: an edge is \"covered\" if a scenario names the target or a keyword from the edge title. */\nfunction edgeCovered(target: string, title: string | undefined, scenarioText: string): boolean {\n if (scenarioText.includes(target.toLowerCase())) return true;\n for (const token of (title ?? \"\").split(/[^A-Za-z0-9]+/)) {\n if (token.length >= 5 && scenarioText.includes(token.toLowerCase())) return true;\n }\n return false;\n}\n"]}
@@ -1,25 +1,24 @@
1
1
  /**
2
- * Fleet-shape advisories over the parsed landscape: the two ways a fleet map
3
- * stops answering questions as it grows, caught while the map is already in
4
- * memory.
2
+ * Fleet-shape advisories over the parsed landscape: the way a fleet map stops
3
+ * answering questions as it grows, caught while the map is already in memory.
5
4
  *
6
- * Both shapes become visible at service three rather than service two.
5
+ * The shape becomes visible at service three rather than service two.
7
6
  * Ubiquitous infrastructure — logging Kafka, an identity provider, service
8
7
  * discovery — takes one inbound edge per modelled service until the map is a
9
8
  * hairball around it; deleting the element fixes the picture and loses "who
10
9
  * depends on the Identity Provider", which is the question the map exists to
11
10
  * answer during an incident. Tagging keeps both: the fleet view excludes
12
- * `#platform`, and a platform view keeps the dependents. And a datastore drawn as a fleet-level
13
- * peer makes a claim its consumer count either supports or refutes: one
14
- * consumer means the drawing is false — the store is that service's
15
- * internals, not a system in its own right — while two or more mean the
16
- * strongest coupling two services can have, which deserves to be stated
17
- * rather than inferred.
11
+ * `#platform`, and a platform view keeps the dependents.
18
12
  *
19
- * Everything here is O(relationships) over data the fleet target already
20
- * holds. loam still reads no code and decides no policy: the team chooses
21
- * what counts as platform and which data is truly shared — these warnings
22
- * only name the shapes that are usually wrong.
13
+ * The DATASTORE half of this pair moved to `../fleet/map/consumers.ts` when its
14
+ * consumer count stopped being a fact about the map alone: a private store now
15
+ * lives nested inside its owner's element, and the services reaching it are
16
+ * attested in their own models (E3/R1). The census both checks read is built
17
+ * there and handed in here as `consumers`, so a hub and a store can never
18
+ * disagree about who consumes what.
19
+ *
20
+ * loam still reads no code and decides no policy: the team chooses what counts
21
+ * as platform — this warning only names a shape that is usually wrong.
23
22
  */
24
23
  import { existsSync } from "node:fs";
25
24
  import { readFile } from "node:fs/promises";
@@ -45,76 +44,30 @@ import { EXTERNAL_TAG, PLATFORM_TAG } from "../../../core/vocabulary/maturity.js
45
44
  * the shared hub starts making the map unreadable.
46
45
  */
47
46
  export const PLATFORM_CANDIDATE_MIN_CONSUMERS = 3;
48
- /** The element kind read as a datastore, compared case-insensitively. */
49
- const DATASTORE_KIND = "database";
50
47
  export function fleetShapeFindings(shape) {
51
- const { drawn, relationships, services, resolve } = shape;
48
+ const { drawn, services } = shape;
52
49
  const findings = [];
53
- // Distinct consumer SERVICES of an element, its nested children included: an
54
- // edge into `kafka.paymentEvents` is consumption of `kafka`. Persons, other
55
- // external systems and unresolved sources never count — a consumer is a
56
- // source the shared resolver files under a real services/<id>/, so two edges
57
- // from one service are one consumer.
58
- const consumersOf = (e) => {
59
- const out = new Set();
60
- for (const r of relationships) {
61
- if (r.target !== e.id && !r.target.startsWith(`${e.id}.`))
62
- continue;
63
- const svc = resolve(r.source);
64
- if (services.has(svc))
65
- out.add(svc);
66
- }
67
- return [...out].sort();
68
- };
69
50
  for (const e of drawn) {
70
51
  // An element that stands for a real service is the fleet itself, not its
71
- // shape: many consumers is what a well-used service looks like, and a
72
- // datastore bound to a directory is graded by the binding checks instead.
52
+ // shape: many consumers is what a well-used service looks like.
73
53
  if (e.service !== undefined || services.has(e.title))
74
54
  continue;
75
- const consumers = consumersOf(e);
76
- if (e.tags.includes(EXTERNAL_TAG) &&
77
- !e.tags.includes(PLATFORM_TAG) &&
78
- consumers.length >= PLATFORM_CANDIDATE_MIN_CONSUMERS) {
79
- findings.push({
80
- severity: "warn",
81
- code: "landscape.platform-candidate",
82
- subject: e.title,
83
- message: `landscape: '${e.title}' is consumed by ${consumers.length} services (${consumers.join(", ")}) ` +
84
- `and is not tagged #platform — a hub like this takes one more edge per modelled service until ` +
85
- `the fleet view is unreadable. Declare \`tag platform\` in the specification block and tag the ` +
86
- `element (LikeC4 refuses an undeclared tag, so both steps are needed): the fleet view then ` +
87
- `excludes it, and a platform view over \`include * -> element.tag = #platform\` keeps ` +
88
- `"who depends on it" answerable`,
89
- });
90
- }
91
- if (e.kind.toLowerCase() !== DATASTORE_KIND || consumers.length === 0)
55
+ if (!e.tags.includes(EXTERNAL_TAG) || e.tags.includes(PLATFORM_TAG))
92
56
  continue;
93
- if (consumers.length === 1) {
94
- findings.push({
95
- severity: "warn",
96
- code: "landscape.datastore-private",
97
- subject: e.title,
98
- message: `landscape: '${e.title}' is a datastore with a single consumer at fleet level ` +
99
- `('${consumers[0]}') — drawn as a peer it reads as a system in its own right, available to ` +
100
- `be depended on. Move it into ${shape.pathOf(consumers[0])}/model.likec4 as a nested container ` +
101
- `and delete it here, or add the second consumer's edge if another service really reaches ` +
102
- `the same data`,
103
- });
104
- }
105
- else {
106
- findings.push({
107
- severity: "warn",
108
- code: "landscape.datastore-shared",
109
- subject: e.title,
110
- message: `landscape: '${e.title}' is a datastore shared by ${consumers.length} services ` +
111
- `(${consumers.join(", ")}) — the strongest coupling two services can have, and the hardest ` +
112
- `to undo. Shared means the same DATA: if they read the same tables or keys, keep it drawn ` +
113
- `and let this warning state the coupling; if they only share a host or cluster (two schemas, ` +
114
- `two lock paths), that is operational blast radius — a runbook fact — and the honest model ` +
115
- `is one private store per service`,
116
- });
117
- }
57
+ const consumers = shape.consumers(e.id);
58
+ if (consumers.length < PLATFORM_CANDIDATE_MIN_CONSUMERS)
59
+ continue;
60
+ findings.push({
61
+ severity: "warn",
62
+ code: "landscape.platform-candidate",
63
+ subject: e.title,
64
+ message: `landscape: '${e.title}' is consumed by ${consumers.length} services (${consumers.join(", ")}) ` +
65
+ `and is not tagged #platform — a hub like this takes one more edge per modelled service until ` +
66
+ `the fleet view is unreadable. Declare \`tag platform\` in the specification block and tag the ` +
67
+ `element (LikeC4 refuses an undeclared tag, so both steps are needed): the fleet view then ` +
68
+ `excludes it, and a platform view over \`include * -> element.tag = #platform\` keeps ` +
69
+ `"who depends on it" answerable`,
70
+ });
118
71
  }
119
72
  return findings;
120
73
  }
@@ -1 +1 @@
1
- {"version":3,"file":"fleet-shape.js","sourceRoot":"","sources":["../../../../src/commands/validate/checks/fleet-shape.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Fleet-shape advisories over the parsed landscape: the two ways a fleet map\n * stops answering questions as it grows, caught while the map is already in\n * memory.\n *\n * Both shapes become visible at service three rather than service two.\n * Ubiquitous infrastructure — logging Kafka, an identity provider, service\n * discovery — takes one inbound edge per modelled service until the map is a\n * hairball around it; deleting the element fixes the picture and loses \"who\n * depends on the Identity Provider\", which is the question the map exists to\n * answer during an incident. Tagging keeps both: the fleet view excludes\n * `#platform`, and a platform view keeps the dependents. And a datastore drawn as a fleet-level\n * peer makes a claim its consumer count either supports or refutes: one\n * consumer means the drawing is false — the store is that service's\n * internals, not a system in its own right — while two or more mean the\n * strongest coupling two services can have, which deserves to be stated\n * rather than inferred.\n *\n * Everything here is O(relationships) over data the fleet target already\n * holds. loam still reads no code and decides no policy: the team chooses\n * what counts as platform and which data is truly shared — these warnings\n * only name the shapes that are usually wrong.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { type Elem, type LoadedDoc, type Rel } from \"../../../core/c4/likec4.js\";\nimport { type DeclaredService, type RawServiceId } from \"../../../core/kernel/ids/service.js\";\nimport { type Finding } from \"../../../core/vocabulary/report.js\";\nimport { type ServiceEntry } from \"../../../core/repo/entries.js\";\nimport { permissionsPath, servicePathsAt } from \"../../../core/repo/paths.js\";\nimport { readVocabulary } from \"../../../core/permissions/permissions.js\";\nimport { readCapabilityVocabulary } from \"../../../core/capabilities/capabilities.js\";\nimport {\n capabilityDocFindings,\n capabilityRequirementIndex,\n docMissingFindings,\n gradableCapabilityIds,\n invalidVocabularyFinding,\n unrealizedFindings,\n} from \"../../../core/capabilities/findings.js\";\nimport { useCaseRequirementClaims } from \"../../../core/usecases/capability.js\";\nimport type { ParsedView } from \"../../../core/c4/parsed/dynamic-views.js\";\nimport { capabilityRollup, usedCapabilities } from \"../../../core/capabilities/rollup.js\";\nimport { requirementUnrealizedFindings } from \"../../../core/capabilities/realizes/findings.js\";\nimport { parseRequirements } from \"../../../core/document/parse.js\";\nimport { type Requirement } from \"../../../core/document/spec.js\";\nimport { FleetContext } from \"../../../core/fleet-context.js\";\nimport { EXTERNAL_TAG, PLATFORM_TAG } from \"../../../core/vocabulary/maturity.js\";\nimport type { DocsDir } from \"../../../core/kernel/ids/dirs.js\";\n\n/**\n * Consumers at which an untagged external hub starts to warn. Three, not two:\n * two services sharing a dependency is a fact about those two services, the\n * same edge from three is a pattern about the element — and three is where\n * the shared hub starts making the map unreadable.\n */\nexport const PLATFORM_CANDIDATE_MIN_CONSUMERS = 3;\n\n/** The element kind read as a datastore, compared case-insensitively. */\nconst DATASTORE_KIND = \"database\";\n\nexport interface FleetShape {\n /** Service-LEVEL elements — the fleet map's own boxes (landscape.ts `drawn`). */\n drawn: Elem[];\n relationships: Rel[];\n /** The services/<id>/ directories that exist. */\n services: ReadonlySet<string>;\n /** The shared element→service resolver every edge join uses. */\n resolve: (id: string) => string;\n /**\n * Where a service id's directory actually sits, repo-relative and without a\n * trailing slash. Injected rather than joined: only the enumeration knows\n * whether a service is filed under a subsystem, and a fix instruction naming\n * the wrong directory is worse than one naming none.\n */\n pathOf: (id: string) => string;\n}\n\nexport function fleetShapeFindings(shape: FleetShape): Finding[] {\n const { drawn, relationships, services, resolve } = shape;\n const findings: Finding[] = [];\n\n // Distinct consumer SERVICES of an element, its nested children included: an\n // edge into `kafka.paymentEvents` is consumption of `kafka`. Persons, other\n // external systems and unresolved sources never count — a consumer is a\n // source the shared resolver files under a real services/<id>/, so two edges\n // from one service are one consumer.\n const consumersOf = (e: Elem): string[] => {\n const out = new Set<string>();\n for (const r of relationships) {\n if (r.target !== e.id && !r.target.startsWith(`${e.id}.`)) continue;\n const svc = resolve(r.source);\n if (services.has(svc)) out.add(svc);\n }\n return [...out].sort();\n };\n\n for (const e of drawn) {\n // An element that stands for a real service is the fleet itself, not its\n // shape: many consumers is what a well-used service looks like, and a\n // datastore bound to a directory is graded by the binding checks instead.\n if (e.service !== undefined || services.has(e.title)) continue;\n const consumers = consumersOf(e);\n\n if (\n e.tags.includes(EXTERNAL_TAG) &&\n !e.tags.includes(PLATFORM_TAG) &&\n consumers.length >= PLATFORM_CANDIDATE_MIN_CONSUMERS\n ) {\n findings.push({\n severity: \"warn\",\n code: \"landscape.platform-candidate\",\n subject: e.title,\n message:\n `landscape: '${e.title}' is consumed by ${consumers.length} services (${consumers.join(\", \")}) ` +\n `and is not tagged #platform — a hub like this takes one more edge per modelled service until ` +\n `the fleet view is unreadable. Declare \\`tag platform\\` in the specification block and tag the ` +\n `element (LikeC4 refuses an undeclared tag, so both steps are needed): the fleet view then ` +\n `excludes it, and a platform view over \\`include * -> element.tag = #platform\\` keeps ` +\n `\"who depends on it\" answerable`,\n });\n }\n\n if (e.kind.toLowerCase() !== DATASTORE_KIND || consumers.length === 0) continue;\n if (consumers.length === 1) {\n findings.push({\n severity: \"warn\",\n code: \"landscape.datastore-private\",\n subject: e.title,\n message:\n `landscape: '${e.title}' is a datastore with a single consumer at fleet level ` +\n `('${consumers[0]!}') — drawn as a peer it reads as a system in its own right, available to ` +\n `be depended on. Move it into ${shape.pathOf(consumers[0]!)}/model.likec4 as a nested container ` +\n `and delete it here, or add the second consumer's edge if another service really reaches ` +\n `the same data`,\n });\n } else {\n findings.push({\n severity: \"warn\",\n code: \"landscape.datastore-shared\",\n subject: e.title,\n message:\n `landscape: '${e.title}' is a datastore shared by ${consumers.length} services ` +\n `(${consumers.join(\", \")}) — the strongest coupling two services can have, and the hardest ` +\n `to undo. Shared means the same DATA: if they read the same tables or keys, keep it drawn ` +\n `and let this warning state the coupling; if they only share a host or cluster (two schemas, ` +\n `two lock paths), that is operational blast radius — a runbook fact — and the honest model ` +\n `is one private store per service`,\n });\n }\n }\n return findings;\n}\n\n/**\n * The #external element that declares it PUBLISHES this message, or null —\n * the landscape-side half of the event spine's fleet question, beside the\n * other checks that read tags off the drawn map.\n *\n * Positive evidence only: an edge carrying `metadata { publishes '<msg>' }`\n * whose source element — itself or a declared ancestor, since the edge may\n * point out of a topic nested inside the broker — is tagged #external and\n * resolves to no known service directory. The resolution guard is what keeps\n * a mis-tagged internal service reading as unproduced: a tag is cheap to\n * write, and a services/<id>/ directory outranks it.\n */\n/**\n * The authorization vocabulary graded against the fleet that uses it — the half\n * of the axis no single service can answer.\n *\n * `permissions.invalid` is an error and it is the only one reported when it\n * fires: a vocabulary nobody can read resolves nothing, so every `Requires:`\n * line in the fleet would follow it as `permissions.unknown`, and a hundred\n * findings about one broken file is a cascade, not a diagnosis.\n *\n * `permissions.unenforced` is the mirror of `api.ungoverned`, and it earns its\n * place for the same reason: a vocabulary is only worth what cites it. A\n * permission nothing requires is either a rule that was removed and left its\n * word behind, or a word nobody adopted — both are drift, and neither is\n * visible from inside the file. Warn, because the honest answer is sometimes\n * \"not modelled yet\".\n */\nexport async function permissionFindings(\n docsDir: DocsDir,\n services: ServiceEntry[],\n fleet: FleetContext | undefined,\n): Promise<Finding[]> {\n const vocabulary = await readVocabulary(permissionsPath(docsDir));\n if (!vocabulary.present) return [];\n if (vocabulary.invalid !== undefined) {\n return [\n {\n severity: \"error\",\n code: \"permissions.invalid\",\n message:\n `landscape: architecture/permissions.yaml does not read as a vocabulary — ${vocabulary.invalid}. ` +\n \"Every `Requires:` line in the fleet resolves against this file, so none of them can be graded until it parses. \" +\n \"The shape is `subjects: {<kind>: {description}}` then `permissions: {<kind>: {<name>: {description, owned_by, enforced_by}}}`.\",\n },\n ];\n }\n const used = new Set<string>();\n for (const entry of services) {\n const paths = servicePathsAt(entry.dir);\n for (const path of [paths.spec, paths.archSpec]) {\n // Existence first, and for BOTH readers: arch.spec.md is optional (most\n // of a legacy fleet has none) and spec.md is missing on a half-adopted\n // service. `FleetContext.readRequirements` throws ENOENT, which surfaces\n // as `repository-unavailable` and takes the whole `--all` run down — an\n // absent optional artifact must never be able to do that.\n if (!existsSync(path)) continue;\n const reqs = fleet === undefined ? await readRequirementsAt(path) : await fleet.readRequirements(path);\n for (const r of reqs) {\n if (r.kind !== \"REMOVED\") for (const p of r.requires) used.add(p);\n }\n }\n }\n const unenforced = [...vocabulary.byId.keys()].filter((id) => !used.has(id));\n if (unenforced.length === 0) return [];\n return [\n {\n severity: \"warn\",\n code: \"permissions.unenforced\",\n message: `landscape: ${unenforced.length} declared permission(s) that no requirement's \\`Requires:\\` line names — write the requirement that gates on each, or drop the declaration`,\n details: unenforced,\n },\n ];\n}\n\n/** The uncached read, for the `--service` path where no fleet context exists. */\nasync function readRequirementsAt(path: string): Promise<Requirement[]> {\n if (!existsSync(path)) return [];\n return parseRequirements(await readFile(path, \"utf8\"));\n}\n\n/**\n * The capability vocabulary graded against the fleet that realizes it — the\n * axis's fleet half, beside permissionFindings and shaped by the same two\n * verdicts. `capability.invalid` is a `validate --all` run's ONE finding about\n * an unreadable file — fleet scope, so single-target runs stay silent about\n * it — and it suppresses the rest of the family (the unknown\n * grades at every service target return [] for the same vocabulary — one\n * breach, one finding, never a cascade). `capability.unrealized` is one warn\n * PER declared-but-unnamed capability, subject = the id, because the roadmap's\n * criterion is 'one warning per capability, not one per service'. Holding\n * NEITHER `architecture/capabilities.yaml` nor `capabilities/` is total\n * silence: the fleet's own files are this axis's opt-in, either of them.\n *\n * The used set comes through capabilityRollup with the fleet's own cached\n * reader, so under --all the walk re-parses nothing the service targets have\n * not already paid for — and the rollup, the warn and `loam list capabilities`\n * can never disagree about what \"realized\" means.\n *\n * The AUTHORED documents are graded here too, and they ride this function\n * rather than a target of their own for `viewsStaleFindings`' reason one tree\n * over: a capability belongs to no service, so only the fleet run has the whole\n * vocabulary and the enumerated tree in view at once. Their reads are NOT\n * wrapped, deliberately — a `capabilities/<id>/spec.md` that is not UTF-8 takes\n * the run down exactly as a `services/<id>/spec.md` does, because a document\n * loam cannot decode is a refusal everywhere else in the product and a quiet\n * \"zero requirements\" here would grade a whole capability green on bytes\n * nobody read.\n */\nexport async function capabilityFleetFindings(\n docsDir: DocsDir,\n services: ServiceEntry[],\n fleet: FleetContext | undefined,\n flows: readonly ParsedView[] | null,\n): Promise<Finding[]> {\n const vocab = fleet === undefined ? await readCapabilityVocabulary(docsDir) : await fleet.capabilities(docsDir);\n if (!vocab.present) return [];\n const read = fleet === undefined ? readRequirementsAt : (p: string) => fleet.readRequirements(p);\n const authored: Finding[] = docMissingFindings(vocab.tree);\n for (const doc of vocab.tree.docs) authored.push(...capabilityDocFindings(await read(doc.spec), doc.id));\n\n const invalid = invalidVocabularyFinding(vocab);\n if (invalid !== null) return [invalid, ...authored];\n // A USE CASE IS THE OTHER WAY TO KEEP A PROMISE, and it is the only carrier a\n // cross-service criterion has: \"I enter a login and a password and I am in\"\n // belongs to no single service's spec, so a fleet could satisfy it perfectly\n // through a `#cap-`/`#req-` tagged flow and still be told nobody realizes it.\n //\n // The claims go INTO the rollup rather than beside it, so both unrealized\n // grades and `loam list capabilities` read one set of rows. `flows === null`\n // means loam could not READ the flows (no preload, or an `architecture/` that\n // did not parse); the key is then OMITTED rather than empty, and every\n // `keptBy` below is `undefined` — the three-state contract `rollup.ts` states.\n const index = await capabilityRequirementIndex(vocab, read);\n const keptByFlows =\n flows === null\n ? undefined\n : useCaseRequirementClaims(flows, gradableCapabilityIds(vocab), (c) => index.byCapability.get(c));\n const rows = await capabilityRollup({ services, vocab, read, ...(keptByFlows === undefined ? {} : { keptByFlows }) });\n\n // The two unrealized grades read the SAME rollup, which is what keeps them\n // from disagreeing about the word: a capability is unrealized when nothing\n // names it by any join, and one of its requirements is unrealized when nothing\n // keeps that promise. Both are warnings, and the second is the one that\n // survives a healthy-looking row — a capability with four requirements and\n // three kept reports nothing at all through the first.\n //\n // The suspension falls out of the DATA rather than a branch: `keptBy` absent\n // means nobody looked, and a promise nobody looked for is not a promise\n // reported unkept.\n const unrealizedRequirements = rows.flatMap((row) =>\n (row.requirements ?? [])\n .filter((req) => req.realizedBy.length === 0 && req.keptBy?.length === 0)\n .map((req) => ({ capability: row.id, id: req.id, name: req.name })),\n );\n return [\n ...authored,\n ...unrealizedFindings(vocab, usedCapabilities(rows)),\n ...requirementUnrealizedFindings(unrealizedRequirements),\n ];\n}\n\nexport function externalProducerOf(\n message: string,\n land: LoadedDoc | null,\n landSvcOf: ((id: string) => DeclaredService) | null,\n known: ReadonlySet<RawServiceId>,\n): string | null {\n if (land === null || land.errors.length > 0 || landSvcOf === null) return null;\n const knownIds: ReadonlySet<string> = known;\n const byId = new Map(land.elements.map((e) => [e.id, e]));\n for (const r of land.relationships) {\n if (r.publishes !== message) continue;\n if (knownIds.has(landSvcOf(r.source))) continue;\n for (let id = r.source; ; ) {\n const e = byId.get(id);\n if (e !== undefined && e.tags.includes(EXTERNAL_TAG)) return e.title;\n const dot = id.lastIndexOf(\".\");\n if (dot === -1) break;\n id = id.slice(0, dot);\n }\n }\n return null;\n}\n"]}
1
+ {"version":3,"file":"fleet-shape.js","sourceRoot":"","sources":["../../../../src/commands/validate/checks/fleet-shape.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Fleet-shape advisories over the parsed landscape: the way a fleet map stops\n * answering questions as it grows, caught while the map is already in memory.\n *\n * The shape becomes visible at service three rather than service two.\n * Ubiquitous infrastructure — logging Kafka, an identity provider, service\n * discovery — takes one inbound edge per modelled service until the map is a\n * hairball around it; deleting the element fixes the picture and loses \"who\n * depends on the Identity Provider\", which is the question the map exists to\n * answer during an incident. Tagging keeps both: the fleet view excludes\n * `#platform`, and a platform view keeps the dependents.\n *\n * The DATASTORE half of this pair moved to `../fleet/map/consumers.ts` when its\n * consumer count stopped being a fact about the map alone: a private store now\n * lives nested inside its owner's element, and the services reaching it are\n * attested in their own models (E3/R1). The census both checks read is built\n * there and handed in here as `consumers`, so a hub and a store can never\n * disagree about who consumes what.\n *\n * loam still reads no code and decides no policy: the team chooses what counts\n * as platform — this warning only names a shape that is usually wrong.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { type Elem, type LoadedDoc } from \"../../../core/c4/likec4.js\";\nimport { type DeclaredService, type RawServiceId } from \"../../../core/kernel/ids/service.js\";\nimport { type Finding } from \"../../../core/vocabulary/report.js\";\nimport { type ServiceEntry } from \"../../../core/repo/entries.js\";\nimport { permissionsPath, servicePathsAt } from \"../../../core/repo/paths.js\";\nimport { readVocabulary } from \"../../../core/permissions/permissions.js\";\nimport { readCapabilityVocabulary } from \"../../../core/capabilities/capabilities.js\";\nimport {\n capabilityDocFindings,\n capabilityRequirementIndex,\n docMissingFindings,\n gradableCapabilityIds,\n invalidVocabularyFinding,\n unrealizedFindings,\n} from \"../../../core/capabilities/findings.js\";\nimport { useCaseRequirementClaims } from \"../../../core/usecases/capability.js\";\nimport type { ParsedView } from \"../../../core/c4/parsed/dynamic-views.js\";\nimport { capabilityRollup, usedCapabilities } from \"../../../core/capabilities/rollup.js\";\nimport { requirementUnrealizedFindings } from \"../../../core/capabilities/realizes/findings.js\";\nimport { parseRequirements } from \"../../../core/document/parse.js\";\nimport { type Requirement } from \"../../../core/document/spec.js\";\nimport { FleetContext } from \"../../../core/fleet-context.js\";\nimport { EXTERNAL_TAG, PLATFORM_TAG } from \"../../../core/vocabulary/maturity.js\";\nimport type { DocsDir } from \"../../../core/kernel/ids/dirs.js\";\n\n/**\n * Consumers at which an untagged external hub starts to warn. Three, not two:\n * two services sharing a dependency is a fact about those two services, the\n * same edge from three is a pattern about the element — and three is where\n * the shared hub starts making the map unreadable.\n */\nexport const PLATFORM_CANDIDATE_MIN_CONSUMERS = 3;\n\nexport interface FleetShape {\n /** Service-LEVEL elements — the fleet map's own boxes (landscape.ts `drawn`). */\n drawn: Elem[];\n /** The services/<id>/ directories that exist. */\n services: ReadonlySet<string>;\n /**\n * Distinct consumer SERVICES of an element, its nested children included —\n * `../fleet/map/consumers.ts` owns the derivation and both checks that read a\n * consumer count share this one instance of it.\n */\n consumers: (elementId: string) => string[];\n}\n\nexport function fleetShapeFindings(shape: FleetShape): Finding[] {\n const { drawn, services } = shape;\n const findings: Finding[] = [];\n\n for (const e of drawn) {\n // An element that stands for a real service is the fleet itself, not its\n // shape: many consumers is what a well-used service looks like.\n if (e.service !== undefined || services.has(e.title)) continue;\n if (!e.tags.includes(EXTERNAL_TAG) || e.tags.includes(PLATFORM_TAG)) continue;\n const consumers = shape.consumers(e.id);\n if (consumers.length < PLATFORM_CANDIDATE_MIN_CONSUMERS) continue;\n findings.push({\n severity: \"warn\",\n code: \"landscape.platform-candidate\",\n subject: e.title,\n message:\n `landscape: '${e.title}' is consumed by ${consumers.length} services (${consumers.join(\", \")}) ` +\n `and is not tagged #platform — a hub like this takes one more edge per modelled service until ` +\n `the fleet view is unreadable. Declare \\`tag platform\\` in the specification block and tag the ` +\n `element (LikeC4 refuses an undeclared tag, so both steps are needed): the fleet view then ` +\n `excludes it, and a platform view over \\`include * -> element.tag = #platform\\` keeps ` +\n `\"who depends on it\" answerable`,\n });\n }\n return findings;\n}\n\n/**\n * The #external element that declares it PUBLISHES this message, or null —\n * the landscape-side half of the event spine's fleet question, beside the\n * other checks that read tags off the drawn map.\n *\n * Positive evidence only: an edge carrying `metadata { publishes '<msg>' }`\n * whose source element — itself or a declared ancestor, since the edge may\n * point out of a topic nested inside the broker — is tagged #external and\n * resolves to no known service directory. The resolution guard is what keeps\n * a mis-tagged internal service reading as unproduced: a tag is cheap to\n * write, and a services/<id>/ directory outranks it.\n */\n/**\n * The authorization vocabulary graded against the fleet that uses it — the half\n * of the axis no single service can answer.\n *\n * `permissions.invalid` is an error and it is the only one reported when it\n * fires: a vocabulary nobody can read resolves nothing, so every `Requires:`\n * line in the fleet would follow it as `permissions.unknown`, and a hundred\n * findings about one broken file is a cascade, not a diagnosis.\n *\n * `permissions.unenforced` is the mirror of `api.ungoverned`, and it earns its\n * place for the same reason: a vocabulary is only worth what cites it. A\n * permission nothing requires is either a rule that was removed and left its\n * word behind, or a word nobody adopted — both are drift, and neither is\n * visible from inside the file. Warn, because the honest answer is sometimes\n * \"not modelled yet\".\n */\nexport async function permissionFindings(\n docsDir: DocsDir,\n services: ServiceEntry[],\n fleet: FleetContext | undefined,\n): Promise<Finding[]> {\n const vocabulary = await readVocabulary(permissionsPath(docsDir));\n if (!vocabulary.present) return [];\n if (vocabulary.invalid !== undefined) {\n return [\n {\n severity: \"error\",\n code: \"permissions.invalid\",\n message:\n `landscape: architecture/permissions.yaml does not read as a vocabulary — ${vocabulary.invalid}. ` +\n \"Every `Requires:` line in the fleet resolves against this file, so none of them can be graded until it parses. \" +\n \"The shape is `subjects: {<kind>: {description}}` then `permissions: {<kind>: {<name>: {description, owned_by, enforced_by}}}`.\",\n },\n ];\n }\n const used = new Set<string>();\n for (const entry of services) {\n const paths = servicePathsAt(entry.dir);\n for (const path of [paths.spec, paths.archSpec]) {\n // Existence first, and for BOTH readers: arch.spec.md is optional (most\n // of a legacy fleet has none) and spec.md is missing on a half-adopted\n // service. `FleetContext.readRequirements` throws ENOENT, which surfaces\n // as `repository-unavailable` and takes the whole `--all` run down — an\n // absent optional artifact must never be able to do that.\n if (!existsSync(path)) continue;\n const reqs = fleet === undefined ? await readRequirementsAt(path) : await fleet.readRequirements(path);\n for (const r of reqs) {\n if (r.kind !== \"REMOVED\") for (const p of r.requires) used.add(p);\n }\n }\n }\n const unenforced = [...vocabulary.byId.keys()].filter((id) => !used.has(id));\n if (unenforced.length === 0) return [];\n return [\n {\n severity: \"warn\",\n code: \"permissions.unenforced\",\n message: `landscape: ${unenforced.length} declared permission(s) that no requirement's \\`Requires:\\` line names — write the requirement that gates on each, or drop the declaration`,\n details: unenforced,\n },\n ];\n}\n\n/** The uncached read, for the `--service` path where no fleet context exists. */\nasync function readRequirementsAt(path: string): Promise<Requirement[]> {\n if (!existsSync(path)) return [];\n return parseRequirements(await readFile(path, \"utf8\"));\n}\n\n/**\n * The capability vocabulary graded against the fleet that realizes it — the\n * axis's fleet half, beside permissionFindings and shaped by the same two\n * verdicts. `capability.invalid` is a `validate --all` run's ONE finding about\n * an unreadable file — fleet scope, so single-target runs stay silent about\n * it — and it suppresses the rest of the family (the unknown\n * grades at every service target return [] for the same vocabulary — one\n * breach, one finding, never a cascade). `capability.unrealized` is one warn\n * PER declared-but-unnamed capability, subject = the id, because the roadmap's\n * criterion is 'one warning per capability, not one per service'. Holding\n * NEITHER `architecture/capabilities.yaml` nor `capabilities/` is total\n * silence: the fleet's own files are this axis's opt-in, either of them.\n *\n * The used set comes through capabilityRollup with the fleet's own cached\n * reader, so under --all the walk re-parses nothing the service targets have\n * not already paid for — and the rollup, the warn and `loam list capabilities`\n * can never disagree about what \"realized\" means.\n *\n * The AUTHORED documents are graded here too, and they ride this function\n * rather than a target of their own for `viewsStaleFindings`' reason one tree\n * over: a capability belongs to no service, so only the fleet run has the whole\n * vocabulary and the enumerated tree in view at once. Their reads are NOT\n * wrapped, deliberately — a `capabilities/<id>/spec.md` that is not UTF-8 takes\n * the run down exactly as a `services/<id>/spec.md` does, because a document\n * loam cannot decode is a refusal everywhere else in the product and a quiet\n * \"zero requirements\" here would grade a whole capability green on bytes\n * nobody read.\n */\nexport async function capabilityFleetFindings(\n docsDir: DocsDir,\n services: ServiceEntry[],\n fleet: FleetContext | undefined,\n flows: readonly ParsedView[] | null,\n): Promise<Finding[]> {\n const vocab = fleet === undefined ? await readCapabilityVocabulary(docsDir) : await fleet.capabilities(docsDir);\n if (!vocab.present) return [];\n const read = fleet === undefined ? readRequirementsAt : (p: string) => fleet.readRequirements(p);\n const authored: Finding[] = docMissingFindings(vocab.tree);\n for (const doc of vocab.tree.docs) authored.push(...capabilityDocFindings(await read(doc.spec), doc.id));\n\n const invalid = invalidVocabularyFinding(vocab);\n if (invalid !== null) return [invalid, ...authored];\n // A USE CASE IS THE OTHER WAY TO KEEP A PROMISE, and it is the only carrier a\n // cross-service criterion has: \"I enter a login and a password and I am in\"\n // belongs to no single service's spec, so a fleet could satisfy it perfectly\n // through a `#cap-`/`#req-` tagged flow and still be told nobody realizes it.\n //\n // The claims go INTO the rollup rather than beside it, so both unrealized\n // grades and `loam list capabilities` read one set of rows. `flows === null`\n // means loam could not READ the flows (no preload, or an `architecture/` that\n // did not parse); the key is then OMITTED rather than empty, and every\n // `keptBy` below is `undefined` — the three-state contract `rollup.ts` states.\n const index = await capabilityRequirementIndex(vocab, read);\n const keptByFlows =\n flows === null\n ? undefined\n : useCaseRequirementClaims(flows, gradableCapabilityIds(vocab), (c) => index.byCapability.get(c));\n const rows = await capabilityRollup({ services, vocab, read, ...(keptByFlows === undefined ? {} : { keptByFlows }) });\n\n // The two unrealized grades read the SAME rollup, which is what keeps them\n // from disagreeing about the word: a capability is unrealized when nothing\n // names it by any join, and one of its requirements is unrealized when nothing\n // keeps that promise. Both are warnings, and the second is the one that\n // survives a healthy-looking row — a capability with four requirements and\n // three kept reports nothing at all through the first.\n //\n // The suspension falls out of the DATA rather than a branch: `keptBy` absent\n // means nobody looked, and a promise nobody looked for is not a promise\n // reported unkept.\n const unrealizedRequirements = rows.flatMap((row) =>\n (row.requirements ?? [])\n .filter((req) => req.realizedBy.length === 0 && req.keptBy?.length === 0)\n .map((req) => ({ capability: row.id, id: req.id, name: req.name })),\n );\n return [\n ...authored,\n ...unrealizedFindings(vocab, usedCapabilities(rows)),\n ...requirementUnrealizedFindings(unrealizedRequirements),\n ];\n}\n\nexport function externalProducerOf(\n message: string,\n land: LoadedDoc | null,\n landSvcOf: ((id: string) => DeclaredService) | null,\n known: ReadonlySet<RawServiceId>,\n): string | null {\n if (land === null || land.errors.length > 0 || landSvcOf === null) return null;\n const knownIds: ReadonlySet<string> = known;\n const byId = new Map(land.elements.map((e) => [e.id, e]));\n for (const r of land.relationships) {\n if (r.publishes !== message) continue;\n if (knownIds.has(landSvcOf(r.source))) continue;\n for (let id = r.source; ; ) {\n const e = byId.get(id);\n if (e !== undefined && e.tags.includes(EXTERNAL_TAG)) return e.title;\n const dot = id.lastIndexOf(\".\");\n if (dot === -1) break;\n id = id.slice(0, dot);\n }\n }\n return null;\n}\n"]}
@@ -4,28 +4,13 @@
4
4
  * A leaf module rather than a paragraph beside its first caller, because the
5
5
  * checks are spread across packages and each of these is read from more than
6
6
  * one of them: the dispatcher's rollup and the service target's sources check
7
- * both name one finding code, and several targets render parser diagnostics
8
- * the same way. ACTOR_KINDS and EXTERNAL_TAG lived here too, until they
7
+ * both name one finding code. ACTOR_KINDS and EXTERNAL_TAG lived here too, until they
9
8
  * reached a third copy (`core/gate/partners.ts`, `core/verify/checklist.ts`);
10
9
  * their shared spelling is now `core/vocabulary/maturity.ts`, beside the
11
- * landscape evidence that asks the same questions.
10
+ * landscape evidence that asks the same questions. The parser-diagnostic line
11
+ * (`errorText`) went the same way, to `core/c4/likec4.ts` beside the type it
12
+ * reads, once four hand-rolled copies had disagreed about the line base.
12
13
  */
13
- import {} from "../../../core/c4/likec4.js";
14
14
  /** The one code the rollup line counts; spelled once so the two cannot drift. */
15
15
  export const UNVERIFIABLE = "sources.unverifiable-from-here";
16
- /**
17
- * One parser diagnostic as a finding's evidence line: `L12:` when the parser
18
- * knew a line, because that lets a reader open the file at the fault instead of
19
- * searching for it.
20
- *
21
- * Three other modules hand-roll the same ternary (`commands/show.ts`,
22
- * `commands/delta.ts`, `core/c4/splice/landscape-merge.ts`). All four belong
23
- * beside `LikeC4Error` in `core/c4/likec4.ts`. That used to be blocked by the
24
- * file limit — nine lines of headroom — and is not any more: moving the resolver
25
- * out to `core/c4/resolve/service.ts` left the loader around 100 lines under.
26
- * The consolidation is now unblocked and simply owed.
27
- */
28
- export function errorText(e) {
29
- return typeof e.line === "number" ? `L${e.line}: ${e.message}` : e.message;
30
- }
31
16
  //# sourceMappingURL=vocabulary.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"vocabulary.js","sourceRoot":"","sources":["../../../../src/commands/validate/checks/vocabulary.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAoB,MAAM,4BAA4B,CAAC;AAE9D,iFAAiF;AACjF,MAAM,CAAC,MAAM,YAAY,GAAG,gCAAgC,CAAC;AAE7D;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,SAAS,CAAC,CAAc;IACtC,OAAO,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;AAC7E,CAAC","sourcesContent":["/**\n * The handful of facts every validate check speaks, and none of them owns.\n *\n * A leaf module rather than a paragraph beside its first caller, because the\n * checks are spread across packages and each of these is read from more than\n * one of them: the dispatcher's rollup and the service target's sources check\n * both name one finding code, and several targets render parser diagnostics\n * the same way. ACTOR_KINDS and EXTERNAL_TAG lived here too, until they\n * reached a third copy (`core/gate/partners.ts`, `core/verify/checklist.ts`);\n * their shared spelling is now `core/vocabulary/maturity.ts`, beside the\n * landscape evidence that asks the same questions.\n */\nimport { type LikeC4Error } from \"../../../core/c4/likec4.js\";\n\n/** The one code the rollup line counts; spelled once so the two cannot drift. */\nexport const UNVERIFIABLE = \"sources.unverifiable-from-here\";\n\n/**\n * One parser diagnostic as a finding's evidence line: `L12:` when the parser\n * knew a line, because that lets a reader open the file at the fault instead of\n * searching for it.\n *\n * Three other modules hand-roll the same ternary (`commands/show.ts`,\n * `commands/delta.ts`, `core/c4/splice/landscape-merge.ts`). All four belong\n * beside `LikeC4Error` in `core/c4/likec4.ts`. That used to be blocked by the\n * file limit — nine lines of headroom — and is not any more: moving the resolver\n * out to `core/c4/resolve/service.ts` left the loader around 100 lines under.\n * The consolidation is now unblocked and simply owed.\n */\nexport function errorText(e: LikeC4Error): string {\n return typeof e.line === \"number\" ? `L${e.line}: ${e.message}` : e.message;\n}\n"]}
1
+ {"version":3,"file":"vocabulary.js","sourceRoot":"","sources":["../../../../src/commands/validate/checks/vocabulary.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,iFAAiF;AACjF,MAAM,CAAC,MAAM,YAAY,GAAG,gCAAgC,CAAC","sourcesContent":["/**\n * The handful of facts every validate check speaks, and none of them owns.\n *\n * A leaf module rather than a paragraph beside its first caller, because the\n * checks are spread across packages and each of these is read from more than\n * one of them: the dispatcher's rollup and the service target's sources check\n * both name one finding code. ACTOR_KINDS and EXTERNAL_TAG lived here too, until they\n * reached a third copy (`core/gate/partners.ts`, `core/verify/checklist.ts`);\n * their shared spelling is now `core/vocabulary/maturity.ts`, beside the\n * landscape evidence that asks the same questions. The parser-diagnostic line\n * (`errorText`) went the same way, to `core/c4/likec4.ts` beside the type it\n * reads, once four hand-rolled copies had disagreed about the line base.\n */\n\n/** The one code the rollup line counts; spelled once so the two cannot drift. */\nexport const UNVERIFIABLE = \"sources.unverifiable-from-here\";\n"]}
@@ -33,7 +33,7 @@ import { documentConflictFinding } from "../../core/conflict-markers.js";
33
33
  import { featureCapabilityDeltas } from "../../core/capabilities/delta/tree.js";
34
34
  import { docMissingFindings } from "../../core/capabilities/findings.js";
35
35
  import { FleetContext } from "../../core/fleet-context.js";
36
- import { errorText } from "./checks/vocabulary.js";
36
+ import { errorText } from "../../core/c4/likec4.js";
37
37
  import { coverageFinding, repeatedListLineFindings } from "./checks/requirements.js";
38
38
  import { deltaArchCoverage } from "./arch-coverage.js";
39
39
  import { featureLinkFindings } from "./links/corpus.js";
@@ -1 +1 @@
1
- {"version":3,"file":"feature.js","sourceRoot":"","sources":["../../../src/commands/validate/feature.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The feature target: a delta graded on its own, before anything merges.\n *\n * Everything here reads the feature directory — delta.likec4, the per-service\n * spec.md/arch.spec.md deltas, the provenance stamp — and grades the documents\n * a merge is about to fold into living ones. A delta is held to the same\n * document rules as the file it becomes (`./checks/requirements.js`), because\n * grading it more leniently is how a defect acquires a signature saying it was\n * reviewed.\n *\n * The one question that needs the LIVING fleet — what in this delta is actually\n * new architecture — is `./arch-coverage.js`.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { loadFile, type Elem, type LoadedDoc, type Rel } from \"../../core/c4/likec4.js\";\nimport { elementService } from \"../../core/c4/resolve/service.js\";\nimport { type PathableService } from \"../../core/kernel/ids/service.js\";\nimport { type FeatureEntry } from \"../../core/repo/entries.js\";\nimport { featurePaths, featureSpecPaths } from \"../../core/repo/paths.js\";\nimport { enumeratedServiceIds } from \"../../core/repo/service-target.js\";\nimport { docsRepoState } from \"../../core/repo/state.js\";\nimport { featureSpecServices, listServices } from \"../../core/repo/repo.js\";\nimport { type Finding, type TargetReport } from \"../../core/vocabulary/report.js\";\nimport { parseRequirements } from \"../../core/document/parse.js\";\nimport {\n assertionlessFindings,\n examplesFindings,\n steplessFindings,\n} from \"../../core/document/scenarios.js\";\nimport { type Requirement } from \"../../core/document/spec.js\";\nimport { featureCoherence } from \"../../core/coherence/coherence.js\";\nimport {\n deltaServiceUnknownFinding,\n invalidSpecServiceFindings,\n} from \"../../core/coherence/living.js\";\nimport { issueFinding } from \"../../core/vocabulary/issue.js\";\nimport { featureProvenance } from \"../../core/provenance/findings.js\";\nimport { documentConflictFinding } from \"../../core/conflict-markers.js\";\nimport { featureCapabilityDeltas } from \"../../core/capabilities/delta/tree.js\";\nimport { docMissingFindings } from \"../../core/capabilities/findings.js\";\nimport { FleetContext } from \"../../core/fleet-context.js\";\nimport { errorText } from \"./checks/vocabulary.js\";\nimport { coverageFinding, repeatedListLineFindings } from \"./checks/requirements.js\";\nimport { deltaArchCoverage } from \"./arch-coverage.js\";\nimport { featureLinkFindings } from \"./links/corpus.js\";\nimport type { DocsDir } from \"../../core/kernel/ids/dirs.js\";\n\nexport async function validateFeature(\n docsDir: DocsDir,\n feature: FeatureEntry,\n preloadedLand?: LoadedDoc | null,\n fleet?: FleetContext,\n): Promise<TargetReport> {\n const findings: Finding[] = [];\n const featureDir = feature.dir;\n const featureId = feature.id;\n\n // delta.likec4 parse + collect tagged edges. The loaded doc is kept and\n // handed to featureCoherence below — loading it is a Langium workspace spin,\n // and paying it twice per feature was the dominant cost of `validate --all`.\n let taggedEls: Elem[] = [];\n let taggedRels: Rel[] = [];\n let elements: Elem[] = [];\n let deltaRels: Rel[] = [];\n let deltaDoc: LoadedDoc | undefined;\n const deltaPath = featurePaths(featureDir).delta;\n if (existsSync(deltaPath)) {\n const res = fleet === undefined ? await loadFile(deltaPath) : await fleet.loadLikeC4(deltaPath);\n deltaDoc = res;\n if (res.errors.length > 0) {\n findings.push({\n severity: \"error\",\n code: \"delta.invalid\",\n message: `delta.likec4 has ${res.errors.length} error(s)`,\n details: res.errors.map(errorText),\n });\n } else {\n elements = res.elements;\n deltaRels = res.relationships;\n taggedEls = res.elements.filter((e) => e.tags.includes(featureId));\n taggedRels = res.relationships.filter((r) => r.tags.includes(featureId));\n findings.push({\n severity: \"ok\",\n code: \"delta.valid\",\n message: `delta.likec4 valid (${res.elements.length} elements · ${res.relationships.length} relationships)`,\n });\n }\n }\n\n findings.push(...(await featureProvenance(featureDir, featureId)));\n\n // Who this feature is allowed to address. `specs/<svc>/` is what the archive\n // materialises `services/<svc>/` from, and nothing used to ask whether that\n // name means anything: one wrong character in `--touches` passed\n // `validate --all` with zero errors, and archive then created the phantom\n // directory. A delta may legitimately name a service that does not exist yet\n // — but only one it INTRODUCES itself, in its own tagged C4. A delta that did\n // not parse proves neither (`delta.invalid` is that finding), so the question\n // is suspended there rather than answered by guessing.\n //\n // Existence is enumeration MEMBERSHIP through the no-services-tolerant\n // spelling, never `existsSync(services/<svc>/)` at the root: a service filed\n // into a subsystem exists wherever the tree walk found it, and the root\n // probe graded every filed service `delta.service-unknown`. The tolerance\n // carries the old carve-out forward — `validate --feature` is allowed to run\n // in a docs repo with no services/ at all, where enumerating is a refusal;\n // `enumeratedServiceIds` swallows exactly that refusal to an empty list, in\n // which every service is honestly absent.\n const featureServices = await featureSpecServices(featureDir, fleet);\n const introduces: ReadonlySet<string> = new Set(taggedEls.map(elementService));\n const deltaReadable = deltaDoc === undefined || deltaDoc.errors.length === 0;\n const enumerated = new Set<string>(await enumeratedServiceIds(docsDir, fleet));\n const unknownServices = deltaReadable\n ? featureServices.filter((svc) => !enumerated.has(svc) && !introduces.has(svc))\n : [];\n // The near-miss hint, on the same rule `service.unknown` uses — a typo is\n // only diagnosable against the ids that DO exist.\n const closeTo =\n unknownServices.length > 0 && docsRepoState(docsDir).kind === \"ok\"\n ? (await listServices(docsDir, fleet)).map((s) => s.id)\n : [];\n // The finding is coherence/living.ts's — the same words archive refuses with,\n // because it is the same conclusion about the same directory.\n for (const svc of unknownServices) findings.push(deltaServiceUnknownFinding(svc, closeTo));\n\n // The grammar half of the same guarantee. `delta.service-unknown` asks whether\n // the directory names a service anyone knows; this asks whether the NAME could\n // ever be one. The two are independent on purpose: a tagged element whose\n // title matches the directory answers the first question, which is exactly how\n // `specs/Payment Service/` used to validate green and archive into a directory\n // no loam command can address. Not suspended on an unreadable delta either —\n // no reading of the architecture axis can make the name legal.\n findings.push(...(await invalidSpecServiceFindings(featureDir, fleet)));\n\n // Requirement coverage across every per-service delta — the business spec and\n // the arch spec through the same check — and collect scenario text.\n let scenarioText = \"\";\n const archDeltas: Array<{ service: PathableService; reqs: Requirement[] }> = [];\n for (const svc of featureServices) {\n const p = featureSpecPaths(featureDir, svc);\n if (existsSync(p.spec)) {\n const raw = fleet === undefined ? await readFile(p.spec, \"utf8\") : await fleet.readText(p.spec);\n scenarioText += \"\\n\" + raw.toLowerCase();\n const reqs = fleet === undefined ? parseRequirements(raw) : await fleet.readRequirements(p.spec);\n // Both document-level breaches carry into the living spec through the\n // merge, so a delta is graded for them exactly as a living document is:\n // conflict markers merge as prose under someone's requirement, and a\n // stepless scenario merges as a requirement the coverage rule calls\n // covered forever after.\n const conflict = documentConflictFinding(`${svc}: spec.md`, svc, raw);\n if (conflict !== null) findings.push(conflict);\n findings.push({ ...coverageFinding(`${svc}: requirements`, reqs), subject: svc });\n findings.push(...steplessFindings(`${svc}: requirements`, svc, reqs));\n findings.push(...assertionlessFindings(`${svc}: requirements`, svc, reqs));\n findings.push(...examplesFindings(`${svc}: requirements`, svc, reqs));\n // The keep-last quirk loses lines in a delta exactly as in a living spec\n // — and a delta's lost Operations: line then merges into the living one.\n findings.push(...repeatedListLineFindings(reqs, `${svc}: spec.md`, svc));\n }\n if (existsSync(p.archSpec)) {\n const raw = fleet === undefined ? await readFile(p.archSpec, \"utf8\") : await fleet.readText(p.archSpec);\n scenarioText += \"\\n\" + raw.toLowerCase();\n const reqs = fleet === undefined ? parseRequirements(raw) : await fleet.readRequirements(p.archSpec);\n archDeltas.push({ service: svc, reqs });\n const conflict = documentConflictFinding(`${svc}: arch.spec.md`, svc, raw);\n if (conflict !== null) findings.push(conflict);\n findings.push({ ...coverageFinding(`${svc}: arch requirements`, reqs), subject: svc });\n findings.push(...steplessFindings(`${svc}: arch requirements`, svc, reqs));\n findings.push(...assertionlessFindings(`${svc}: arch requirements`, svc, reqs));\n findings.push(...examplesFindings(`${svc}: arch requirements`, svc, reqs));\n findings.push(...repeatedListLineFindings(reqs, `${svc}: arch.spec.md`, svc));\n }\n }\n\n // The BUSINESS corpus's delta documents, graded for the same document-level\n // breach as the service ones and for the same reason: a conflicted delta\n // parses as prose, so every other check reads it as a valid document, and\n // `loam archive` then re-serializes both sides of somebody's merge into the\n // LIVING capability document. It is worse there than one tree over — once the\n // feature is archived, nothing walks the delta again, and the living\n // capability documents are only conflict-scanned while some feature still\n // carries a delta for them.\n const capabilityDeltas =\n fleet === undefined ? await featureCapabilityDeltas(featureDir) : await fleet.featureCapabilityDeltas(featureDir);\n // The half-created directory, on the FEATURE side. The living tree has warned\n // about `capabilities/<id>/` with no document since the axis landed; the same\n // `mkdir` inside `features/<FEAT>/capabilities/` earned nothing, and it is the\n // quieter of the two — an empty directory is not a delta, so the walk finds\n // no document, the merge carries nothing, and the business change ships as\n // zero at exit 0. Same code, feature-scoped message (`../../core/capabilities/findings.ts`).\n // `dirName`, not the id: the directory is `features/FEAT-1-split/`, and a\n // message spelling `features/FEAT-1/` names a path nobody can open.\n findings.push(...docMissingFindings(capabilityDeltas, feature.dirName));\n for (const doc of capabilityDeltas.docs) {\n const raw = fleet === undefined ? await readFile(doc.spec, \"utf8\") : await fleet.readText(doc.spec);\n const conflict = documentConflictFinding(`capability ${doc.id}: spec.md`, doc.id, raw);\n if (conflict !== null) findings.push(conflict);\n }\n\n // The links this feature's own documents write — the intent, each service's\n // two spec deltas, its ADRs and its capability deltas. Same place in the\n // order as the service target's: after every join loam defined a line for.\n findings.push(...(await featureLinkFindings({ docsDir, subject: featureId, fleet }, featureDir)));\n\n findings.push(\n ...(await deltaArchCoverage({\n docsDir,\n elements,\n relationships: deltaRels,\n taggedEls,\n taggedRels,\n archDeltas,\n featureServices,\n scenarioText,\n preloadedLand,\n fleet,\n })),\n );\n\n\n // Coherence — cross-axis consistency (C4 ↔ requirements ↔ OpenAPI).\n const issues = await featureCoherence({ docsDir, featureDir, featureId, preloadedDelta: deltaDoc, context: fleet });\n if (issues.length === 0) {\n findings.push({\n severity: \"ok\",\n code: \"coherence.ok\",\n message: \"coherence: ✓ C4 · requirements · OpenAPI agree\",\n text: { indent: 2, marker: false },\n });\n } else {\n for (const i of issues) {\n findings.push({ ...issueFinding(i), text: { indent: 4, header: \"coherence:\" } });\n }\n }\n\n return { kind: \"feature\", id: featureId, path: `features/${feature.dirName}`, findings };\n}\n"]}
1
+ {"version":3,"file":"feature.js","sourceRoot":"","sources":["../../../src/commands/validate/feature.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The feature target: a delta graded on its own, before anything merges.\n *\n * Everything here reads the feature directory — delta.likec4, the per-service\n * spec.md/arch.spec.md deltas, the provenance stamp — and grades the documents\n * a merge is about to fold into living ones. A delta is held to the same\n * document rules as the file it becomes (`./checks/requirements.js`), because\n * grading it more leniently is how a defect acquires a signature saying it was\n * reviewed.\n *\n * The one question that needs the LIVING fleet — what in this delta is actually\n * new architecture — is `./arch-coverage.js`.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { loadFile, type Elem, type LoadedDoc, type Rel } from \"../../core/c4/likec4.js\";\nimport { elementService } from \"../../core/c4/resolve/service.js\";\nimport { type PathableService } from \"../../core/kernel/ids/service.js\";\nimport { type FeatureEntry } from \"../../core/repo/entries.js\";\nimport { featurePaths, featureSpecPaths } from \"../../core/repo/paths.js\";\nimport { enumeratedServiceIds } from \"../../core/repo/service-target.js\";\nimport { docsRepoState } from \"../../core/repo/state.js\";\nimport { featureSpecServices, listServices } from \"../../core/repo/repo.js\";\nimport { type Finding, type TargetReport } from \"../../core/vocabulary/report.js\";\nimport { parseRequirements } from \"../../core/document/parse.js\";\nimport {\n assertionlessFindings,\n examplesFindings,\n steplessFindings,\n} from \"../../core/document/scenarios.js\";\nimport { type Requirement } from \"../../core/document/spec.js\";\nimport { featureCoherence } from \"../../core/coherence/coherence.js\";\nimport {\n deltaServiceUnknownFinding,\n invalidSpecServiceFindings,\n} from \"../../core/coherence/living.js\";\nimport { issueFinding } from \"../../core/vocabulary/issue.js\";\nimport { featureProvenance } from \"../../core/provenance/findings.js\";\nimport { documentConflictFinding } from \"../../core/conflict-markers.js\";\nimport { featureCapabilityDeltas } from \"../../core/capabilities/delta/tree.js\";\nimport { docMissingFindings } from \"../../core/capabilities/findings.js\";\nimport { FleetContext } from \"../../core/fleet-context.js\";\nimport { errorText } from \"../../core/c4/likec4.js\";\nimport { coverageFinding, repeatedListLineFindings } from \"./checks/requirements.js\";\nimport { deltaArchCoverage } from \"./arch-coverage.js\";\nimport { featureLinkFindings } from \"./links/corpus.js\";\nimport type { DocsDir } from \"../../core/kernel/ids/dirs.js\";\n\nexport async function validateFeature(\n docsDir: DocsDir,\n feature: FeatureEntry,\n preloadedLand?: LoadedDoc | null,\n fleet?: FleetContext,\n): Promise<TargetReport> {\n const findings: Finding[] = [];\n const featureDir = feature.dir;\n const featureId = feature.id;\n\n // delta.likec4 parse + collect tagged edges. The loaded doc is kept and\n // handed to featureCoherence below — loading it is a Langium workspace spin,\n // and paying it twice per feature was the dominant cost of `validate --all`.\n let taggedEls: Elem[] = [];\n let taggedRels: Rel[] = [];\n let elements: Elem[] = [];\n let deltaRels: Rel[] = [];\n let deltaDoc: LoadedDoc | undefined;\n const deltaPath = featurePaths(featureDir).delta;\n if (existsSync(deltaPath)) {\n const res = fleet === undefined ? await loadFile(deltaPath) : await fleet.loadLikeC4(deltaPath);\n deltaDoc = res;\n if (res.errors.length > 0) {\n findings.push({\n severity: \"error\",\n code: \"delta.invalid\",\n message: `delta.likec4 has ${res.errors.length} error(s)`,\n details: res.errors.map(errorText),\n });\n } else {\n elements = res.elements;\n deltaRels = res.relationships;\n taggedEls = res.elements.filter((e) => e.tags.includes(featureId));\n taggedRels = res.relationships.filter((r) => r.tags.includes(featureId));\n findings.push({\n severity: \"ok\",\n code: \"delta.valid\",\n message: `delta.likec4 valid (${res.elements.length} elements · ${res.relationships.length} relationships)`,\n });\n }\n }\n\n findings.push(...(await featureProvenance(featureDir, featureId)));\n\n // Who this feature is allowed to address. `specs/<svc>/` is what the archive\n // materialises `services/<svc>/` from, and nothing used to ask whether that\n // name means anything: one wrong character in `--touches` passed\n // `validate --all` with zero errors, and archive then created the phantom\n // directory. A delta may legitimately name a service that does not exist yet\n // — but only one it INTRODUCES itself, in its own tagged C4. A delta that did\n // not parse proves neither (`delta.invalid` is that finding), so the question\n // is suspended there rather than answered by guessing.\n //\n // Existence is enumeration MEMBERSHIP through the no-services-tolerant\n // spelling, never `existsSync(services/<svc>/)` at the root: a service filed\n // into a subsystem exists wherever the tree walk found it, and the root\n // probe graded every filed service `delta.service-unknown`. The tolerance\n // carries the old carve-out forward — `validate --feature` is allowed to run\n // in a docs repo with no services/ at all, where enumerating is a refusal;\n // `enumeratedServiceIds` swallows exactly that refusal to an empty list, in\n // which every service is honestly absent.\n const featureServices = await featureSpecServices(featureDir, fleet);\n const introduces: ReadonlySet<string> = new Set(taggedEls.map(elementService));\n const deltaReadable = deltaDoc === undefined || deltaDoc.errors.length === 0;\n const enumerated = new Set<string>(await enumeratedServiceIds(docsDir, fleet));\n const unknownServices = deltaReadable\n ? featureServices.filter((svc) => !enumerated.has(svc) && !introduces.has(svc))\n : [];\n // The near-miss hint, on the same rule `service.unknown` uses — a typo is\n // only diagnosable against the ids that DO exist.\n const closeTo =\n unknownServices.length > 0 && docsRepoState(docsDir).kind === \"ok\"\n ? (await listServices(docsDir, fleet)).map((s) => s.id)\n : [];\n // The finding is coherence/living.ts's — the same words archive refuses with,\n // because it is the same conclusion about the same directory.\n for (const svc of unknownServices) findings.push(deltaServiceUnknownFinding(svc, closeTo));\n\n // The grammar half of the same guarantee. `delta.service-unknown` asks whether\n // the directory names a service anyone knows; this asks whether the NAME could\n // ever be one. The two are independent on purpose: a tagged element whose\n // title matches the directory answers the first question, which is exactly how\n // `specs/Payment Service/` used to validate green and archive into a directory\n // no loam command can address. Not suspended on an unreadable delta either —\n // no reading of the architecture axis can make the name legal.\n findings.push(...(await invalidSpecServiceFindings(featureDir, fleet)));\n\n // Requirement coverage across every per-service delta — the business spec and\n // the arch spec through the same check — and collect scenario text.\n let scenarioText = \"\";\n const archDeltas: Array<{ service: PathableService; reqs: Requirement[] }> = [];\n for (const svc of featureServices) {\n const p = featureSpecPaths(featureDir, svc);\n if (existsSync(p.spec)) {\n const raw = fleet === undefined ? await readFile(p.spec, \"utf8\") : await fleet.readText(p.spec);\n scenarioText += \"\\n\" + raw.toLowerCase();\n const reqs = fleet === undefined ? parseRequirements(raw) : await fleet.readRequirements(p.spec);\n // Both document-level breaches carry into the living spec through the\n // merge, so a delta is graded for them exactly as a living document is:\n // conflict markers merge as prose under someone's requirement, and a\n // stepless scenario merges as a requirement the coverage rule calls\n // covered forever after.\n const conflict = documentConflictFinding(`${svc}: spec.md`, svc, raw);\n if (conflict !== null) findings.push(conflict);\n findings.push({ ...coverageFinding(`${svc}: requirements`, reqs), subject: svc });\n findings.push(...steplessFindings(`${svc}: requirements`, svc, reqs));\n findings.push(...assertionlessFindings(`${svc}: requirements`, svc, reqs));\n findings.push(...examplesFindings(`${svc}: requirements`, svc, reqs));\n // The keep-last quirk loses lines in a delta exactly as in a living spec\n // — and a delta's lost Operations: line then merges into the living one.\n findings.push(...repeatedListLineFindings(reqs, `${svc}: spec.md`, svc));\n }\n if (existsSync(p.archSpec)) {\n const raw = fleet === undefined ? await readFile(p.archSpec, \"utf8\") : await fleet.readText(p.archSpec);\n scenarioText += \"\\n\" + raw.toLowerCase();\n const reqs = fleet === undefined ? parseRequirements(raw) : await fleet.readRequirements(p.archSpec);\n archDeltas.push({ service: svc, reqs });\n const conflict = documentConflictFinding(`${svc}: arch.spec.md`, svc, raw);\n if (conflict !== null) findings.push(conflict);\n findings.push({ ...coverageFinding(`${svc}: arch requirements`, reqs), subject: svc });\n findings.push(...steplessFindings(`${svc}: arch requirements`, svc, reqs));\n findings.push(...assertionlessFindings(`${svc}: arch requirements`, svc, reqs));\n findings.push(...examplesFindings(`${svc}: arch requirements`, svc, reqs));\n findings.push(...repeatedListLineFindings(reqs, `${svc}: arch.spec.md`, svc));\n }\n }\n\n // The BUSINESS corpus's delta documents, graded for the same document-level\n // breach as the service ones and for the same reason: a conflicted delta\n // parses as prose, so every other check reads it as a valid document, and\n // `loam archive` then re-serializes both sides of somebody's merge into the\n // LIVING capability document. It is worse there than one tree over — once the\n // feature is archived, nothing walks the delta again, and the living\n // capability documents are only conflict-scanned while some feature still\n // carries a delta for them.\n const capabilityDeltas =\n fleet === undefined ? await featureCapabilityDeltas(featureDir) : await fleet.featureCapabilityDeltas(featureDir);\n // The half-created directory, on the FEATURE side. The living tree has warned\n // about `capabilities/<id>/` with no document since the axis landed; the same\n // `mkdir` inside `features/<FEAT>/capabilities/` earned nothing, and it is the\n // quieter of the two — an empty directory is not a delta, so the walk finds\n // no document, the merge carries nothing, and the business change ships as\n // zero at exit 0. Same code, feature-scoped message (`../../core/capabilities/findings.ts`).\n // `dirName`, not the id: the directory is `features/FEAT-1-split/`, and a\n // message spelling `features/FEAT-1/` names a path nobody can open.\n findings.push(...docMissingFindings(capabilityDeltas, feature.dirName));\n for (const doc of capabilityDeltas.docs) {\n const raw = fleet === undefined ? await readFile(doc.spec, \"utf8\") : await fleet.readText(doc.spec);\n const conflict = documentConflictFinding(`capability ${doc.id}: spec.md`, doc.id, raw);\n if (conflict !== null) findings.push(conflict);\n }\n\n // The links this feature's own documents write — the intent, each service's\n // two spec deltas, its ADRs and its capability deltas. Same place in the\n // order as the service target's: after every join loam defined a line for.\n findings.push(...(await featureLinkFindings({ docsDir, subject: featureId, fleet }, featureDir)));\n\n findings.push(\n ...(await deltaArchCoverage({\n docsDir,\n elements,\n relationships: deltaRels,\n taggedEls,\n taggedRels,\n archDeltas,\n featureServices,\n scenarioText,\n preloadedLand,\n fleet,\n })),\n );\n\n\n // Coherence — cross-axis consistency (C4 ↔ requirements ↔ OpenAPI).\n const issues = await featureCoherence({ docsDir, featureDir, featureId, preloadedDelta: deltaDoc, context: fleet });\n if (issues.length === 0) {\n findings.push({\n severity: \"ok\",\n code: \"coherence.ok\",\n message: \"coherence: ✓ C4 · requirements · OpenAPI agree\",\n text: { indent: 2, marker: false },\n });\n } else {\n for (const i of issues) {\n findings.push({ ...issueFinding(i), text: { indent: 4, header: \"coherence:\" } });\n }\n }\n\n return { kind: \"feature\", id: featureId, path: `features/${feature.dirName}`, findings };\n}\n"]}