@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
@@ -0,0 +1,163 @@
1
+ /**
2
+ * How a service's `model.likec4` is GRADED, once `c4/service-model/` has
3
+ * decided how to read it.
4
+ *
5
+ * A package of its own beside `../service.ts` because that module is the ORDER
6
+ * — it reads what more than one axis joins through and pushes findings in the
7
+ * sequence the report is read in — and this is one axis's verdicts. Splitting it
8
+ * out is what let the model gain three arms without the order module gaining
9
+ * any: the four states below are the loader's four, and each one has exactly one
10
+ * right answer here.
11
+ *
12
+ * - the map is unreadable (an extending model only): NOTHING. The model could
13
+ * not be read, so neither `c4.invalid` nor `c4.valid` is a claim anybody may
14
+ * make about it, and blaming N services for one broken document is the
15
+ * report. `spine.landscape-invalid` already fires per service and gains a
16
+ * clause saying the model went unread with it (`../spine.ts`).
17
+ * - errors: `c4.invalid`, exactly as before. For an extending model the details
18
+ * name the FILE as well as the line, because a duplicate declaration is
19
+ * reported against the map as well as the model, and a bare `L8:` then points
20
+ * at somebody else's text.
21
+ * - clean, and nothing in the map resolves to this directory:
22
+ * `landscape.service-unmodelled` on the SERVICE target. An extending model
23
+ * says what is inside an element the map binds; with no such element there is
24
+ * nothing to be inside, and every count below would be zero for a file full
25
+ * of architecture.
26
+ * - clean: `c4.valid`, and one `c4.element-unowned` per element the model
27
+ * declared outside its own.
28
+ */
29
+ import { readFile } from "node:fs/promises";
30
+ import { relative } from "node:path";
31
+ import { maskSource } from "../../../../core/c4/source-mask.js";
32
+ import { serviceResolver } from "../../../../core/c4/resolve/service.js";
33
+ import { ARTIFACT_FILES } from "../../../../core/repo/paths.js";
34
+ import { errorText } from "../../../../core/c4/likec4.js";
35
+ export async function modelFindings(grade) {
36
+ const { service, model } = grade;
37
+ // The map did not parse, so the model was never read. Silence here is the
38
+ // whole point: `spine.landscape-invalid` is the one finding, and it names the
39
+ // map rather than blaming a file nobody opened.
40
+ if (model.mapUnreadable)
41
+ return [];
42
+ if (model.doc.errors.length > 0) {
43
+ return [
44
+ {
45
+ severity: "error",
46
+ code: "c4.invalid",
47
+ message: `${service}: C4 model has ${model.doc.errors.length} error(s)`,
48
+ // The hint FIRST, the errors after it: `capDetails` keeps the first ten
49
+ // lines and drops the rest, and a model written in both shapes at once
50
+ // stops resolving every kind and every reference in the file — dozens of
51
+ // cascading errors — so appending the one line that diagnoses it put it
52
+ // past the cap in exactly the case it exists for (verification
53
+ // 2026-09-04). It is the diagnosis; the errors are the evidence.
54
+ details: [...(await extendHint(grade)), ...model.doc.errors.map((err) => detail(err, grade))],
55
+ },
56
+ ];
57
+ }
58
+ if (model.shape === "extending" && !mapBindsAnElement(grade)) {
59
+ return [
60
+ {
61
+ severity: "error",
62
+ code: "landscape.service-unmodelled",
63
+ subject: service,
64
+ message: `${service}: ${ARTIFACT_FILES.model} extends the fleet map, and no element in ` +
65
+ `architecture/landscape.likec4 resolves to ${grade.treePath}/ — nothing in the model can be graded ` +
66
+ `until the map binds one (metadata { service '${service}' })`,
67
+ },
68
+ ];
69
+ }
70
+ const findings = [
71
+ {
72
+ severity: "ok",
73
+ code: "c4.valid",
74
+ message: `${service}: C4 model valid (` +
75
+ (model.shape === "extending" ? "extends the fleet map — " : "") +
76
+ `${model.doc.elements.length} elements · ${model.doc.relationships.length} relationships)` +
77
+ // Outside the parentheses because the counts inside are facts about
78
+ // model.likec4 alone and the flows are read from the whole project.
79
+ (grade.gradedFlows === 0 ? "" : ` · ${grade.gradedFlows} tagged flow(s) graded`),
80
+ },
81
+ ];
82
+ for (const element of model.unowned) {
83
+ findings.push({
84
+ severity: "warn",
85
+ code: "c4.element-unowned",
86
+ subject: service,
87
+ message: `${service}: ${ARTIFACT_FILES.model} declares '${element.id}' (${element.kind}) outside this service's ` +
88
+ "own element — an extending model adds elements under the element that resolves to " +
89
+ `${service} only. Three remedies, and the FIRST is the one a store or a component usually ` +
90
+ // The nested arm led the other two from 2026-09-04: a private store's own
91
+ // remedy was missing here, so following what this printed moved a store
92
+ // to fleet level and earned `landscape.service-undocumented` (#01/W8).
93
+ `wants: a store or component this service owns goes INSIDE the \`extend <fqn> { }\` block, ` +
94
+ "where its id becomes `<fqn>.<name>`; a system this service reaches belongs in " +
95
+ "architecture/landscape.likec4, declared once; and another service's internals belong in " +
96
+ "that service's model",
97
+ });
98
+ }
99
+ return findings;
100
+ }
101
+ /**
102
+ * Does anything in the per-service project resolve to this service?
103
+ *
104
+ * The same question `sliceForService` asks to build `own`, asked again rather
105
+ * than carried on the `ServiceModel`: the slice publishes what a check READS
106
+ * (the elements, the edges, the strays), and "was the set empty" is a fact
107
+ * about the map's bindings rather than a fourth list. Resolved over the
108
+ * PROJECT's elements, because that is the index the slice used and a
109
+ * nearest-ancestor binding must win here exactly as it does there.
110
+ *
111
+ * A standalone model never reaches this: it declares its own elements and
112
+ * answers for them whether or not the map has heard of the directory — the
113
+ * fleet target's own `landscape.service-unmodelled` is where that is said.
114
+ */
115
+ function mapBindsAnElement(grade) {
116
+ const project = grade.model.project;
117
+ if (project === null)
118
+ return true;
119
+ const resolve = serviceResolver(project.elements, grade.known);
120
+ return project.elements.some((e) => resolve(e.id) === grade.service);
121
+ }
122
+ /**
123
+ * One parse error, spelled for the shape it came from.
124
+ *
125
+ * A standalone model is one file, so its errors keep the bare `L8: …` form they
126
+ * have always had — every fixture and every reader of that message is pinned to
127
+ * it. An extending model is a PROJECT, and LikeC4 reports a duplicate
128
+ * declaration against BOTH documents: without the file name the reader is sent
129
+ * to a line number in a file they were never told about, which is the exact
130
+ * failure `landscape.invalid` fixed one altitude up.
131
+ */
132
+ function detail(err, grade) {
133
+ if (grade.model.shape === "standalone" || err.sourceFsPath === undefined)
134
+ return errorText(err);
135
+ return `${relative(grade.docsDir, err.sourceFsPath).split(/[\\/]/).join("/")} ${errorText(err)}`;
136
+ }
137
+ /** An `extend` in a masked model — the bytes, not a parse, because the parse is what failed. */
138
+ const EXTENDS = /\bextend\s+[A-Za-z_][\w.-]*\s*\{/;
139
+ /**
140
+ * The one line that explains a model written in BOTH shapes at once.
141
+ *
142
+ * A file declaring an element kind is standalone (SCHEMA.md, "Two shapes of a
143
+ * service model"), so it is parsed alone — and every `extend <fqn>` in it then
144
+ * resolves nothing, because the fqn belongs to a map this load never opened.
145
+ * LikeC4 says `Could not resolve reference to Element named '<fqn>'`, which
146
+ * reads as a typo; nothing said which of the two lines the author has to drop
147
+ * (verification 2026-09-04, D10). Emitted only for the standalone shape, where
148
+ * the diagnosis is certain; an unreadable file adds nothing, since the loader
149
+ * already answered `standalone` for the same reason and `c4.invalid` names it.
150
+ */
151
+ async function extendHint(grade) {
152
+ if (grade.model.shape !== "standalone")
153
+ return [];
154
+ const text = await readFile(grade.modelPath, "utf8").catch(() => null);
155
+ if (text === null || !EXTENDS.test(maskSource(text).code))
156
+ return [];
157
+ return [
158
+ `${ARTIFACT_FILES.model} declares an element kind in \`specification { }\`, so it is read as the ` +
159
+ "standalone shape — parsed alone, where `extend` resolves nothing. Drop the kind declaration to " +
160
+ "extend the fleet map, or drop the `extend` and declare the elements here",
161
+ ];
162
+ }
163
+ //# sourceMappingURL=grade.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grade.js","sourceRoot":"","sources":["../../../../../src/commands/validate/service/model/grade.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * How a service's `model.likec4` is GRADED, once `c4/service-model/` has\n * decided how to read it.\n *\n * A package of its own beside `../service.ts` because that module is the ORDER\n * — it reads what more than one axis joins through and pushes findings in the\n * sequence the report is read in — and this is one axis's verdicts. Splitting it\n * out is what let the model gain three arms without the order module gaining\n * any: the four states below are the loader's four, and each one has exactly one\n * right answer here.\n *\n * - the map is unreadable (an extending model only): NOTHING. The model could\n * not be read, so neither `c4.invalid` nor `c4.valid` is a claim anybody may\n * make about it, and blaming N services for one broken document is the\n * report. `spine.landscape-invalid` already fires per service and gains a\n * clause saying the model went unread with it (`../spine.ts`).\n * - errors: `c4.invalid`, exactly as before. For an extending model the details\n * name the FILE as well as the line, because a duplicate declaration is\n * reported against the map as well as the model, and a bare `L8:` then points\n * at somebody else's text.\n * - clean, and nothing in the map resolves to this directory:\n * `landscape.service-unmodelled` on the SERVICE target. An extending model\n * says what is inside an element the map binds; with no such element there is\n * nothing to be inside, and every count below would be zero for a file full\n * of architecture.\n * - clean: `c4.valid`, and one `c4.element-unowned` per element the model\n * declared outside its own.\n */\nimport { readFile } from \"node:fs/promises\";\nimport { relative } from \"node:path\";\nimport { maskSource } from \"../../../../core/c4/source-mask.js\";\nimport type { ServiceModel } from \"../../../../core/c4/service-model/load.js\";\nimport { serviceResolver } from \"../../../../core/c4/resolve/service.js\";\nimport type { LikeC4Error } from \"../../../../core/c4/likec4.js\";\nimport type { DocsDir } from \"../../../../core/kernel/ids/dirs.js\";\nimport type { PathableService } from \"../../../../core/kernel/ids/service.js\";\nimport { ARTIFACT_FILES } from \"../../../../core/repo/paths.js\";\nimport type { Finding } from \"../../../../core/vocabulary/report.js\";\nimport { errorText } from \"../../../../core/c4/likec4.js\";\n\nexport interface ModelGrade {\n service: PathableService;\n /** Repo-relative, `/`-separated — the directory the map has to bind, named in the unmodelled arm. */\n treePath: string;\n docsDir: DocsDir;\n model: ServiceModel;\n /** Absolute path of `model.likec4` — read only to explain a parse error, see `extendHint`. */\n modelPath: string;\n /** Every service directory that exists — the resolver's positive evidence, exactly as the slice used it. */\n known: ReadonlySet<string>;\n /**\n * How many of this service's own `dynamic view`s carried a reserved tag and\n * were graded (`core/usecases/service/flows.ts`), counted by the caller\n * because the scan is the use-case axis's and this is the model's.\n *\n * It rides on `c4.valid` and nowhere else. A healthy intra-service flow used\n * to be invisible on every loam surface — the axis reports only what is wrong\n * with one — so a team that wrote the slot correctly could not tell it from a\n * team whose views were never read (verification 2026-09-04, D10).\n */\n gradedFlows: number;\n}\n\nexport async function modelFindings(grade: ModelGrade): Promise<Finding[]> {\n const { service, model } = grade;\n // The map did not parse, so the model was never read. Silence here is the\n // whole point: `spine.landscape-invalid` is the one finding, and it names the\n // map rather than blaming a file nobody opened.\n if (model.mapUnreadable) return [];\n\n if (model.doc.errors.length > 0) {\n return [\n {\n severity: \"error\",\n code: \"c4.invalid\",\n message: `${service}: C4 model has ${model.doc.errors.length} error(s)`,\n // The hint FIRST, the errors after it: `capDetails` keeps the first ten\n // lines and drops the rest, and a model written in both shapes at once\n // stops resolving every kind and every reference in the file — dozens of\n // cascading errors — so appending the one line that diagnoses it put it\n // past the cap in exactly the case it exists for (verification\n // 2026-09-04). It is the diagnosis; the errors are the evidence.\n details: [...(await extendHint(grade)), ...model.doc.errors.map((err) => detail(err, grade))],\n },\n ];\n }\n\n if (model.shape === \"extending\" && !mapBindsAnElement(grade)) {\n return [\n {\n severity: \"error\",\n code: \"landscape.service-unmodelled\",\n subject: service,\n message:\n `${service}: ${ARTIFACT_FILES.model} extends the fleet map, and no element in ` +\n `architecture/landscape.likec4 resolves to ${grade.treePath}/ — nothing in the model can be graded ` +\n `until the map binds one (metadata { service '${service}' })`,\n },\n ];\n }\n\n const findings: Finding[] = [\n {\n severity: \"ok\",\n code: \"c4.valid\",\n message:\n `${service}: C4 model valid (` +\n (model.shape === \"extending\" ? \"extends the fleet map — \" : \"\") +\n `${model.doc.elements.length} elements · ${model.doc.relationships.length} relationships)` +\n // Outside the parentheses because the counts inside are facts about\n // model.likec4 alone and the flows are read from the whole project.\n (grade.gradedFlows === 0 ? \"\" : ` · ${grade.gradedFlows} tagged flow(s) graded`),\n },\n ];\n for (const element of model.unowned) {\n findings.push({\n severity: \"warn\",\n code: \"c4.element-unowned\",\n subject: service,\n message:\n `${service}: ${ARTIFACT_FILES.model} declares '${element.id}' (${element.kind}) outside this service's ` +\n \"own element — an extending model adds elements under the element that resolves to \" +\n `${service} only. Three remedies, and the FIRST is the one a store or a component usually ` +\n // The nested arm led the other two from 2026-09-04: a private store's own\n // remedy was missing here, so following what this printed moved a store\n // to fleet level and earned `landscape.service-undocumented` (#01/W8).\n `wants: a store or component this service owns goes INSIDE the \\`extend <fqn> { }\\` block, ` +\n \"where its id becomes `<fqn>.<name>`; a system this service reaches belongs in \" +\n \"architecture/landscape.likec4, declared once; and another service's internals belong in \" +\n \"that service's model\",\n });\n }\n return findings;\n}\n\n/**\n * Does anything in the per-service project resolve to this service?\n *\n * The same question `sliceForService` asks to build `own`, asked again rather\n * than carried on the `ServiceModel`: the slice publishes what a check READS\n * (the elements, the edges, the strays), and \"was the set empty\" is a fact\n * about the map's bindings rather than a fourth list. Resolved over the\n * PROJECT's elements, because that is the index the slice used and a\n * nearest-ancestor binding must win here exactly as it does there.\n *\n * A standalone model never reaches this: it declares its own elements and\n * answers for them whether or not the map has heard of the directory — the\n * fleet target's own `landscape.service-unmodelled` is where that is said.\n */\nfunction mapBindsAnElement(grade: ModelGrade): boolean {\n const project = grade.model.project;\n if (project === null) return true;\n const resolve = serviceResolver(project.elements, grade.known);\n return project.elements.some((e) => resolve(e.id) === (grade.service as string));\n}\n\n/**\n * One parse error, spelled for the shape it came from.\n *\n * A standalone model is one file, so its errors keep the bare `L8: …` form they\n * have always had — every fixture and every reader of that message is pinned to\n * it. An extending model is a PROJECT, and LikeC4 reports a duplicate\n * declaration against BOTH documents: without the file name the reader is sent\n * to a line number in a file they were never told about, which is the exact\n * failure `landscape.invalid` fixed one altitude up.\n */\nfunction detail(err: LikeC4Error, grade: ModelGrade): string {\n if (grade.model.shape === \"standalone\" || err.sourceFsPath === undefined) return errorText(err);\n return `${relative(grade.docsDir, err.sourceFsPath).split(/[\\\\/]/).join(\"/\")} ${errorText(err)}`;\n}\n\n/** An `extend` in a masked model — the bytes, not a parse, because the parse is what failed. */\nconst EXTENDS = /\\bextend\\s+[A-Za-z_][\\w.-]*\\s*\\{/;\n\n/**\n * The one line that explains a model written in BOTH shapes at once.\n *\n * A file declaring an element kind is standalone (SCHEMA.md, \"Two shapes of a\n * service model\"), so it is parsed alone — and every `extend <fqn>` in it then\n * resolves nothing, because the fqn belongs to a map this load never opened.\n * LikeC4 says `Could not resolve reference to Element named '<fqn>'`, which\n * reads as a typo; nothing said which of the two lines the author has to drop\n * (verification 2026-09-04, D10). Emitted only for the standalone shape, where\n * the diagnosis is certain; an unreadable file adds nothing, since the loader\n * already answered `standalone` for the same reason and `c4.invalid` names it.\n */\nasync function extendHint(grade: ModelGrade): Promise<string[]> {\n if (grade.model.shape !== \"standalone\") return [];\n const text = await readFile(grade.modelPath, \"utf8\").catch(() => null);\n if (text === null || !EXTENDS.test(maskSource(text).code)) return [];\n return [\n `${ARTIFACT_FILES.model} declares an element kind in \\`specification { }\\`, so it is read as the ` +\n \"standalone shape — parsed alone, where `extend` resolves nothing. Drop the kind declaration to \" +\n \"extend the fleet map, or drop the `extend` and declare the elements here\",\n ];\n}\n"]}
@@ -11,7 +11,8 @@
11
11
  */
12
12
  import { existsSync } from "node:fs";
13
13
  import { relative } from "node:path";
14
- import { loadFile } from "../../../core/c4/likec4.js";
14
+ import {} from "../../../core/c4/likec4.js";
15
+ import {} from "../../../core/c4/service-model/load.js";
15
16
  import { serviceResolver } from "../../../core/c4/resolve/service.js";
16
17
  import {} from "../../../core/kernel/ids/service.js";
17
18
  import { landscapePath as landscapeFile, permissionsPath } from "../../../core/repo/paths.js";
@@ -32,7 +33,6 @@ import { gherkinRoot } from "../../../core/gherkin/stamp.js";
32
33
  import { UnsafePathError } from "../../../core/kernel/path-safety.js";
33
34
  import { interruptedCommitFinding } from "../../../core/staging/recovery/finding.js";
34
35
  import { FleetContext } from "../../../core/fleet-context.js";
35
- import { errorText } from "../checks/vocabulary.js";
36
36
  import { serviceLinkFindings } from "../links/corpus.js";
37
37
  import { sourceScopeFindings } from "../checks/sources.js";
38
38
  import { apiAxisFindings, generatedContractFindings } from "./api.js";
@@ -40,6 +40,10 @@ import { evidencePinFindings } from "./evidence-pins.js";
40
40
  import { spineFindings } from "./spine.js";
41
41
  import { eventAxisFindings } from "./events/events.js";
42
42
  import { archAxisFindings, readServiceSpecs } from "./specs.js";
43
+ import { modelFindings } from "./model/grade.js";
44
+ import { divergedFindings } from "./model/diverged.js";
45
+ import { requirementIdsOf, serviceUseCaseFindings } from "./usecases/usecases.js";
46
+ import { readServiceFlows } from "../../../core/usecases/service/flows.js";
43
47
  import { unknownDirectiveFindings } from "../../../core/document/grammar/directives.js";
44
48
  export async function validateService(check) {
45
49
  const { docsDir, service, repoDir, preloaded, gherkinDir, contracts, fleet } = check;
@@ -53,10 +57,13 @@ export async function validateService(check) {
53
57
  const me = service;
54
58
  const findings = [];
55
59
  const paths = await locateServicePaths(docsDir, service, fleet);
60
+ // Repo-relative, `/`-separated: the report's own scope, and the spelling
61
+ // every service-local use-case finding opens with.
62
+ const treePath = relative(docsDir, paths.dir).replaceAll("\\", "/");
56
63
  const report = {
57
64
  kind: "service",
58
65
  id: service,
59
- path: relative(docsDir, paths.dir).replaceAll("\\", "/"),
66
+ path: treePath,
60
67
  findings,
61
68
  };
62
69
  // A directory that does not exist is a different fact from a directory with
@@ -74,6 +81,25 @@ export async function validateService(check) {
74
81
  });
75
82
  return report;
76
83
  }
84
+ // Which service directories actually exist — the positive evidence
85
+ // `serviceOf` needs to resolve an edge drawn into a modelled CONTAINER
86
+ // (`paymentService.api`) back to the service that owns it. Without it every
87
+ // such edge resolves to the container's own title, i.e. to a service nobody
88
+ // has ever adopted, and drops out of the spine unnoticed.
89
+ //
90
+ // Read before the model rather than after it, because the model is now read
91
+ // through the same resolver: an extending model's own slice is exactly the
92
+ // elements this set makes resolvable.
93
+ const known = new Set((await listServices(docsDir, fleet)).map((s) => s.id));
94
+ /**
95
+ * The read index this target reads the fleet map and the model through.
96
+ *
97
+ * A single-service run may arrive without one, and it gets its own rather
98
+ * than a second reading rule: `serviceModel` needs the enumeration and a
99
+ * document memo to answer at all, and both live here. Request-scoped like
100
+ * every other `FleetContext` — created for this call, dropped with it.
101
+ */
102
+ const context = fleet ?? new FleetContext();
77
103
  // C4 model. Its absence is an error — this is where `adopt` comes in — but it
78
104
  // must NOT silence the rest of the gate stack: `loam archive` of a feature
79
105
  // introducing a new service creates exactly this state (spec.md, arch.spec.md,
@@ -85,6 +111,19 @@ export async function validateService(check) {
85
111
  const hasModel = existsSync(paths.model);
86
112
  let elements = [];
87
113
  let relationships = [];
114
+ // The model as its own SHAPE demands it be read — a file parsed alone, or a
115
+ // slice of the project it extends (`core/c4/service-model/load.ts`). Kept for
116
+ // the service's own use cases below, which need both its views and which
117
+ // shape produced them.
118
+ let model = null;
119
+ // The service's OWN use cases, scanned here rather than beside the grades it
120
+ // feeds: `c4.valid` reports how many were graded, and that count is the only
121
+ // surface a HEALTHY flow appears on. Null means no scan was made — the model
122
+ // does not parse (`c4.invalid` owns that, and a second load repeats its
123
+ // cascade under a use-case code) or its map does not (an extending model then
124
+ // comes back with no errors AND no model, and the flow project would be
125
+ // staged over the same broken map).
126
+ let flows = null;
88
127
  if (!hasModel) {
89
128
  findings.push({
90
129
  severity: "error",
@@ -94,40 +133,53 @@ export async function validateService(check) {
94
133
  });
95
134
  }
96
135
  else {
97
- const model = fleet === undefined ? await loadFile(paths.model) : await fleet.loadLikeC4(paths.model);
98
- elements = model.elements;
99
- relationships = model.relationships;
100
- if (model.errors.length > 0) {
101
- findings.push({
102
- severity: "error",
103
- code: "c4.invalid",
104
- message: `${service}: C4 model has ${model.errors.length} error(s)`,
105
- details: model.errors.map(errorText),
106
- });
107
- }
108
- else {
109
- findings.push({
110
- severity: "ok",
111
- code: "c4.valid",
112
- message: `${service}: C4 model valid (${elements.length} elements · ${relationships.length} relationships)`,
136
+ model = await context.serviceModel(docsDir, paths);
137
+ elements = model.doc.elements;
138
+ relationships = model.doc.relationships;
139
+ if (!model.mapUnreadable && model.doc.errors.length === 0) {
140
+ flows = await readServiceFlows({
141
+ paths,
142
+ model: model.doc,
143
+ known,
144
+ // An extending model's siblings resolve against the fleet map, so their
145
+ // project is the docs root's, never the service directory. The project
146
+ // the model was READ in travels with it because that is the scope of a
147
+ // view written inside model.likec4: `model.doc` is the service's own
148
+ // slice, and a hop backed by one of the map's edges is backed wherever
149
+ // the view sits.
150
+ ...(model.shape === "extending" && model.project !== null
151
+ ? { extending: { docsDir, project: model.project } }
152
+ : {}),
113
153
  });
114
154
  }
155
+ findings.push(...(await modelFindings({
156
+ service,
157
+ treePath,
158
+ docsDir,
159
+ model,
160
+ modelPath: paths.model,
161
+ known,
162
+ gradedFlows: flows?.kind === "read" ? flows.views.length : 0,
163
+ })));
115
164
  }
116
- // The living landscape, parsed at most once per run: under --all the caller
117
- // hands in the doc it already loaded, single-service runs load on demand. It
118
- // serves two checks below — the no-openapi grace and the spine.
119
- const land = preloaded ??
120
- (existsSync(landscapeFile(docsDir))
121
- ? fleet === undefined
122
- ? await loadFile(landscapeFile(docsDir))
123
- : await fleet.loadLikeC4(landscapeFile(docsDir))
124
- : null);
125
- // Which service directories actually exist — the positive evidence
126
- // `serviceOf` needs to resolve an edge drawn into a modelled CONTAINER
127
- // (`paymentService.api`) back to the service that owns it. Without it every
128
- // such edge resolves to the container's own title, i.e. to a service nobody
129
- // has ever adopted, and drops out of the spine unnoticed.
130
- const known = new Set((await listServices(docsDir, fleet)).map((s) => s.id));
165
+ // The fleet map as the PROJECT it is, in EVERY mode — landscape plus every
166
+ // `architecture/**/*.likec4` minus the generated views file. Single-service
167
+ // runs used to read `landscape.likec4` alone, which made three answers depend
168
+ // on which flag the caller happened to pass: a use-case file that does not
169
+ // parse was `spine.landscape-invalid` under `--all` and silence under
170
+ // `--service`, an extending model was readable under one and not the other,
171
+ // and `c4.declaration-diverged` would have compared against a different
172
+ // document. Under `--all` the caller hands in the doc it already loaded.
173
+ //
174
+ // Still gated on the landscape FILE existing: `land === null` means "this repo
175
+ // draws no fleet", and a repo with an `architecture/usecases/` and no map is
176
+ // that state, not a map that loaded empty.
177
+ const land = preloaded ?? (existsSync(landscapeFile(docsDir)) ? await context.architecture(docsDir) : null);
178
+ // Where a standalone model's copies of the map's elements have drifted. After
179
+ // `c4.valid` because it is a claim about a model that parsed, and silent for
180
+ // an extending model, which has no copy to diverge.
181
+ if (model !== null)
182
+ findings.push(...divergedFindings({ service, model, architecture: land, known }));
131
183
  // The living landscape's element→service resolver, container-aware and
132
184
  // memoized. Every question below that asks "does this edge point at me?"
133
185
  // asks it through this one function, so the no-openapi grace, the HTTP spine
@@ -149,11 +201,16 @@ export async function validateService(check) {
149
201
  findings.push(...api.findings);
150
202
  findings.push(...spineFindings({
151
203
  service,
204
+ docsDir,
205
+ treePath,
152
206
  me,
153
207
  land,
154
208
  landSvcOf,
155
209
  contract: api.contract,
156
210
  landscapeReported: check.landscapeReported,
211
+ // An unreadable map takes an extending model with it, and the spine
212
+ // finding is where that is said — see `Spine.modelExtends`.
213
+ modelExtends: model?.shape === "extending",
157
214
  }));
158
215
  findings.push(...(await eventAxisFindings({
159
216
  docsDir,
@@ -175,10 +232,29 @@ export async function validateService(check) {
175
232
  archReqs,
176
233
  elements,
177
234
  relationships,
235
+ // The one fact that tells "this service models nothing" apart from "this
236
+ // service's model could not be read": both arrive here as empty lists, and
237
+ // only the second one must silence `covers.unknown`.
238
+ mapUnreadable: model?.mapUnreadable === true,
178
239
  land,
179
240
  known,
180
241
  docsDir,
242
+ fleet: context,
181
243
  })));
244
+ // The service's own use cases, GRADED — scanned beside the model above. Here
245
+ // rather than on the fleet target because the hops name this service's
246
+ // containers, which only its project resolves; after the arch axis because a
247
+ // `#req-` tag resolves against the ids both spec documents declare.
248
+ if (flows !== null) {
249
+ findings.push(...serviceUseCaseFindings({
250
+ service,
251
+ treePath,
252
+ docsDir,
253
+ scan: flows,
254
+ services: known,
255
+ requirementIds: requirementIdsOf(paths, livingReqs, archReqs),
256
+ }));
257
+ }
182
258
  // The authorization axis. Both requirement documents are graded against one
183
259
  // fleet vocabulary — a permission is a fleet fact, and an arch requirement
184
260
  // gates on one exactly as a business requirement does. The capability axis
@@ -1 +1 @@
1
- {"version":3,"file":"service.js","sourceRoot":"","sources":["../../../../src/commands/validate/service/service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAuB,MAAM,4BAA4B,CAAC;AAC3E,OAAO,EAAE,eAAe,EAAE,MAAM,qCAAqC,CAAC;AACtE,OAAO,EAAwB,MAAM,qCAAqC,CAAC;AAC3E,OAAO,EAAE,aAAa,IAAI,aAAa,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAC9F,OAAO,EAAE,kBAAkB,EAAE,MAAM,sCAAsC,CAAC;AAC1E,OAAO,EAAE,cAAc,EAAE,MAAM,0CAA0C,CAAC;AAC1E,OAAO,EAAE,wBAAwB,EAAE,MAAM,4CAA4C,CAAC;AACtF,OAAO,EAAE,0BAA0B,EAAE,yBAAyB,EAAE,MAAM,wCAAwC,CAAC;AAC/G,OAAO,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,MAAM,iDAAiD,CAAC;AACjH,OAAO,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,MAAM,iCAAiC,CAAC;AAC9F,OAAO,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AACpE,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC1D,OAAO,EAAkB,MAAM,4BAA4B,CAAC;AAC5D,OAAO,EAAmC,MAAM,oCAAoC,CAAC;AACrF,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AACzE,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,qCAAqC,CAAC;AACtE,OAAO,EAAE,wBAAwB,EAAE,MAAM,2CAA2C,CAAC;AACrF,OAAO,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAC9D,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,yBAAyB,EAAE,MAAM,UAAU,CAAC;AACtE,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAChE,OAAO,EAAE,wBAAwB,EAAE,MAAM,8CAA8C,CAAC;AA4CxF,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,KAAmB;IACvD,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,KAAK,CAAC;IACrF,uEAAuE;IACvE,uEAAuE;IACvE,0EAA0E;IAC1E,4EAA4E;IAC5E,qEAAqE;IACrE,qFAAqF;IACrF,4DAA4D;IAC5D,MAAM,EAAE,GAAW,OAAO,CAAC;IAC3B,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,MAAM,KAAK,GAAG,MAAM,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;IAChE,MAAM,MAAM,GAAiB;QAC3B,IAAI,EAAE,SAAS;QACf,EAAE,EAAE,OAAO;QACX,IAAI,EAAE,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC;QACxD,QAAQ;KACT,CAAC;IAEF,4EAA4E;IAC5E,0EAA0E;IAC1E,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QAC3B,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,EAAE,CAAC,MAAM,YAAY,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACvF,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,iBAAiB;YACvB,OAAO,EACL,2BAA2B,KAAK,CAAC,GAAG,GAAG;gBACvC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC;oBACf,CAAC,CAAC,kBAAkB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;oBACvC,CAAC,CAAC,0CAA0C,CAAC;YACjD,IAAI,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE;SACxB,CAAC,CAAC;QACH,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,8EAA8E;IAC9E,2EAA2E;IAC3E,+EAA+E;IAC/E,0EAA0E;IAC1E,2EAA2E;IAC3E,6EAA6E;IAC7E,6EAA6E;IAC7E,qCAAqC;IACrC,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACzC,IAAI,QAAQ,GAAW,EAAE,CAAC;IAC1B,IAAI,aAAa,GAAU,EAAE,CAAC;IAC9B,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,kBAAkB;YACxB,OAAO,EAAE,kBAAkB,KAAK,CAAC,KAAK,6BAA6B,OAAO,UAAU;YACpF,IAAI,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE;SACxB,CAAC,CAAC;IACL,CAAC;SAAM,CAAC;QACN,MAAM,KAAK,GAAG,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACtG,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC;QAC1B,aAAa,GAAG,KAAK,CAAC,aAAa,CAAC;QACpC,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC5B,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,OAAO;gBACjB,IAAI,EAAE,YAAY;gBAClB,OAAO,EAAE,GAAG,OAAO,kBAAkB,KAAK,CAAC,MAAM,CAAC,MAAM,WAAW;gBACnE,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC;aACrC,CAAC,CAAC;QACL,CAAC;aAAM,CAAC;YACN,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,IAAI;gBACd,IAAI,EAAE,UAAU;gBAChB,OAAO,EAAE,GAAG,OAAO,qBAAqB,QAAQ,CAAC,MAAM,eAAe,aAAa,CAAC,MAAM,iBAAiB;aAC5G,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,4EAA4E;IAC5E,6EAA6E;IAC7E,gEAAgE;IAChE,MAAM,IAAI,GACR,SAAS;QACT,CAAC,UAAU,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;YACjC,CAAC,CAAC,KAAK,KAAK,SAAS;gBACnB,CAAC,CAAC,MAAM,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;gBACxC,CAAC,CAAC,MAAM,KAAK,CAAC,UAAU,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;YAClD,CAAC,CAAC,IAAI,CAAC,CAAC;IAEZ,mEAAmE;IACnE,uEAAuE;IACvE,4EAA4E;IAC5E,4EAA4E;IAC5E,0DAA0D;IAC1D,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,YAAY,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAG7E,uEAAuE;IACvE,yEAAyE;IACzE,6EAA6E;IAC7E,wEAAwE;IACxE,MAAM,SAAS,GAAG,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,eAAe,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;IAE/E,MAAM,KAAK,GAAG,MAAM,gBAAgB,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;IAChE,QAAQ,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAC;IACjC,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC;IAEvD,MAAM,GAAG,GAAG,MAAM,eAAe,CAAC;QAChC,OAAO;QACP,EAAE;QACF,KAAK;QACL,IAAI;QACJ,UAAU;QACV,IAAI;QACJ,SAAS;QACT,KAAK;KACN,CAAC,CAAC;IACH,QAAQ,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,QAAQ,CAAC,CAAC;IAC/B,QAAQ,CAAC,IAAI,CACX,GAAG,aAAa,CAAC;QACf,OAAO;QACP,EAAE;QACF,IAAI;QACJ,SAAS;QACT,QAAQ,EAAE,GAAG,CAAC,QAAQ;QACtB,iBAAiB,EAAE,KAAK,CAAC,iBAAiB;KAC3C,CAAC,CACH,CAAC;IACF,QAAQ,CAAC,IAAI,CACX,GAAG,CAAC,MAAM,iBAAiB,CAAC;QAC1B,OAAO;QACP,OAAO;QACP,EAAE;QACF,KAAK;QACL,IAAI;QACJ,UAAU;QACV,QAAQ;QACR,IAAI;QACJ,SAAS;QACT,KAAK;QACL,KAAK;KACN,CAAC,CAAC,CACJ,CAAC;IACF,QAAQ,CAAC,IAAI,CACX,GAAG,CAAC,MAAM,gBAAgB,CAAC;QACzB,OAAO;QACP,KAAK;QACL,QAAQ;QACR,QAAQ;QACR,QAAQ;QACR,aAAa;QACb,IAAI;QACJ,KAAK;QACL,OAAO;KACR,CAAC,CAAC,CACJ,CAAC;IAEF,4EAA4E;IAC5E,2EAA2E;IAC3E,2EAA2E;IAC3E,0EAA0E;IAC1E,+CAA+C;IAC/C,MAAM,UAAU,GAAG,MAAM,cAAc,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC;IAClE,MAAM,YAAY,GAChB,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,wBAAwB,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;IACpG,4EAA4E;IAC5E,6EAA6E;IAC7E,2EAA2E;IAC3E,wEAAwE;IACxE,MAAM,cAAc,GAAG,MAAM,0BAA0B,CACrD,YAAY,EACZ,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,MAAM,wBAAwB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAC3H,CAAC;IACF,KAAK,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI;QAC7B,CAAC,SAAS,EAAE,IAAI,CAAC;QACjB,CAAC,cAAc,EAAE,QAAQ,CAAC;KAClB,EAAE,CAAC;QACX,MAAM,MAAM,GAAG,EAAE,KAAK,EAAE,GAAG,OAAO,KAAK,KAAK,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;QACnE,QAAQ,CAAC,IAAI,CAAC,GAAG,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC;QACvE,QAAQ,CAAC,IAAI,CAAC,GAAG,yBAAyB,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC;QAC3E,QAAQ,CAAC,IAAI,CAAC,GAAG,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC;QAC3E,QAAQ,CAAC,IAAI,CAAC,GAAG,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC;QACzE,2EAA2E;QAC3E,yEAAyE;QACzE,sEAAsE;QACtE,QAAQ,CAAC,IAAI,CAAC,GAAG,wBAAwB,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;IAC9D,CAAC;IAED,0EAA0E;IAC1E,+EAA+E;IAC/E,yEAAyE;IACzE,2DAA2D;IAC3D,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,mBAAmB,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IAE3F,4EAA4E;IAC5E,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;IAClF,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,mBAAmB,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IAChF,0EAA0E;IAC1E,4EAA4E;IAC5E,4EAA4E;IAC5E,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,yBAAyB,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IAC3G,CAAC;IAED,6EAA6E;IAC7E,sEAAsE;IACtE,+EAA+E;IAC/E,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,eAAe,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;IAE5F,2EAA2E;IAC3E,0EAA0E;IAC1E,4EAA4E;IAC5E,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,mBAAmB,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IAEhF,4EAA4E;IAC5E,yEAAyE;IACzE,2EAA2E;IAC3E,0EAA0E;IAC1E,0EAA0E;IAC1E,sBAAsB;IACtB,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,IAAI,CAAC;YACH,MAAM,WAAW,GAAG,MAAM,wBAAwB,CAAC,WAAW,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC;YACrF,IAAI,WAAW,KAAK,IAAI;gBAAE,QAAQ,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;QAC1D,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,qEAAqE;YACrE,+DAA+D;YAC/D,IAAI,CAAC,CAAC,GAAG,YAAY,eAAe,CAAC;gBAAE,MAAM,GAAG,CAAC;QACnD,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["/**\n * The service target: one adopted service graded across every axis it owns.\n *\n * This module is the ORDER, not the checks. It reads what more than one axis\n * joins through — the C4 model, the living landscape and its resolver, the two\n * requirement documents — hands each of those to the axis that grades it, and\n * pushes the findings in the sequence the report is read in. The axes\n * themselves cannot see each other, which is the point: `./api.js` decides once\n * whether there is a contract to read, and `./spine.js` is handed the answer\n * instead of re-deriving it against a file it would open a second time.\n */\nimport { existsSync } from \"node:fs\";\nimport { relative } from \"node:path\";\nimport { loadFile, type Elem, type Rel } from \"../../../core/c4/likec4.js\";\nimport { serviceResolver } from \"../../../core/c4/resolve/service.js\";\nimport { type PathableService } from \"../../../core/kernel/ids/service.js\";\nimport { landscapePath as landscapeFile, permissionsPath } from \"../../../core/repo/paths.js\";\nimport { locateServicePaths } from \"../../../core/repo/service-target.js\";\nimport { readVocabulary } from \"../../../core/permissions/permissions.js\";\nimport { readCapabilityVocabulary } from \"../../../core/capabilities/capabilities.js\";\nimport { capabilityRequirementIndex, capabilityUnknownFindings } from \"../../../core/capabilities/findings.js\";\nimport { realizesStaleFindings, realizesUnknownFindings } from \"../../../core/capabilities/realizes/findings.js\";\nimport { parseRequirements, readRequirementsDocument } from \"../../../core/document/parse.js\";\nimport { requiresUnknownFindings } from \"../checks/requirements.js\";\nimport { listServices } from \"../../../core/repo/repo.js\";\nimport { type LoadedDoc } from \"../../../core/c4/likec4.js\";\nimport { type Finding, type TargetReport } from \"../../../core/vocabulary/report.js\";\nimport { closeIds } from \"../../../core/c4/arch.js\";\nimport { serviceProvenance } from \"../../../core/provenance/findings.js\";\nimport { gherkinFindings } from \"../../../core/gherkin/stale.js\";\nimport { gherkinRoot } from \"../../../core/gherkin/stamp.js\";\nimport { UnsafePathError } from \"../../../core/kernel/path-safety.js\";\nimport { interruptedCommitFinding } from \"../../../core/staging/recovery/finding.js\";\nimport { FleetContext } from \"../../../core/fleet-context.js\";\nimport { errorText } from \"../checks/vocabulary.js\";\nimport { serviceLinkFindings } from \"../links/corpus.js\";\nimport { sourceScopeFindings } from \"../checks/sources.js\";\nimport { apiAxisFindings, generatedContractFindings } from \"./api.js\";\nimport { evidencePinFindings } from \"./evidence-pins.js\";\nimport { spineFindings } from \"./spine.js\";\nimport { eventAxisFindings } from \"./events/events.js\";\nimport { archAxisFindings, readServiceSpecs } from \"./specs.js\";\nimport { unknownDirectiveFindings } from \"../../../core/document/grammar/directives.js\";\nimport type { DocsDir } from \"../../../core/kernel/ids/dirs.js\";\n\n/**\n * A service's absences are graded by what each one proves.\n *\n * `service.unknown` (error): the directory itself does not exist — the id is a\n * typo until proven otherwise, so the hint names ids that DO exist and never\n * `loam adopt`, which would faithfully document the misspelling.\n * `service.no-model` (error): the directory is real but the C4 center is not —\n * adopt is the right hint. Every check that does not read the model still runs\n * (spec coverage, the arch axis, provenance, gherkin staleness): archive\n * creates exactly this state for a new service, and those signals must not go\n * quiet there.\n * `service.no-spec` / `service.no-openapi` (warn): the adopt brief marks both\n * required, but a fleet mid-rollout legitimately has part-adopted services —\n * the absence must stay visible without gating CI for months. The openapi warn\n * keeps quiet when the landscape proves nobody calls an operation on this\n * service: a worker with no API is not missing one.\n * `api.ops-unlinked` (warn): an OpenAPI and requirements that never name each\n * other pass every cross-axis check vacuously — a repo migrated from OpenSpec\n * does exactly that by default, and vacuous is not the same as checked.\n */\ninterface ServiceCheck {\n docsDir: DocsDir;\n service: PathableService;\n /** The service's own repo, when loam is standing in it. Undefined from the docs repo. */\n repoDir?: string;\n /** The build outputs `loam.json` names, when this repo declares any. */\n contracts?: { openapi?: string };\n /** The living landscape under --all; undefined means \"load it if you need it\", null means \"there is none\". */\n preloaded?: LoadedDoc | null;\n gherkinDir?: string;\n fleet?: FleetContext;\n /**\n * True when this run ALSO emits a `landscape` target. It decides one thing:\n * whether a landscape parse error's details are repeated here. Under --all\n * they are not — the landscape target already carries them once, and N copies\n * of one parser's cascade is the whole report.\n */\n landscapeReported?: boolean;\n}\n\n\nexport async function validateService(check: ServiceCheck): Promise<TargetReport> {\n const { docsDir, service, repoDir, preloaded, gherkinDir, contracts, fleet } = check;\n // The spine and event joins below all ask the landscape's resolver one\n // question: does this edge's endpoint resolve into the directory being\n // validated? The resolver answers with DOCUMENT text and `service` is the\n // repository's own name — disjoint brands, so `===` between them is TS2367.\n // Widening to `string` is not a cast: membership questions cross the\n // provenance line through plain-string containers by design (kernel/ids/service.ts),\n // and nothing pathable ever comes back out of a comparison.\n const me: string = service;\n const findings: Finding[] = [];\n const paths = await locateServicePaths(docsDir, service, fleet);\n const report: TargetReport = {\n kind: \"service\",\n id: service,\n path: relative(docsDir, paths.dir).replaceAll(\"\\\\\", \"/\"),\n findings,\n };\n\n // A directory that does not exist is a different fact from a directory with\n // everything missing: validating a typo must say \"typo\", not \"unadopted\".\n if (!existsSync(paths.dir)) {\n const close = closeIds(service, (await listServices(docsDir, fleet)).map((s) => s.id));\n findings.push({\n severity: \"error\",\n code: \"service.unknown\",\n message:\n `No service directory at ${paths.dir}.` +\n (close.length > 0\n ? ` Did you mean: ${close.join(\", \")}?`\n : \" `loam list services` shows what exists.\"),\n text: { marker: false },\n });\n return report;\n }\n\n // C4 model. Its absence is an error — this is where `adopt` comes in — but it\n // must NOT silence the rest of the gate stack: `loam archive` of a feature\n // introducing a new service creates exactly this state (spec.md, arch.spec.md,\n // openapi.yaml, no model.likec4), and an early return here suspended arch\n // coverage, health.uncovered, provenance (content_digest included) and the\n // gherkin staleness chain for the very services vouch had just promised them\n // to. So the finding is emitted and the walk CONTINUES; only the checks that\n // read the model itself are guarded.\n const hasModel = existsSync(paths.model);\n let elements: Elem[] = [];\n let relationships: Rel[] = [];\n if (!hasModel) {\n findings.push({\n severity: \"error\",\n code: \"service.no-model\",\n message: `No C4 model at ${paths.model}. Run \\`loam adopt\\` for '${service}' first.`,\n text: { marker: false },\n });\n } else {\n const model = fleet === undefined ? await loadFile(paths.model) : await fleet.loadLikeC4(paths.model);\n elements = model.elements;\n relationships = model.relationships;\n if (model.errors.length > 0) {\n findings.push({\n severity: \"error\",\n code: \"c4.invalid\",\n message: `${service}: C4 model has ${model.errors.length} error(s)`,\n details: model.errors.map(errorText),\n });\n } else {\n findings.push({\n severity: \"ok\",\n code: \"c4.valid\",\n message: `${service}: C4 model valid (${elements.length} elements · ${relationships.length} relationships)`,\n });\n }\n }\n\n // The living landscape, parsed at most once per run: under --all the caller\n // hands in the doc it already loaded, single-service runs load on demand. It\n // serves two checks below — the no-openapi grace and the spine.\n const land =\n preloaded ??\n (existsSync(landscapeFile(docsDir))\n ? fleet === undefined\n ? await loadFile(landscapeFile(docsDir))\n : await fleet.loadLikeC4(landscapeFile(docsDir))\n : null);\n\n // Which service directories actually exist — the positive evidence\n // `serviceOf` needs to resolve an edge drawn into a modelled CONTAINER\n // (`paymentService.api`) back to the service that owns it. Without it every\n // such edge resolves to the container's own title, i.e. to a service nobody\n // has ever adopted, and drops out of the spine unnoticed.\n const known = new Set((await listServices(docsDir, fleet)).map((s) => s.id));\n\n\n // The living landscape's element→service resolver, container-aware and\n // memoized. Every question below that asks \"does this edge point at me?\"\n // asks it through this one function, so the no-openapi grace, the HTTP spine\n // and the event spine can never disagree about which edges are inbound.\n const landSvcOf = land === null ? null : serviceResolver(land.elements, known);\n\n const specs = await readServiceSpecs({ service, paths, fleet });\n findings.push(...specs.findings);\n const { reqs, livingReqs, archText, archReqs } = specs;\n\n const api = await apiAxisFindings({\n service,\n me,\n paths,\n reqs,\n livingReqs,\n land,\n landSvcOf,\n fleet,\n });\n findings.push(...api.findings);\n findings.push(\n ...spineFindings({\n service,\n me,\n land,\n landSvcOf,\n contract: api.contract,\n landscapeReported: check.landscapeReported,\n }),\n );\n findings.push(\n ...(await eventAxisFindings({\n docsDir,\n service,\n me,\n paths,\n reqs,\n livingReqs,\n archReqs,\n land,\n landSvcOf,\n known,\n fleet,\n })),\n );\n findings.push(\n ...(await archAxisFindings({\n service,\n paths,\n archText,\n archReqs,\n elements,\n relationships,\n land,\n known,\n docsDir,\n })),\n );\n\n // The authorization axis. Both requirement documents are graded against one\n // fleet vocabulary — a permission is a fleet fact, and an arch requirement\n // gates on one exactly as a business requirement does. The capability axis\n // rides the same loop against ITS fleet vocabulary; under --all the fleet\n // memo makes that one parse for the whole run.\n const vocabulary = await readVocabulary(permissionsPath(docsDir));\n const capabilities =\n fleet === undefined ? await readCapabilityVocabulary(docsDir) : await fleet.capabilities(docsDir);\n // The requirement-level index behind `Realizes:`. Built once for both axes,\n // and behind the same ladder `capabilityUnknownFindings` applies — an absent\n // or unreadable vocabulary means no reads and no grades at all, so a fleet\n // that has not adopted the axis pays nothing for a line it never wrote.\n const capabilityReqs = await capabilityRequirementIndex(\n capabilities,\n fleet === undefined ? async (p) => parseRequirements(await readRequirementsDocument(p)) : (p) => fleet.readRequirements(p),\n );\n for (const [label, docReqs] of [\n [\"spec.md\", reqs],\n [\"arch.spec.md\", archReqs],\n ] as const) {\n const target = { where: `${service}: ${label}`, subject: service };\n findings.push(...requiresUnknownFindings(docReqs, target, vocabulary));\n findings.push(...capabilityUnknownFindings(docReqs, target, capabilities));\n findings.push(...realizesUnknownFindings(docReqs, target, capabilityReqs));\n findings.push(...realizesStaleFindings(docReqs, target, capabilityReqs));\n // The grammar guard, last in the loop: it grades the KEYS the three checks\n // above resolve values from, so a line none of them saw is reported here\n // rather than staying invisible for having produced nothing to check.\n findings.push(...unknownDirectiveFindings(docReqs, target));\n }\n\n // The links this service's own documents write. Its place in the order is\n // deliberate: every axis above joins through a LINE loam defined, and this one\n // grades the join an author writes in prose — so it reads last among the\n // document checks and adds no input any of them depend on.\n findings.push(...(await serviceLinkFindings({ docsDir, subject: service, fleet }, paths)));\n\n // Provenance last: who vouched for this, and what code it was written from.\n findings.push(...(await serviceProvenance(docsDir, service, { repoDir, fleet })));\n findings.push(...(await sourceScopeFindings(docsDir, service, repoDir, fleet)));\n // The build's own contract against the committed copy. Service-repo only:\n // from the docs repo there is no build output to read, and the family stays\n // silent rather than guessing — the same rule `sources` provenance follows.\n if (repoDir !== undefined) {\n findings.push(...(await generatedContractFindings(service, repoDir, contracts?.openapi, paths.openapi)));\n }\n\n // The generated-gherkin freshness chain, service-repo-scoped like sources.*:\n // it needs the repo (the suite lives there), and it stays quiet until\n // <gherkinDir>/loam/ exists — a service that never generated has not opted in.\n findings.push(...(await gherkinFindings({ docsDir, service, repoDir, gherkinDir, fleet })));\n\n // The evidence-pin re-check, the same service-repo-scoped family: recorded\n // verification citations graded against THIS working tree, quiet from the\n // docs repo — sources.unverifiable-from-here already names that blind spot.\n findings.push(...(await evidencePinFindings(docsDir, service, repoDir, fleet)));\n\n // The one journal that does NOT live in the docs repo: gherkin commits into\n // the service repo's emission root. A half-committed suite graded by the\n // freshness chain above reads as merely stale — warn-severity, which never\n // gates — while doctor calls the same state a blocker. Same finding, same\n // code, led rather than appended, exactly as validate leads with the docs\n // repo's own journal.\n if (repoDir !== undefined) {\n try {\n const interrupted = await interruptedCommitFinding(gherkinRoot(repoDir, gherkinDir));\n if (interrupted !== null) findings.unshift(interrupted);\n } catch (err) {\n // An unsafe gherkinDir is `loam gherkin`'s refusal to make, with its\n // own sentence; grading declines to scan what it cannot trust.\n if (!(err instanceof UnsafePathError)) throw err;\n }\n }\n\n return report;\n}\n"]}
1
+ {"version":3,"file":"service.js","sourceRoot":"","sources":["../../../../src/commands/validate/service/service.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The service target: one adopted service graded across every axis it owns.\n *\n * This module is the ORDER, not the checks. It reads what more than one axis\n * joins through — the C4 model, the living landscape and its resolver, the two\n * requirement documents — hands each of those to the axis that grades it, and\n * pushes the findings in the sequence the report is read in. The axes\n * themselves cannot see each other, which is the point: `./api.js` decides once\n * whether there is a contract to read, and `./spine.js` is handed the answer\n * instead of re-deriving it against a file it would open a second time.\n */\nimport { existsSync } from \"node:fs\";\nimport { relative } from \"node:path\";\nimport { type Elem, type Rel } from \"../../../core/c4/likec4.js\";\nimport { type ServiceModel } from \"../../../core/c4/service-model/load.js\";\nimport { serviceResolver } from \"../../../core/c4/resolve/service.js\";\nimport { type PathableService } from \"../../../core/kernel/ids/service.js\";\nimport { landscapePath as landscapeFile, permissionsPath } from \"../../../core/repo/paths.js\";\nimport { locateServicePaths } from \"../../../core/repo/service-target.js\";\nimport { readVocabulary } from \"../../../core/permissions/permissions.js\";\nimport { readCapabilityVocabulary } from \"../../../core/capabilities/capabilities.js\";\nimport { capabilityRequirementIndex, capabilityUnknownFindings } from \"../../../core/capabilities/findings.js\";\nimport { realizesStaleFindings, realizesUnknownFindings } from \"../../../core/capabilities/realizes/findings.js\";\nimport { parseRequirements, readRequirementsDocument } from \"../../../core/document/parse.js\";\nimport { requiresUnknownFindings } from \"../checks/requirements.js\";\nimport { listServices } from \"../../../core/repo/repo.js\";\nimport { type LoadedDoc } from \"../../../core/c4/likec4.js\";\nimport { type Finding, type TargetReport } from \"../../../core/vocabulary/report.js\";\nimport { closeIds } from \"../../../core/c4/arch.js\";\nimport { serviceProvenance } from \"../../../core/provenance/findings.js\";\nimport { gherkinFindings } from \"../../../core/gherkin/stale.js\";\nimport { gherkinRoot } from \"../../../core/gherkin/stamp.js\";\nimport { UnsafePathError } from \"../../../core/kernel/path-safety.js\";\nimport { interruptedCommitFinding } from \"../../../core/staging/recovery/finding.js\";\nimport { FleetContext } from \"../../../core/fleet-context.js\";\nimport { serviceLinkFindings } from \"../links/corpus.js\";\nimport { sourceScopeFindings } from \"../checks/sources.js\";\nimport { apiAxisFindings, generatedContractFindings } from \"./api.js\";\nimport { evidencePinFindings } from \"./evidence-pins.js\";\nimport { spineFindings } from \"./spine.js\";\nimport { eventAxisFindings } from \"./events/events.js\";\nimport { archAxisFindings, readServiceSpecs } from \"./specs.js\";\nimport { modelFindings } from \"./model/grade.js\";\nimport { divergedFindings } from \"./model/diverged.js\";\nimport { requirementIdsOf, serviceUseCaseFindings } from \"./usecases/usecases.js\";\nimport { readServiceFlows, type ServiceFlowScan } from \"../../../core/usecases/service/flows.js\";\nimport { unknownDirectiveFindings } from \"../../../core/document/grammar/directives.js\";\nimport type { DocsDir } from \"../../../core/kernel/ids/dirs.js\";\n\n/**\n * A service's absences are graded by what each one proves.\n *\n * `service.unknown` (error): the directory itself does not exist — the id is a\n * typo until proven otherwise, so the hint names ids that DO exist and never\n * `loam adopt`, which would faithfully document the misspelling.\n * `service.no-model` (error): the directory is real but the C4 center is not —\n * adopt is the right hint. Every check that does not read the model still runs\n * (spec coverage, the arch axis, provenance, gherkin staleness): archive\n * creates exactly this state for a new service, and those signals must not go\n * quiet there.\n * `service.no-spec` / `service.no-openapi` (warn): the adopt brief marks both\n * required, but a fleet mid-rollout legitimately has part-adopted services —\n * the absence must stay visible without gating CI for months. The openapi warn\n * keeps quiet when the landscape proves nobody calls an operation on this\n * service: a worker with no API is not missing one.\n * `api.ops-unlinked` (warn): an OpenAPI and requirements that never name each\n * other pass every cross-axis check vacuously — a repo migrated from OpenSpec\n * does exactly that by default, and vacuous is not the same as checked.\n */\ninterface ServiceCheck {\n docsDir: DocsDir;\n service: PathableService;\n /** The service's own repo, when loam is standing in it. Undefined from the docs repo. */\n repoDir?: string;\n /** The build outputs `loam.json` names, when this repo declares any. */\n contracts?: { openapi?: string };\n /** The living landscape under --all; undefined means \"load it if you need it\", null means \"there is none\". */\n preloaded?: LoadedDoc | null;\n gherkinDir?: string;\n fleet?: FleetContext;\n /**\n * True when this run ALSO emits a `landscape` target. It decides one thing:\n * whether a landscape parse error's details are repeated here. Under --all\n * they are not — the landscape target already carries them once, and N copies\n * of one parser's cascade is the whole report.\n */\n landscapeReported?: boolean;\n}\n\n\nexport async function validateService(check: ServiceCheck): Promise<TargetReport> {\n const { docsDir, service, repoDir, preloaded, gherkinDir, contracts, fleet } = check;\n // The spine and event joins below all ask the landscape's resolver one\n // question: does this edge's endpoint resolve into the directory being\n // validated? The resolver answers with DOCUMENT text and `service` is the\n // repository's own name — disjoint brands, so `===` between them is TS2367.\n // Widening to `string` is not a cast: membership questions cross the\n // provenance line through plain-string containers by design (kernel/ids/service.ts),\n // and nothing pathable ever comes back out of a comparison.\n const me: string = service;\n const findings: Finding[] = [];\n const paths = await locateServicePaths(docsDir, service, fleet);\n // Repo-relative, `/`-separated: the report's own scope, and the spelling\n // every service-local use-case finding opens with.\n const treePath = relative(docsDir, paths.dir).replaceAll(\"\\\\\", \"/\");\n const report: TargetReport = {\n kind: \"service\",\n id: service,\n path: treePath,\n findings,\n };\n\n // A directory that does not exist is a different fact from a directory with\n // everything missing: validating a typo must say \"typo\", not \"unadopted\".\n if (!existsSync(paths.dir)) {\n const close = closeIds(service, (await listServices(docsDir, fleet)).map((s) => s.id));\n findings.push({\n severity: \"error\",\n code: \"service.unknown\",\n message:\n `No service directory at ${paths.dir}.` +\n (close.length > 0\n ? ` Did you mean: ${close.join(\", \")}?`\n : \" `loam list services` shows what exists.\"),\n text: { marker: false },\n });\n return report;\n }\n\n // Which service directories actually exist — the positive evidence\n // `serviceOf` needs to resolve an edge drawn into a modelled CONTAINER\n // (`paymentService.api`) back to the service that owns it. Without it every\n // such edge resolves to the container's own title, i.e. to a service nobody\n // has ever adopted, and drops out of the spine unnoticed.\n //\n // Read before the model rather than after it, because the model is now read\n // through the same resolver: an extending model's own slice is exactly the\n // elements this set makes resolvable.\n const known = new Set((await listServices(docsDir, fleet)).map((s) => s.id));\n\n /**\n * The read index this target reads the fleet map and the model through.\n *\n * A single-service run may arrive without one, and it gets its own rather\n * than a second reading rule: `serviceModel` needs the enumeration and a\n * document memo to answer at all, and both live here. Request-scoped like\n * every other `FleetContext` — created for this call, dropped with it.\n */\n const context = fleet ?? new FleetContext();\n\n // C4 model. Its absence is an error — this is where `adopt` comes in — but it\n // must NOT silence the rest of the gate stack: `loam archive` of a feature\n // introducing a new service creates exactly this state (spec.md, arch.spec.md,\n // openapi.yaml, no model.likec4), and an early return here suspended arch\n // coverage, health.uncovered, provenance (content_digest included) and the\n // gherkin staleness chain for the very services vouch had just promised them\n // to. So the finding is emitted and the walk CONTINUES; only the checks that\n // read the model itself are guarded.\n const hasModel = existsSync(paths.model);\n let elements: Elem[] = [];\n let relationships: Rel[] = [];\n // The model as its own SHAPE demands it be read — a file parsed alone, or a\n // slice of the project it extends (`core/c4/service-model/load.ts`). Kept for\n // the service's own use cases below, which need both its views and which\n // shape produced them.\n let model: ServiceModel | null = null;\n // The service's OWN use cases, scanned here rather than beside the grades it\n // feeds: `c4.valid` reports how many were graded, and that count is the only\n // surface a HEALTHY flow appears on. Null means no scan was made — the model\n // does not parse (`c4.invalid` owns that, and a second load repeats its\n // cascade under a use-case code) or its map does not (an extending model then\n // comes back with no errors AND no model, and the flow project would be\n // staged over the same broken map).\n let flows: ServiceFlowScan | null = null;\n if (!hasModel) {\n findings.push({\n severity: \"error\",\n code: \"service.no-model\",\n message: `No C4 model at ${paths.model}. Run \\`loam adopt\\` for '${service}' first.`,\n text: { marker: false },\n });\n } else {\n model = await context.serviceModel(docsDir, paths);\n elements = model.doc.elements;\n relationships = model.doc.relationships;\n if (!model.mapUnreadable && model.doc.errors.length === 0) {\n flows = await readServiceFlows({\n paths,\n model: model.doc,\n known,\n // An extending model's siblings resolve against the fleet map, so their\n // project is the docs root's, never the service directory. The project\n // the model was READ in travels with it because that is the scope of a\n // view written inside model.likec4: `model.doc` is the service's own\n // slice, and a hop backed by one of the map's edges is backed wherever\n // the view sits.\n ...(model.shape === \"extending\" && model.project !== null\n ? { extending: { docsDir, project: model.project } }\n : {}),\n });\n }\n findings.push(\n ...(await modelFindings({\n service,\n treePath,\n docsDir,\n model,\n modelPath: paths.model,\n known,\n gradedFlows: flows?.kind === \"read\" ? flows.views.length : 0,\n })),\n );\n }\n\n // The fleet map as the PROJECT it is, in EVERY mode — landscape plus every\n // `architecture/**/*.likec4` minus the generated views file. Single-service\n // runs used to read `landscape.likec4` alone, which made three answers depend\n // on which flag the caller happened to pass: a use-case file that does not\n // parse was `spine.landscape-invalid` under `--all` and silence under\n // `--service`, an extending model was readable under one and not the other,\n // and `c4.declaration-diverged` would have compared against a different\n // document. Under `--all` the caller hands in the doc it already loaded.\n //\n // Still gated on the landscape FILE existing: `land === null` means \"this repo\n // draws no fleet\", and a repo with an `architecture/usecases/` and no map is\n // that state, not a map that loaded empty.\n const land =\n preloaded ?? (existsSync(landscapeFile(docsDir)) ? await context.architecture(docsDir) : null);\n\n // Where a standalone model's copies of the map's elements have drifted. After\n // `c4.valid` because it is a claim about a model that parsed, and silent for\n // an extending model, which has no copy to diverge.\n if (model !== null) findings.push(...divergedFindings({ service, model, architecture: land, known }));\n\n\n // The living landscape's element→service resolver, container-aware and\n // memoized. Every question below that asks \"does this edge point at me?\"\n // asks it through this one function, so the no-openapi grace, the HTTP spine\n // and the event spine can never disagree about which edges are inbound.\n const landSvcOf = land === null ? null : serviceResolver(land.elements, known);\n\n const specs = await readServiceSpecs({ service, paths, fleet });\n findings.push(...specs.findings);\n const { reqs, livingReqs, archText, archReqs } = specs;\n\n const api = await apiAxisFindings({\n service,\n me,\n paths,\n reqs,\n livingReqs,\n land,\n landSvcOf,\n fleet,\n });\n findings.push(...api.findings);\n findings.push(\n ...spineFindings({\n service,\n docsDir,\n treePath,\n me,\n land,\n landSvcOf,\n contract: api.contract,\n landscapeReported: check.landscapeReported,\n // An unreadable map takes an extending model with it, and the spine\n // finding is where that is said — see `Spine.modelExtends`.\n modelExtends: model?.shape === \"extending\",\n }),\n );\n findings.push(\n ...(await eventAxisFindings({\n docsDir,\n service,\n me,\n paths,\n reqs,\n livingReqs,\n archReqs,\n land,\n landSvcOf,\n known,\n fleet,\n })),\n );\n findings.push(\n ...(await archAxisFindings({\n service,\n paths,\n archText,\n archReqs,\n elements,\n relationships,\n // The one fact that tells \"this service models nothing\" apart from \"this\n // service's model could not be read\": both arrive here as empty lists, and\n // only the second one must silence `covers.unknown`.\n mapUnreadable: model?.mapUnreadable === true,\n land,\n known,\n docsDir,\n fleet: context,\n })),\n );\n\n // The service's own use cases, GRADED — scanned beside the model above. Here\n // rather than on the fleet target because the hops name this service's\n // containers, which only its project resolves; after the arch axis because a\n // `#req-` tag resolves against the ids both spec documents declare.\n if (flows !== null) {\n findings.push(\n ...serviceUseCaseFindings({\n service,\n treePath,\n docsDir,\n scan: flows,\n services: known,\n requirementIds: requirementIdsOf(paths, livingReqs, archReqs),\n }),\n );\n }\n\n // The authorization axis. Both requirement documents are graded against one\n // fleet vocabulary — a permission is a fleet fact, and an arch requirement\n // gates on one exactly as a business requirement does. The capability axis\n // rides the same loop against ITS fleet vocabulary; under --all the fleet\n // memo makes that one parse for the whole run.\n const vocabulary = await readVocabulary(permissionsPath(docsDir));\n const capabilities =\n fleet === undefined ? await readCapabilityVocabulary(docsDir) : await fleet.capabilities(docsDir);\n // The requirement-level index behind `Realizes:`. Built once for both axes,\n // and behind the same ladder `capabilityUnknownFindings` applies — an absent\n // or unreadable vocabulary means no reads and no grades at all, so a fleet\n // that has not adopted the axis pays nothing for a line it never wrote.\n const capabilityReqs = await capabilityRequirementIndex(\n capabilities,\n fleet === undefined ? async (p) => parseRequirements(await readRequirementsDocument(p)) : (p) => fleet.readRequirements(p),\n );\n for (const [label, docReqs] of [\n [\"spec.md\", reqs],\n [\"arch.spec.md\", archReqs],\n ] as const) {\n const target = { where: `${service}: ${label}`, subject: service };\n findings.push(...requiresUnknownFindings(docReqs, target, vocabulary));\n findings.push(...capabilityUnknownFindings(docReqs, target, capabilities));\n findings.push(...realizesUnknownFindings(docReqs, target, capabilityReqs));\n findings.push(...realizesStaleFindings(docReqs, target, capabilityReqs));\n // The grammar guard, last in the loop: it grades the KEYS the three checks\n // above resolve values from, so a line none of them saw is reported here\n // rather than staying invisible for having produced nothing to check.\n findings.push(...unknownDirectiveFindings(docReqs, target));\n }\n\n // The links this service's own documents write. Its place in the order is\n // deliberate: every axis above joins through a LINE loam defined, and this one\n // grades the join an author writes in prose — so it reads last among the\n // document checks and adds no input any of them depend on.\n findings.push(...(await serviceLinkFindings({ docsDir, subject: service, fleet }, paths)));\n\n // Provenance last: who vouched for this, and what code it was written from.\n findings.push(...(await serviceProvenance(docsDir, service, { repoDir, fleet })));\n findings.push(...(await sourceScopeFindings(docsDir, service, repoDir, fleet)));\n // The build's own contract against the committed copy. Service-repo only:\n // from the docs repo there is no build output to read, and the family stays\n // silent rather than guessing — the same rule `sources` provenance follows.\n if (repoDir !== undefined) {\n findings.push(...(await generatedContractFindings(service, repoDir, contracts?.openapi, paths.openapi)));\n }\n\n // The generated-gherkin freshness chain, service-repo-scoped like sources.*:\n // it needs the repo (the suite lives there), and it stays quiet until\n // <gherkinDir>/loam/ exists — a service that never generated has not opted in.\n findings.push(...(await gherkinFindings({ docsDir, service, repoDir, gherkinDir, fleet })));\n\n // The evidence-pin re-check, the same service-repo-scoped family: recorded\n // verification citations graded against THIS working tree, quiet from the\n // docs repo — sources.unverifiable-from-here already names that blind spot.\n findings.push(...(await evidencePinFindings(docsDir, service, repoDir, fleet)));\n\n // The one journal that does NOT live in the docs repo: gherkin commits into\n // the service repo's emission root. A half-committed suite graded by the\n // freshness chain above reads as merely stale — warn-severity, which never\n // gates — while doctor calls the same state a blocker. Same finding, same\n // code, led rather than appended, exactly as validate leads with the docs\n // repo's own journal.\n if (repoDir !== undefined) {\n try {\n const interrupted = await interruptedCommitFinding(gherkinRoot(repoDir, gherkinDir));\n if (interrupted !== null) findings.unshift(interrupted);\n } catch (err) {\n // An unsafe gherkinDir is `loam gherkin`'s refusal to make, with its\n // own sentence; grading declines to scan what it cannot trust.\n if (!(err instanceof UnsafePathError)) throw err;\n }\n }\n\n return report;\n}\n"]}