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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (270) hide show
  1. package/CHANGELOG.md +947 -0
  2. package/MIGRATING-from-OpenSpec.md +7 -1
  3. package/QUICKSTART.md +8 -3
  4. package/README.md +20 -13
  5. package/ROADMAP.md +329 -22
  6. package/SCHEMA.md +472 -24
  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 +184 -39
  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 +29 -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 +128 -114
  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 +278 -0
  75. package/dist/commands/validate/fleet/views/projects.js.map +1 -0
  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 -2
  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 -2
  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 +125 -6
  113. package/dist/core/agent/workflows/adopt.js.map +1 -1
  114. package/dist/core/agent/workflows/check.js +35 -19
  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 +138 -3
  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 -26
  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 +31 -22
  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 +48 -9
  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/families.js +1 -1
  213. package/dist/core/explain/families.js.map +1 -1
  214. package/dist/core/explain/refusals.js +1 -1
  215. package/dist/core/explain/refusals.js.map +1 -1
  216. package/dist/core/explore/explore.js +44 -9
  217. package/dist/core/explore/explore.js.map +1 -1
  218. package/dist/core/fleet-context.js +47 -0
  219. package/dist/core/fleet-context.js.map +1 -1
  220. package/dist/core/gate/partners.js +5 -9
  221. package/dist/core/gate/partners.js.map +1 -1
  222. package/dist/core/kernel/ids/fqn/ancestors.js +65 -0
  223. package/dist/core/kernel/ids/fqn/ancestors.js.map +1 -0
  224. package/dist/core/pack/living.js +47 -13
  225. package/dist/core/pack/living.js.map +1 -1
  226. package/dist/core/pack/pack.js +9 -6
  227. package/dist/core/pack/pack.js.map +1 -1
  228. package/dist/core/projection/arch-slice.js +2 -2
  229. package/dist/core/projection/arch-slice.js.map +1 -1
  230. package/dist/core/repo/paths.js +40 -0
  231. package/dist/core/repo/paths.js.map +1 -1
  232. package/dist/core/repo/tree/render/projects.js +182 -0
  233. package/dist/core/repo/tree/render/projects.js.map +1 -0
  234. package/dist/core/repo/tree/render/stale.js +2 -3
  235. package/dist/core/repo/tree/render/stale.js.map +1 -1
  236. package/dist/core/repo/tree/render/views.js +51 -3
  237. package/dist/core/repo/tree/render/views.js.map +1 -1
  238. package/dist/core/repo/tree/walk.js +45 -2
  239. package/dist/core/repo/tree/walk.js.map +1 -1
  240. package/dist/core/scaffold/landscape.js +219 -9
  241. package/dist/core/scaffold/landscape.js.map +1 -1
  242. package/dist/core/staging/writes.js.map +1 -1
  243. package/dist/core/usecases/delta/claims.js +1 -0
  244. package/dist/core/usecases/delta/claims.js.map +1 -1
  245. package/dist/core/usecases/delta/overlay.js +4 -0
  246. package/dist/core/usecases/delta/overlay.js.map +1 -1
  247. package/dist/core/usecases/fleet.js +18 -6
  248. package/dist/core/usecases/fleet.js.map +1 -1
  249. package/dist/core/usecases/operations.js +7 -0
  250. package/dist/core/usecases/operations.js.map +1 -1
  251. package/dist/core/usecases/service/exclude.js +119 -0
  252. package/dist/core/usecases/service/exclude.js.map +1 -0
  253. package/dist/core/usecases/service/flows.js +161 -0
  254. package/dist/core/usecases/service/flows.js.map +1 -0
  255. package/dist/core/vocabulary/maturity.js +10 -0
  256. package/dist/core/vocabulary/maturity.js.map +1 -1
  257. package/examples/README.md +23 -0
  258. package/examples/docs/architecture/landscape.likec4 +41 -0
  259. package/examples/docs/architecture/usecases/order-notification.likec4 +5 -2
  260. package/examples/docs/likec4.config.json +0 -1
  261. package/examples/docs/services/checkout-web/model.likec4 +39 -56
  262. package/examples/docs/services/order-service/arch.spec.md +13 -6
  263. package/examples/docs/services/order-service/model.likec4 +53 -67
  264. package/examples/docs/services/payment-service/arch.spec.md +7 -4
  265. package/examples/docs/services/payment-service/model.likec4 +32 -33
  266. package/examples/docs/services/platform/identity-service/arch.spec.md +7 -4
  267. package/examples/docs/services/platform/identity-service/model.likec4 +29 -33
  268. package/examples/docs/services/platform/notification-service/health.yaml +8 -3
  269. package/examples/docs/services/platform/notification-service/model.likec4 +39 -59
  270. package/package.json +1 -1
@@ -138,17 +138,21 @@ export async function readServiceSpecs(input) {
138
138
  * service — a state in which the entry is a typo anyway, and one load is what
139
139
  * buys it a message that names the real ids.
140
140
  */
141
- async function topology(land, archReqs, docsDir) {
141
+ async function topology(land, archReqs, docsDir, fleet) {
142
142
  if (land?.deployment !== undefined && hasDeployment(land.deployment))
143
143
  return land.deployment;
144
144
  const wanted = coversEntries(archReqs).some((e) => e.form === "node" || e.form === "node-edge");
145
145
  if (!wanted)
146
146
  return undefined;
147
- const project = await loadArchitecture(docsDir);
147
+ // Through the index when there is one: `land` above is now that same project
148
+ // in every mode, so this fallback fires only for the caller that handed in
149
+ // nothing, and it must not be a second parse of documents already in the memo.
150
+ const project = fleet === undefined ? await loadArchitecture(docsDir) : await fleet.architecture(docsDir);
148
151
  return project.errors.length === 0 ? project.deployment : undefined;
149
152
  }
150
153
  export async function archAxisFindings(axis) {
151
154
  const { service, paths, archText, archReqs, elements, relationships, land, known, docsDir } = axis;
155
+ const { fleet } = axis;
152
156
  const findings = [];
153
157
  // The architecture spec axis — the obligations a business spec never carries
154
158
  // (the transactional outbox, retries, metrics, alerts). arch.spec.md is
@@ -168,7 +172,18 @@ export async function archAxisFindings(axis) {
168
172
  // warn needs either a second nested element with no edge joining anything,
169
173
  // or dependencies declared in this service's own health.yaml. The health
170
174
  // half goes quiet when the file is unreadable — no claims on bad data.
171
- const nested = elements.filter((e) => e.id.includes(".")).length;
175
+ // "Nested" is a PARENT in this slice, not a dot in the id, and the two stopped
176
+ // meaning the same thing when a model could extend the map: an extending
177
+ // placeholder `extend marketplace.orderService { }` contributes one element
178
+ // whose id is dotted and whose parent belongs to the map, so a dot count read
179
+ // it as nested architecture that reaches nothing — the standalone placeholder
180
+ // `orderService = softwareSystem` was silent for the same shape. One rule for
181
+ // both: `a`/`a.x`/`kafka`/`kafka.t` still counts two.
182
+ const sliceIds = new Set(elements.map((e) => e.id));
183
+ const nested = elements.filter((e) => {
184
+ const dot = e.id.lastIndexOf(".");
185
+ return dot !== -1 && sliceIds.has(e.id.slice(0, dot));
186
+ }).length;
172
187
  const deps = health.unreadable ? [] : health.dependencies;
173
188
  if (elements.length > 0 && relationships.length === 0 && (nested > 1 || deps.length > 0)) {
174
189
  const evidence = [
@@ -218,7 +233,7 @@ export async function archAxisFindings(axis) {
218
233
  });
219
234
  }
220
235
  const landParses = land !== null && land.errors.length === 0 ? land : null;
221
- const deployment = await topology(landParses, archReqs, docsDir);
236
+ const deployment = await topology(landParses, archReqs, docsDir, fleet);
222
237
  const scope = {
223
238
  elements: [...elements, ...(landParses?.elements ?? [])],
224
239
  relationships: [...relationships, ...(landParses?.relationships ?? [])],
@@ -226,7 +241,26 @@ export async function archAxisFindings(axis) {
226
241
  health: health.ids,
227
242
  known,
228
243
  };
229
- findings.push(...coversUnknownFindings(archReqs, { where: `${service}: arch.spec.md`, subject: service }, scope, health.unreadable));
244
+ // `covers.unknown` is suspended entirely when the model EXTENDS a map that
245
+ // does not parse, and this is the standing rule of the whole axis rather than
246
+ // a special case: nothing may be graded out of a document nobody could read
247
+ // (`core/c4/service-model/load.ts` states it; `model/grade.ts`, `model/diverged.ts`
248
+ // and the flow skip in `./service.ts` all honour it). Such a model comes back
249
+ // with no elements and no edges AND the landscape half of the scope is empty
250
+ // for the same reason, so every `Covers:` line in the service's arch.spec.md
251
+ // would resolve to nothing — one unparseable `architecture/usecases/x.likec4`
252
+ // convicting every Covers: line of every migrated service in the fleet as a
253
+ // typo, under a code whose whole job is to catch typos.
254
+ //
255
+ // The alert:/sli: forms could still be answered out of health.yaml, which IS
256
+ // readable, and they are suspended with the rest on purpose: a verdict that
257
+ // grades two of the Covers: forms and drops the others is a report whose
258
+ // completeness depends on which form the author happened to write, and the one
259
+ // repair — fix the map — brings all of them back on the next run. The health
260
+ // axis keeps its own grade either way (`health.uncovered`, below).
261
+ if (axis.mapUnreadable !== true) {
262
+ findings.push(...coversUnknownFindings(archReqs, { where: `${service}: arch.spec.md`, subject: service }, scope, health.unreadable));
263
+ }
230
264
  const activeCovers = coversEntries(archReqs);
231
265
  for (const { form, ids } of [
232
266
  { form: "alert", ids: health.ids.alerts },
@@ -1 +1 @@
1
- {"version":3,"file":"specs.js","sourceRoot":"","sources":["../../../../src/commands/validate/service/specs.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * This service's requirement documents: spec.md, arch.spec.md, and the\n * obligations the second of them carries that the first never does.\n *\n * Two entry points because the two happen at different moments in a service's\n * grading and the order of findings is part of the report. `readServiceSpecs`\n * runs first — everything downstream joins through the requirements it parses —\n * while `archAxisFindings` runs last, after both spines, because `covers.unknown`\n * resolves against the C4 model and the landscape and cannot be answered until\n * those are in hand.\n *\n * The arch axis is advisory in what it ASKS for (covers.unknown,\n * health.uncovered are warnings; arch.spec.md's absence is not a finding at all,\n * because partial adoption is supported) but not in whether the file is\n * readable: a conflict marker or a stepless scenario is graded exactly as it is\n * on the business axis. One file, one rule.\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 PathableService } from \"../../../core/kernel/ids/service.js\";\nimport { type ServicePaths } from \"../../../core/repo/paths.js\";\nimport { type Finding } 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 { type CoverageScope } from \"../../../core/c4/arch.js\";\nimport { hasDeployment, type DeploymentModel } from \"../../../core/c4/parsed/deployment.js\";\nimport { loadArchitecture } from \"../../../core/c4/project/architecture.js\";\nimport { type DocsDir } from \"../../../core/kernel/ids/dirs.js\";\nimport { readHealth } from \"../../../core/vocabulary/health.js\";\nimport { documentConflictFinding } from \"../../../core/conflict-markers.js\";\nimport { FleetContext } from \"../../../core/fleet-context.js\";\nimport { healthDependencyFindings } from \"../checks/health-deps.js\";\nimport {\n coverageFinding,\n coversEntries,\n coversUnknownFindings,\n duplicateRequirementFindings,\n repeatedListLineFindings,\n requirementIdFindings,\n} from \"../checks/requirements.js\";\n\n/** Both requirement documents, parsed once for every axis that joins through them. */\nexport interface ServiceSpecs {\n /** spec.md's findings, emitted before any cross-axis check runs. */\n findings: Finding[];\n /** Every requirement spec.md declares, REMOVED ones included. */\n reqs: Requirement[];\n /** The ones that still govern something. */\n livingReqs: Requirement[];\n /** arch.spec.md's text, or null when the service has none. */\n archText: string | null;\n archReqs: Requirement[];\n}\n\nexport async function readServiceSpecs(input: {\n service: PathableService;\n paths: ServicePaths;\n fleet?: FleetContext;\n}): Promise<ServiceSpecs> {\n const { service, paths, fleet } = input;\n const findings: Finding[] = [];\n\n // Requirement coverage.\n let reqs: Requirement[] = [];\n if (existsSync(paths.spec)) {\n const specText =\n fleet === undefined ? await readFile(paths.spec, \"utf8\") : await fleet.readText(paths.spec);\n reqs = fleet === undefined ? parseRequirements(specText) : await fleet.readRequirements(paths.spec);\n // Conflict markers first, because they say that nothing below this line is\n // anyone's text. An error, gating by the default rule and deliberately\n // without `gates`: that field is coherence's (issue.ts) and archive does\n // not read validate's findings, so claiming it here would be a promise\n // about a gate that is not wired. What makes it an error rather than a\n // warning is what the next merge does — `loam archive` rewrites the\n // requirements section of this very file and takes whichever marker lines\n // fall inside it, turning a conflict anyone can see into a document nobody\n // can tell is wrong.\n const conflict = documentConflictFinding(`${service}: spec.md`, service, specText);\n if (conflict !== null) findings.push(conflict);\n // A living spec with no `### Requirement:` block at all is the baseline\n // `loam adopt` scaffolds and nobody ever filled in. \"requirements covered\n // (0 requirements, all with scenarios)\" is true of it and says the opposite\n // of what a reader needs: every cross-axis check downstream — API\n // governance, the Operations: spine, gherkin generation — is vacuous here,\n // and the green tick is what let a whole fleet score `sourced` over empty\n // files.\n if (reqs.length === 0) {\n findings.push({\n severity: \"warn\",\n code: \"spec.no-requirements\",\n subject: service,\n message: `${service}: spec.md holds no '### Requirement:' blocks — every requirement-driven check below is vacuous, and nothing here can go out of date because nothing is written down`,\n });\n } else {\n findings.push(coverageFinding(`${service}: requirements`, reqs));\n }\n // The other half of the coverage question, and an error for the same\n // reason the first half is: the whole claim of coverage is that a\n // requirement has a scenario something can RUN. A heading with no steps\n // satisfies the count and tests nothing — cucumber runs it vacuously green\n // and `loam verify --results` can never confirm it — so a fleet gate that\n // called it covered was certifying the absence of a test.\n findings.push(...steplessFindings(`${service}: requirements`, service, reqs));\n findings.push(...assertionlessFindings(`${service}: requirements`, service, reqs));\n findings.push(...examplesFindings(`${service}: requirements`, service, reqs));\n findings.push(...duplicateRequirementFindings(reqs, `${service}: spec.md`, service));\n findings.push(...requirementIdFindings(reqs, `${service}: spec.md`, service));\n findings.push(...repeatedListLineFindings(reqs, `${service}: spec.md`, service));\n } else {\n findings.push({\n severity: \"warn\",\n code: \"service.no-spec\",\n message: `No living spec at ${paths.spec} — requirement coverage and API governance are unchecked`,\n });\n }\n\n // The requirements that still govern anything. A REMOVED requirement is on\n // its way out together with the operations it names: it makes no claim on the\n // contract and governs nothing once the retiring feature archives — the same\n // position coherence/coherence.ts takes on the delta side.\n //\n // Spelled once because it was spelled three times and dropped on the fourth:\n // `governedOps` filtered, `spec-api.op-undefined` filtered, and the\n // `api.covered` join did not — so an operation whose only requirement was\n // REMOVED counted as governed, and the one operation about to be left without\n // a requirement was the one the report called covered.\n const livingReqs = reqs.filter((r) => r.kind !== \"REMOVED\");\n\n // The architecture spec, read HERE with spec.md rather than with the rest of\n // its own axis below, because the event links are a requirement-level join and\n // BOTH requirement files carry them. Events are architecture as often as they\n // are business — the outbox requirement in a service's arch.spec.md is the\n // canonical place a `Publishes:` line belongs — so handing the event axis only\n // spec.md would silently ignore the lines most authors write.\n const archText = existsSync(paths.archSpec)\n ? fleet === undefined\n ? await readFile(paths.archSpec, \"utf8\")\n : await fleet.readText(paths.archSpec)\n : null;\n const archReqs =\n archText === null\n ? []\n : fleet === undefined\n ? parseRequirements(archText)\n : await fleet.readRequirements(paths.archSpec);\n\n return { findings, reqs, livingReqs, archText, archReqs };\n}\n\n/** What the arch axis needs once the model and the landscape have been read. */\nexport interface ArchAxis {\n service: PathableService;\n paths: ServicePaths;\n /** arch.spec.md's text, or null — its absence is not a finding. */\n archText: string | null;\n archReqs: Requirement[];\n /** The service's own C4 model, empty when it has none. */\n elements: Elem[];\n relationships: Rel[];\n /** The living landscape, or null when the repo has none. */\n land: LoadedDoc | null;\n /** The docs root, for the one lazy load below — see `deployment` in the scope. */\n docsDir: DocsDir;\n /**\n * The enumerated fleet, for the Covers matcher's endpoint resolution — the\n * same set the caller's own spine resolution uses, so a `Covers:` line\n * naming a service matches an edge drawn into that service's container.\n */\n known?: ReadonlySet<string>;\n}\n\n/**\n * The topology a `node:` Covers entry resolves against, loaded at most once and\n * only when something asks for it.\n *\n * Under `--all` the caller hands in the doc it already loaded — the whole\n * `architecture/` project — so the deployment model is already here and this\n * returns it. A SINGLE-service run loads only `landscape.likec4`, and a fleet\n * that keeps its `deployment { }` block in a file beside it, which is the\n * ordinary layout once one landscape file is the fleet's convention, would then\n * resolve one `Covers:` line two different ways depending on which command\n * asked. `loam status`'s two forms had exactly that disagreement and it was a\n * defect both times; this is the same class, prevented rather than found.\n *\n * The cost is paid only by a service whose `arch.spec.md` actually writes a\n * `node:` entry. A fleet with no topology at all pays one project load per such\n * service — a state in which the entry is a typo anyway, and one load is what\n * buys it a message that names the real ids.\n */\nasync function topology(\n land: LoadedDoc | null,\n archReqs: Requirement[],\n docsDir: DocsDir,\n): Promise<DeploymentModel | undefined> {\n if (land?.deployment !== undefined && hasDeployment(land.deployment)) return land.deployment;\n const wanted = coversEntries(archReqs).some((e) => e.form === \"node\" || e.form === \"node-edge\");\n if (!wanted) return undefined;\n const project = await loadArchitecture(docsDir);\n return project.errors.length === 0 ? project.deployment : undefined;\n}\n\nexport async function archAxisFindings(axis: ArchAxis): Promise<Finding[]> {\n const { service, paths, archText, archReqs, elements, relationships, land, known, docsDir } = axis;\n const findings: Finding[] = [];\n\n // The architecture spec axis — the obligations a business spec never carries\n // (the transactional outbox, retries, metrics, alerts). arch.spec.md is\n // optional and its ABSENCE is not a finding (partial adoption is supported);\n // what exists must hold together: every requirement needs a scenario, every\n // `Covers:` entry must resolve (covers.unknown), and every alert/SLI that\n // health.yaml declares wants a covering requirement (health.uncovered) — the\n // moment health.yaml stops being inert. Warnings, never gates: `--strict` is\n // the CI escalation.\n const health = await readHealth(paths.health);\n\n // A model that reaches nothing is almost never true — but only when\n // something else attests it should reach. A deliberately shallow synthetic\n // fixture can hold elements without relationships beside sources that name\n // dependencies, yet `0 errors, 0 warnings` would still read as done.\n // Evidence-gated on purpose: a bare root-plus-one-container baseline stays\n // silent (nothing PROVES it thin — the standard mid-adoption shape), the\n // warn needs either a second nested element with no edge joining anything,\n // or dependencies declared in this service's own health.yaml. The health\n // half goes quiet when the file is unreadable — no claims on bad data.\n const nested = elements.filter((e) => e.id.includes(\".\")).length;\n const deps = health.unreadable ? [] : health.dependencies;\n if (elements.length > 0 && relationships.length === 0 && (nested > 1 || deps.length > 0)) {\n const evidence = [\n ...(nested > 1 ? [`${nested} nested elements with no edge joining them`] : []),\n ...(deps.length > 0\n ? [`health.yaml declares ${deps.length} dependenc${deps.length === 1 ? \"y\" : \"ies\"} (${deps.join(\", \")})`]\n : []),\n ].join(\", and \");\n findings.push({\n severity: \"warn\",\n code: \"c4.no-relationships\",\n subject: service,\n message:\n `${service}: model.likec4 declares ${elements.length} element(s) and 0 relationships, yet ${evidence} — ` +\n `a model that reaches nothing is almost never true; draw the edges the service actually has ` +\n `(what its containers call, what reads and writes its stores, what it depends on at runtime)`,\n });\n }\n\n if (archText !== null) {\n // The arch axis is advisory in what it ASKS for (covers.unknown,\n // health.uncovered are warnings) but not in whether the file is readable:\n // both breaches below are about the document itself, and they are graded\n // exactly as they are on the business axis — one file, one rule.\n const conflict = documentConflictFinding(`${service}: arch.spec.md`, service, archText);\n if (conflict !== null) findings.push(conflict);\n findings.push(coverageFinding(`${service}: arch requirements`, archReqs));\n findings.push(...steplessFindings(`${service}: arch requirements`, service, archReqs));\n findings.push(...assertionlessFindings(`${service}: arch requirements`, service, archReqs));\n findings.push(...examplesFindings(`${service}: arch requirements`, service, archReqs));\n findings.push(...duplicateRequirementFindings(archReqs, `${service}: arch.spec.md`, service));\n findings.push(...requirementIdFindings(archReqs, `${service}: arch.spec.md`, service));\n findings.push(...repeatedListLineFindings(archReqs, `${service}: arch.spec.md`, service));\n }\n // A health.yaml that exists but does not read is reported once, and the\n // checks that read it go quiet BOTH ways: its ids are unknown, so no\n // health.uncovered obligation exists (the empty id set below is already\n // silent), and no `Covers: alert:/sli:` entry may be graded a typo against\n // ids nobody could read (coversUnknownFindings mutes on the flag). A warn,\n // not an error: the axis it feeds is advisory end to end.\n if (health.unreadable) {\n findings.push({\n severity: \"warn\",\n code: \"health.invalid\",\n subject: service,\n message: `${service}: health.yaml does not parse — alert/SLI ids are unreadable, so Covers: alert:/sli: entries and health coverage are unchecked`,\n ...(health.error === undefined ? {} : { details: [health.error] }),\n });\n }\n const landParses = land !== null && land.errors.length === 0 ? land : null;\n const deployment = await topology(landParses, archReqs, docsDir);\n const scope: CoverageScope = {\n elements: [...elements, ...(landParses?.elements ?? [])],\n relationships: [...relationships, ...(landParses?.relationships ?? [])],\n ...(deployment === undefined ? {} : { deployment }),\n health: health.ids,\n known,\n };\n findings.push(\n ...coversUnknownFindings(archReqs, { where: `${service}: arch.spec.md`, subject: service }, scope, health.unreadable),\n );\n const activeCovers = coversEntries(archReqs);\n for (const { form, ids } of [\n { form: \"alert\" as const, ids: health.ids.alerts },\n { form: \"sli\" as const, ids: health.ids.slis },\n ]) {\n for (const id of ids) {\n if (activeCovers.some((e) => e.form === form && e.id === id)) continue;\n findings.push({\n severity: \"warn\",\n code: \"health.uncovered\",\n subject: service,\n message: `${service}: health.yaml declares ${form === \"alert\" ? \"alert\" : \"SLI\"} '${id}' but no arch.spec.md requirement covers it — write one with 'Covers: ${form}:${id}', or the signal ships with nothing testing it`,\n });\n }\n }\n // The other direction health.yaml is read in: its dependencies must be the\n // model's dependencies. `deps` is already muted on an unreadable file above.\n findings.push(...healthDependencyFindings({ service, dependencies: deps, elements, modelPath: paths.model }));\n\n return findings;\n}\n"]}
1
+ {"version":3,"file":"specs.js","sourceRoot":"","sources":["../../../../src/commands/validate/service/specs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAuC,MAAM,4BAA4B,CAAC;AACjF,OAAO,EAAwB,MAAM,qCAAqC,CAAC;AAC3E,OAAO,EAAqB,MAAM,6BAA6B,CAAC;AAChE,OAAO,EAAgB,MAAM,oCAAoC,CAAC;AAClE,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACpE,OAAO,EACL,qBAAqB,EACrB,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EAAoB,MAAM,gCAAgC,CAAC;AAClE,OAAO,EAAsB,MAAM,0BAA0B,CAAC;AAC9D,OAAO,EAAE,aAAa,EAAwB,MAAM,uCAAuC,CAAC;AAC5F,OAAO,EAAE,gBAAgB,EAAE,MAAM,0CAA0C,CAAC;AAC5E,OAAO,EAAgB,MAAM,kCAAkC,CAAC;AAChE,OAAO,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAC;AAChE,OAAO,EAAE,uBAAuB,EAAE,MAAM,mCAAmC,CAAC;AAC5E,OAAO,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAC9D,OAAO,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AACpE,OAAO,EACL,eAAe,EACf,aAAa,EACb,qBAAqB,EACrB,4BAA4B,EAC5B,wBAAwB,EACxB,qBAAqB,GACtB,MAAM,2BAA2B,CAAC;AAenC,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,KAItC;IACC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,KAAK,CAAC;IACxC,MAAM,QAAQ,GAAc,EAAE,CAAC;IAE/B,wBAAwB;IACxB,IAAI,IAAI,GAAkB,EAAE,CAAC;IAC7B,IAAI,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QAC3B,MAAM,QAAQ,GACZ,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC9F,IAAI,GAAG,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,gBAAgB,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACpG,2EAA2E;QAC3E,uEAAuE;QACvE,yEAAyE;QACzE,uEAAuE;QACvE,uEAAuE;QACvE,oEAAoE;QACpE,0EAA0E;QAC1E,2EAA2E;QAC3E,qBAAqB;QACrB,MAAM,QAAQ,GAAG,uBAAuB,CAAC,GAAG,OAAO,WAAW,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC;QACnF,IAAI,QAAQ,KAAK,IAAI;YAAE,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC/C,wEAAwE;QACxE,0EAA0E;QAC1E,4EAA4E;QAC5E,kEAAkE;QAClE,2EAA2E;QAC3E,0EAA0E;QAC1E,SAAS;QACT,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACtB,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,MAAM;gBAChB,IAAI,EAAE,sBAAsB;gBAC5B,OAAO,EAAE,OAAO;gBAChB,OAAO,EAAE,GAAG,OAAO,qKAAqK;aACzL,CAAC,CAAC;QACL,CAAC;aAAM,CAAC;YACN,QAAQ,CAAC,IAAI,CAAC,eAAe,CAAC,GAAG,OAAO,gBAAgB,EAAE,IAAI,CAAC,CAAC,CAAC;QACnE,CAAC;QACD,qEAAqE;QACrE,kEAAkE;QAClE,wEAAwE;QACxE,2EAA2E;QAC3E,0EAA0E;QAC1E,0DAA0D;QAC1D,QAAQ,CAAC,IAAI,CAAC,GAAG,gBAAgB,CAAC,GAAG,OAAO,gBAAgB,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC;QAC9E,QAAQ,CAAC,IAAI,CAAC,GAAG,qBAAqB,CAAC,GAAG,OAAO,gBAAgB,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC;QACnF,QAAQ,CAAC,IAAI,CAAC,GAAG,gBAAgB,CAAC,GAAG,OAAO,gBAAgB,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC;QAC9E,QAAQ,CAAC,IAAI,CAAC,GAAG,4BAA4B,CAAC,IAAI,EAAE,GAAG,OAAO,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC;QACrF,QAAQ,CAAC,IAAI,CAAC,GAAG,qBAAqB,CAAC,IAAI,EAAE,GAAG,OAAO,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC;QAC9E,QAAQ,CAAC,IAAI,CAAC,GAAG,wBAAwB,CAAC,IAAI,EAAE,GAAG,OAAO,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC;IACnF,CAAC;SAAM,CAAC;QACN,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ,EAAE,MAAM;YAChB,IAAI,EAAE,iBAAiB;YACvB,OAAO,EAAE,qBAAqB,KAAK,CAAC,IAAI,0DAA0D;SACnG,CAAC,CAAC;IACL,CAAC;IAED,2EAA2E;IAC3E,8EAA8E;IAC9E,6EAA6E;IAC7E,2DAA2D;IAC3D,EAAE;IACF,6EAA6E;IAC7E,oEAAoE;IACpE,0EAA0E;IAC1E,8EAA8E;IAC9E,uDAAuD;IACvD,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC;IAE5D,6EAA6E;IAC7E,+EAA+E;IAC/E,8EAA8E;IAC9E,2EAA2E;IAC3E,+EAA+E;IAC/E,8DAA8D;IAC9D,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAC,QAAQ,CAAC;QACzC,CAAC,CAAC,KAAK,KAAK,SAAS;YACnB,CAAC,CAAC,MAAM,QAAQ,CAAC,KAAK,CAAC,QAAQ,EAAE,MAAM,CAAC;YACxC,CAAC,CAAC,MAAM,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC;QACxC,CAAC,CAAC,IAAI,CAAC;IACT,MAAM,QAAQ,GACZ,QAAQ,KAAK,IAAI;QACf,CAAC,CAAC,EAAE;QACJ,CAAC,CAAC,KAAK,KAAK,SAAS;YACnB,CAAC,CAAC,iBAAiB,CAAC,QAAQ,CAAC;YAC7B,CAAC,CAAC,MAAM,KAAK,CAAC,gBAAgB,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAErD,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC;AAC5D,CAAC;AAoCD;;;;;;;;;;;;;;;;;GAiBG;AACH,KAAK,UAAU,QAAQ,CACrB,IAAsB,EACtB,QAAuB,EACvB,OAAgB,EAChB,KAAoB;IAEpB,IAAI,IAAI,EAAE,UAAU,KAAK,SAAS,IAAI,aAAa,CAAC,IAAI,CAAC,UAAU,CAAC;QAAE,OAAO,IAAI,CAAC,UAAU,CAAC;IAC7F,MAAM,MAAM,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,IAAI,CAAC,CAAC,IAAI,KAAK,WAAW,CAAC,CAAC;IAChG,IAAI,CAAC,MAAM;QAAE,OAAO,SAAS,CAAC;IAC9B,6EAA6E;IAC7E,2EAA2E;IAC3E,+EAA+E;IAC/E,MAAM,OAAO,GAAG,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,gBAAgB,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;IAC1G,OAAO,OAAO,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC;AACtE,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,IAAc;IACnD,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,aAAa,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IACnG,MAAM,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC;IACvB,MAAM,QAAQ,GAAc,EAAE,CAAC;IAE/B,6EAA6E;IAC7E,wEAAwE;IACxE,6EAA6E;IAC7E,4EAA4E;IAC5E,0EAA0E;IAC1E,6EAA6E;IAC7E,6EAA6E;IAC7E,qBAAqB;IACrB,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAE9C,oEAAoE;IACpE,2EAA2E;IAC3E,2EAA2E;IAC3E,qEAAqE;IACrE,2EAA2E;IAC3E,yEAAyE;IACzE,2EAA2E;IAC3E,yEAAyE;IACzE,uEAAuE;IACvE,+EAA+E;IAC/E,yEAAyE;IACzE,4EAA4E;IAC5E,8EAA8E;IAC9E,8EAA8E;IAC9E,8EAA8E;IAC9E,sDAAsD;IACtD,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACpD,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE;QACnC,MAAM,GAAG,GAAG,CAAC,CAAC,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;QAClC,OAAO,GAAG,KAAK,CAAC,CAAC,IAAI,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC,MAAM,CAAC;IACV,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC;IAC1D,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC;QACzF,MAAM,QAAQ,GAAG;YACf,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,4CAA4C,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YAC9E,GAAG,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC;gBACjB,CAAC,CAAC,CAAC,wBAAwB,IAAI,CAAC,MAAM,aAAa,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,KAAK,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;gBAC1G,CAAC,CAAC,EAAE,CAAC;SACR,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACjB,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ,EAAE,MAAM;YAChB,IAAI,EAAE,qBAAqB;YAC3B,OAAO,EAAE,OAAO;YAChB,OAAO,EACL,GAAG,OAAO,2BAA2B,QAAQ,CAAC,MAAM,wCAAwC,QAAQ,KAAK;gBACzG,6FAA6F;gBAC7F,6FAA6F;SAChG,CAAC,CAAC;IACL,CAAC;IAED,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QACtB,iEAAiE;QACjE,0EAA0E;QAC1E,yEAAyE;QACzE,iEAAiE;QACjE,MAAM,QAAQ,GAAG,uBAAuB,CAAC,GAAG,OAAO,gBAAgB,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC;QACxF,IAAI,QAAQ,KAAK,IAAI;YAAE,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC/C,QAAQ,CAAC,IAAI,CAAC,eAAe,CAAC,GAAG,OAAO,qBAAqB,EAAE,QAAQ,CAAC,CAAC,CAAC;QAC1E,QAAQ,CAAC,IAAI,CAAC,GAAG,gBAAgB,CAAC,GAAG,OAAO,qBAAqB,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC;QACvF,QAAQ,CAAC,IAAI,CAAC,GAAG,qBAAqB,CAAC,GAAG,OAAO,qBAAqB,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC;QAC5F,QAAQ,CAAC,IAAI,CAAC,GAAG,gBAAgB,CAAC,GAAG,OAAO,qBAAqB,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC;QACvF,QAAQ,CAAC,IAAI,CAAC,GAAG,4BAA4B,CAAC,QAAQ,EAAE,GAAG,OAAO,gBAAgB,EAAE,OAAO,CAAC,CAAC,CAAC;QAC9F,QAAQ,CAAC,IAAI,CAAC,GAAG,qBAAqB,CAAC,QAAQ,EAAE,GAAG,OAAO,gBAAgB,EAAE,OAAO,CAAC,CAAC,CAAC;QACvF,QAAQ,CAAC,IAAI,CAAC,GAAG,wBAAwB,CAAC,QAAQ,EAAE,GAAG,OAAO,gBAAgB,EAAE,OAAO,CAAC,CAAC,CAAC;IAC5F,CAAC;IACD,wEAAwE;IACxE,qEAAqE;IACrE,wEAAwE;IACxE,2EAA2E;IAC3E,2EAA2E;IAC3E,0DAA0D;IAC1D,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;QACtB,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ,EAAE,MAAM;YAChB,IAAI,EAAE,gBAAgB;YACtB,OAAO,EAAE,OAAO;YAChB,OAAO,EAAE,GAAG,OAAO,+HAA+H;YAClJ,GAAG,CAAC,MAAM,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;SACnE,CAAC,CAAC;IACL,CAAC;IACD,MAAM,UAAU,GAAG,IAAI,KAAK,IAAI,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;IAC3E,MAAM,UAAU,GAAG,MAAM,QAAQ,CAAC,UAAU,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;IACxE,MAAM,KAAK,GAAkB;QAC3B,QAAQ,EAAE,CAAC,GAAG,QAAQ,EAAE,GAAG,CAAC,UAAU,EAAE,QAAQ,IAAI,EAAE,CAAC,CAAC;QACxD,aAAa,EAAE,CAAC,GAAG,aAAa,EAAE,GAAG,CAAC,UAAU,EAAE,aAAa,IAAI,EAAE,CAAC,CAAC;QACvE,GAAG,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC;QACnD,MAAM,EAAE,MAAM,CAAC,GAAG;QAClB,KAAK;KACN,CAAC;IACF,2EAA2E;IAC3E,8EAA8E;IAC9E,4EAA4E;IAC5E,oFAAoF;IACpF,8EAA8E;IAC9E,6EAA6E;IAC7E,6EAA6E;IAC7E,8EAA8E;IAC9E,4EAA4E;IAC5E,wDAAwD;IACxD,EAAE;IACF,6EAA6E;IAC7E,4EAA4E;IAC5E,yEAAyE;IACzE,+EAA+E;IAC/E,6EAA6E;IAC7E,mEAAmE;IACnE,IAAI,IAAI,CAAC,aAAa,KAAK,IAAI,EAAE,CAAC;QAChC,QAAQ,CAAC,IAAI,CACX,GAAG,qBAAqB,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,GAAG,OAAO,gBAAgB,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,UAAU,CAAC,CACtH,CAAC;IACJ,CAAC;IACD,MAAM,YAAY,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;IAC7C,KAAK,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI;QAC1B,EAAE,IAAI,EAAE,OAAgB,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE;QAClD,EAAE,IAAI,EAAE,KAAc,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE;KAC/C,EAAE,CAAC;QACF,KAAK,MAAM,EAAE,IAAI,GAAG,EAAE,CAAC;YACrB,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,IAAI,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC;gBAAE,SAAS;YACvE,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,MAAM;gBAChB,IAAI,EAAE,kBAAkB;gBACxB,OAAO,EAAE,OAAO;gBAChB,OAAO,EAAE,GAAG,OAAO,0BAA0B,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,KAAK,EAAE,yEAAyE,IAAI,IAAI,EAAE,gDAAgD;aAC1N,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IACD,2EAA2E;IAC3E,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,CAAC,GAAG,wBAAwB,CAAC,EAAE,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IAE9G,OAAO,QAAQ,CAAC;AAClB,CAAC","sourcesContent":["/**\n * This service's requirement documents: spec.md, arch.spec.md, and the\n * obligations the second of them carries that the first never does.\n *\n * Two entry points because the two happen at different moments in a service's\n * grading and the order of findings is part of the report. `readServiceSpecs`\n * runs first — everything downstream joins through the requirements it parses —\n * while `archAxisFindings` runs last, after both spines, because `covers.unknown`\n * resolves against the C4 model and the landscape and cannot be answered until\n * those are in hand.\n *\n * The arch axis is advisory in what it ASKS for (covers.unknown,\n * health.uncovered are warnings; arch.spec.md's absence is not a finding at all,\n * because partial adoption is supported) but not in whether the file is\n * readable: a conflict marker or a stepless scenario is graded exactly as it is\n * on the business axis. One file, one rule.\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 PathableService } from \"../../../core/kernel/ids/service.js\";\nimport { type ServicePaths } from \"../../../core/repo/paths.js\";\nimport { type Finding } 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 { type CoverageScope } from \"../../../core/c4/arch.js\";\nimport { hasDeployment, type DeploymentModel } from \"../../../core/c4/parsed/deployment.js\";\nimport { loadArchitecture } from \"../../../core/c4/project/architecture.js\";\nimport { type DocsDir } from \"../../../core/kernel/ids/dirs.js\";\nimport { readHealth } from \"../../../core/vocabulary/health.js\";\nimport { documentConflictFinding } from \"../../../core/conflict-markers.js\";\nimport { FleetContext } from \"../../../core/fleet-context.js\";\nimport { healthDependencyFindings } from \"../checks/health-deps.js\";\nimport {\n coverageFinding,\n coversEntries,\n coversUnknownFindings,\n duplicateRequirementFindings,\n repeatedListLineFindings,\n requirementIdFindings,\n} from \"../checks/requirements.js\";\n\n/** Both requirement documents, parsed once for every axis that joins through them. */\nexport interface ServiceSpecs {\n /** spec.md's findings, emitted before any cross-axis check runs. */\n findings: Finding[];\n /** Every requirement spec.md declares, REMOVED ones included. */\n reqs: Requirement[];\n /** The ones that still govern something. */\n livingReqs: Requirement[];\n /** arch.spec.md's text, or null when the service has none. */\n archText: string | null;\n archReqs: Requirement[];\n}\n\nexport async function readServiceSpecs(input: {\n service: PathableService;\n paths: ServicePaths;\n fleet?: FleetContext;\n}): Promise<ServiceSpecs> {\n const { service, paths, fleet } = input;\n const findings: Finding[] = [];\n\n // Requirement coverage.\n let reqs: Requirement[] = [];\n if (existsSync(paths.spec)) {\n const specText =\n fleet === undefined ? await readFile(paths.spec, \"utf8\") : await fleet.readText(paths.spec);\n reqs = fleet === undefined ? parseRequirements(specText) : await fleet.readRequirements(paths.spec);\n // Conflict markers first, because they say that nothing below this line is\n // anyone's text. An error, gating by the default rule and deliberately\n // without `gates`: that field is coherence's (issue.ts) and archive does\n // not read validate's findings, so claiming it here would be a promise\n // about a gate that is not wired. What makes it an error rather than a\n // warning is what the next merge does — `loam archive` rewrites the\n // requirements section of this very file and takes whichever marker lines\n // fall inside it, turning a conflict anyone can see into a document nobody\n // can tell is wrong.\n const conflict = documentConflictFinding(`${service}: spec.md`, service, specText);\n if (conflict !== null) findings.push(conflict);\n // A living spec with no `### Requirement:` block at all is the baseline\n // `loam adopt` scaffolds and nobody ever filled in. \"requirements covered\n // (0 requirements, all with scenarios)\" is true of it and says the opposite\n // of what a reader needs: every cross-axis check downstream — API\n // governance, the Operations: spine, gherkin generation — is vacuous here,\n // and the green tick is what let a whole fleet score `sourced` over empty\n // files.\n if (reqs.length === 0) {\n findings.push({\n severity: \"warn\",\n code: \"spec.no-requirements\",\n subject: service,\n message: `${service}: spec.md holds no '### Requirement:' blocks — every requirement-driven check below is vacuous, and nothing here can go out of date because nothing is written down`,\n });\n } else {\n findings.push(coverageFinding(`${service}: requirements`, reqs));\n }\n // The other half of the coverage question, and an error for the same\n // reason the first half is: the whole claim of coverage is that a\n // requirement has a scenario something can RUN. A heading with no steps\n // satisfies the count and tests nothing — cucumber runs it vacuously green\n // and `loam verify --results` can never confirm it — so a fleet gate that\n // called it covered was certifying the absence of a test.\n findings.push(...steplessFindings(`${service}: requirements`, service, reqs));\n findings.push(...assertionlessFindings(`${service}: requirements`, service, reqs));\n findings.push(...examplesFindings(`${service}: requirements`, service, reqs));\n findings.push(...duplicateRequirementFindings(reqs, `${service}: spec.md`, service));\n findings.push(...requirementIdFindings(reqs, `${service}: spec.md`, service));\n findings.push(...repeatedListLineFindings(reqs, `${service}: spec.md`, service));\n } else {\n findings.push({\n severity: \"warn\",\n code: \"service.no-spec\",\n message: `No living spec at ${paths.spec} — requirement coverage and API governance are unchecked`,\n });\n }\n\n // The requirements that still govern anything. A REMOVED requirement is on\n // its way out together with the operations it names: it makes no claim on the\n // contract and governs nothing once the retiring feature archives — the same\n // position coherence/coherence.ts takes on the delta side.\n //\n // Spelled once because it was spelled three times and dropped on the fourth:\n // `governedOps` filtered, `spec-api.op-undefined` filtered, and the\n // `api.covered` join did not — so an operation whose only requirement was\n // REMOVED counted as governed, and the one operation about to be left without\n // a requirement was the one the report called covered.\n const livingReqs = reqs.filter((r) => r.kind !== \"REMOVED\");\n\n // The architecture spec, read HERE with spec.md rather than with the rest of\n // its own axis below, because the event links are a requirement-level join and\n // BOTH requirement files carry them. Events are architecture as often as they\n // are business — the outbox requirement in a service's arch.spec.md is the\n // canonical place a `Publishes:` line belongs — so handing the event axis only\n // spec.md would silently ignore the lines most authors write.\n const archText = existsSync(paths.archSpec)\n ? fleet === undefined\n ? await readFile(paths.archSpec, \"utf8\")\n : await fleet.readText(paths.archSpec)\n : null;\n const archReqs =\n archText === null\n ? []\n : fleet === undefined\n ? parseRequirements(archText)\n : await fleet.readRequirements(paths.archSpec);\n\n return { findings, reqs, livingReqs, archText, archReqs };\n}\n\n/** What the arch axis needs once the model and the landscape have been read. */\nexport interface ArchAxis {\n service: PathableService;\n paths: ServicePaths;\n /** arch.spec.md's text, or null — its absence is not a finding. */\n archText: string | null;\n archReqs: Requirement[];\n /** The service's own C4 model, empty when it has none. */\n elements: Elem[];\n relationships: Rel[];\n /**\n * True when this service's model EXTENDS a fleet map that does not parse, so\n * `elements` and `relationships` above are empty for that reason and no other\n * — see `covers.unknown` below, which is suspended on it.\n */\n mapUnreadable?: boolean;\n /** The living landscape, or null when the repo has none. */\n land: LoadedDoc | null;\n /** The docs root, for the one lazy load below — see `deployment` in the scope. */\n docsDir: DocsDir;\n /**\n * The enumerated fleet, for the Covers matcher's endpoint resolution — the\n * same set the caller's own spine resolution uses, so a `Covers:` line\n * naming a service matches an edge drawn into that service's container.\n */\n known?: ReadonlySet<string>;\n /**\n * The run's read index, so the one lazy project load below is the same\n * memoised parse the rest of the target already made rather than a second\n * workspace spun for one `node:` entry.\n */\n fleet?: FleetContext;\n}\n\n/**\n * The topology a `node:` Covers entry resolves against, loaded at most once and\n * only when something asks for it.\n *\n * Under `--all` the caller hands in the doc it already loaded — the whole\n * `architecture/` project — so the deployment model is already here and this\n * returns it. A SINGLE-service run loads only `landscape.likec4`, and a fleet\n * that keeps its `deployment { }` block in a file beside it, which is the\n * ordinary layout once one landscape file is the fleet's convention, would then\n * resolve one `Covers:` line two different ways depending on which command\n * asked. `loam status`'s two forms had exactly that disagreement and it was a\n * defect both times; this is the same class, prevented rather than found.\n *\n * The cost is paid only by a service whose `arch.spec.md` actually writes a\n * `node:` entry. A fleet with no topology at all pays one project load per such\n * service — a state in which the entry is a typo anyway, and one load is what\n * buys it a message that names the real ids.\n */\nasync function topology(\n land: LoadedDoc | null,\n archReqs: Requirement[],\n docsDir: DocsDir,\n fleet?: FleetContext,\n): Promise<DeploymentModel | undefined> {\n if (land?.deployment !== undefined && hasDeployment(land.deployment)) return land.deployment;\n const wanted = coversEntries(archReqs).some((e) => e.form === \"node\" || e.form === \"node-edge\");\n if (!wanted) return undefined;\n // Through the index when there is one: `land` above is now that same project\n // in every mode, so this fallback fires only for the caller that handed in\n // nothing, and it must not be a second parse of documents already in the memo.\n const project = fleet === undefined ? await loadArchitecture(docsDir) : await fleet.architecture(docsDir);\n return project.errors.length === 0 ? project.deployment : undefined;\n}\n\nexport async function archAxisFindings(axis: ArchAxis): Promise<Finding[]> {\n const { service, paths, archText, archReqs, elements, relationships, land, known, docsDir } = axis;\n const { fleet } = axis;\n const findings: Finding[] = [];\n\n // The architecture spec axis — the obligations a business spec never carries\n // (the transactional outbox, retries, metrics, alerts). arch.spec.md is\n // optional and its ABSENCE is not a finding (partial adoption is supported);\n // what exists must hold together: every requirement needs a scenario, every\n // `Covers:` entry must resolve (covers.unknown), and every alert/SLI that\n // health.yaml declares wants a covering requirement (health.uncovered) — the\n // moment health.yaml stops being inert. Warnings, never gates: `--strict` is\n // the CI escalation.\n const health = await readHealth(paths.health);\n\n // A model that reaches nothing is almost never true — but only when\n // something else attests it should reach. A deliberately shallow synthetic\n // fixture can hold elements without relationships beside sources that name\n // dependencies, yet `0 errors, 0 warnings` would still read as done.\n // Evidence-gated on purpose: a bare root-plus-one-container baseline stays\n // silent (nothing PROVES it thin — the standard mid-adoption shape), the\n // warn needs either a second nested element with no edge joining anything,\n // or dependencies declared in this service's own health.yaml. The health\n // half goes quiet when the file is unreadable — no claims on bad data.\n // \"Nested\" is a PARENT in this slice, not a dot in the id, and the two stopped\n // meaning the same thing when a model could extend the map: an extending\n // placeholder `extend marketplace.orderService { }` contributes one element\n // whose id is dotted and whose parent belongs to the map, so a dot count read\n // it as nested architecture that reaches nothing — the standalone placeholder\n // `orderService = softwareSystem` was silent for the same shape. One rule for\n // both: `a`/`a.x`/`kafka`/`kafka.t` still counts two.\n const sliceIds = new Set(elements.map((e) => e.id));\n const nested = elements.filter((e) => {\n const dot = e.id.lastIndexOf(\".\");\n return dot !== -1 && sliceIds.has(e.id.slice(0, dot));\n }).length;\n const deps = health.unreadable ? [] : health.dependencies;\n if (elements.length > 0 && relationships.length === 0 && (nested > 1 || deps.length > 0)) {\n const evidence = [\n ...(nested > 1 ? [`${nested} nested elements with no edge joining them`] : []),\n ...(deps.length > 0\n ? [`health.yaml declares ${deps.length} dependenc${deps.length === 1 ? \"y\" : \"ies\"} (${deps.join(\", \")})`]\n : []),\n ].join(\", and \");\n findings.push({\n severity: \"warn\",\n code: \"c4.no-relationships\",\n subject: service,\n message:\n `${service}: model.likec4 declares ${elements.length} element(s) and 0 relationships, yet ${evidence} — ` +\n `a model that reaches nothing is almost never true; draw the edges the service actually has ` +\n `(what its containers call, what reads and writes its stores, what it depends on at runtime)`,\n });\n }\n\n if (archText !== null) {\n // The arch axis is advisory in what it ASKS for (covers.unknown,\n // health.uncovered are warnings) but not in whether the file is readable:\n // both breaches below are about the document itself, and they are graded\n // exactly as they are on the business axis — one file, one rule.\n const conflict = documentConflictFinding(`${service}: arch.spec.md`, service, archText);\n if (conflict !== null) findings.push(conflict);\n findings.push(coverageFinding(`${service}: arch requirements`, archReqs));\n findings.push(...steplessFindings(`${service}: arch requirements`, service, archReqs));\n findings.push(...assertionlessFindings(`${service}: arch requirements`, service, archReqs));\n findings.push(...examplesFindings(`${service}: arch requirements`, service, archReqs));\n findings.push(...duplicateRequirementFindings(archReqs, `${service}: arch.spec.md`, service));\n findings.push(...requirementIdFindings(archReqs, `${service}: arch.spec.md`, service));\n findings.push(...repeatedListLineFindings(archReqs, `${service}: arch.spec.md`, service));\n }\n // A health.yaml that exists but does not read is reported once, and the\n // checks that read it go quiet BOTH ways: its ids are unknown, so no\n // health.uncovered obligation exists (the empty id set below is already\n // silent), and no `Covers: alert:/sli:` entry may be graded a typo against\n // ids nobody could read (coversUnknownFindings mutes on the flag). A warn,\n // not an error: the axis it feeds is advisory end to end.\n if (health.unreadable) {\n findings.push({\n severity: \"warn\",\n code: \"health.invalid\",\n subject: service,\n message: `${service}: health.yaml does not parse — alert/SLI ids are unreadable, so Covers: alert:/sli: entries and health coverage are unchecked`,\n ...(health.error === undefined ? {} : { details: [health.error] }),\n });\n }\n const landParses = land !== null && land.errors.length === 0 ? land : null;\n const deployment = await topology(landParses, archReqs, docsDir, fleet);\n const scope: CoverageScope = {\n elements: [...elements, ...(landParses?.elements ?? [])],\n relationships: [...relationships, ...(landParses?.relationships ?? [])],\n ...(deployment === undefined ? {} : { deployment }),\n health: health.ids,\n known,\n };\n // `covers.unknown` is suspended entirely when the model EXTENDS a map that\n // does not parse, and this is the standing rule of the whole axis rather than\n // a special case: nothing may be graded out of a document nobody could read\n // (`core/c4/service-model/load.ts` states it; `model/grade.ts`, `model/diverged.ts`\n // and the flow skip in `./service.ts` all honour it). Such a model comes back\n // with no elements and no edges AND the landscape half of the scope is empty\n // for the same reason, so every `Covers:` line in the service's arch.spec.md\n // would resolve to nothing — one unparseable `architecture/usecases/x.likec4`\n // convicting every Covers: line of every migrated service in the fleet as a\n // typo, under a code whose whole job is to catch typos.\n //\n // The alert:/sli: forms could still be answered out of health.yaml, which IS\n // readable, and they are suspended with the rest on purpose: a verdict that\n // grades two of the Covers: forms and drops the others is a report whose\n // completeness depends on which form the author happened to write, and the one\n // repair — fix the map — brings all of them back on the next run. The health\n // axis keeps its own grade either way (`health.uncovered`, below).\n if (axis.mapUnreadable !== true) {\n findings.push(\n ...coversUnknownFindings(archReqs, { where: `${service}: arch.spec.md`, subject: service }, scope, health.unreadable),\n );\n }\n const activeCovers = coversEntries(archReqs);\n for (const { form, ids } of [\n { form: \"alert\" as const, ids: health.ids.alerts },\n { form: \"sli\" as const, ids: health.ids.slis },\n ]) {\n for (const id of ids) {\n if (activeCovers.some((e) => e.form === form && e.id === id)) continue;\n findings.push({\n severity: \"warn\",\n code: \"health.uncovered\",\n subject: service,\n message: `${service}: health.yaml declares ${form === \"alert\" ? \"alert\" : \"SLI\"} '${id}' but no arch.spec.md requirement covers it — write one with 'Covers: ${form}:${id}', or the signal ships with nothing testing it`,\n });\n }\n }\n // The other direction health.yaml is read in: its dependencies must be the\n // model's dependencies. `deps` is already muted on an unreadable file above.\n findings.push(...healthDependencyFindings({ service, dependencies: deps, elements, modelPath: paths.model }));\n\n return findings;\n}\n"]}
@@ -13,11 +13,13 @@
13
13
  * PRODUCER owns the contract and it lives in another repository, so the two
14
14
  * could not share a walk even though they share the shape.
15
15
  */
16
+ import { relative } from "node:path";
16
17
  import {} from "../../../core/kernel/ids/service.js";
17
18
  import {} from "../../../core/c4/likec4.js";
19
+ import {} from "../../../core/kernel/ids/dirs.js";
18
20
  import {} from "../../../core/kernel/ids/service.js";
19
21
  import {} from "../../../core/vocabulary/report.js";
20
- import { errorText } from "../checks/vocabulary.js";
22
+ import { errorText } from "../../../core/c4/likec4.js";
21
23
  import {} from "./api.js";
22
24
  export function spineFindings(spine) {
23
25
  const { service, me, land, landSvcOf, contract } = spine;
@@ -35,15 +37,51 @@ export function spineFindings(spine) {
35
37
  // becomes N copies of a dozen cascading diagnostics on a fleet of N
36
38
  // services — the report stops being readable at exactly the moment
37
39
  // somebody needs to read it, and the fix is one file either way.
40
+ // Which DOCUMENT broke, spelled exactly as the fleet arm spells it
41
+ // (`fleet/landscape.ts`): `land` is the whole `architecture/` project, so
42
+ // the broken file is as often a `usecases/*.likec4` as the map itself,
43
+ // and the filename used to be hardcoded here — an agent told to "fix the
44
+ // file the message names" opened a document that parses (verification
45
+ // 2026-09-04, W2).
46
+ const broken = [
47
+ ...new Set(land.errors.map((e) => e.sourceFsPath).filter((p) => p !== undefined)),
48
+ ]
49
+ .map((abs) => relative(spine.docsDir, abs).split(/[\\/]/).join("/"))
50
+ .sort();
51
+ const named = broken.length === 0 ? "architecture/landscape.likec4" : broken.join(", ");
52
+ // The verb agrees with the LIST, not with the count after it: `named` is a
53
+ // comma-joined series once two documents broke, and "a, b, c has 6
54
+ // error(s)" is a sentence a reader trips over — the same care `remedy()`
55
+ // in model/diverged.ts takes over "the kind differ".
56
+ const verb = broken.length > 1 ? "have" : "has";
38
57
  findings.push({
39
58
  severity: "error",
40
59
  code: "spine.landscape-invalid",
41
60
  subject: service,
42
- message: `${service}: landscape.likec4 has ${land.errors.length} error(s) — spine check impossible` +
61
+ message: `${service}: ${named} ${verb} ${land.errors.length} error(s) — spine check impossible` +
62
+ (spine.modelExtends === true
63
+ ? " — and model.likec4 extends it, so the model cannot be read either"
64
+ : "") +
43
65
  (spine.landscapeReported === true
44
66
  ? "; the parser output is reported once, on the landscape target"
45
67
  : ""),
46
- ...(spine.landscapeReported === true ? {} : { details: land.errors.map(errorText) }),
68
+ ...(spine.landscapeReported === true
69
+ ? {}
70
+ : {
71
+ // Every line carries its own file once more than one document is
72
+ // broken, the fleet arm's rule verbatim: a bare `L8:` across two
73
+ // documents is unactionable.
74
+ details: land.errors.map((e) => e.sourceFsPath === undefined || broken.length < 2
75
+ ? errorText(e)
76
+ : `${relative(spine.docsDir, e.sourceFsPath).split(/[\\/]/).join("/")} ${errorText(e)}`),
77
+ }),
78
+ // The broken document first, the service second: the finding is about a
79
+ // file under `architecture/` and is FILED on this service, and without
80
+ // the pair the payload carried only the service directory.
81
+ locations: [
82
+ ...(broken.length === 0 ? [] : [{ path: broken[0], role: "primary" }]),
83
+ { path: spine.treePath, role: "scope" },
84
+ ],
47
85
  });
48
86
  }
49
87
  else {
@@ -1 +1 @@
1
- {"version":3,"file":"spine.js","sourceRoot":"","sources":["../../../../src/commands/validate/service/spine.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The inbound half of the C4↔API spine: every cross-system edge the living\n * landscape draws INTO this service must resolve to a real operation in its\n * OpenAPI.\n *\n * Separate from `./api.js` because the two grade different documents and the\n * fix lives in different files — a broken edge is the fleet map's mistake, a\n * missing operation is the contract's — but it may conclude nothing without\n * `api.js`'s answer, which is why `ServiceContract` is a parameter rather than\n * a second read of openapi.yaml.\n *\n * The event-side mirror of this check is inside `./events.js`: there the\n * PRODUCER owns the contract and it lives in another repository, so the two\n * could not share a walk even though they share the shape.\n */\nimport { type DeclaredService } from \"../../../core/kernel/ids/service.js\";\nimport { type LoadedDoc } from \"../../../core/c4/likec4.js\";\nimport { type PathableService } from \"../../../core/kernel/ids/service.js\";\nimport { type Finding } from \"../../../core/vocabulary/report.js\";\nimport { errorText } from \"../checks/vocabulary.js\";\nimport { type ServiceContract } from \"./api.js\";\n\n/** What the service target hands the spine check. */\nexport interface Spine {\n service: PathableService;\n /** `service` widened for `===` against the resolver's DOCUMENT text — see service.ts. */\n me: string;\n /** The living landscape, or null when the repo has none. */\n land: LoadedDoc | null;\n /** Its element→service resolver; null exactly when `land` is. */\n landSvcOf: ((id: string) => DeclaredService) | null;\n contract: ServiceContract;\n /** True when this run also emits a landscape target — see `ServiceCheck`. */\n landscapeReported?: boolean;\n}\n\nexport function spineFindings(spine: Spine): Finding[] {\n const { service, me, land, landSvcOf, contract } = spine;\n const findings: Finding[] = [];\n\n // Landscape spine: cross-system edges calling THIS service must resolve to a real\n // operation in its OpenAPI — the C4↔API contract, checked in the living landscape,\n // not only in feature mode. Catches dangling / de-linked op edges.\n if (land !== null) {\n if (land.errors.length > 0) {\n // A living landscape that does not parse disables the C4↔API spine check —\n // that is a broken source of truth, not a skippable detail.\n //\n // The parser's own output is attached ONCE per run, and not here when the\n // run has a landscape target to carry it. One syntax error in one file\n // becomes N copies of a dozen cascading diagnostics on a fleet of N\n // services — the report stops being readable at exactly the moment\n // somebody needs to read it, and the fix is one file either way.\n findings.push({\n severity: \"error\",\n code: \"spine.landscape-invalid\",\n subject: service,\n message:\n `${service}: landscape.likec4 has ${land.errors.length} error(s) — spine check impossible` +\n (spine.landscapeReported === true\n ? \"; the parser output is reported once, on the landscape target\"\n : \"\"),\n ...(spine.landscapeReported === true ? {} : { details: land.errors.map(errorText) }),\n });\n } else {\n // Which element IS this service is the binding's call, then a title that\n // names a real services/<id>/, and an edge into a modelled container\n // counts as an edge into its service — matching the exact id alone meant a\n // container edge left the spine without a word.\n const svcOf = landSvcOf!;\n const opset = new Set(contract.ops);\n let checked = 0;\n let broken = 0;\n for (const r of land.relationships) {\n if (svcOf(r.target) !== me) continue;\n if (r.op !== undefined) {\n // A contract that cannot be read — broken, or not there at all —\n // proves nothing about this edge, neither broken nor resolved.\n // Grading the edges against an empty operation set turned ONE root\n // cause (the file) into one `spine.op-undefined` per inbound edge, so\n // a service with twelve consumers reported twelve landscape defects\n // and never named the file. `openapi.invalid` / `service.no-openapi`\n // already did, from ./api.js; only op-link-missing (which never reads\n // the contract) stays live, and `checked` stays 0 so no false\n // spine.resolved is claimed either.\n if (contract.unreadable) continue;\n checked += 1;\n if (!opset.has(r.op)) {\n broken += 1;\n findings.push({\n severity: \"error\",\n code: \"spine.op-undefined\",\n message: `${service}: landscape edge ${svcOf(r.source)} → ${service} calls '${r.op}', not defined in ${service}'s OpenAPI`,\n });\n } else if (contract.deprecated.has(r.op)) {\n // The contract holds — the op is defined — but it is marked\n // `deprecated: true`: the consumer is standing on a contract being\n // retired, and should be migrating off it. Warn per inbound edge;\n // a deprecated op nobody calls raises no spine finding at all.\n findings.push({\n severity: \"warn\",\n code: \"spine.op-deprecated\",\n message: `${service}: landscape edge ${svcOf(r.source)} → ${service} calls '${r.op}', which ${service}'s OpenAPI marks deprecated — the consumer should migrate off it`,\n });\n }\n } else if ((r.title ?? \"\").toLowerCase().startsWith(\"call\")) {\n findings.push({\n severity: \"warn\",\n code: \"spine.op-link-missing\",\n message: `${service}: landscape edge ${svcOf(r.source)} → ${service} (\"${r.title}\") has no operation link (metadata { op })`,\n });\n }\n }\n if (broken === 0 && checked > 0) {\n findings.push({\n severity: \"ok\",\n code: \"spine.resolved\",\n message: `${service}: landscape spine (${checked} inbound call(s) resolve to OpenAPI)`,\n });\n }\n }\n }\n\n return findings;\n}\n"]}
1
+ {"version":3,"file":"spine.js","sourceRoot":"","sources":["../../../../src/commands/validate/service/spine.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The inbound half of the C4↔API spine: every cross-system edge the living\n * landscape draws INTO this service must resolve to a real operation in its\n * OpenAPI.\n *\n * Separate from `./api.js` because the two grade different documents and the\n * fix lives in different files — a broken edge is the fleet map's mistake, a\n * missing operation is the contract's — but it may conclude nothing without\n * `api.js`'s answer, which is why `ServiceContract` is a parameter rather than\n * a second read of openapi.yaml.\n *\n * The event-side mirror of this check is inside `./events.js`: there the\n * PRODUCER owns the contract and it lives in another repository, so the two\n * could not share a walk even though they share the shape.\n */\nimport { relative } from \"node:path\";\nimport { type DeclaredService } from \"../../../core/kernel/ids/service.js\";\nimport { type LoadedDoc } from \"../../../core/c4/likec4.js\";\nimport { type DocsDir } from \"../../../core/kernel/ids/dirs.js\";\nimport { type PathableService } from \"../../../core/kernel/ids/service.js\";\nimport { type Finding } from \"../../../core/vocabulary/report.js\";\nimport { errorText } from \"../../../core/c4/likec4.js\";\nimport { type ServiceContract } from \"./api.js\";\n\n/** What the service target hands the spine check. */\nexport interface Spine {\n service: PathableService;\n /** The docs root, so a project error's absolute path can be spelled repo-relative. */\n docsDir: DocsDir;\n /** The service directory, repo-relative and `/`-separated — the scope location beside the broken file. */\n treePath: string;\n /** `service` widened for `===` against the resolver's DOCUMENT text — see service.ts. */\n me: string;\n /** The living landscape, or null when the repo has none. */\n land: LoadedDoc | null;\n /** Its element→service resolver; null exactly when `land` is. */\n landSvcOf: ((id: string) => DeclaredService) | null;\n contract: ServiceContract;\n /** True when this run also emits a landscape target — see `ServiceCheck`. */\n landscapeReported?: boolean;\n /**\n * True when this service's `model.likec4` EXTENDS the map rather than\n * standing alone (`core/c4/service-model/shape.ts`).\n *\n * It changes one sentence and no verdict. An extending model is parsed inside\n * the `architecture/` project, so a map that does not parse takes the model\n * with it: there is no `c4.invalid` and no `c4.valid` on the service target,\n * and without this clause the report shows a service whose model is simply\n * never mentioned. Said here rather than as a finding of its own because it is\n * the same fact — one unreadable document — and a second code would be a\n * second thing to fix for one repair.\n */\n modelExtends?: boolean;\n}\n\nexport function spineFindings(spine: Spine): Finding[] {\n const { service, me, land, landSvcOf, contract } = spine;\n const findings: Finding[] = [];\n\n // Landscape spine: cross-system edges calling THIS service must resolve to a real\n // operation in its OpenAPI — the C4↔API contract, checked in the living landscape,\n // not only in feature mode. Catches dangling / de-linked op edges.\n if (land !== null) {\n if (land.errors.length > 0) {\n // A living landscape that does not parse disables the C4↔API spine check —\n // that is a broken source of truth, not a skippable detail.\n //\n // The parser's own output is attached ONCE per run, and not here when the\n // run has a landscape target to carry it. One syntax error in one file\n // becomes N copies of a dozen cascading diagnostics on a fleet of N\n // services — the report stops being readable at exactly the moment\n // somebody needs to read it, and the fix is one file either way.\n // Which DOCUMENT broke, spelled exactly as the fleet arm spells it\n // (`fleet/landscape.ts`): `land` is the whole `architecture/` project, so\n // the broken file is as often a `usecases/*.likec4` as the map itself,\n // and the filename used to be hardcoded here — an agent told to \"fix the\n // file the message names\" opened a document that parses (verification\n // 2026-09-04, W2).\n const broken = [\n ...new Set(land.errors.map((e) => e.sourceFsPath).filter((p): p is string => p !== undefined)),\n ]\n .map((abs) => relative(spine.docsDir, abs).split(/[\\\\/]/).join(\"/\"))\n .sort();\n const named = broken.length === 0 ? \"architecture/landscape.likec4\" : broken.join(\", \");\n // The verb agrees with the LIST, not with the count after it: `named` is a\n // comma-joined series once two documents broke, and \"a, b, c has 6\n // error(s)\" is a sentence a reader trips over — the same care `remedy()`\n // in model/diverged.ts takes over \"the kind differ\".\n const verb = broken.length > 1 ? \"have\" : \"has\";\n findings.push({\n severity: \"error\",\n code: \"spine.landscape-invalid\",\n subject: service,\n message:\n `${service}: ${named} ${verb} ${land.errors.length} error(s) — spine check impossible` +\n (spine.modelExtends === true\n ? \" — and model.likec4 extends it, so the model cannot be read either\"\n : \"\") +\n (spine.landscapeReported === true\n ? \"; the parser output is reported once, on the landscape target\"\n : \"\"),\n ...(spine.landscapeReported === true\n ? {}\n : {\n // Every line carries its own file once more than one document is\n // broken, the fleet arm's rule verbatim: a bare `L8:` across two\n // documents is unactionable.\n details: land.errors.map((e) =>\n e.sourceFsPath === undefined || broken.length < 2\n ? errorText(e)\n : `${relative(spine.docsDir, e.sourceFsPath).split(/[\\\\/]/).join(\"/\")} ${errorText(e)}`,\n ),\n }),\n // The broken document first, the service second: the finding is about a\n // file under `architecture/` and is FILED on this service, and without\n // the pair the payload carried only the service directory.\n locations: [\n ...(broken.length === 0 ? [] : [{ path: broken[0]!, role: \"primary\" as const }]),\n { path: spine.treePath, role: \"scope\" as const },\n ],\n });\n } else {\n // Which element IS this service is the binding's call, then a title that\n // names a real services/<id>/, and an edge into a modelled container\n // counts as an edge into its service — matching the exact id alone meant a\n // container edge left the spine without a word.\n const svcOf = landSvcOf!;\n const opset = new Set(contract.ops);\n let checked = 0;\n let broken = 0;\n for (const r of land.relationships) {\n if (svcOf(r.target) !== me) continue;\n if (r.op !== undefined) {\n // A contract that cannot be read — broken, or not there at all —\n // proves nothing about this edge, neither broken nor resolved.\n // Grading the edges against an empty operation set turned ONE root\n // cause (the file) into one `spine.op-undefined` per inbound edge, so\n // a service with twelve consumers reported twelve landscape defects\n // and never named the file. `openapi.invalid` / `service.no-openapi`\n // already did, from ./api.js; only op-link-missing (which never reads\n // the contract) stays live, and `checked` stays 0 so no false\n // spine.resolved is claimed either.\n if (contract.unreadable) continue;\n checked += 1;\n if (!opset.has(r.op)) {\n broken += 1;\n findings.push({\n severity: \"error\",\n code: \"spine.op-undefined\",\n message: `${service}: landscape edge ${svcOf(r.source)} → ${service} calls '${r.op}', not defined in ${service}'s OpenAPI`,\n });\n } else if (contract.deprecated.has(r.op)) {\n // The contract holds — the op is defined — but it is marked\n // `deprecated: true`: the consumer is standing on a contract being\n // retired, and should be migrating off it. Warn per inbound edge;\n // a deprecated op nobody calls raises no spine finding at all.\n findings.push({\n severity: \"warn\",\n code: \"spine.op-deprecated\",\n message: `${service}: landscape edge ${svcOf(r.source)} → ${service} calls '${r.op}', which ${service}'s OpenAPI marks deprecated — the consumer should migrate off it`,\n });\n }\n } else if ((r.title ?? \"\").toLowerCase().startsWith(\"call\")) {\n findings.push({\n severity: \"warn\",\n code: \"spine.op-link-missing\",\n message: `${service}: landscape edge ${svcOf(r.source)} → ${service} (\"${r.title}\") has no operation link (metadata { op })`,\n });\n }\n }\n if (broken === 0 && checked > 0) {\n findings.push({\n severity: \"ok\",\n code: \"spine.resolved\",\n message: `${service}: landscape spine (${checked} inbound call(s) resolve to OpenAPI)`,\n });\n }\n }\n }\n\n return findings;\n}\n"]}
@@ -0,0 +1,113 @@
1
+ /**
2
+ * The two tag grades of a SERVICE-LOCAL use case: `usecase.requirement-unresolved`
3
+ * with service-worded arms, and `usecase.capability-unresolved`'s placement arm.
4
+ *
5
+ * Both codes are REUSED from the fleet target rather than minted, under
6
+ * add-error-code's "prefer existing" rule: a caller acts the same way (fix the
7
+ * tag, or the `Requirement-ID:` line it names), and only the sentences differ.
8
+ * The fleet's own graders (`../../fleet/usecases/{requirement,capability}-tag.ts`)
9
+ * are not reused for the sentences, because every one of theirs names a
10
+ * capability document, and a service-local flow has none.
11
+ *
12
+ * WHY `#req-` IS SCOPED BY THE SERVICE. Inside `architecture/`, a `#req-` tag
13
+ * means nothing until a `#cap-` tag has said which capability document the
14
+ * `Requirement-ID` is unique inside. Beside a model there is no such question:
15
+ * the directory IS the scope, and the ids are the ones THIS service's `spec.md`
16
+ * and `arch.spec.md` declare — the flow keeps a container-level promise the way
17
+ * a fleet flow keeps a business one. The join is `resolveRequirementTags` with
18
+ * the service as its one-element scope, so the arms fall out mechanically and
19
+ * `unscoped` is unreachable.
20
+ *
21
+ * WHY `#cap-` IS REFUSED, NOT IGNORED AND NOT RESOLVED. A capability is
22
+ * realized by a hop sequence across the fleet map — "the only join that
23
+ * crosses services" — and `keptBy` reads the fleet project only. Ignoring the
24
+ * tag would leave `#cap-checkout` on a service flow green and silently unread;
25
+ * resolving it against the vocabulary would make validate see a claim no other
26
+ * surface sees. So it is refused by PLACEMENT, whether or not
27
+ * `architecture/capabilities.yaml` exists: the reason is where the tag sits,
28
+ * not what the vocabulary says. The view stays opted in and its steps are
29
+ * graded — either prefix is the opt-in, exactly as at fleet altitude.
30
+ */
31
+ import { CAP_TAG_PREFIX, REQ_TAG_PREFIX, resolveRequirementTags, tagSlug, } from "../../../../core/capabilities/usecase-join.js";
32
+ import { compareIds } from "../../../../core/repo/entries.js";
33
+ /** A `Requirement-ID` as an author must write it in a tag: `PAY.AUTH (#req-PAY-AUTH)`. */
34
+ function idAndTag(id) {
35
+ return `${id} (#${REQ_TAG_PREFIX}${tagSlug(id)})`;
36
+ }
37
+ /**
38
+ * The fix for one unresolved claim, in the service's own vocabulary. The same
39
+ * arms the fleet grade has, each sentence pointing at the document that
40
+ * carries the fix here — spec.md and arch.spec.md, never a capability.
41
+ */
42
+ function advice(claim, scope) {
43
+ const where = `${scope.service} (spec.md or arch.spec.md)`;
44
+ switch (claim.kind) {
45
+ case "unscoped":
46
+ // Unreachable: the scope is always exactly one service. A sentence
47
+ // rather than a throw, because this runs inside a validate run over
48
+ // somebody's fleet, and a broken invariant there is one finding, not a
49
+ // crash.
50
+ return "the flow could not be scoped to this service — report this: a service-local flow is always scoped to its own directory.";
51
+ case "undocumented":
52
+ return (`${scope.treePath}/ has no spec.md and no arch.spec.md, so it carries no requirements to satisfy. ` +
53
+ "Write the requirement first — a flow keeps a promise a document makes.");
54
+ case "empty":
55
+ return (`${scope.service}'s spec.md and arch.spec.md declare no \`Requirement-ID:\` at all — a #req- tag joins by ` +
56
+ "stable id, and a requirement without one cannot be named by a flow. Add `Requirement-ID: <id>` to the " +
57
+ "requirement this flow keeps.");
58
+ case "none":
59
+ return claim.close.length > 0
60
+ ? `no requirement of ${where} flattens to '${claim.slug}'. Did you mean: ${claim.close.map(idAndTag).join(", ")}?`
61
+ : `no requirement of ${where} flattens to '${claim.slug}' — check the \`Requirement-ID:\` lines in both documents.`;
62
+ default:
63
+ return (`${claim.ids.length} requirements of ${scope.service} flatten to '${claim.slug}' (${claim.ids.join(", ")}) — ` +
64
+ "a Requirement-ID is unique inside ONE document and this service has two, so nothing in the tag can say " +
65
+ "which promise the flow keeps. Rename one of them.");
66
+ }
67
+ }
68
+ /**
69
+ * One finding per `#req-` tag that does not resolve against the service's own
70
+ * ids — never one per view, for the fleet grade's reason: several `#req-` tags
71
+ * on one view are legal and normal, and a broken tag beside a working one must
72
+ * neither swallow the break nor lose the promise the view genuinely keeps.
73
+ */
74
+ function requirementFindings(view, scope, place) {
75
+ const findings = [];
76
+ for (const claim of resolveRequirementTags(view.tags, [scope.service], () => scope.requirementIds)) {
77
+ if (claim.kind === "resolved")
78
+ continue;
79
+ findings.push({
80
+ severity: "error",
81
+ code: "usecase.requirement-unresolved",
82
+ // The VIEW, not the tag, as at fleet altitude: a subject is what a reader
83
+ // goes and opens, and one broken use case counts once however many of
84
+ // its tags are wrong.
85
+ subject: view.id,
86
+ message: `${place} is tagged #${claim.tag}, and it does not resolve: ${advice(claim, scope)}`,
87
+ });
88
+ }
89
+ return findings;
90
+ }
91
+ /**
92
+ * One finding per `#cap-` tag the view carries — the placement refusal. Sorted
93
+ * and de-duplicated for `resolveCapabilityTags`' reason: nothing in loam has
94
+ * measured that LikeC4 preserves the author's tag order, and one breach earns
95
+ * one finding.
96
+ */
97
+ function capabilityFindings(view, place) {
98
+ const claimed = [...new Set(view.tags.filter((tag) => tag.startsWith(CAP_TAG_PREFIX)))].sort(compareIds);
99
+ return claimed.map((tag) => ({
100
+ severity: "error",
101
+ code: "usecase.capability-unresolved",
102
+ subject: view.id,
103
+ message: `${place} is tagged #${tag} inside this service's own project, where no capability can be claimed: ` +
104
+ "a capability is a fleet promise, and only a flow in architecture/usecases/ can keep one; a flow beside " +
105
+ "model.likec4 keeps THIS service's promises (#req-<Requirement-ID>). Drop the tag: the capability's promise " +
106
+ "is kept by the fleet flow that calls this service, and this flow is the hop sequence inside it.",
107
+ }));
108
+ }
109
+ /** Every tag finding one service-local view earns: the placement refusals first, then the requirement claims. */
110
+ export function serviceTagFindings(view, scope, place) {
111
+ return [...capabilityFindings(view, place), ...requirementFindings(view, scope, place)];
112
+ }
113
+ //# sourceMappingURL=tags.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tags.js","sourceRoot":"","sources":["../../../../../src/commands/validate/service/usecases/tags.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The two tag grades of a SERVICE-LOCAL use case: `usecase.requirement-unresolved`\n * with service-worded arms, and `usecase.capability-unresolved`'s placement arm.\n *\n * Both codes are REUSED from the fleet target rather than minted, under\n * add-error-code's \"prefer existing\" rule: a caller acts the same way (fix the\n * tag, or the `Requirement-ID:` line it names), and only the sentences differ.\n * The fleet's own graders (`../../fleet/usecases/{requirement,capability}-tag.ts`)\n * are not reused for the sentences, because every one of theirs names a\n * capability document, and a service-local flow has none.\n *\n * WHY `#req-` IS SCOPED BY THE SERVICE. Inside `architecture/`, a `#req-` tag\n * means nothing until a `#cap-` tag has said which capability document the\n * `Requirement-ID` is unique inside. Beside a model there is no such question:\n * the directory IS the scope, and the ids are the ones THIS service's `spec.md`\n * and `arch.spec.md` declare — the flow keeps a container-level promise the way\n * a fleet flow keeps a business one. The join is `resolveRequirementTags` with\n * the service as its one-element scope, so the arms fall out mechanically and\n * `unscoped` is unreachable.\n *\n * WHY `#cap-` IS REFUSED, NOT IGNORED AND NOT RESOLVED. A capability is\n * realized by a hop sequence across the fleet map — \"the only join that\n * crosses services\" — and `keptBy` reads the fleet project only. Ignoring the\n * tag would leave `#cap-checkout` on a service flow green and silently unread;\n * resolving it against the vocabulary would make validate see a claim no other\n * surface sees. So it is refused by PLACEMENT, whether or not\n * `architecture/capabilities.yaml` exists: the reason is where the tag sits,\n * not what the vocabulary says. The view stays opted in and its steps are\n * graded — either prefix is the opt-in, exactly as at fleet altitude.\n */\nimport {\n CAP_TAG_PREFIX,\n REQ_TAG_PREFIX,\n resolveRequirementTags,\n tagSlug,\n type RequirementClaim,\n} from \"../../../../core/capabilities/usecase-join.js\";\nimport type { ParsedView } from \"../../../../core/c4/parsed/dynamic-views.js\";\nimport { compareIds } from \"../../../../core/repo/entries.js\";\nimport type { Finding } from \"../../../../core/vocabulary/report.js\";\n\n/** What the tag grades resolve against: the service, and its own identified requirements. */\nexport interface ServiceTagScope {\n service: string;\n /** Repo-relative tree path of the service directory, for the sentences. */\n treePath: string;\n /**\n * Every `Requirement-ID` this service's spec.md (living, REMOVED skipped) and\n * arch.spec.md declare, or `undefined` when NEITHER document exists — the\n * same two answers `requirementsOf` gives one join over: no document at all,\n * against documents declaring no identified promise yet.\n */\n requirementIds: ReadonlySet<string> | undefined;\n}\n\n/** A `Requirement-ID` as an author must write it in a tag: `PAY.AUTH (#req-PAY-AUTH)`. */\nfunction idAndTag(id: string): string {\n return `${id} (#${REQ_TAG_PREFIX}${tagSlug(id)})`;\n}\n\n/**\n * The fix for one unresolved claim, in the service's own vocabulary. The same\n * arms the fleet grade has, each sentence pointing at the document that\n * carries the fix here — spec.md and arch.spec.md, never a capability.\n */\nfunction advice(claim: Exclude<RequirementClaim, { kind: \"resolved\" }>, scope: ServiceTagScope): string {\n const where = `${scope.service} (spec.md or arch.spec.md)`;\n switch (claim.kind) {\n case \"unscoped\":\n // Unreachable: the scope is always exactly one service. A sentence\n // rather than a throw, because this runs inside a validate run over\n // somebody's fleet, and a broken invariant there is one finding, not a\n // crash.\n return \"the flow could not be scoped to this service — report this: a service-local flow is always scoped to its own directory.\";\n case \"undocumented\":\n return (\n `${scope.treePath}/ has no spec.md and no arch.spec.md, so it carries no requirements to satisfy. ` +\n \"Write the requirement first — a flow keeps a promise a document makes.\"\n );\n case \"empty\":\n return (\n `${scope.service}'s spec.md and arch.spec.md declare no \\`Requirement-ID:\\` at all — a #req- tag joins by ` +\n \"stable id, and a requirement without one cannot be named by a flow. Add `Requirement-ID: <id>` to the \" +\n \"requirement this flow keeps.\"\n );\n case \"none\":\n return claim.close.length > 0\n ? `no requirement of ${where} flattens to '${claim.slug}'. Did you mean: ${claim.close.map(idAndTag).join(\", \")}?`\n : `no requirement of ${where} flattens to '${claim.slug}' — check the \\`Requirement-ID:\\` lines in both documents.`;\n default:\n return (\n `${claim.ids.length} requirements of ${scope.service} flatten to '${claim.slug}' (${claim.ids.join(\", \")}) — ` +\n \"a Requirement-ID is unique inside ONE document and this service has two, so nothing in the tag can say \" +\n \"which promise the flow keeps. Rename one of them.\"\n );\n }\n}\n\n/**\n * One finding per `#req-` tag that does not resolve against the service's own\n * ids — never one per view, for the fleet grade's reason: several `#req-` tags\n * on one view are legal and normal, and a broken tag beside a working one must\n * neither swallow the break nor lose the promise the view genuinely keeps.\n */\nfunction requirementFindings(view: ParsedView, scope: ServiceTagScope, place: string): Finding[] {\n const findings: Finding[] = [];\n for (const claim of resolveRequirementTags(view.tags, [scope.service], () => scope.requirementIds)) {\n if (claim.kind === \"resolved\") continue;\n findings.push({\n severity: \"error\",\n code: \"usecase.requirement-unresolved\",\n // The VIEW, not the tag, as at fleet altitude: a subject is what a reader\n // goes and opens, and one broken use case counts once however many of\n // its tags are wrong.\n subject: view.id,\n message: `${place} is tagged #${claim.tag}, and it does not resolve: ${advice(claim, scope)}`,\n });\n }\n return findings;\n}\n\n/**\n * One finding per `#cap-` tag the view carries — the placement refusal. Sorted\n * and de-duplicated for `resolveCapabilityTags`' reason: nothing in loam has\n * measured that LikeC4 preserves the author's tag order, and one breach earns\n * one finding.\n */\nfunction capabilityFindings(view: ParsedView, place: string): Finding[] {\n const claimed = [...new Set(view.tags.filter((tag) => tag.startsWith(CAP_TAG_PREFIX)))].sort(compareIds);\n return claimed.map((tag) => ({\n severity: \"error\",\n code: \"usecase.capability-unresolved\",\n subject: view.id,\n message:\n `${place} is tagged #${tag} inside this service's own project, where no capability can be claimed: ` +\n \"a capability is a fleet promise, and only a flow in architecture/usecases/ can keep one; a flow beside \" +\n \"model.likec4 keeps THIS service's promises (#req-<Requirement-ID>). Drop the tag: the capability's promise \" +\n \"is kept by the fleet flow that calls this service, and this flow is the hop sequence inside it.\",\n }));\n}\n\n/** Every tag finding one service-local view earns: the placement refusals first, then the requirement claims. */\nexport function serviceTagFindings(view: ParsedView, scope: ServiceTagScope, place: string): Finding[] {\n return [...capabilityFindings(view, place), ...requirementFindings(view, scope, place)];\n}\n"]}