@ybotok/loam 0.2.0-alpha.7 → 0.2.0-alpha.9

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 (648) hide show
  1. package/CHANGELOG.md +1315 -1
  2. package/COMPARISON.md +91 -3
  3. package/CONTRIBUTING.md +37 -0
  4. package/MIGRATING-from-OpenSpec.md +40 -22
  5. package/QUICKSTART.md +181 -9
  6. package/README.md +256 -57
  7. package/ROADMAP.md +83 -7
  8. package/SCHEMA.md +569 -98
  9. package/WORKFLOW.md +272 -62
  10. package/dist/cli.js +26 -8
  11. package/dist/cli.js.map +1 -1
  12. package/dist/commands/adopt/adopt.js +5 -10
  13. package/dist/commands/adopt/adopt.js.map +1 -1
  14. package/dist/commands/archive/archive.js +15 -18
  15. package/dist/commands/archive/archive.js.map +1 -1
  16. package/dist/commands/archive/plan/contracts/asyncapi.js +27 -41
  17. package/dist/commands/archive/plan/contracts/asyncapi.js.map +1 -1
  18. package/dist/commands/archive/plan/contracts/consumers.js +56 -0
  19. package/dist/commands/archive/plan/contracts/consumers.js.map +1 -0
  20. package/dist/commands/archive/plan/contracts/openapi.js +25 -42
  21. package/dist/commands/archive/plan/contracts/openapi.js.map +1 -1
  22. package/dist/commands/archive/plan/contracts/verdict-lines.js +41 -0
  23. package/dist/commands/archive/plan/contracts/verdict-lines.js.map +1 -0
  24. package/dist/commands/archive/plan/gate.js +3 -7
  25. package/dist/commands/archive/plan/gate.js.map +1 -1
  26. package/dist/commands/archive/plan/landscape.js +4 -4
  27. package/dist/commands/archive/plan/landscape.js.map +1 -1
  28. package/dist/commands/archive/plan/refusal.js +8 -18
  29. package/dist/commands/archive/plan/refusal.js.map +1 -1
  30. package/dist/commands/archive/plan/specs.js +134 -25
  31. package/dist/commands/archive/plan/specs.js.map +1 -1
  32. package/dist/commands/archive/plan/state.js +1 -0
  33. package/dist/commands/archive/plan/state.js.map +1 -1
  34. package/dist/commands/archive/run.js +33 -11
  35. package/dist/commands/archive/run.js.map +1 -1
  36. package/dist/commands/archive/shipped.js +49 -0
  37. package/dist/commands/archive/shipped.js.map +1 -0
  38. package/dist/commands/context/context.js +4 -7
  39. package/dist/commands/context/context.js.map +1 -1
  40. package/dist/commands/context/print.js +2 -2
  41. package/dist/commands/context/print.js.map +1 -1
  42. package/dist/commands/delta/delta.js +44 -12
  43. package/dist/commands/delta/delta.js.map +1 -1
  44. package/dist/commands/delta/print.js +1 -1
  45. package/dist/commands/delta/print.js.map +1 -1
  46. package/dist/commands/dependencies.js +3 -7
  47. package/dist/commands/dependencies.js.map +1 -1
  48. package/dist/commands/diff/diff.js +20 -27
  49. package/dist/commands/diff/diff.js.map +1 -1
  50. package/dist/commands/explore.js +4 -8
  51. package/dist/commands/explore.js.map +1 -1
  52. package/dist/commands/gate/gate.js +45 -8
  53. package/dist/commands/gate/gate.js.map +1 -1
  54. package/dist/commands/gate/print.js +15 -3
  55. package/dist/commands/gate/print.js.map +1 -1
  56. package/dist/commands/gherkin/commit.js +13 -54
  57. package/dist/commands/gherkin/commit.js.map +1 -1
  58. package/dist/commands/gherkin/gherkin.js +36 -20
  59. package/dist/commands/gherkin/gherkin.js.map +1 -1
  60. package/dist/commands/gherkin/reconcile.js +10 -13
  61. package/dist/commands/gherkin/reconcile.js.map +1 -1
  62. package/dist/commands/init/first-hour.js +44 -0
  63. package/dist/commands/init/first-hour.js.map +1 -1
  64. package/dist/commands/init/init.js +18 -22
  65. package/dist/commands/init/init.js.map +1 -1
  66. package/dist/commands/list/campaign/campaign.js +4 -6
  67. package/dist/commands/list/campaign/campaign.js.map +1 -1
  68. package/dist/commands/list/json.js +0 -4
  69. package/dist/commands/list/json.js.map +1 -1
  70. package/dist/commands/list/list.js +38 -23
  71. package/dist/commands/list/list.js.map +1 -1
  72. package/dist/commands/list/print.js +38 -6
  73. package/dist/commands/list/print.js.map +1 -1
  74. package/dist/commands/list/suites/suites.js +140 -0
  75. package/dist/commands/list/suites/suites.js.map +1 -0
  76. package/dist/commands/list/views.js +28 -33
  77. package/dist/commands/list/views.js.map +1 -1
  78. package/dist/commands/mcp/dispatch.js +91 -36
  79. package/dist/commands/mcp/dispatch.js.map +1 -1
  80. package/dist/commands/mcp/mcp.js +56 -15
  81. package/dist/commands/mcp/mcp.js.map +1 -1
  82. package/dist/commands/mcp/serve.js.map +1 -1
  83. package/dist/commands/migrate-openspec/materialize/active.js +27 -22
  84. package/dist/commands/migrate-openspec/materialize/active.js.map +1 -1
  85. package/dist/commands/migrate-openspec/materialize/stage.js +4 -3
  86. package/dist/commands/migrate-openspec/materialize/stage.js.map +1 -1
  87. package/dist/commands/migrate-openspec/materialize/target.js +2 -2
  88. package/dist/commands/migrate-openspec/materialize/target.js.map +1 -1
  89. package/dist/commands/migrate-openspec/openspec/decisions.js +9 -14
  90. package/dist/commands/migrate-openspec/openspec/decisions.js.map +1 -1
  91. package/dist/commands/migrate-openspec/openspec/error.js +7 -8
  92. package/dist/commands/migrate-openspec/openspec/error.js.map +1 -1
  93. package/dist/commands/migrate-openspec/openspec/mapping.js +1 -9
  94. package/dist/commands/migrate-openspec/openspec/mapping.js.map +1 -1
  95. package/dist/commands/migrate-openspec/openspec/paths.js +3 -6
  96. package/dist/commands/migrate-openspec/openspec/paths.js.map +1 -1
  97. package/dist/commands/new/capability-template.js +118 -0
  98. package/dist/commands/new/capability-template.js.map +1 -0
  99. package/dist/commands/new/commit.js +8 -22
  100. package/dist/commands/new/commit.js.map +1 -1
  101. package/dist/commands/new/modifies.js +206 -0
  102. package/dist/commands/new/modifies.js.map +1 -0
  103. package/dist/commands/new/new.js +59 -14
  104. package/dist/commands/new/new.js.map +1 -1
  105. package/dist/commands/new/templates.js +48 -124
  106. package/dist/commands/new/templates.js.map +1 -1
  107. package/dist/commands/open.js +3 -7
  108. package/dist/commands/open.js.map +1 -1
  109. package/dist/commands/policy/gate.js +27 -0
  110. package/dist/commands/policy/gate.js.map +1 -1
  111. package/dist/commands/rebase/plan.js +2 -2
  112. package/dist/commands/rebase/plan.js.map +1 -1
  113. package/dist/commands/rebase/rebase.js +60 -35
  114. package/dist/commands/rebase/rebase.js.map +1 -1
  115. package/dist/commands/seed/commit.js +21 -25
  116. package/dist/commands/seed/commit.js.map +1 -1
  117. package/dist/commands/seed/seed.js +6 -9
  118. package/dist/commands/seed/seed.js.map +1 -1
  119. package/dist/commands/show/show.js +3 -7
  120. package/dist/commands/show/show.js.map +1 -1
  121. package/dist/commands/status/print.js +13 -4
  122. package/dist/commands/status/print.js.map +1 -1
  123. package/dist/commands/status/status.js +3 -7
  124. package/dist/commands/status/status.js.map +1 -1
  125. package/dist/commands/steps/steps.js +3 -7
  126. package/dist/commands/steps/steps.js.map +1 -1
  127. package/dist/commands/subsystem/edit.js +3 -5
  128. package/dist/commands/subsystem/edit.js.map +1 -1
  129. package/dist/commands/subsystem/move.js +5 -8
  130. package/dist/commands/subsystem/move.js.map +1 -1
  131. package/dist/commands/subsystem/query.js +2 -4
  132. package/dist/commands/subsystem/query.js.map +1 -1
  133. package/dist/commands/subsystem/subsystem.js +4 -7
  134. package/dist/commands/subsystem/subsystem.js.map +1 -1
  135. package/dist/commands/subsystem/sync.js +14 -41
  136. package/dist/commands/subsystem/sync.js.map +1 -1
  137. package/dist/commands/subsystem/txn/txn.js +17 -38
  138. package/dist/commands/subsystem/txn/txn.js.map +1 -1
  139. package/dist/commands/subsystem/txn/views.js +32 -5
  140. package/dist/commands/subsystem/txn/views.js.map +1 -1
  141. package/dist/commands/tasks/render.js +116 -0
  142. package/dist/commands/tasks/render.js.map +1 -0
  143. package/dist/commands/tasks/tasks.js +113 -0
  144. package/dist/commands/tasks/tasks.js.map +1 -0
  145. package/dist/commands/unarchive/manifest.js +6 -9
  146. package/dist/commands/unarchive/manifest.js.map +1 -1
  147. package/dist/commands/unarchive/restore.js +17 -28
  148. package/dist/commands/unarchive/restore.js.map +1 -1
  149. package/dist/commands/unarchive/unarchive.js +22 -30
  150. package/dist/commands/unarchive/unarchive.js.map +1 -1
  151. package/dist/commands/validate/arch-coverage.js +6 -15
  152. package/dist/commands/validate/arch-coverage.js.map +1 -1
  153. package/dist/commands/validate/checks/fleet-shape.js +4 -49
  154. package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
  155. package/dist/commands/validate/checks/requirements.js +18 -8
  156. package/dist/commands/validate/checks/requirements.js.map +1 -1
  157. package/dist/commands/validate/checks/sources.js +6 -2
  158. package/dist/commands/validate/checks/sources.js.map +1 -1
  159. package/dist/commands/validate/feature.js +23 -19
  160. package/dist/commands/validate/feature.js.map +1 -1
  161. package/dist/commands/validate/fleet/landscape.js +23 -23
  162. package/dist/commands/validate/fleet/landscape.js.map +1 -1
  163. package/dist/commands/validate/fleet/map/attest.js +6 -10
  164. package/dist/commands/validate/fleet/map/attest.js.map +1 -1
  165. package/dist/commands/validate/fleet/map/bindings.js +1 -3
  166. package/dist/commands/validate/fleet/map/bindings.js.map +1 -1
  167. package/dist/commands/validate/fleet/map/usecases.js +5 -11
  168. package/dist/commands/validate/fleet/map/usecases.js.map +1 -1
  169. package/dist/commands/validate/fleet/obligations.js +1 -5
  170. package/dist/commands/validate/fleet/obligations.js.map +1 -1
  171. package/dist/commands/validate/fleet/scorecard/print.js +5 -1
  172. package/dist/commands/validate/fleet/scorecard/print.js.map +1 -1
  173. package/dist/commands/validate/fleet/scorecard/scorecard.js +17 -11
  174. package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -1
  175. package/dist/commands/validate/fleet/usecases/usecases.js +6 -4
  176. package/dist/commands/validate/fleet/usecases/usecases.js.map +1 -1
  177. package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -1
  178. package/dist/commands/validate/fleet/views/root-documents.js +8 -28
  179. package/dist/commands/validate/fleet/views/root-documents.js.map +1 -1
  180. package/dist/commands/validate/links/corpus.js +1 -4
  181. package/dist/commands/validate/links/corpus.js.map +1 -1
  182. package/dist/commands/validate/links/glossary.js.map +1 -1
  183. package/dist/commands/validate/report.js +5 -4
  184. package/dist/commands/validate/report.js.map +1 -1
  185. package/dist/commands/validate/scope.js +113 -0
  186. package/dist/commands/validate/scope.js.map +1 -0
  187. package/dist/commands/validate/service/api.js +16 -104
  188. package/dist/commands/validate/service/api.js.map +1 -1
  189. package/dist/commands/validate/service/contracts/generated.js +134 -0
  190. package/dist/commands/validate/service/contracts/generated.js.map +1 -0
  191. package/dist/commands/validate/service/events/events.js +1 -1
  192. package/dist/commands/validate/service/events/events.js.map +1 -1
  193. package/dist/commands/validate/service/events/message-producers.js +1 -1
  194. package/dist/commands/validate/service/events/message-producers.js.map +1 -1
  195. package/dist/commands/validate/service/evidence-pins.js +56 -10
  196. package/dist/commands/validate/service/evidence-pins.js.map +1 -1
  197. package/dist/commands/validate/service/service.js +16 -26
  198. package/dist/commands/validate/service/service.js.map +1 -1
  199. package/dist/commands/validate/service/specs.js +31 -38
  200. package/dist/commands/validate/service/specs.js.map +1 -1
  201. package/dist/commands/validate/service/spine.js +8 -11
  202. package/dist/commands/validate/service/spine.js.map +1 -1
  203. package/dist/commands/validate/service/usecases/usecases.js +3 -3
  204. package/dist/commands/validate/service/usecases/usecases.js.map +1 -1
  205. package/dist/commands/validate/validate.js +44 -26
  206. package/dist/commands/validate/validate.js.map +1 -1
  207. package/dist/commands/verify/cross/diff.js +2 -6
  208. package/dist/commands/verify/cross/diff.js.map +1 -1
  209. package/dist/commands/verify/evidence/contract.js +1 -1
  210. package/dist/commands/verify/evidence/contract.js.map +1 -1
  211. package/dist/commands/verify/evidence/read.js +12 -3
  212. package/dist/commands/verify/evidence/read.js.map +1 -1
  213. package/dist/commands/verify/frozen.js +45 -23
  214. package/dist/commands/verify/frozen.js.map +1 -1
  215. package/dist/commands/verify/record.js +69 -17
  216. package/dist/commands/verify/record.js.map +1 -1
  217. package/dist/commands/verify/report.js +27 -44
  218. package/dist/commands/verify/report.js.map +1 -1
  219. package/dist/commands/verify/results.js +112 -30
  220. package/dist/commands/verify/results.js.map +1 -1
  221. package/dist/commands/verify/verify.js +54 -51
  222. package/dist/commands/verify/verify.js.map +1 -1
  223. package/dist/commands/vouch/pack/pack.js +2 -2
  224. package/dist/commands/vouch/pack/pack.js.map +1 -1
  225. package/dist/commands/vouch/pack/print.js +1 -6
  226. package/dist/commands/vouch/pack/print.js.map +1 -1
  227. package/dist/commands/vouch/pack/sections.js +1 -2
  228. package/dist/commands/vouch/pack/sections.js.map +1 -1
  229. package/dist/commands/vouch/run.js +26 -81
  230. package/dist/commands/vouch/run.js.map +1 -1
  231. package/dist/commands/vouch/sample/plan.js +20 -34
  232. package/dist/commands/vouch/sample/plan.js.map +1 -1
  233. package/dist/commands/vouch/vet/verify.js +39 -7
  234. package/dist/commands/vouch/vet/verify.js.map +1 -1
  235. package/dist/commands/vouch/vouch.js +10 -17
  236. package/dist/commands/vouch/vouch.js.map +1 -1
  237. package/dist/core/agent/agents-md/command-map.js +56 -101
  238. package/dist/core/agent/agents-md/command-map.js.map +1 -1
  239. package/dist/core/agent/agents-md/map/explain.js +4 -3
  240. package/dist/core/agent/agents-md/map/explain.js.map +1 -1
  241. package/dist/core/agent/agents-md/map/lenses/context.js +4 -3
  242. package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -1
  243. package/dist/core/agent/agents-md/map/lenses/diff.js +33 -20
  244. package/dist/core/agent/agents-md/map/lenses/diff.js.map +1 -1
  245. package/dist/core/agent/agents-md/map/lenses/gate.js +13 -5
  246. package/dist/core/agent/agents-md/map/lenses/gate.js.map +1 -1
  247. package/dist/core/agent/agents-md/map/mcp.js +10 -7
  248. package/dist/core/agent/agents-md/map/mcp.js.map +1 -1
  249. package/dist/core/agent/agents-md/map/status.js +9 -3
  250. package/dist/core/agent/agents-md/map/status.js.map +1 -1
  251. package/dist/core/agent/agents-md/map/subsystem.js +4 -3
  252. package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
  253. package/dist/core/agent/agents-md/map/usecases.js +24 -6
  254. package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
  255. package/dist/core/agent/agents-md/refusals.js +32 -18
  256. package/dist/core/agent/agents-md/refusals.js.map +1 -1
  257. package/dist/core/agent/agents-md.js +9 -3
  258. package/dist/core/agent/agents-md.js.map +1 -1
  259. package/dist/core/agent/agents-stamp.js +10 -0
  260. package/dist/core/agent/agents-stamp.js.map +1 -1
  261. package/dist/core/agent/scaffold.js +10 -15
  262. package/dist/core/agent/scaffold.js.map +1 -1
  263. package/dist/core/agent/tools/dialects.js +1 -0
  264. package/dist/core/agent/tools/dialects.js.map +1 -1
  265. package/dist/core/agent/tools/registry.js +41 -76
  266. package/dist/core/agent/tools/registry.js.map +1 -1
  267. package/dist/core/agent/workflows/adopt.js +5 -3
  268. package/dist/core/agent/workflows/adopt.js.map +1 -1
  269. package/dist/core/agent/workflows/check.js +20 -16
  270. package/dist/core/agent/workflows/check.js.map +1 -1
  271. package/dist/core/agent/workflows/closing.js +54 -21
  272. package/dist/core/agent/workflows/closing.js.map +1 -1
  273. package/dist/core/agent/workflows/feature.js +79 -11
  274. package/dist/core/agent/workflows/feature.js.map +1 -1
  275. package/dist/core/agent/workflows/implement.js +91 -13
  276. package/dist/core/agent/workflows/implement.js.map +1 -1
  277. package/dist/core/agent/workflows/reference/authoring.js +3 -1
  278. package/dist/core/agent/workflows/reference/authoring.js.map +1 -1
  279. package/dist/core/agent/workflows/reference/codes.js +8 -6
  280. package/dist/core/agent/workflows/reference/codes.js.map +1 -1
  281. package/dist/core/agent/workflows/reference/done-check.js +37 -15
  282. package/dist/core/agent/workflows/reference/done-check.js.map +1 -1
  283. package/dist/core/agent/workflows/reference/reference.js.map +1 -1
  284. package/dist/core/agent/workflows/reference/spine.js +50 -18
  285. package/dist/core/agent/workflows/reference/spine.js.map +1 -1
  286. package/dist/core/asyncapi/baseline/plan.js +14 -81
  287. package/dist/core/asyncapi/baseline/plan.js.map +1 -1
  288. package/dist/core/asyncapi/digest.js +30 -25
  289. package/dist/core/asyncapi/digest.js.map +1 -1
  290. package/dist/core/asyncapi/merge/components.js +5 -4
  291. package/dist/core/asyncapi/merge/components.js.map +1 -1
  292. package/dist/core/asyncapi/merge/error.js +7 -2
  293. package/dist/core/asyncapi/merge/error.js.map +1 -1
  294. package/dist/core/asyncapi/merge/markers.js +30 -50
  295. package/dist/core/asyncapi/merge/markers.js.map +1 -1
  296. package/dist/core/asyncapi/merge/merge.js +31 -53
  297. package/dist/core/asyncapi/merge/merge.js.map +1 -1
  298. package/dist/core/asyncapi/merge/pin.js +10 -36
  299. package/dist/core/asyncapi/merge/pin.js.map +1 -1
  300. package/dist/core/asyncapi/model.js.map +1 -1
  301. package/dist/core/asyncapi/read.js +36 -12
  302. package/dist/core/asyncapi/read.js.map +1 -1
  303. package/dist/core/brief/brief.js +6 -18
  304. package/dist/core/brief/brief.js.map +1 -1
  305. package/dist/core/brief/landscape.js +8 -12
  306. package/dist/core/brief/landscape.js.map +1 -1
  307. package/dist/core/brief/map/extend.js +8 -5
  308. package/dist/core/brief/map/extend.js.map +1 -1
  309. package/dist/core/brief/map/owed.js +8 -26
  310. package/dist/core/brief/map/owed.js.map +1 -1
  311. package/dist/core/brief/targets.js +15 -5
  312. package/dist/core/brief/targets.js.map +1 -1
  313. package/dist/core/c4/likec4.js +147 -16
  314. package/dist/core/c4/likec4.js.map +1 -1
  315. package/dist/core/c4/locate/unlocated.js +150 -0
  316. package/dist/core/c4/locate/unlocated.js.map +1 -0
  317. package/dist/core/c4/parsed/values.js +54 -1
  318. package/dist/core/c4/parsed/values.js.map +1 -1
  319. package/dist/core/c4/prefetch/fleet.js +1 -9
  320. package/dist/core/c4/prefetch/fleet.js.map +1 -1
  321. package/dist/core/c4/project/documents.js +31 -6
  322. package/dist/core/c4/project/documents.js.map +1 -1
  323. package/dist/core/c4/project/load.js +6 -17
  324. package/dist/core/c4/project/load.js.map +1 -1
  325. package/dist/core/c4/resolve/external.js +51 -0
  326. package/dist/core/c4/resolve/external.js.map +1 -0
  327. package/dist/core/c4/resolve/service.js +25 -1
  328. package/dist/core/c4/resolve/service.js.map +1 -1
  329. package/dist/core/c4/seed/template.js +15 -1
  330. package/dist/core/c4/seed/template.js.map +1 -1
  331. package/dist/core/c4/service-model/shape.js +2 -22
  332. package/dist/core/c4/service-model/shape.js.map +1 -1
  333. package/dist/core/c4/source-mask.js +288 -4
  334. package/dist/core/c4/source-mask.js.map +1 -1
  335. package/dist/core/c4/source-scan.js +203 -147
  336. package/dist/core/c4/source-scan.js.map +1 -1
  337. package/dist/core/c4/splice/authored-source.js +19 -0
  338. package/dist/core/c4/splice/authored-source.js.map +1 -1
  339. package/dist/core/c4/splice/delta-blocks.js +5 -18
  340. package/dist/core/c4/splice/delta-blocks.js.map +1 -1
  341. package/dist/core/c4/splice/landscape-merge.js +13 -8
  342. package/dist/core/c4/splice/landscape-merge.js.map +1 -1
  343. package/dist/core/c4/splice/model/merge.js +10 -6
  344. package/dist/core/c4/splice/model/merge.js.map +1 -1
  345. package/dist/core/c4/splice/placement.js +108 -18
  346. package/dist/core/c4/splice/placement.js.map +1 -1
  347. package/dist/core/c4/workspace.js +13 -22
  348. package/dist/core/c4/workspace.js.map +1 -1
  349. package/dist/core/capabilities/capabilities.js +6 -6
  350. package/dist/core/capabilities/capabilities.js.map +1 -1
  351. package/dist/core/capabilities/delta/doc.js +7 -10
  352. package/dist/core/capabilities/delta/doc.js.map +1 -1
  353. package/dist/core/capabilities/findings.js +15 -20
  354. package/dist/core/capabilities/findings.js.map +1 -1
  355. package/dist/core/capabilities/realizes/findings.js +12 -19
  356. package/dist/core/capabilities/realizes/findings.js.map +1 -1
  357. package/dist/core/capabilities/realizes/join.js +0 -20
  358. package/dist/core/capabilities/realizes/join.js.map +1 -1
  359. package/dist/core/capabilities/rollup.js +6 -0
  360. package/dist/core/capabilities/rollup.js.map +1 -1
  361. package/dist/core/capabilities/tree.js +6 -28
  362. package/dist/core/capabilities/tree.js.map +1 -1
  363. package/dist/core/capabilities/usecase-join.js +3 -13
  364. package/dist/core/capabilities/usecase-join.js.map +1 -1
  365. package/dist/core/coherence/authoring/scaffold.js +11 -3
  366. package/dist/core/coherence/authoring/scaffold.js.map +1 -1
  367. package/dist/core/coherence/authoring/sentinels.js +2 -1
  368. package/dist/core/coherence/authoring/sentinels.js.map +1 -1
  369. package/dist/core/coherence/coherence.js +30 -16
  370. package/dist/core/coherence/coherence.js.map +1 -1
  371. package/dist/core/coherence/consumed/operations.js +70 -0
  372. package/dist/core/coherence/consumed/operations.js.map +1 -0
  373. package/dist/core/coherence/consumed/refusal.js +38 -0
  374. package/dist/core/coherence/consumed/refusal.js.map +1 -0
  375. package/dist/core/coherence/declared.js +60 -34
  376. package/dist/core/coherence/declared.js.map +1 -1
  377. package/dist/core/coherence/events/declared.js +44 -59
  378. package/dist/core/coherence/events/declared.js.map +1 -1
  379. package/dist/core/coherence/events/events.js +119 -8
  380. package/dist/core/coherence/events/events.js.map +1 -1
  381. package/dist/core/coherence/events/grades.js +31 -8
  382. package/dist/core/coherence/events/grades.js.map +1 -1
  383. package/dist/core/coherence/events/lookups.js +14 -82
  384. package/dist/core/coherence/events/lookups.js.map +1 -1
  385. package/dist/core/coherence/lookups.js +18 -78
  386. package/dist/core/coherence/lookups.js.map +1 -1
  387. package/dist/core/coherence/retired/archived.js +285 -0
  388. package/dist/core/coherence/retired/archived.js.map +1 -0
  389. package/dist/core/coherence/retired/event-pair.js +62 -0
  390. package/dist/core/coherence/retired/event-pair.js.map +1 -0
  391. package/dist/core/coherence/retired/merged.js +79 -0
  392. package/dist/core/coherence/retired/merged.js.map +1 -0
  393. package/dist/core/coherence/retired/retirement.js +49 -0
  394. package/dist/core/coherence/retired/retirement.js.map +1 -0
  395. package/dist/core/coherence/retired/revival/arrivals.js +131 -0
  396. package/dist/core/coherence/retired/revival/arrivals.js.map +1 -0
  397. package/dist/core/coherence/retired/revival/revived.js +297 -0
  398. package/dist/core/coherence/retired/revival/revived.js.map +1 -0
  399. package/dist/core/consumers/after.js +113 -0
  400. package/dist/core/consumers/after.js.map +1 -0
  401. package/dist/core/consumers/consumers.js +296 -0
  402. package/dist/core/consumers/consumers.js.map +1 -0
  403. package/dist/core/consumers/current.js +211 -0
  404. package/dist/core/consumers/current.js.map +1 -0
  405. package/dist/core/consumers/scan.js +46 -0
  406. package/dist/core/consumers/scan.js.map +1 -0
  407. package/dist/core/consumers/state.js +27 -0
  408. package/dist/core/consumers/state.js.map +1 -0
  409. package/dist/core/delta/delta.js +1 -1
  410. package/dist/core/delta/delta.js.map +1 -1
  411. package/dist/core/deployment/delta.js +3 -6
  412. package/dist/core/deployment/delta.js.map +1 -1
  413. package/dist/core/diff/base-git.js +94 -12
  414. package/dist/core/diff/base-git.js.map +1 -1
  415. package/dist/core/diff/base-state.js +28 -16
  416. package/dist/core/diff/base-state.js.map +1 -1
  417. package/dist/core/diff/joins.js +136 -0
  418. package/dist/core/diff/joins.js.map +1 -0
  419. package/dist/core/diff/scope/changed-paths.js +22 -6
  420. package/dist/core/diff/scope/changed-paths.js.map +1 -1
  421. package/dist/core/diff/scope/edges.js +82 -0
  422. package/dist/core/diff/scope/edges.js.map +1 -0
  423. package/dist/core/diff/semantic.js +100 -31
  424. package/dist/core/diff/semantic.js.map +1 -1
  425. package/dist/core/doctor/agents.js +34 -18
  426. package/dist/core/doctor/agents.js.map +1 -1
  427. package/dist/core/doctor/report.js.map +1 -1
  428. package/dist/core/document/apply.js +10 -2
  429. package/dist/core/document/apply.js.map +1 -1
  430. package/dist/core/document/frontmatter.js +55 -0
  431. package/dist/core/document/frontmatter.js.map +1 -1
  432. package/dist/core/document/parse.js +29 -6
  433. package/dist/core/document/parse.js.map +1 -1
  434. package/dist/core/document/scenarios.js +17 -0
  435. package/dist/core/document/scenarios.js.map +1 -1
  436. package/dist/core/document/spec.js +4 -11
  437. package/dist/core/document/spec.js.map +1 -1
  438. package/dist/core/envelope/config.js +10 -0
  439. package/dist/core/envelope/config.js.map +1 -1
  440. package/dist/core/envelope/json.js +34 -0
  441. package/dist/core/envelope/json.js.map +1 -1
  442. package/dist/core/explain/families.js +17 -12
  443. package/dist/core/explain/families.js.map +1 -1
  444. package/dist/core/explain/refusals.js +3 -3
  445. package/dist/core/explain/refusals.js.map +1 -1
  446. package/dist/core/explain/terms.js +18 -9
  447. package/dist/core/explain/terms.js.map +1 -1
  448. package/dist/core/explore/explore.js +2 -7
  449. package/dist/core/explore/explore.js.map +1 -1
  450. package/dist/core/fleet-context.js +34 -95
  451. package/dist/core/fleet-context.js.map +1 -1
  452. package/dist/core/gate/checks.js +5 -2
  453. package/dist/core/gate/checks.js.map +1 -1
  454. package/dist/core/gate/partners.js +4 -2
  455. package/dist/core/gate/partners.js.map +1 -1
  456. package/dist/core/gate/report.js.map +1 -1
  457. package/dist/core/gate/verification.js +75 -12
  458. package/dist/core/gate/verification.js.map +1 -1
  459. package/dist/core/gherkin/stale.js +13 -37
  460. package/dist/core/gherkin/stale.js.map +1 -1
  461. package/dist/core/glossary/tree.js +6 -19
  462. package/dist/core/glossary/tree.js.map +1 -1
  463. package/dist/core/kernel/concurrency.js +19 -0
  464. package/dist/core/kernel/concurrency.js.map +1 -1
  465. package/dist/core/kernel/records.js +11 -0
  466. package/dist/core/kernel/records.js.map +1 -1
  467. package/dist/core/links/backlinks.js +1 -3
  468. package/dist/core/links/backlinks.js.map +1 -1
  469. package/dist/core/links/corpus.js +5 -36
  470. package/dist/core/links/corpus.js.map +1 -1
  471. package/dist/core/mcp/argv.js +101 -0
  472. package/dist/core/mcp/argv.js.map +1 -1
  473. package/dist/core/mcp/author-tools.js +2 -1
  474. package/dist/core/mcp/author-tools.js.map +1 -1
  475. package/dist/core/mcp/protocol.js +2 -2
  476. package/dist/core/mcp/protocol.js.map +1 -1
  477. package/dist/core/mcp/tools.js +43 -33
  478. package/dist/core/mcp/tools.js.map +1 -1
  479. package/dist/core/openapi/baseline/gate.js +53 -12
  480. package/dist/core/openapi/baseline/gate.js.map +1 -1
  481. package/dist/core/openapi/baseline/plan.js +22 -8
  482. package/dist/core/openapi/baseline/plan.js.map +1 -1
  483. package/dist/core/openapi/baseline/record.js +0 -1
  484. package/dist/core/openapi/baseline/record.js.map +1 -1
  485. package/dist/core/openapi/contract-text.js +33 -0
  486. package/dist/core/openapi/contract-text.js.map +1 -0
  487. package/dist/core/openapi/contract-tree.js +56 -0
  488. package/dist/core/openapi/contract-tree.js.map +1 -0
  489. package/dist/core/openapi/depth.js +38 -24
  490. package/dist/core/openapi/depth.js.map +1 -1
  491. package/dist/core/openapi/digest.js +52 -2
  492. package/dist/core/openapi/digest.js.map +1 -1
  493. package/dist/core/openapi/doc.js +12 -4
  494. package/dist/core/openapi/doc.js.map +1 -1
  495. package/dist/core/openapi/merge/components.js +25 -60
  496. package/dist/core/openapi/merge/components.js.map +1 -1
  497. package/dist/core/openapi/merge/error.js +3 -13
  498. package/dist/core/openapi/merge/error.js.map +1 -1
  499. package/dist/core/openapi/merge/markers.js +26 -30
  500. package/dist/core/openapi/merge/markers.js.map +1 -1
  501. package/dist/core/openapi/merge/merge.js +26 -52
  502. package/dist/core/openapi/merge/merge.js.map +1 -1
  503. package/dist/core/openapi/merge/pin.js +15 -35
  504. package/dist/core/openapi/merge/pin.js.map +1 -1
  505. package/dist/core/openspec/decide/verdict.js.map +1 -1
  506. package/dist/core/openspec/inventory.js +4 -5
  507. package/dist/core/openspec/inventory.js.map +1 -1
  508. package/dist/core/openspec/model/mapping.js +30 -28
  509. package/dist/core/openspec/model/mapping.js.map +1 -1
  510. package/dist/core/openspec/model/model.js +12 -7
  511. package/dist/core/openspec/model/model.js.map +1 -1
  512. package/dist/core/openspec/scan/changes.js +10 -4
  513. package/dist/core/openspec/scan/changes.js.map +1 -1
  514. package/dist/core/openspec/scan/config.js +7 -16
  515. package/dist/core/openspec/scan/config.js.map +1 -1
  516. package/dist/core/openspec/scan/shape.js +63 -19
  517. package/dist/core/openspec/scan/shape.js.map +1 -1
  518. package/dist/core/openspec/scan/walk.js +21 -38
  519. package/dist/core/openspec/scan/walk.js.map +1 -1
  520. package/dist/core/pack/living.js +2 -16
  521. package/dist/core/pack/living.js.map +1 -1
  522. package/dist/core/provenance/findings.js +48 -12
  523. package/dist/core/provenance/findings.js.map +1 -1
  524. package/dist/core/provenance/git.js +33 -57
  525. package/dist/core/provenance/git.js.map +1 -1
  526. package/dist/core/provenance/gitq/moves.js +71 -114
  527. package/dist/core/provenance/gitq/moves.js.map +1 -1
  528. package/dist/core/provenance/gitq/vouched-ancestor.js +2 -2
  529. package/dist/core/provenance/gitq/vouched-ancestor.js.map +1 -1
  530. package/dist/core/provenance/sources.js +23 -6
  531. package/dist/core/provenance/sources.js.map +1 -1
  532. package/dist/core/provenance/stamp.js +5 -17
  533. package/dist/core/provenance/stamp.js.map +1 -1
  534. package/dist/core/repo/entries.js.map +1 -1
  535. package/dist/core/repo/repo.js +24 -14
  536. package/dist/core/repo/repo.js.map +1 -1
  537. package/dist/core/repo/tree/find.js +12 -3
  538. package/dist/core/repo/tree/find.js.map +1 -1
  539. package/dist/core/repo/tree/fs.js +27 -1
  540. package/dist/core/repo/tree/fs.js.map +1 -1
  541. package/dist/core/repo/tree/walk.js +8 -30
  542. package/dist/core/repo/tree/walk.js.map +1 -1
  543. package/dist/core/results.js +47 -12
  544. package/dist/core/results.js.map +1 -1
  545. package/dist/core/staging/commit.js +10 -12
  546. package/dist/core/staging/commit.js.map +1 -1
  547. package/dist/core/staging/lock.js +74 -12
  548. package/dist/core/staging/lock.js.map +1 -1
  549. package/dist/core/staging/recovery/intent.js +2 -5
  550. package/dist/core/staging/recovery/intent.js.map +1 -1
  551. package/dist/core/staging/recovery/recover.js +60 -1
  552. package/dist/core/staging/recovery/recover.js.map +1 -1
  553. package/dist/core/staging/snapshot/manifest.js +152 -0
  554. package/dist/core/staging/snapshot/manifest.js.map +1 -0
  555. package/dist/core/staging/{snapshot.js → snapshot/snapshot.js} +10 -34
  556. package/dist/core/staging/snapshot/snapshot.js.map +1 -0
  557. package/dist/core/status/actions/execution.js +11 -0
  558. package/dist/core/status/actions/execution.js.map +1 -1
  559. package/dist/core/status/feature/artifacts.js +2 -2
  560. package/dist/core/status/feature/artifacts.js.map +1 -1
  561. package/dist/core/status/feature/feature.js +5 -11
  562. package/dist/core/status/feature/feature.js.map +1 -1
  563. package/dist/core/status/fleet/fleet.js +6 -2
  564. package/dist/core/status/fleet/fleet.js.map +1 -1
  565. package/dist/core/status/fleet/next.js +26 -3
  566. package/dist/core/status/fleet/next.js.map +1 -1
  567. package/dist/core/status/report.js.map +1 -1
  568. package/dist/core/status/verification.js +30 -15
  569. package/dist/core/status/verification.js.map +1 -1
  570. package/dist/core/tasks/packet.js +174 -0
  571. package/dist/core/tasks/packet.js.map +1 -0
  572. package/dist/core/tasks/record.js +43 -0
  573. package/dist/core/tasks/record.js.map +1 -0
  574. package/dist/core/tasks/sources.js +181 -0
  575. package/dist/core/tasks/sources.js.map +1 -0
  576. package/dist/core/tasks/stamps.js +76 -0
  577. package/dist/core/tasks/stamps.js.map +1 -0
  578. package/dist/core/tasks/unit.js +0 -0
  579. package/dist/core/tasks/unit.js.map +1 -0
  580. package/dist/core/usecases/capability.js +11 -14
  581. package/dist/core/usecases/capability.js.map +1 -1
  582. package/dist/core/usecases/delta/flows.js +5 -7
  583. package/dist/core/usecases/delta/flows.js.map +1 -1
  584. package/dist/core/usecases/fleet.js +178 -31
  585. package/dist/core/usecases/fleet.js.map +1 -1
  586. package/dist/core/usecases/operations.js +5 -4
  587. package/dist/core/usecases/operations.js.map +1 -1
  588. package/dist/core/usecases/service/flows.js +9 -6
  589. package/dist/core/usecases/service/flows.js.map +1 -1
  590. package/dist/core/usecases/suites/participants.js +83 -0
  591. package/dist/core/usecases/suites/participants.js.map +1 -0
  592. package/dist/core/usecases/suites/suites.js +45 -0
  593. package/dist/core/usecases/suites/suites.js.map +1 -0
  594. package/dist/core/verify/answers.js +12 -5
  595. package/dist/core/verify/answers.js.map +1 -1
  596. package/dist/core/verify/build.js +102 -82
  597. package/dist/core/verify/build.js.map +1 -1
  598. package/dist/core/verify/checklist.js +3 -0
  599. package/dist/core/verify/checklist.js.map +1 -1
  600. package/dist/core/verify/location.js +67 -0
  601. package/dist/core/verify/location.js.map +1 -0
  602. package/dist/core/verify/pins/lint.js +3 -2
  603. package/dist/core/verify/pins/lint.js.map +1 -1
  604. package/dist/core/verify/pins/pin.js +1 -1
  605. package/dist/core/verify/pins/pin.js.map +1 -1
  606. package/dist/core/verify/read/file.js +224 -0
  607. package/dist/core/verify/read/file.js.map +1 -0
  608. package/dist/core/verify/{file.js → read/shape.js} +16 -104
  609. package/dist/core/verify/read/shape.js.map +1 -0
  610. package/dist/core/verify/read/union.js +105 -0
  611. package/dist/core/verify/read/union.js.map +1 -0
  612. package/dist/core/verify/record.js +35 -12
  613. package/dist/core/verify/record.js.map +1 -1
  614. package/dist/core/verify/store/commit.js +40 -30
  615. package/dist/core/verify/store/commit.js.map +1 -1
  616. package/dist/core/verify/store/render.js +8 -4
  617. package/dist/core/verify/store/render.js.map +1 -1
  618. package/dist/core/vocabulary/issue.js +1 -0
  619. package/dist/core/vocabulary/issue.js.map +1 -1
  620. package/dist/core/vocabulary/maturity.js +12 -2
  621. package/dist/core/vocabulary/maturity.js.map +1 -1
  622. package/examples/README.md +21 -6
  623. package/examples/docs/architecture/landscape.likec4 +8 -0
  624. package/examples/docs/architecture/usecases/order-notification.likec4 +6 -2
  625. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/asyncapi.yaml +11 -8
  626. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/openapi.yaml +6 -6
  627. package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/openapi.yaml +13 -10
  628. package/examples/docs/services/order-service/asyncapi.yaml +3 -2
  629. package/examples/docs/services/platform/notification-service/asyncapi.yaml +2 -1
  630. package/package.json +1 -1
  631. package/dist/commands/validate/checks/vocabulary.js +0 -16
  632. package/dist/commands/validate/checks/vocabulary.js.map +0 -1
  633. package/dist/core/agent/agents-md/artifacts.js +0 -221
  634. package/dist/core/agent/agents-md/artifacts.js.map +0 -1
  635. package/dist/core/agent/agents-md/cycle.js +0 -180
  636. package/dist/core/agent/agents-md/cycle.js.map +0 -1
  637. package/dist/core/agent/agents-md/shipped/archive-gate.js +0 -98
  638. package/dist/core/agent/agents-md/shipped/archive-gate.js.map +0 -1
  639. package/dist/core/agent/agents-md/spine.js +0 -40
  640. package/dist/core/agent/agents-md/spine.js.map +0 -1
  641. package/dist/core/coherence/events/merged.js +0 -40
  642. package/dist/core/coherence/events/merged.js.map +0 -1
  643. package/dist/core/diff/current-state.js +0 -96
  644. package/dist/core/diff/current-state.js.map +0 -1
  645. package/dist/core/diff/victims.js +0 -256
  646. package/dist/core/diff/victims.js.map +0 -1
  647. package/dist/core/staging/snapshot.js.map +0 -1
  648. package/dist/core/verify/file.js.map +0 -1
@@ -3,7 +3,8 @@ import { existsSync } from "node:fs";
3
3
  import { readFile } from "node:fs/promises";
4
4
  import { parse } from "yaml";
5
5
  import { danglingRefs, deref, payloadUndeclared } from "./depth.js";
6
- import { isSlotRemoval, removalMarkerPaths } from "./digest.js";
6
+ import { entriesOf, isSlotRemoval, removalMarkerPaths } from "./digest.js";
7
+ import { closureDigester } from "../openapi/digest.js";
7
8
  /**
8
9
  * The async contract axis: AsyncAPI 3.0, read the way `core/openapi/doc.ts` reads
9
10
  * OpenAPI — a shallow structural walk over the `yaml` parse, never a real
@@ -15,9 +16,20 @@ import { isSlotRemoval, removalMarkerPaths } from "./digest.js";
15
16
  * `action: send|receive`, which is the exact analog of an `operationId`; 2.x
16
17
  * spells the same thing as `publish`/`subscribe` nested under a channel, where
17
18
  * the two words are notoriously ambiguous about whose perspective they take.
18
- * Supporting one dialect is cheaper than supporting two, and a 2.x document
19
- * declares no `operations` at all — so it reads as a contract with no messages
20
- * rather than as a silently mis-parsed one.
19
+ * Supporting one dialect is cheaper than supporting two. A 2.x (or 1.x)
20
+ * document declares no `operations` at all, and it used to read as a contract
21
+ * with no messages: one `asyncapi.yaml` copied in from an older service turned
22
+ * into a `spine.message-undefined` per landscape edge, a
23
+ * `spec-event.message-undefined` per requirement and a knock-on
24
+ * `spine.message-unproduced` on every consumer, none of them naming the
25
+ * version and every fix text saying "declare the operation" about a file that
26
+ * does declare it, as `publish`/`subscribe`. That is exactly the false
27
+ * diagnosis the unreadable policy below exists to prevent, so an older
28
+ * version now reads as UNREADABLE (`asyncapiDocOf`): one `asyncapi.invalid`
29
+ * naming the version and the upgrade command, and nothing graded against it.
30
+ * `loam diff` skips such a document when the base ref holds the same bytes
31
+ * (core/diff/semantic.ts's `UNTOUCHED_SKIPS`); without that, one unconverted
32
+ * file suspended the event axis on every run and failed every PR's diff.
21
33
  *
22
34
  * WHAT THIS READER MUST NEVER DO: join on anything inside a message's `payload`.
23
35
  * The payload is JSON Schema today and may be Avro later, and that switch is
@@ -32,7 +44,9 @@ import { isSlotRemoval, removalMarkerPaths } from "./digest.js";
32
44
  * slot digests (./digest.ts) are COMPATIBLE with it, not an exception: they
33
45
  * hash payload bytes as content equality — a changed payload moves the slot's
34
46
  * identity — and content equality is not a join; nothing a digest reads ever
35
- * contributes a name to the spine.
47
+ * contributes a name to the spine. The per-message `closureDigest` `loam diff`
48
+ * compares is the same kind of read followed through `$ref`s: it hashes what
49
+ * the payload points at, transitively, and still never names a field.
36
50
  */
37
51
  /*
38
52
  * The `EventMessage`/`AsyncapiDoc` read model lives in `./model.ts` — split
@@ -54,12 +68,6 @@ export function messageName(node, key) {
54
68
  }
55
69
  return key;
56
70
  }
57
- /** Entries of a mapping node, or nothing — every structural read here goes through it. */
58
- function entriesOf(node) {
59
- if (node === null || typeof node !== "object" || Array.isArray(node))
60
- return [];
61
- return Object.entries(node);
62
- }
63
71
  /**
64
72
  * Read an AsyncAPI 3.0 document into the slice loam joins on: the messages it
65
73
  * declares, and which of them it sends and receives.
@@ -125,8 +133,24 @@ export function asyncapiDocOf(doc) {
125
133
  if (doc === null)
126
134
  return empty();
127
135
  const root = doc;
136
+ // A pre-3.0 document is not a 3.0 document with nothing in it, so it takes
137
+ // the unreadable path rather than the empty one (see the module header).
138
+ // Only a version that SAYS 1.x or 2.x is refused: a file with no `asyncapi:`
139
+ // key at all (a hand-trimmed feature delta that restates slots and not the
140
+ // header) keeps reading as 3.0, and so does any later major, which is a
141
+ // question for the day it exists rather than a guess made now. A bare `2`
142
+ // or `2.6` parses as a YAML number, hence the String() and the `$`.
143
+ const version = root["asyncapi"];
144
+ if ((typeof version === "string" || typeof version === "number") && /^[12](\.|$)/.test(String(version))) {
145
+ return {
146
+ ...empty(),
147
+ unreadable: true,
148
+ error: `AsyncAPI ${String(version)}: loam reads AsyncAPI 3.0 only; \`asyncapi convert --target-version=3.0.0\` upgrades it`,
149
+ };
150
+ }
128
151
  const messages = [];
129
152
  const seen = new Map();
153
+ const closureDigest = closureDigester(root);
130
154
  const declare = (name, slot, node) => {
131
155
  // A declaration carrying the removal marker asserts a slot rather than
132
156
  // declaring a message: it is listed (with `remove`) so the gate can see
@@ -135,7 +159,7 @@ export function asyncapiDocOf(doc) {
135
159
  messages.push({ name, slot, remove: true });
136
160
  return;
137
161
  }
138
- messages.push({ name, slot, ...(payloadUndeclared(root, node) ? { payloadEmpty: true } : {}) });
162
+ messages.push({ name, slot, closureDigest: closureDigest(node), ...(payloadUndeclared(root, node) ? { payloadEmpty: true } : {}) });
139
163
  seen.set(name, [...(seen.get(name) ?? []), slot]);
140
164
  };
141
165
  // `components.messages` — the ordinary place a message is declared.
@@ -1 +1 @@
1
- {"version":3,"file":"read.js","sourceRoot":"","sources":["../../../src/core/asyncapi/read.ts"],"names":[],"mappings":"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","sourcesContent":["import { isUtf8 } from \"node:buffer\";\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { parse } from \"yaml\";\nimport { danglingRefs, deref, payloadUndeclared } from \"./depth.js\";\nimport { isSlotRemoval, removalMarkerPaths } from \"./digest.js\";\nimport type { AsyncapiDoc, EventMessage } from \"./model.js\";\nimport type { FleetContext } from \"../fleet-context.js\";\n\n/**\n * The async contract axis: AsyncAPI 3.0, read the way `core/openapi/doc.ts` reads\n * OpenAPI — a shallow structural walk over the `yaml` parse, never a real\n * AsyncAPI parser. `@asyncapi/parser` would be a fourth runtime dependency\n * (AGENTS.md: adding one is a product decision), and loam needs a slice of the\n * document so thin that the parser's whole model would be dead weight.\n *\n * 3.0 only, deliberately. Its operations are named top-level objects carrying\n * `action: send|receive`, which is the exact analog of an `operationId`; 2.x\n * spells the same thing as `publish`/`subscribe` nested under a channel, where\n * the two words are notoriously ambiguous about whose perspective they take.\n * Supporting one dialect is cheaper than supporting two, and a 2.x document\n * declares no `operations` at all — so it reads as a contract with no messages\n * rather than as a silently mis-parsed one.\n *\n * WHAT THIS READER MUST NEVER DO: join on anything inside a message's `payload`.\n * The payload is JSON Schema today and may be Avro later, and that switch is\n * meant to be a line in the document (`schemaFormat`) rather than a branch in\n * loam. Every check on this axis joins on a message NAME; nothing joins on a\n * field, and nothing under `payload` may ever contribute one. The day\n * something here reads a `required` array or a `fields` list INTO A JOIN, the\n * Avro migration stops being free. The one look the reader takes inside is\n * `payloadUndeclared` (depth.ts) — a presence probe that checks whether any\n * shape was declared at all, reads key existence and never a value, and skips\n * any payload declaring a non-JSON `schemaFormat`, so the stance holds. The\n * slot digests (./digest.ts) are COMPATIBLE with it, not an exception: they\n * hash payload bytes as content equality — a changed payload moves the slot's\n * identity — and content equality is not a join; nothing a digest reads ever\n * contributes a name to the spine.\n */\n\n/*\n * The `EventMessage`/`AsyncapiDoc` read model lives in `./model.ts` — split\n * there when the `asyncapiFromText` extraction pushed this module past the\n * 300-line ceiling. This module keeps the walk; that one keeps what the walk\n * answers.\n */\n\n/**\n * A message's join token: its `name`, or the key it is declared under.\n * Exported for the merge (./merge/merge.ts), whose removal exactness compares\n * this same spelling — a second definition of the fallback rule is how a\n * marker would delete a message the reader joins under a different name.\n */\nexport function messageName(node: unknown, key: string): string {\n if (node !== null && typeof node === \"object\" && !Array.isArray(node)) {\n const name = (node as Record<string, unknown>)[\"name\"];\n if (typeof name === \"string\" && name.trim().length > 0) return name.trim();\n }\n return key;\n}\n\n/** Entries of a mapping node, or nothing — every structural read here goes through it. */\nfunction entriesOf(node: unknown): [string, unknown][] {\n if (node === null || typeof node !== \"object\" || Array.isArray(node)) return [];\n return Object.entries(node as Record<string, unknown>);\n}\n\n/**\n * Read an AsyncAPI 3.0 document into the slice loam joins on: the messages it\n * declares, and which of them it sends and receives.\n *\n * The send/receive sets are what make the spine directional, and the direction\n * is not decoration: on the HTTP axis the PROVIDER owns the contract and a\n * consumer's edge is checked against it, while here the PRODUCER owns the\n * message and a consumer joins to it from another repository entirely. That\n * inversion is why `spine.message-unproduced` has no OpenAPI analog.\n *\n * An unreadable document yields empty sets PLUS the `unreadable` flag, the\n * policy every reader here follows: parsers never diagnose. Swallowing the\n * failure into empty sets would make a broken contract indistinguishable from\n * one that declares nothing, and every inbound edge would be graded\n * `spine.message-undefined` — a false diagnosis pointing at the landscape when\n * the truth is this file.\n */\nexport async function readAsyncapi(asyncapiPath: string, context?: FleetContext): Promise<AsyncapiDoc> {\n if (context !== undefined) return context.readAsyncapi(asyncapiPath);\n if (!existsSync(asyncapiPath)) return empty();\n const bytes = await readFile(asyncapiPath);\n // Bytes that are not UTF-8 are as unreadable as broken YAML and for a worse\n // reason: `toString(\"utf8\")` substitutes U+FFFD and hands the parser a\n // document nobody wrote. The openapi axis learned this the same way.\n if (!isUtf8(bytes)) return { ...empty(), unreadable: true, error: \"file is not valid UTF-8\" };\n return asyncapiFromText(bytes.toString(\"utf8\"));\n}\n\n/**\n * The same read over text already in hand — the parse half of `readAsyncapi`,\n * split out so `loam diff` can read a contract fetched from a base git ref\n * (core/diff/base-state.ts) without writing it to disk. The YAML-throw catch\n * lives here and nowhere else; `asyncapiDocOf` below owns everything after\n * the parse, and the byte concerns (existence, UTF-8) stay with the reader\n * that holds the bytes.\n */\nexport function asyncapiFromText(text: string): AsyncapiDoc {\n let doc: unknown;\n try {\n doc = parse(text);\n } catch (e) {\n return { ...empty(), unreadable: true, error: e instanceof Error ? e.message : String(e) };\n }\n return asyncapiDocOf(doc);\n}\n\n/**\n * The same read over an already-parsed root — what the coherence gate's\n * merge simulation grades (core/coherence/events/), where the \"file\" is a\n * document the merge computed and never wrote. One spelling of the walk for\n * both entries, so a simulated contract can never read differently from the\n * living file the archive would produce from it.\n */\nexport function asyncapiDocOf(doc: unknown): AsyncapiDoc {\n // A scalar or sequence document has no mapping to look `channels` up in, so\n // nothing can be concluded from it. null (an empty file) stays readable — it\n // declares nothing, and says so honestly.\n if (doc !== null && (typeof doc !== \"object\" || Array.isArray(doc))) {\n return { ...empty(), unreadable: true, error: \"document is not a YAML mapping\" };\n }\n if (doc === null) return empty();\n\n const root = doc as Record<string, unknown>;\n const messages: EventMessage[] = [];\n const seen = new Map<string, string[]>();\n const declare = (name: string, slot: string, node: unknown): void => {\n // A declaration carrying the removal marker asserts a slot rather than\n // declaring a message: it is listed (with `remove`) so the gate can see\n // it, but it joins nothing — not sent/received, not the duplicate count.\n if (isSlotRemoval(node)) {\n messages.push({ name, slot, remove: true as const });\n return;\n }\n messages.push({ name, slot, ...(payloadUndeclared(root, node) ? { payloadEmpty: true as const } : {}) });\n seen.set(name, [...(seen.get(name) ?? []), slot]);\n };\n\n // `components.messages` — the ordinary place a message is declared.\n for (const [key, node] of entriesOf((root[\"components\"] as Record<string, unknown> | undefined)?.[\"messages\"])) {\n declare(messageName(node, key), `components.messages.${key}`, node);\n }\n\n // `channels.<ck>.messages.<mk>` — an entry here is either an ALIAS of a\n // components declaration (a `$ref`) or an inline declaration of its own. Only\n // the second is a new slot: counting an alias would report every properly\n // factored document as declaring each message twice, which is the shape the\n // spec's own examples use.\n const channelMessages = new Map<string, string[]>();\n for (const [ck, channel] of entriesOf(root[\"channels\"])) {\n // A channel-slot removal retires the whole channel, inline interior\n // included: nothing under it declares or joins anything any more.\n if (isSlotRemoval(channel)) {\n channelMessages.set(ck, []);\n continue;\n }\n const names: string[] = [];\n for (const [mk, entry] of entriesOf((channel as Record<string, unknown> | null)?.[\"messages\"])) {\n const isAlias =\n entry !== null && typeof entry === \"object\" && !Array.isArray(entry) && \"$ref\" in (entry as object);\n const { node, key } = deref(root, entry, mk);\n const name = messageName(node, key);\n // A name whose declaration is being retired stays off the channel's\n // list, whether the marker sits on the aliased components entry or on\n // the inline declaration itself — the operation fallback below must\n // not join through a message the feature is removing.\n if (!isSlotRemoval(node)) names.push(name);\n if (!isAlias) declare(name, `channels.${ck}.messages.${mk}`, node);\n }\n channelMessages.set(ck, names);\n }\n\n const sent = new Set<string>();\n const received = new Set<string>();\n for (const [, operation] of entriesOf(root[\"operations\"])) {\n const op = operation as Record<string, unknown> | null;\n // An operation-slot removal retires the operation: it contributes no\n // send/receive whatever its `action` says.\n if (isSlotRemoval(op)) continue;\n const action = op?.[\"action\"];\n if (action !== \"send\" && action !== \"receive\") continue;\n const into = action === \"send\" ? sent : received;\n // The channel this operation acts on, by pointer — its key is what the\n // fallback below indexes.\n const channelRef = (op?.[\"channel\"] as Record<string, unknown> | undefined)?.[\"$ref\"];\n const channelKey = typeof channelRef === \"string\" ? channelRef.slice(channelRef.lastIndexOf(\"/\") + 1) : undefined;\n const listed = op?.[\"messages\"];\n if (Array.isArray(listed) && listed.length > 0) {\n for (const entry of listed) {\n const { node, key } = deref(root, entry, \"\");\n // A pointer that resolves nowhere contributes no name rather than an\n // empty one: a phantom \"\" would join to nothing and read as a message\n // the service produces.\n if (node === undefined && key === \"\") continue;\n // A pointer into a declaration being retired joins nothing either —\n // the marker means the message is leaving the contract.\n if (isSlotRemoval(node)) continue;\n const name = messageName(node, key);\n if (name.length > 0) into.add(name);\n }\n continue;\n }\n // AsyncAPI 3: an operation that lists no messages applies to every message\n // of its channel. Omitting the list is the normal shape of a minimal\n // hand-written contract — exactly what a fleet with no registry writes — so\n // reading it as \"produces nothing\" would report the common case broken.\n if (channelKey !== undefined) for (const name of channelMessages.get(channelKey) ?? []) into.add(name);\n }\n\n const duplicateNames = [...seen].filter(([, slots]) => slots.length > 1).map(([name]) => name);\n return {\n messages,\n sent: [...sent],\n received: [...received],\n duplicateNames,\n unreadable: false,\n danglingRefs: danglingRefs(root),\n // One deep scan rather than pushes scattered through the walk above: the\n // walk only visits the depths that JOIN, and a marker leaked anywhere\n // else (the root, `info`) was invisible to the living sweep while the\n // strip still owed its removal.\n markers: removalMarkerPaths(root, \"\"),\n };\n}\n\nfunction empty(): AsyncapiDoc {\n return { messages: [], sent: [], received: [], duplicateNames: [], unreadable: false, danglingRefs: [], markers: [] };\n}\n\n/** The slots one message name is declared in — what `asyncapi.duplicate-message` names. */\nexport function slotsOf(doc: AsyncapiDoc, name: string): string[] {\n return doc.messages.filter((m) => m.name === name).map((m) => m.slot);\n}\n\n"]}
1
+ {"version":3,"file":"read.js","sourceRoot":"","sources":["../../../src/core/asyncapi/read.ts"],"names":[],"mappings":"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","sourcesContent":["import { isUtf8 } from \"node:buffer\";\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { parse } from \"yaml\";\nimport { danglingRefs, deref, payloadUndeclared } from \"./depth.js\";\nimport { entriesOf, isSlotRemoval, removalMarkerPaths } from \"./digest.js\";\nimport { closureDigester } from \"../openapi/digest.js\";\nimport type { AsyncapiDoc, EventMessage } from \"./model.js\";\nimport type { FleetContext } from \"../fleet-context.js\";\n\n/**\n * The async contract axis: AsyncAPI 3.0, read the way `core/openapi/doc.ts` reads\n * OpenAPI — a shallow structural walk over the `yaml` parse, never a real\n * AsyncAPI parser. `@asyncapi/parser` would be a fourth runtime dependency\n * (AGENTS.md: adding one is a product decision), and loam needs a slice of the\n * document so thin that the parser's whole model would be dead weight.\n *\n * 3.0 only, deliberately. Its operations are named top-level objects carrying\n * `action: send|receive`, which is the exact analog of an `operationId`; 2.x\n * spells the same thing as `publish`/`subscribe` nested under a channel, where\n * the two words are notoriously ambiguous about whose perspective they take.\n * Supporting one dialect is cheaper than supporting two. A 2.x (or 1.x)\n * document declares no `operations` at all, and it used to read as a contract\n * with no messages: one `asyncapi.yaml` copied in from an older service turned\n * into a `spine.message-undefined` per landscape edge, a\n * `spec-event.message-undefined` per requirement and a knock-on\n * `spine.message-unproduced` on every consumer, none of them naming the\n * version and every fix text saying \"declare the operation\" about a file that\n * does declare it, as `publish`/`subscribe`. That is exactly the false\n * diagnosis the unreadable policy below exists to prevent, so an older\n * version now reads as UNREADABLE (`asyncapiDocOf`): one `asyncapi.invalid`\n * naming the version and the upgrade command, and nothing graded against it.\n * `loam diff` skips such a document when the base ref holds the same bytes\n * (core/diff/semantic.ts's `UNTOUCHED_SKIPS`); without that, one unconverted\n * file suspended the event axis on every run and failed every PR's diff.\n *\n * WHAT THIS READER MUST NEVER DO: join on anything inside a message's `payload`.\n * The payload is JSON Schema today and may be Avro later, and that switch is\n * meant to be a line in the document (`schemaFormat`) rather than a branch in\n * loam. Every check on this axis joins on a message NAME; nothing joins on a\n * field, and nothing under `payload` may ever contribute one. The day\n * something here reads a `required` array or a `fields` list INTO A JOIN, the\n * Avro migration stops being free. The one look the reader takes inside is\n * `payloadUndeclared` (depth.ts) — a presence probe that checks whether any\n * shape was declared at all, reads key existence and never a value, and skips\n * any payload declaring a non-JSON `schemaFormat`, so the stance holds. The\n * slot digests (./digest.ts) are COMPATIBLE with it, not an exception: they\n * hash payload bytes as content equality — a changed payload moves the slot's\n * identity — and content equality is not a join; nothing a digest reads ever\n * contributes a name to the spine. The per-message `closureDigest` `loam diff`\n * compares is the same kind of read followed through `$ref`s: it hashes what\n * the payload points at, transitively, and still never names a field.\n */\n\n/*\n * The `EventMessage`/`AsyncapiDoc` read model lives in `./model.ts` — split\n * there when the `asyncapiFromText` extraction pushed this module past the\n * 300-line ceiling. This module keeps the walk; that one keeps what the walk\n * answers.\n */\n\n/**\n * A message's join token: its `name`, or the key it is declared under.\n * Exported for the merge (./merge/merge.ts), whose removal exactness compares\n * this same spelling — a second definition of the fallback rule is how a\n * marker would delete a message the reader joins under a different name.\n */\nexport function messageName(node: unknown, key: string): string {\n if (node !== null && typeof node === \"object\" && !Array.isArray(node)) {\n const name = (node as Record<string, unknown>)[\"name\"];\n if (typeof name === \"string\" && name.trim().length > 0) return name.trim();\n }\n return key;\n}\n\n/**\n * Read an AsyncAPI 3.0 document into the slice loam joins on: the messages it\n * declares, and which of them it sends and receives.\n *\n * The send/receive sets are what make the spine directional, and the direction\n * is not decoration: on the HTTP axis the PROVIDER owns the contract and a\n * consumer's edge is checked against it, while here the PRODUCER owns the\n * message and a consumer joins to it from another repository entirely. That\n * inversion is why `spine.message-unproduced` has no OpenAPI analog.\n *\n * An unreadable document yields empty sets PLUS the `unreadable` flag, the\n * policy every reader here follows: parsers never diagnose. Swallowing the\n * failure into empty sets would make a broken contract indistinguishable from\n * one that declares nothing, and every inbound edge would be graded\n * `spine.message-undefined` — a false diagnosis pointing at the landscape when\n * the truth is this file.\n */\nexport async function readAsyncapi(asyncapiPath: string, context?: FleetContext): Promise<AsyncapiDoc> {\n if (context !== undefined) return context.readAsyncapi(asyncapiPath);\n if (!existsSync(asyncapiPath)) return empty();\n const bytes = await readFile(asyncapiPath);\n // Bytes that are not UTF-8 are as unreadable as broken YAML and for a worse\n // reason: `toString(\"utf8\")` substitutes U+FFFD and hands the parser a\n // document nobody wrote. The openapi axis learned this the same way.\n if (!isUtf8(bytes)) return { ...empty(), unreadable: true, error: \"file is not valid UTF-8\" };\n return asyncapiFromText(bytes.toString(\"utf8\"));\n}\n\n/**\n * The same read over text already in hand — the parse half of `readAsyncapi`,\n * split out so `loam diff` can read a contract fetched from a base git ref\n * (core/diff/base-state.ts) without writing it to disk. The YAML-throw catch\n * lives here and nowhere else; `asyncapiDocOf` below owns everything after\n * the parse, and the byte concerns (existence, UTF-8) stay with the reader\n * that holds the bytes.\n */\nexport function asyncapiFromText(text: string): AsyncapiDoc {\n let doc: unknown;\n try {\n doc = parse(text);\n } catch (e) {\n return { ...empty(), unreadable: true, error: e instanceof Error ? e.message : String(e) };\n }\n return asyncapiDocOf(doc);\n}\n\n/**\n * The same read over an already-parsed root — what the coherence gate's\n * merge simulation grades (core/coherence/events/), where the \"file\" is a\n * document the merge computed and never wrote. One spelling of the walk for\n * both entries, so a simulated contract can never read differently from the\n * living file the archive would produce from it.\n */\nexport function asyncapiDocOf(doc: unknown): AsyncapiDoc {\n // A scalar or sequence document has no mapping to look `channels` up in, so\n // nothing can be concluded from it. null (an empty file) stays readable — it\n // declares nothing, and says so honestly.\n if (doc !== null && (typeof doc !== \"object\" || Array.isArray(doc))) {\n return { ...empty(), unreadable: true, error: \"document is not a YAML mapping\" };\n }\n if (doc === null) return empty();\n\n const root = doc as Record<string, unknown>;\n // A pre-3.0 document is not a 3.0 document with nothing in it, so it takes\n // the unreadable path rather than the empty one (see the module header).\n // Only a version that SAYS 1.x or 2.x is refused: a file with no `asyncapi:`\n // key at all (a hand-trimmed feature delta that restates slots and not the\n // header) keeps reading as 3.0, and so does any later major, which is a\n // question for the day it exists rather than a guess made now. A bare `2`\n // or `2.6` parses as a YAML number, hence the String() and the `$`.\n const version = root[\"asyncapi\"];\n if ((typeof version === \"string\" || typeof version === \"number\") && /^[12](\\.|$)/.test(String(version))) {\n return {\n ...empty(),\n unreadable: true,\n error: `AsyncAPI ${String(version)}: loam reads AsyncAPI 3.0 only; \\`asyncapi convert --target-version=3.0.0\\` upgrades it`,\n };\n }\n const messages: EventMessage[] = [];\n const seen = new Map<string, string[]>();\n const closureDigest = closureDigester(root);\n const declare = (name: string, slot: string, node: unknown): void => {\n // A declaration carrying the removal marker asserts a slot rather than\n // declaring a message: it is listed (with `remove`) so the gate can see\n // it, but it joins nothing — not sent/received, not the duplicate count.\n if (isSlotRemoval(node)) {\n messages.push({ name, slot, remove: true as const });\n return;\n }\n messages.push({ name, slot, closureDigest: closureDigest(node), ...(payloadUndeclared(root, node) ? { payloadEmpty: true as const } : {}) });\n seen.set(name, [...(seen.get(name) ?? []), slot]);\n };\n\n // `components.messages` — the ordinary place a message is declared.\n for (const [key, node] of entriesOf((root[\"components\"] as Record<string, unknown> | undefined)?.[\"messages\"])) {\n declare(messageName(node, key), `components.messages.${key}`, node);\n }\n\n // `channels.<ck>.messages.<mk>` — an entry here is either an ALIAS of a\n // components declaration (a `$ref`) or an inline declaration of its own. Only\n // the second is a new slot: counting an alias would report every properly\n // factored document as declaring each message twice, which is the shape the\n // spec's own examples use.\n const channelMessages = new Map<string, string[]>();\n for (const [ck, channel] of entriesOf(root[\"channels\"])) {\n // A channel-slot removal retires the whole channel, inline interior\n // included: nothing under it declares or joins anything any more.\n if (isSlotRemoval(channel)) {\n channelMessages.set(ck, []);\n continue;\n }\n const names: string[] = [];\n for (const [mk, entry] of entriesOf((channel as Record<string, unknown> | null)?.[\"messages\"])) {\n const isAlias =\n entry !== null && typeof entry === \"object\" && !Array.isArray(entry) && \"$ref\" in (entry as object);\n const { node, key } = deref(root, entry, mk);\n const name = messageName(node, key);\n // A name whose declaration is being retired stays off the channel's\n // list, whether the marker sits on the aliased components entry or on\n // the inline declaration itself — the operation fallback below must\n // not join through a message the feature is removing.\n if (!isSlotRemoval(node)) names.push(name);\n if (!isAlias) declare(name, `channels.${ck}.messages.${mk}`, node);\n }\n channelMessages.set(ck, names);\n }\n\n const sent = new Set<string>();\n const received = new Set<string>();\n for (const [, operation] of entriesOf(root[\"operations\"])) {\n const op = operation as Record<string, unknown> | null;\n // An operation-slot removal retires the operation: it contributes no\n // send/receive whatever its `action` says.\n if (isSlotRemoval(op)) continue;\n const action = op?.[\"action\"];\n if (action !== \"send\" && action !== \"receive\") continue;\n const into = action === \"send\" ? sent : received;\n // The channel this operation acts on, by pointer — its key is what the\n // fallback below indexes.\n const channelRef = (op?.[\"channel\"] as Record<string, unknown> | undefined)?.[\"$ref\"];\n const channelKey = typeof channelRef === \"string\" ? channelRef.slice(channelRef.lastIndexOf(\"/\") + 1) : undefined;\n const listed = op?.[\"messages\"];\n if (Array.isArray(listed) && listed.length > 0) {\n for (const entry of listed) {\n const { node, key } = deref(root, entry, \"\");\n // A pointer that resolves nowhere contributes no name rather than an\n // empty one: a phantom \"\" would join to nothing and read as a message\n // the service produces.\n if (node === undefined && key === \"\") continue;\n // A pointer into a declaration being retired joins nothing either —\n // the marker means the message is leaving the contract.\n if (isSlotRemoval(node)) continue;\n const name = messageName(node, key);\n if (name.length > 0) into.add(name);\n }\n continue;\n }\n // AsyncAPI 3: an operation that lists no messages applies to every message\n // of its channel. Omitting the list is the normal shape of a minimal\n // hand-written contract — exactly what a fleet with no registry writes — so\n // reading it as \"produces nothing\" would report the common case broken.\n if (channelKey !== undefined) for (const name of channelMessages.get(channelKey) ?? []) into.add(name);\n }\n\n const duplicateNames = [...seen].filter(([, slots]) => slots.length > 1).map(([name]) => name);\n return {\n messages,\n sent: [...sent],\n received: [...received],\n duplicateNames,\n unreadable: false,\n danglingRefs: danglingRefs(root),\n // One deep scan rather than pushes scattered through the walk above: the\n // walk only visits the depths that JOIN, and a marker leaked anywhere\n // else (the root, `info`) was invisible to the living sweep while the\n // strip still owed its removal.\n markers: removalMarkerPaths(root, \"\"),\n };\n}\n\nfunction empty(): AsyncapiDoc {\n return { messages: [], sent: [], received: [], duplicateNames: [], unreadable: false, danglingRefs: [], markers: [] };\n}\n\n/** The slots one message name is declared in — what `asyncapi.duplicate-message` names. */\nexport function slotsOf(doc: AsyncapiDoc, name: string): string[] {\n return doc.messages.filter((m) => m.name === name).map((m) => m.slot);\n}\n\n"]}
@@ -19,11 +19,12 @@
19
19
  * ties the result to a repository, which is why the brief argues for it instead
20
20
  * of merely listing it.
21
21
  */
22
- import { existsSync, statSync } from "node:fs";
22
+ import { existsSync } from "node:fs";
23
23
  import { readdir } from "node:fs/promises";
24
24
  import { join, relative } from "node:path";
25
25
  import { landscapePath } from "../repo/paths.js";
26
26
  import { locateServicePaths } from "../repo/service-target.js";
27
+ import { entryIs } from "../repo/tree/fs.js";
27
28
  import { VALIDATE_CHECKS } from "./checks.js";
28
29
  import { UNCHECKED } from "./unchecked.js";
29
30
  import { landscapeContext } from "./landscape.js";
@@ -246,23 +247,10 @@ async function hasEntry(dir, ext) {
246
247
  if (!existsSync(dir))
247
248
  return false;
248
249
  const entries = await readdir(dir, { withFileTypes: true });
249
- // `statSync` for the symlink case: a Dirent never follows one, so `isFile()`
250
- // is false for a symlinked ADR and the directory reads as empty (repo.ts's
251
- // `entryIs` is the same rule for `services/` and `features/`).
252
- return entries.some((e) => {
253
- if (!e.name.endsWith(ext))
254
- return false;
255
- if (e.isFile())
256
- return true;
257
- if (!e.isSymbolicLink())
258
- return false;
259
- try {
260
- return statSync(join(dir, e.name)).isFile();
261
- }
262
- catch {
263
- return false;
264
- }
265
- });
250
+ // `entryIs` for the symlink case: a Dirent never follows one, so `isFile()`
251
+ // is false for a symlinked ADR and the directory would read as empty — the
252
+ // same rule `services/` and `features/` are enumerated by.
253
+ return entries.some((e) => e.name.endsWith(ext) && entryIs(dir, e, "file"));
266
254
  }
267
255
  /**
268
256
  * Does the fleet expect an API of this service?
@@ -1 +1 @@
1
- {"version":3,"file":"brief.js","sourceRoot":"","sources":["../../../src/core/brief/brief.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The adoption brief: what an agent needs in order to write one service's\n * baseline documentation, and what will be checked when it does.\n *\n * `adopt` was specified for years as \"read the code, emit the C4\". It is not\n * being built, because it cannot be: nothing deterministic reads a legacy\n * service and says what its architecture MEANS, and a guessed model is worse\n * than no model — everyone downstream has to re-derive it to know whether to\n * believe it.\n *\n * So loam takes the half of the job that IS deterministic. It states the work\n * precisely (which files, in which grammar, bound to which existing elements),\n * it says exactly which checks the result will face, and then it says which\n * checks do not exist — because everything in that last list is honesty no\n * validator will ever supply, and an agent that does not know where the checking\n * stops will assume it never does.\n *\n * Nothing here reads code. `sources` in the frontmatter is the only line that\n * ties the result to a repository, which is why the brief argues for it instead\n * of merely listing it.\n */\nimport { existsSync, statSync } from \"node:fs\";\nimport { readdir } from \"node:fs/promises\";\nimport { join, relative } from \"node:path\";\nimport type { PathableService } from \"../kernel/ids/service.js\";\nimport { landscapePath, type ServicePaths } from \"../repo/paths.js\";\nimport { locateServicePaths } from \"../repo/service-target.js\";\nimport { VALIDATE_CHECKS, type BriefCheck } from \"./checks.js\";\nimport { UNCHECKED } from \"./unchecked.js\";\nimport { landscapeContext, type LandscapeContext } from \"./landscape.js\";\nimport { landscapeArtifact } from \"./map/owed.js\";\nimport { exampleFor, extendPoint, FQN } from \"./map/extend.js\";\nimport { ARTIFACTS, USECASES_DIR, type BriefTarget } from \"./targets.js\";\nimport type { DocsDir } from \"../kernel/ids/dirs.js\";\nimport { EXTERNAL_TAG } from \"../vocabulary/maturity.js\";\n\n/* ------------------------------------------------------------------ */\n/* The walk — what to read, in what order, and where each finding lands */\n/* ------------------------------------------------------------------ */\n\nexport interface WalkStop {\n /** What to open, named as a thing to go and find rather than a file name. */\n where: string;\n /** What to take out of it. */\n find: string;\n /** The artifacts this stop feeds, by `artifact` name from `targets[]`. */\n lands: string[];\n}\n\n/**\n * The architecture walk: the order a service is read in, and what each stop is\n * for.\n *\n * The brief said \"read the code: entry points, HTTP routes and handlers,\n * published events, config, deploy manifests, tests\" — one sentence, six nouns,\n * no order and no landing site. What came back matched the sentence: the HTTP\n * surface documented well, because it is the noun an agent recognises fastest,\n * and the scheduler, the consumer group and the outbox missing entirely. Not\n * because they were judged unimportant — because nothing said to go and look,\n * and every check loam owns is computed from what was written rather than from\n * what is there. `UNCHECKED` has said COMPLETENESS is unverifiable since the\n * beginning; that is an argument for stating the sweep, not for leaving it\n * implied.\n *\n * The order is deliberate and it is not the order a reader browses in. Stops 1\n * and 2 fix the SHAPE of the service — which processes exist at all — before\n * any surface is enumerated, because an agent that starts at the HTTP routes\n * has already concluded the service is an API by the time it meets the\n * scheduler. Everything after them is one surface each.\n *\n * `lands` names artifacts rather than describing them, because the two lists\n * are joined: test/agent-contract.test.ts requires every name here to be a\n * target the same brief hands over, so a stop can never feed a file nobody was\n * asked to write.\n */\nexport const WALK: WalkStop[] = [\n {\n where: \"the build and dependency manifests — pom.xml, build.gradle, package.json, go.mod, requirements.txt, and whatever the repo's own README calls the build\",\n find: \"what this service is built from, and every client library it links against. A broker client, an HTTP client, a database driver: each one is a dependency that has to appear as an edge or a container later, and this list is the only place they are all named at once. A library whose SEMANTICS decide behaviour — a retry budget, a delivery guarantee, a serialization format — is an arch requirement (arch.spec.md), not a fact to leave in your head.\",\n lands: [\"model.likec4\", \"runbook.md\", \"arch.spec.md\"],\n },\n {\n where: \"the entry points — main/Application/server bootstrap, the DI or module wiring, and anything the deploy manifests actually start\",\n find: \"how many PROCESSES this service is, and what each one is. An API, a worker, a scheduler, a consumer — a service that is three of those and documented as one is the most common way a baseline is wrong, and it is wrong in a shape no later check can see.\",\n lands: [\"model.likec4\"],\n },\n {\n where: \"the HTTP surface — every route, controller, handler and middleware, counted\",\n find: \"the operation set, in full, and inside each operation the DECISIONS its code makes: the permission and role checks — each one is a `Requires:` entry, and a name `architecture/permissions.yaml` does not declare yet is a line to ADD to that shared file, never to invent locally — the fields required only when another field is present, the defaults and normalisations applied before anything is stored, the transitions a request is refused for. Count both — operations, and branches per operation — before you write, and say both counts in the hand-back. `api.ungoverned` can tell you an operation you WROTE has no requirement; nothing at all can tell you about the twelve you never wrote down, or about the thirty branches inside the one you did. A guard is not implementation detail: what a service REFUSES is behaviour its caller observes and a test pins, and it is the half of a legacy service that lives only in the code and in one developer's head.\",\n lands: [\"openapi.yaml\", \"spec.md\"],\n },\n {\n where: \"the message surface — producers, consumers, listener annotations, topic and queue names, the outbox table and its relay\",\n find: \"every message this service puts on and takes off the bus, with its direction, and — on the consuming side — the DECISIONS the handler makes about a message before it acts on one. The direction is the half that gets lost: a consumer documented as a producer is a contract the whole fleet then joins against backwards. The guards are the half nobody looks for at all: which messages are filtered out and on what field, what happens to one whose entity has since been deleted, what is idempotency-keyed against a replay, what is dead-lettered rather than retried. Each of those is a scenario, and none of them appears anywhere in the contract — asyncapi.yaml describes the message, never what this service refuses to do with it.\",\n lands: [\"asyncapi.yaml\", \"landscape.likec4\"],\n },\n {\n where: \"the scheduled and background work — cron entries, timers, retry loops, reconcilers, migrations that run at boot\",\n find: \"behaviour no request ever triggers, and the conditions under which it does or does not run. It has requirements like anything else, and it is invisible to every surface above — which is why a baseline written from routes alone describes a service that does half of what it does. Take the decisions with it: which rows a reconciler SELECTS and which it skips, what a retry loop gives up on and after how long, what a migration does when it finds the state it is meant to fix already fixed. A scheduled job documented as 'runs nightly' is a requirement with no behaviour in it.\",\n lands: [\"spec.md\", \"arch.spec.md\"],\n },\n {\n where: \"the state — datastores, caches, buckets, and the schema or migration directory that says what is in them\",\n find: \"what this service OWNS versus what it borrows. Ownership is the fact the C4 is for, and it is the one nobody can reconstruct later from the code without re-reading all of it.\",\n lands: [\"model.likec4\"],\n },\n {\n where: \"the outbound calls — HTTP clients, generated stubs, gRPC channels, and the config that says which host each one points at\",\n find: \"who this service calls, and which operation each call names. These become the fleet map's edges; an edge missing here is a dependency the fleet cannot see, and `loam explore` will under-report the blast radius of every future change to the callee. An edge that only configuration attests is PROVISIONAL until the runtime stop: hold it until the deploy manifests confirm nothing switches that client off.\",\n lands: [\"landscape.likec4\"],\n },\n {\n where: \"the runtime — config and env vars, deploy manifests, pipelines, alert rules, dashboards\",\n find: \"how it is run and what pages whom. Alerts are the honest source for health.yaml: an SLO nobody agreed to is invention, but an alert that already fires is a fact somebody wrote down. Deployment manifests OVERRIDE configuration files, and they usually live in another repository — go and find them before any dependency that only configuration attests goes in the model, and check for an environment variable that disables the feature. A capability switched off at deploy time is not a dependency, however completely the config wires it: delete its edge and container, and record it in runbook.md's 'configured but not a dependency, and why' list. A manifest read from another repository is named in the runbook's prose — never in `sources`, which may only hold paths of this service's own repository. An EFFECTIVE configuration value behaviour depends on — the timeout the chart sets, the flag that switches a code path — is an arch requirement in arch.spec.md, with a Covers: line naming what it decides; loam holds its coverage even though it can never check the value.\",\n lands: [\"runbook.md\", \"health.yaml\", \"arch.spec.md\"],\n },\n {\n where: \"the tests, integration and component ones first\",\n find: \"the behaviours somebody already thought worth pinning. This is the PRIMARY source for scenarios, not the last sweep: a behaviour a test already pins is written FROM that test, and the code is read only for what no test covers. A test suite is the only part of a legacy service that states intent rather than mechanism, and a scenario derived from one is the only kind that starts out true. Carry the shape across as well as the content — a parameterised test (`@ParameterizedTest`, `@parametrize`, `test.each`) is one scenario with a table, not one scenario per case. Then say in the hand-back how well this service is covered and how many of its tests became scenarios: that number is the ceiling on what this whole baseline can be worth, and nothing downstream can compute it.\",\n lands: [\"spec.md\", \"arch.spec.md\"],\n },\n];\n\n/**\n * The stop that closes the walk. Separate from `WALK` because it is not a place\n * to go — it is the question asked once the walking is done, and the only\n * defence against a sweep that felt thorough because the parts it covered were\n * covered well.\n */\nexport const WALK_CLOSE =\n \"Then list the directories you did NOT open, and say why for each. `loam validate --service <id>`, run inside the service's own repo, reports the same list back as `sources.unwalked` — it compares your `sources` against the files git tracks. A directory you deliberately skipped is a sentence in the hand-back; a directory you skipped without noticing is the gap this walk exists to find.\";\n\n/* ------------------------------------------------------------------ */\n/* The frontmatter */\n/* ------------------------------------------------------------------ */\n\nexport interface FrontmatterBrief {\n /** Field -> what to write in it. */\n fields: Record<string, string>;\n /** Fields to leave alone entirely. */\n never: string[];\n /** Why `sources` carries the weight. */\n why: string;\n}\n\nconst FRONTMATTER_BRIEF: FrontmatterBrief = {\n fields: {\n service: \"the directory this file lives under, exactly. A mismatch is an error (`frontmatter.field-mismatch`).\",\n status:\n \"`draft`, always. `verified` is a human's word, stamped by `loam vouch --service <id>` run inside the service's own repo — never written by hand.\",\n owner: \"the team or person who answers for this service.\",\n sources:\n \"the paths in THIS SERVICE'S repository you actually read to write the document — files and directories only, a directory covering everything beneath it; glob patterns are refused. Not the paths a reader would expect to have been read. This is NOT a reading list for a human: nobody follows these, and nothing about the document asks them to.\",\n },\n // `vouch_scope` is in this list for a sharper reason than its siblings: the\n // others are stamps an agent could only forge, while this one is a stamp an\n // agent could DELETE — and deleting it turns a vouch that read four sections\n // into one that read the document, on every surface at once.\n never: [\"last_verified\", \"sources_digest\", \"content_digest\", \"sources_files\", \"vouch_scope\"],\n why:\n \"`sources` is digest input, not a citation. It is the only mechanical tie between this document and the code — everything else loam checks is internal consistency, and a corpus can agree with itself perfectly while describing nothing that exists. `loam validate`, run inside the service's repo, checks every listed path is still there; `loam vouch` hashes their CONTENT so that a later `validate` can say the code has moved since anyone read it. That hash is the entire point, and it is why padding the list costs something real rather than merely being untidy: a path nobody read still gets hashed, so its next change reports as drift in a document it never described, and a path that WAS read and went unlisted moves under the document in silence. Neither failure is visible to a reader; both are decided by whether this list is honest.\",\n};\n\nexport interface Brief {\n service: string;\n /** Absolute docs repo root — the anchor the repo-relative paths hang off. */\n docsDir: DocsDir;\n /** Repo-relative path of services/<id>/. */\n path: string;\n targets: BriefTarget[];\n /** The order to read the service in, and what each stop feeds. */\n walk: WalkStop[];\n /** The question the walk ends on — see `WALK_CLOSE`. */\n walkClose: string;\n landscape: LandscapeContext;\n frontmatter: FrontmatterBrief;\n checks: BriefCheck[];\n unchecked: string[];\n /** The one instruction that outranks the rest. */\n rule: string;\n}\n\nconst NEVER_OVERWRITE =\n \"Do not overwrite an artifact that already exists. Read it, diff your findings against it, and report what disagrees — a document somebody wrote is evidence, and replacing it destroys the only copy of what they knew.\";\n\n/**\n * Assemble the brief for one service. Reads the docs repo; writes nothing.\n * `at` overrides where the artifacts are briefed to land — `adopt\n * --subsystem`'s target inside a group — and is only ever passed for a\n * service the enumeration does NOT answer: an existing service is briefed\n * where it already lives, because the brief's one rule is to never overwrite,\n * and a second address would brief a duplicate.\n */\nexport async function serviceBrief(\n docsDir: DocsDir,\n service: PathableService,\n at?: ServicePaths,\n): Promise<Brief> {\n const paths = at ?? (await locateServicePaths(docsDir, service));\n const rel = (abs: string): string => relative(docsDir, abs).split(/[\\\\/]/).join(\"/\");\n\n // Read before the artifact loop, not after it: what the fleet already says\n // about this service decides whether it owes an API contract at all. The\n // model path travels with it — located through the enumeration above, never\n // joined at `services/<id>/` — so the edgeless state can name the calls the\n // service's own model attests.\n const landscape = await landscapeContext(docsDir, service, paths);\n const owesApi = apiExpected(landscape);\n\n // The one id the model target's grammar and example are written against,\n // resolved here because `ARTIFACTS` is static on purpose (a target list that\n // varied per repository would be a second thing to keep true) and because this\n // is the only place the fleet map has already been read.\n const extend = extendPoint(landscape);\n\n const targets: BriefTarget[] = [];\n for (const { key, ...artifact } of ARTIFACTS) {\n // `adrs/` and `usecases/` are DIRECTORIES, and an empty one is not an\n // existing artifact — reporting it as present would tell the agent to diff\n // against nothing, and for `usecases/` it would also tell the agent an\n // optional slot was already taken.\n //\n // `usecases/` is the one target with no `ServicePaths` entry, and that is\n // deliberate rather than an omission: nothing loam checks JOINS on that\n // path. The slot a service's flows are graded out of is its whole LikeC4\n // project (`core/usecases/service/flows.ts` reads every `.likec4` under the\n // directory), so `usecases/<name>.likec4` is a convention this brief\n // publishes, not a path a writer writes — and `targets.ts` is where a\n // convention belongs.\n const ext = key === \"adrsDir\" ? \".md\" : key === \"usecasesDir\" ? \".likec4\" : null;\n const at = key === \"usecasesDir\" ? join(paths.dir, USECASES_DIR) : paths[key];\n const exists = ext === null ? existsSync(at) : await hasEntry(at, ext);\n targets.push({\n ...artifact,\n ...(key === \"openapi\" ? { required: owesApi } : {}),\n // Every `<fqn>` in the model target, replaced by the element this service\n // actually extends. The placeholder is left literal in `targets.ts` — the\n // brief is handed to an agent that will WRITE the file, and\n // `extend <fqn> {` copied verbatim is a document that does not parse.\n shape: artifact.shape.map((rule) => rule.replaceAll(FQN, extend.id)),\n ...(artifact.example === undefined ? {} : { example: exampleFor(artifact.example, extend, service) }),\n path: ext === null ? rel(at) : `${rel(at)}/`,\n exists,\n action: exists ? \"diff\" : \"create\",\n });\n }\n\n // The eighth target is the fleet map, and it is conditional on TWO facts: an\n // element resolving to the service, and an edge touching it. A service the\n // landscape already draws AND reaches owes it nothing, and briefing an edit\n // there would invite an agent to draw a second box for a service that has\n // one. A service with a box and no edge — what `loam seed` leaves for every\n // service nobody listed under `calls:` — owes it the edges, and until\n // 2026-09-03 element existence alone read as \"the map is complete\" here, so\n // the brief declared the map finished for exactly the services it had not\n // reached. Everything else about the target is unconditional — the file is\n // shared, so `action` is `edit` whenever it exists and `create` only for the\n // very first service of a brand-new docs repo.\n //\n // `modelled === false` and not `!== true`: `null` is \"loam could not read the\n // map\", and it fell through the same branch as \"the map genuinely draws\n // nothing for this service\". The brief then handed over the UNDRAWN target —\n // whose shape rule is \"add one top-level element bound to its directory\" and\n // whose example is a block to splice — for services the map already declares\n // and binds, and an agent that spliced it earned `landscape.binding-duplicate`\n // for doing exactly what the brief asked. With the map unreadable there is no\n // write to ask for; `landscape.instruction` carries the whole answer and names\n // the documents that failed.\n if (landscape.modelled === false || landscape.touched === false) {\n targets.push({\n ...landscapeArtifact({\n service,\n expects: landscape.expects,\n present: landscape.present,\n servicePath: rel(paths.dir),\n // The existing element's id, in the edgeless state only: it switches\n // the example from a block to edges on that element.\n // …and whether it is `#external`, which decides what the shape rule may\n // promise: `landscape.service-isolated` never fires on such an element.\n ...(landscape.touched === false && landscape.elements[0] !== undefined\n ? { elementId: landscape.elements[0].id, external: landscape.elements[0].tags.includes(EXTERNAL_TAG) }\n : {}),\n attested: landscape.attested,\n }),\n path: rel(landscapePath(docsDir)),\n exists: landscape.present,\n action: landscape.present ? \"edit\" : \"create\",\n });\n }\n\n return {\n service,\n docsDir,\n path: rel(paths.dir),\n targets,\n walk: WALK,\n walkClose: WALK_CLOSE,\n landscape,\n frontmatter: FRONTMATTER_BRIEF,\n checks: VALIDATE_CHECKS,\n unchecked: UNCHECKED,\n rule: NEVER_OVERWRITE,\n };\n}\n\n/**\n * Does this directory already hold an artifact of the given extension? The\n * \"exists\" question for the two DIRECTORY targets — `adrs/` (`.md`) and\n * `usecases/` (`.likec4`) — where an empty or absent directory is the same\n * answer, because neither is something to diff against.\n */\nasync function hasEntry(dir: string, ext: string): Promise<boolean> {\n if (!existsSync(dir)) return false;\n const entries = await readdir(dir, { withFileTypes: true });\n // `statSync` for the symlink case: a Dirent never follows one, so `isFile()`\n // is false for a symlinked ADR and the directory reads as empty (repo.ts's\n // `entryIs` is the same rule for `services/` and `features/`).\n return entries.some((e) => {\n if (!e.name.endsWith(ext)) return false;\n if (e.isFile()) return true;\n if (!e.isSymbolicLink()) return false;\n try {\n return statSync(join(dir, e.name)).isFile();\n } catch {\n return false;\n }\n });\n}\n\n/**\n * Does the fleet expect an API of this service?\n *\n * The brief marked `openapi.yaml` required unconditionally, and printed the\n * absence as `MISSING` in capitals — while `loam validate` and `loam list`\n * derive the opposite from the same landscape (`service.no-openapi` stays quiet\n * when nothing calls the service, and `list`'s maturity ladder stops demanding\n * a contract). For a UI, a worker or a cron the brief was therefore telling an\n * agent to INVENT an API contract into the source of truth — the one class of\n * guess this whole module exists to refuse, and the one the brief itself names\n * in `openapi.yaml`'s shape rules (`ARTIFACTS`, targets.ts).\n *\n * Positive evidence only, exactly as `list` reads it: a landscape that is\n * absent or unreadable proves nothing about who calls this service, so the\n * contract is still owed. `expects` is the ops on inbound edges, which is\n * `list`'s `called` set seen from one service.\n *\n * `callersKnown` and not `parses`, because since W4 `parses` answers about the\n * whole `architecture/` PROJECT: a broken palette or use case beside a landscape\n * loam reads perfectly made this discard the map and demand an OpenAPI contract\n * of a browser UI — inventing an API into the source of truth, which is the one\n * class of guess this module exists to refuse. `callersKnown` is the narrower\n * fact this question actually needs, and it is the one `list` and `context` read.\n */\nfunction apiExpected(landscape: LandscapeContext): boolean {\n return !landscape.callersKnown || landscape.expects.length > 0;\n}\n"]}
1
+ {"version":3,"file":"brief.js","sourceRoot":"","sources":["../../../src/core/brief/brief.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The adoption brief: what an agent needs in order to write one service's\n * baseline documentation, and what will be checked when it does.\n *\n * `adopt` was specified for years as \"read the code, emit the C4\". It is not\n * being built, because it cannot be: nothing deterministic reads a legacy\n * service and says what its architecture MEANS, and a guessed model is worse\n * than no model — everyone downstream has to re-derive it to know whether to\n * believe it.\n *\n * So loam takes the half of the job that IS deterministic. It states the work\n * precisely (which files, in which grammar, bound to which existing elements),\n * it says exactly which checks the result will face, and then it says which\n * checks do not exist — because everything in that last list is honesty no\n * validator will ever supply, and an agent that does not know where the checking\n * stops will assume it never does.\n *\n * Nothing here reads code. `sources` in the frontmatter is the only line that\n * ties the result to a repository, which is why the brief argues for it instead\n * of merely listing it.\n */\nimport { existsSync } from \"node:fs\";\nimport { readdir } from \"node:fs/promises\";\nimport { join, relative } from \"node:path\";\nimport type { PathableService } from \"../kernel/ids/service.js\";\nimport { landscapePath, type ServicePaths } from \"../repo/paths.js\";\nimport { locateServicePaths } from \"../repo/service-target.js\";\nimport { entryIs } from \"../repo/tree/fs.js\";\nimport { VALIDATE_CHECKS, type BriefCheck } from \"./checks.js\";\nimport { UNCHECKED } from \"./unchecked.js\";\nimport { landscapeContext, type LandscapeContext } from \"./landscape.js\";\nimport { landscapeArtifact } from \"./map/owed.js\";\nimport { exampleFor, extendPoint, FQN } from \"./map/extend.js\";\nimport { ARTIFACTS, USECASES_DIR, type BriefTarget } from \"./targets.js\";\nimport type { DocsDir } from \"../kernel/ids/dirs.js\";\nimport { EXTERNAL_TAG } from \"../vocabulary/maturity.js\";\n\n/* ------------------------------------------------------------------ */\n/* The walk — what to read, in what order, and where each finding lands */\n/* ------------------------------------------------------------------ */\n\nexport interface WalkStop {\n /** What to open, named as a thing to go and find rather than a file name. */\n where: string;\n /** What to take out of it. */\n find: string;\n /** The artifacts this stop feeds, by `artifact` name from `targets[]`. */\n lands: string[];\n}\n\n/**\n * The architecture walk: the order a service is read in, and what each stop is\n * for.\n *\n * The brief said \"read the code: entry points, HTTP routes and handlers,\n * published events, config, deploy manifests, tests\" — one sentence, six nouns,\n * no order and no landing site. What came back matched the sentence: the HTTP\n * surface documented well, because it is the noun an agent recognises fastest,\n * and the scheduler, the consumer group and the outbox missing entirely. Not\n * because they were judged unimportant — because nothing said to go and look,\n * and every check loam owns is computed from what was written rather than from\n * what is there. `UNCHECKED` has said COMPLETENESS is unverifiable since the\n * beginning; that is an argument for stating the sweep, not for leaving it\n * implied.\n *\n * The order is deliberate and it is not the order a reader browses in. Stops 1\n * and 2 fix the SHAPE of the service — which processes exist at all — before\n * any surface is enumerated, because an agent that starts at the HTTP routes\n * has already concluded the service is an API by the time it meets the\n * scheduler. Everything after them is one surface each.\n *\n * `lands` names artifacts rather than describing them, because the two lists\n * are joined: test/agent-contract.test.ts requires every name here to be a\n * target the same brief hands over, so a stop can never feed a file nobody was\n * asked to write.\n */\nexport const WALK: WalkStop[] = [\n {\n where: \"the build and dependency manifests — pom.xml, build.gradle, package.json, go.mod, requirements.txt, and whatever the repo's own README calls the build\",\n find: \"what this service is built from, and every client library it links against. A broker client, an HTTP client, a database driver: each one is a dependency that has to appear as an edge or a container later, and this list is the only place they are all named at once. A library whose SEMANTICS decide behaviour — a retry budget, a delivery guarantee, a serialization format — is an arch requirement (arch.spec.md), not a fact to leave in your head.\",\n lands: [\"model.likec4\", \"runbook.md\", \"arch.spec.md\"],\n },\n {\n where: \"the entry points — main/Application/server bootstrap, the DI or module wiring, and anything the deploy manifests actually start\",\n find: \"how many PROCESSES this service is, and what each one is. An API, a worker, a scheduler, a consumer — a service that is three of those and documented as one is the most common way a baseline is wrong, and it is wrong in a shape no later check can see.\",\n lands: [\"model.likec4\"],\n },\n {\n where: \"the HTTP surface — every route, controller, handler and middleware, counted\",\n find: \"the operation set, in full, and inside each operation the DECISIONS its code makes: the permission and role checks — each one is a `Requires:` entry, and a name `architecture/permissions.yaml` does not declare yet is a line to ADD to that shared file, never to invent locally — the fields required only when another field is present, the defaults and normalisations applied before anything is stored, the transitions a request is refused for. Count both — operations, and branches per operation — before you write, and say both counts in the hand-back. `api.ungoverned` can tell you an operation you WROTE has no requirement; nothing at all can tell you about the twelve you never wrote down, or about the thirty branches inside the one you did. A guard is not implementation detail: what a service REFUSES is behaviour its caller observes and a test pins, and it is the half of a legacy service that lives only in the code and in one developer's head.\",\n lands: [\"openapi.yaml\", \"spec.md\"],\n },\n {\n where: \"the message surface — producers, consumers, listener annotations, topic and queue names, the outbox table and its relay\",\n find: \"every message this service puts on and takes off the bus, with its direction, and — on the consuming side — the DECISIONS the handler makes about a message before it acts on one. The direction is the half that gets lost: a consumer documented as a producer is a contract the whole fleet then joins against backwards. The guards are the half nobody looks for at all: which messages are filtered out and on what field, what happens to one whose entity has since been deleted, what is idempotency-keyed against a replay, what is dead-lettered rather than retried. Each of those is a scenario, and none of them appears anywhere in the contract — asyncapi.yaml describes the message, never what this service refuses to do with it.\",\n lands: [\"asyncapi.yaml\", \"landscape.likec4\"],\n },\n {\n where: \"the scheduled and background work — cron entries, timers, retry loops, reconcilers, migrations that run at boot\",\n find: \"behaviour no request ever triggers, and the conditions under which it does or does not run. It has requirements like anything else, and it is invisible to every surface above — which is why a baseline written from routes alone describes a service that does half of what it does. Take the decisions with it: which rows a reconciler SELECTS and which it skips, what a retry loop gives up on and after how long, what a migration does when it finds the state it is meant to fix already fixed. A scheduled job documented as 'runs nightly' is a requirement with no behaviour in it.\",\n lands: [\"spec.md\", \"arch.spec.md\"],\n },\n {\n where: \"the state — datastores, caches, buckets, and the schema or migration directory that says what is in them\",\n find: \"what this service OWNS versus what it borrows. Ownership is the fact the C4 is for, and it is the one nobody can reconstruct later from the code without re-reading all of it.\",\n lands: [\"model.likec4\"],\n },\n {\n where: \"the outbound calls — HTTP clients, generated stubs, gRPC channels, and the config that says which host each one points at\",\n find: \"who this service calls, and which operation each call names. These become the fleet map's edges; an edge missing here is a dependency the fleet cannot see, and `loam explore` will under-report the blast radius of every future change to the callee. An edge that only configuration attests is PROVISIONAL until the runtime stop: hold it until the deploy manifests confirm nothing switches that client off.\",\n lands: [\"landscape.likec4\"],\n },\n {\n where: \"the runtime — config and env vars, deploy manifests, pipelines, alert rules, dashboards\",\n find: \"how it is run and what pages whom. Alerts are the honest source for health.yaml: an SLO nobody agreed to is invention, but an alert that already fires is a fact somebody wrote down. Deployment manifests OVERRIDE configuration files, and they usually live in another repository — go and find them before any dependency that only configuration attests goes in the model, and check for an environment variable that disables the feature. A capability switched off at deploy time is not a dependency, however completely the config wires it: delete its edge and container, and record it in runbook.md's 'configured but not a dependency, and why' list. A manifest read from another repository is named in the runbook's prose — never in `sources`, which may only hold paths of this service's own repository. An EFFECTIVE configuration value behaviour depends on — the timeout the chart sets, the flag that switches a code path — is an arch requirement in arch.spec.md, with a Covers: line naming what it decides; loam holds its coverage even though it can never check the value.\",\n lands: [\"runbook.md\", \"health.yaml\", \"arch.spec.md\"],\n },\n {\n where: \"the tests, integration and component ones first\",\n find: \"the behaviours somebody already thought worth pinning. This is the PRIMARY source for scenarios, not the last sweep: a behaviour a test already pins is written FROM that test, and the code is read only for what no test covers. A test suite is the only part of a legacy service that states intent rather than mechanism, and a scenario derived from one is the only kind that starts out true. Carry the shape across as well as the content — a parameterised test (`@ParameterizedTest`, `@parametrize`, `test.each`) is one scenario with a table, not one scenario per case. Then say in the hand-back how well this service is covered and how many of its tests became scenarios: that number is the ceiling on what this whole baseline can be worth, and nothing downstream can compute it.\",\n lands: [\"spec.md\", \"arch.spec.md\"],\n },\n];\n\n/**\n * The stop that closes the walk. Separate from `WALK` because it is not a place\n * to go — it is the question asked once the walking is done, and the only\n * defence against a sweep that felt thorough because the parts it covered were\n * covered well.\n */\nexport const WALK_CLOSE =\n \"Then list the directories you did NOT open, and say why for each. `loam validate --service <id>`, run inside the service's own repo, reports the same list back as `sources.unwalked` — it compares your `sources` against the files git tracks. A directory you deliberately skipped is a sentence in the hand-back; a directory you skipped without noticing is the gap this walk exists to find.\";\n\n/* ------------------------------------------------------------------ */\n/* The frontmatter */\n/* ------------------------------------------------------------------ */\n\nexport interface FrontmatterBrief {\n /** Field -> what to write in it. */\n fields: Record<string, string>;\n /** Fields to leave alone entirely. */\n never: string[];\n /** Why `sources` carries the weight. */\n why: string;\n}\n\nconst FRONTMATTER_BRIEF: FrontmatterBrief = {\n fields: {\n service: \"the directory this file lives under, exactly. A mismatch is an error (`frontmatter.field-mismatch`).\",\n status:\n \"`draft`, always. `verified` is a human's word, stamped by `loam vouch --service <id>` run inside the service's own repo — never written by hand.\",\n owner: \"the team or person who answers for this service.\",\n sources:\n \"the paths in THIS SERVICE'S repository you actually read to write the document — files and directories only, a directory covering everything beneath it; glob patterns are refused. Not the paths a reader would expect to have been read. This is NOT a reading list for a human: nobody follows these, and nothing about the document asks them to.\",\n },\n // `vouch_scope` is in this list for a sharper reason than its siblings: the\n // others are stamps an agent could only forge, while this one is a stamp an\n // agent could DELETE — and deleting it turns a vouch that read four sections\n // into one that read the document, on every surface at once.\n never: [\"last_verified\", \"sources_digest\", \"content_digest\", \"sources_files\", \"vouch_scope\"],\n why:\n \"`sources` is digest input, not a citation. It is the only mechanical tie between this document and the code — everything else loam checks is internal consistency, and a corpus can agree with itself perfectly while describing nothing that exists. `loam validate`, run inside the service's repo, checks every listed path is still there; `loam vouch` hashes their CONTENT so that a later `validate` can say the code has moved since anyone read it. That hash is the entire point, and it is why padding the list costs something real rather than merely being untidy: a path nobody read still gets hashed, so its next change reports as drift in a document it never described, and a path that WAS read and went unlisted moves under the document in silence. Neither failure is visible to a reader; both are decided by whether this list is honest.\",\n};\n\nexport interface Brief {\n service: string;\n /** Absolute docs repo root — the anchor the repo-relative paths hang off. */\n docsDir: DocsDir;\n /** Repo-relative path of services/<id>/. */\n path: string;\n targets: BriefTarget[];\n /** The order to read the service in, and what each stop feeds. */\n walk: WalkStop[];\n /** The question the walk ends on — see `WALK_CLOSE`. */\n walkClose: string;\n landscape: LandscapeContext;\n frontmatter: FrontmatterBrief;\n checks: BriefCheck[];\n unchecked: string[];\n /** The one instruction that outranks the rest. */\n rule: string;\n}\n\nconst NEVER_OVERWRITE =\n \"Do not overwrite an artifact that already exists. Read it, diff your findings against it, and report what disagrees — a document somebody wrote is evidence, and replacing it destroys the only copy of what they knew.\";\n\n/**\n * Assemble the brief for one service. Reads the docs repo; writes nothing.\n * `at` overrides where the artifacts are briefed to land — `adopt\n * --subsystem`'s target inside a group — and is only ever passed for a\n * service the enumeration does NOT answer: an existing service is briefed\n * where it already lives, because the brief's one rule is to never overwrite,\n * and a second address would brief a duplicate.\n */\nexport async function serviceBrief(\n docsDir: DocsDir,\n service: PathableService,\n at?: ServicePaths,\n): Promise<Brief> {\n const paths = at ?? (await locateServicePaths(docsDir, service));\n const rel = (abs: string): string => relative(docsDir, abs).split(/[\\\\/]/).join(\"/\");\n\n // Read before the artifact loop, not after it: what the fleet already says\n // about this service decides whether it owes an API contract at all. The\n // model path travels with it — located through the enumeration above, never\n // joined at `services/<id>/` — so the edgeless state can name the calls the\n // service's own model attests.\n const landscape = await landscapeContext(docsDir, service, paths);\n const owesApi = apiExpected(landscape);\n\n // The one id the model target's grammar and example are written against,\n // resolved here because `ARTIFACTS` is static on purpose (a target list that\n // varied per repository would be a second thing to keep true) and because this\n // is the only place the fleet map has already been read.\n const extend = extendPoint(landscape);\n\n const targets: BriefTarget[] = [];\n for (const { key, ...artifact } of ARTIFACTS) {\n // `adrs/` and `usecases/` are DIRECTORIES, and an empty one is not an\n // existing artifact — reporting it as present would tell the agent to diff\n // against nothing, and for `usecases/` it would also tell the agent an\n // optional slot was already taken.\n //\n // `usecases/` is the one target with no `ServicePaths` entry, and that is\n // deliberate rather than an omission: nothing loam checks JOINS on that\n // path. The slot a service's flows are graded out of is its whole LikeC4\n // project (`core/usecases/service/flows.ts` reads every `.likec4` under the\n // directory), so `usecases/<name>.likec4` is a convention this brief\n // publishes, not a path a writer writes — and `targets.ts` is where a\n // convention belongs.\n const ext = key === \"adrsDir\" ? \".md\" : key === \"usecasesDir\" ? \".likec4\" : null;\n const at = key === \"usecasesDir\" ? join(paths.dir, USECASES_DIR) : paths[key];\n const exists = ext === null ? existsSync(at) : await hasEntry(at, ext);\n targets.push({\n ...artifact,\n ...(key === \"openapi\" ? { required: owesApi } : {}),\n // Every `<fqn>` in the model target, replaced by the element this service\n // actually extends. The placeholder is left literal in `targets.ts` — the\n // brief is handed to an agent that will WRITE the file, and\n // `extend <fqn> {` copied verbatim is a document that does not parse.\n shape: artifact.shape.map((rule) => rule.replaceAll(FQN, extend.id)),\n ...(artifact.example === undefined ? {} : { example: exampleFor(artifact.example, extend, service) }),\n path: ext === null ? rel(at) : `${rel(at)}/`,\n exists,\n action: exists ? \"diff\" : \"create\",\n });\n }\n\n // The eighth target is the fleet map, and it is conditional on TWO facts: an\n // element resolving to the service, and an edge touching it. A service the\n // landscape already draws AND reaches owes it nothing, and briefing an edit\n // there would invite an agent to draw a second box for a service that has\n // one. A service with a box and no edge — what `loam seed` leaves for every\n // service nobody listed under `calls:` — owes it the edges, and until\n // 2026-09-03 element existence alone read as \"the map is complete\" here, so\n // the brief declared the map finished for exactly the services it had not\n // reached. Everything else about the target is unconditional — the file is\n // shared, so `action` is `edit` whenever it exists and `create` only for the\n // very first service of a brand-new docs repo.\n //\n // `modelled === false` and not `!== true`: `null` is \"loam could not read the\n // map\", and it fell through the same branch as \"the map genuinely draws\n // nothing for this service\". The brief then handed over the UNDRAWN target —\n // whose shape rule is \"add one top-level element bound to its directory\" and\n // whose example is a block to splice — for services the map already declares\n // and binds, and an agent that spliced it earned `landscape.binding-duplicate`\n // for doing exactly what the brief asked. With the map unreadable there is no\n // write to ask for; `landscape.instruction` carries the whole answer and names\n // the documents that failed.\n if (landscape.modelled === false || landscape.touched === false) {\n targets.push({\n ...landscapeArtifact({\n service,\n expects: landscape.expects,\n present: landscape.present,\n servicePath: rel(paths.dir),\n // The existing element's id, in the edgeless state only: it switches\n // the example from a block to edges on that element.\n // …and whether it is `#external`, which decides what the shape rule may\n // promise: `landscape.service-isolated` never fires on such an element.\n ...(landscape.touched === false && landscape.elements[0] !== undefined\n ? { elementId: landscape.elements[0].id, external: landscape.elements[0].tags.includes(EXTERNAL_TAG) }\n : {}),\n attested: landscape.attested,\n }),\n path: rel(landscapePath(docsDir)),\n exists: landscape.present,\n action: landscape.present ? \"edit\" : \"create\",\n });\n }\n\n return {\n service,\n docsDir,\n path: rel(paths.dir),\n targets,\n walk: WALK,\n walkClose: WALK_CLOSE,\n landscape,\n frontmatter: FRONTMATTER_BRIEF,\n checks: VALIDATE_CHECKS,\n unchecked: UNCHECKED,\n rule: NEVER_OVERWRITE,\n };\n}\n\n/**\n * Does this directory already hold an artifact of the given extension? The\n * \"exists\" question for the two DIRECTORY targets — `adrs/` (`.md`) and\n * `usecases/` (`.likec4`) — where an empty or absent directory is the same\n * answer, because neither is something to diff against.\n */\nasync function hasEntry(dir: string, ext: string): Promise<boolean> {\n if (!existsSync(dir)) return false;\n const entries = await readdir(dir, { withFileTypes: true });\n // `entryIs` for the symlink case: a Dirent never follows one, so `isFile()`\n // is false for a symlinked ADR and the directory would read as empty — the\n // same rule `services/` and `features/` are enumerated by.\n return entries.some((e) => e.name.endsWith(ext) && entryIs(dir, e, \"file\"));\n}\n\n/**\n * Does the fleet expect an API of this service?\n *\n * The brief marked `openapi.yaml` required unconditionally, and printed the\n * absence as `MISSING` in capitals — while `loam validate` and `loam list`\n * derive the opposite from the same landscape (`service.no-openapi` stays quiet\n * when nothing calls the service, and `list`'s maturity ladder stops demanding\n * a contract). For a UI, a worker or a cron the brief was therefore telling an\n * agent to INVENT an API contract into the source of truth — the one class of\n * guess this whole module exists to refuse, and the one the brief itself names\n * in `openapi.yaml`'s shape rules (`ARTIFACTS`, targets.ts).\n *\n * Positive evidence only, exactly as `list` reads it: a landscape that is\n * absent or unreadable proves nothing about who calls this service, so the\n * contract is still owed. `expects` is the ops on inbound edges, which is\n * `list`'s `called` set seen from one service.\n *\n * `callersKnown` and not `parses`, because since W4 `parses` answers about the\n * whole `architecture/` PROJECT: a broken palette or use case beside a landscape\n * loam reads perfectly made this discard the map and demand an OpenAPI contract\n * of a browser UI — inventing an API into the source of truth, which is the one\n * class of guess this module exists to refuse. `callersKnown` is the narrower\n * fact this question actually needs, and it is the one `list` and `context` read.\n */\nfunction apiExpected(landscape: LandscapeContext): boolean {\n return !landscape.callersKnown || landscape.expects.length > 0;\n}\n"]}
@@ -27,15 +27,6 @@ import { enumeratedServiceIds, serviceTreePathOf } from "../repo/service-target.
27
27
  import { EXTERNAL_TAG, GRADED_TAGS } from "../vocabulary/maturity.js";
28
28
  import { brokenDocuments, instructionFor } from "./map/owed.js";
29
29
  import { properAncestorIds } from "../kernel/ids/fqn/ancestors.js";
30
- /**
31
- * The service ids the resolver may fall back on when an element carries no
32
- * `metadata { service }` binding — the same positive evidence `validate` and
33
- * `list` hand it. A docs repo with no `services/` yet (`adopt` runs there)
34
- * leaves the resolver with bindings alone, which is what it had before.
35
- */
36
- async function knownServices(docsDir) {
37
- return new Set(await enumeratedServiceIds(docsDir));
38
- }
39
30
  /**
40
31
  * The tags loam reads on an ELEMENT. `GRADED_TAGS` is the vocabulary's own
41
32
  * list of the tags that change what loam concludes — spelled once there, so
@@ -45,7 +36,6 @@ async function knownServices(docsDir) {
45
36
  * never sit on one, so they are not in this list.
46
37
  */
47
38
  const LOAM_TAGS = new Set(GRADED_TAGS);
48
- const LOAM_TAG_PREFIXES = [OBLIGATION_TAG_PREFIX];
49
39
  /**
50
40
  * The element's tags loam does NOT read — named in the instruction and never
51
41
  * interpreted. A placeholder convention ("drawn ahead of adoption") is the
@@ -53,7 +43,7 @@ const LOAM_TAG_PREFIXES = [OBLIGATION_TAG_PREFIX];
53
43
  * element.tag = #<that>` keeps an adopted service hidden until it is cleared.
54
44
  */
55
45
  function foreignTagsOf(elements) {
56
- const unread = (t) => !LOAM_TAGS.has(t) && !LOAM_TAG_PREFIXES.some((p) => t.startsWith(p));
46
+ const unread = (t) => !LOAM_TAGS.has(t) && !t.startsWith(OBLIGATION_TAG_PREFIX);
57
47
  return [...new Set(elements.flatMap((e) => e.tags))].filter(unread);
58
48
  }
59
49
  /**
@@ -137,7 +127,13 @@ export async function landscapeContext(docsDir, service, paths) {
137
127
  // this set: reading the landscape alone made `adopt` report `touched: false`,
138
128
  // the attested calls and an edit target for a service the fleet run on the
139
129
  // same tree called matched, with nothing to tell an agent which was right.
140
- const known = await knownServices(docsDir);
130
+ //
131
+ // `known`: the service ids the resolver may fall back on when an element
132
+ // carries no `metadata { service }` binding — the same positive evidence
133
+ // `validate` and `list` hand it. A docs repo with no `services/` yet (`adopt`
134
+ // runs there) leaves the resolver with bindings alone, which is what it had
135
+ // before.
136
+ const known = new Set(await enumeratedServiceIds(docsDir));
141
137
  const land = await loadArchitecture(docsDir);
142
138
  if (land.errors.length > 0) {
143
139
  // Since W4 this arm answers for the whole `architecture/` PROJECT, so the
@@ -1 +1 @@
1
- {"version":3,"file":"landscape.js","sourceRoot":"","sources":["../../../src/core/brief/landscape.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The fleet-map half of the adoption brief, READ side: what the fleet map\n * already says about one service — its elements, its inbound and outbound\n * edges, the operations the fleet already calls on it — and which of the four\n * states the map is in for it. The write the service still owes the file — the\n * block, when nothing in it resolves to the service yet, or the EDGES, when an\n * element does and no edge touches it — is spelled in `./map/owed.ts`\n * (`landscapeArtifact`, `instructionFor`), which this module hands its findings\n * to. Both halves exist to stop an agent inventing a parallel model: the fleet\n * has already drawn some of these boxes and edges, and a baseline has to attach.\n *\n * \"The fleet map\" is the `architecture/` PROJECT — the landscape plus every\n * other `.likec4` under it, minus the generated views file — and not the\n * landscape FILE. The two are the same document set `validate --all` grades\n * `landscape.service-isolated` over, and reading one file here let the two\n * surfaces disagree about the same tree.\n */\nimport { existsSync } from \"node:fs\";\nimport { loadFile } from \"../c4/likec4.js\";\nimport { loadArchitecture } from \"../c4/project/architecture.js\";\nimport { attestedCalls, type AttestedCall } from \"../c4/resolve/attested.js\";\nimport { serviceResolver } from \"../c4/resolve/service.js\";\nimport { ServiceModels } from \"../c4/prefetch/fleet.js\";\nimport { OBLIGATION_TAG_PREFIX } from \"../obligations/obligations.js\";\nimport { landscapePath, type ServicePaths } from \"../repo/paths.js\";\nimport { enumeratedServiceIds, serviceTreePathOf } from \"../repo/service-target.js\";\nimport { EXTERNAL_TAG, GRADED_TAGS } from \"../vocabulary/maturity.js\";\nimport { brokenDocuments, instructionFor } from \"./map/owed.js\";\nimport { properAncestorIds } from \"../kernel/ids/fqn/ancestors.js\";\nimport type { DocsDir } from \"../kernel/ids/dirs.js\";\n\n/* ------------------------------------------------------------------ */\n/* What the fleet already says about this service */\n/* ------------------------------------------------------------------ */\n\nexport interface LandscapeElement {\n id: string;\n title: string;\n kind: string;\n tags: string[];\n /** True when the element binds explicitly with `metadata { service }`. */\n bound: boolean;\n}\n\nexport interface LandscapeEdge {\n from?: string;\n to?: string;\n op: string | null;\n title: string | null;\n}\n\nexport interface LandscapeContext {\n /** Whether `architecture/landscape.likec4` — the file the brief asks for edits to — is on disk. */\n present: boolean;\n /**\n * False when the landscape exists but the `architecture/` project does not\n * parse. A sibling under `architecture/` counts: a broken use case leaves the\n * map unusable for `validate --all` too, and a brief that read the landscape\n * alone would report a fleet nobody can render.\n */\n parses: boolean;\n /**\n * Whether loam could read WHO CALLS this service — the only evidence an EMPTY\n * `expects` may be read as \"nothing calls it\".\n *\n * A separate fact from `parses` because they answer about different documents.\n * `parses` is about the whole `architecture/` PROJECT (W4), and the callers\n * live in the landscape FILE: a broken palette or use case beside a landscape\n * that reads perfectly left `parses` false, and `apiExpected` then demanded an\n * OpenAPI contract of a browser UI the map proves nobody calls — while `loam\n * list` and `loam context`, which read the file (core/pack/living.ts), said the\n * opposite about the same tree.\n */\n callersKnown: boolean;\n /** Whether any element resolves to this service — null when nothing could be read. */\n modelled: boolean | null;\n /**\n * The service-LEVEL elements that resolve to this service: an element whose\n * ancestor also resolves to it is one of its containers, not a second box.\n */\n elements: LandscapeElement[];\n /**\n * The map's edges into and out of this service, INTERNAL edges excluded: an\n * edge whose two endpoints both resolve here (`svc.web -> svc.db`) is this\n * service's own wiring, and filed as inbound it named the service as its own\n * caller.\n */\n inbound: LandscapeEdge[];\n outbound: LandscapeEdge[];\n /** Operations other services already call on this one: the contract owes them. */\n expects: string[];\n /**\n * Whether at least one relationship in the map has an endpoint resolving to\n * this service — read off the relationships, NOT off `inbound`/`outbound`,\n * because an intra-service edge counts here (it is `landscape.service-\n * isolated`'s predicate) while those two lists drop it. `null` exactly when\n * `modelled` is `null` or `false`. It exists\n * because `instruction` used to go null on element existence alone, and\n * `loam seed` writes a bound, edgeless element for every service nobody\n * listed under `calls:` — so the brief declared the map finished for exactly\n * the services the map had not reached.\n *\n * Binary by design, and the fifth silent case of `landscape.service-isolated`\n * with it: ONE edge closes the state, so a map drawing one of five attested\n * calls reports `touched: true`, `attested: []` and no instruction. The check\n * is touched/untouched, not a set difference — loam cannot tell a call the map\n * omits from a call it deliberately does not draw at fleet level, and a\n * difference reported as an omission would be an invented edge with a code\n * beside it.\n */\n touched: boolean | null;\n /**\n * The calls `services/<…>/<svc>/model.likec4` declares ACROSS its boundary\n * (`attestedCalls`, core/c4/resolve/attested.ts). `[]` when the model is\n * absent, unparseable, or declares none — deliberately not told apart, since\n * the only thing the brief may do with the list is name edges ALREADY\n * ATTESTED, never invent one. Read only when `touched` is `false`: every\n * other state either has edges (nothing owed) or no element (the block, not\n * the edges, is owed), and a first adoption has no model to read anyway.\n */\n attested: AttestedCall[];\n /**\n * The write this service still owes the fleet map, in prose — null once an\n * element resolves to it AND an edge touches that element. It duplicates the\n * `landscape.likec4` target's shape rules on purpose: an agent driving off\n * `--json` reads `landscape` to learn what the fleet already says, and every\n * `targets[]` consumer walks the artifact list. Only one of those two readers\n * used to exist, so `adopt` briefed eight files and never once said the ninth\n * thing — that a documented service nobody drew is invisible to the whole\n * cross-service layer.\n */\n instruction: string | null;\n}\n\n/**\n * The service ids the resolver may fall back on when an element carries no\n * `metadata { service }` binding — the same positive evidence `validate` and\n * `list` hand it. A docs repo with no `services/` yet (`adopt` runs there)\n * leaves the resolver with bindings alone, which is what it had before.\n */\nasync function knownServices(docsDir: DocsDir): Promise<ReadonlySet<string>> {\n return new Set(await enumeratedServiceIds(docsDir));\n}\n\n/**\n * The tags loam reads on an ELEMENT. `GRADED_TAGS` is the vocabulary's own\n * list of the tags that change what loam concludes — spelled once there, so\n * the day a third tag is graded this set follows and the instruction below\n * does not tell an agent to clear it as unread. The obligation prefix is the\n * one tag PREFIX loam reads on an element; `#cap-`/`#req-` are view tags and\n * never sit on one, so they are not in this list.\n */\nconst LOAM_TAGS: ReadonlySet<string> = new Set(GRADED_TAGS);\nconst LOAM_TAG_PREFIXES: readonly string[] = [OBLIGATION_TAG_PREFIX];\n\n/**\n * The element's tags loam does NOT read — named in the instruction and never\n * interpreted. A placeholder convention (\"drawn ahead of adoption\") is the\n * fleet's own, `loam seed` writes none, and a view written `exclude\n * element.tag = #<that>` keeps an adopted service hidden until it is cleared.\n */\nfunction foreignTagsOf(elements: LandscapeElement[]): string[] {\n const unread = (t: string): boolean => !LOAM_TAGS.has(t) && !LOAM_TAG_PREFIXES.some((p) => t.startsWith(p));\n return [...new Set(elements.flatMap((e) => e.tags))].filter(unread);\n}\n\n/**\n * The attested calls, or nothing off a model that could not be read: `c4.invalid`\n * is validate's finding, and an edge list read off half a document is invention.\n *\n * Through the ONE reader of `model.likec4` (`core/c4/service-model/load.ts`),\n * because the two shapes are read differently and this brief is handed to an\n * agent that will act on it: a model that EXTENDS the fleet map, opened as a\n * lone file, comes back as a pile of parse errors — so the brief would tell\n * every migrated service that its model attests nothing and quietly drop the\n * edges the map most likely owes. `mapUnreadable` is the same empty answer for\n * the same reason: an extending model whose map does not parse was never read.\n */\nasync function attestedFrom(\n docsDir: DocsDir,\n paths: ServicePaths,\n service: string,\n known: ReadonlySet<string>,\n): Promise<AttestedCall[]> {\n // The memo directly rather than through `core/fleet-context.ts`, and the\n // reason is the package graph: this module already depends on `core/c4/`, and\n // reaching for the read index would add a `brief -> core-root` edge for one\n // read in a command that grades a single service. The three things the memo\n // cannot know for itself are supplied here — the enumeration this function\n // was already handed, the per-file loader, and a counter nobody is keeping.\n const models = new ServiceModels({\n onProjectLoad: () => {\n // `adopt` reports no read counters, so there is nothing to increment. The\n // hook is the memo's way of letting a caller own its own statistics, and\n // an empty one is that answer, not a forgotten line.\n },\n known: async () => known,\n standalone: (path) => loadFile(path),\n });\n const model = await models.model(docsDir, paths);\n if (model.mapUnreadable || model.doc.errors.length > 0) return [];\n return attestedCalls(model.doc, service, known);\n}\n\n/**\n * What the living fleet map already says about this service — the half of the\n * brief that stops an agent inventing a parallel model: the fleet has already\n * drawn some of these boxes and edges, and the baseline has to attach to them.\n * An `architecture/` project that does not parse yields `modelled: null` —\n * \"nothing models it\" would be a claim about documents nobody could read; the\n * landscape FILE parsing on its own does not rescue it, because the map the\n * renderer and `validate --all` see is the whole project. `paths` is the\n * service's own artifact set, located by the caller through the enumeration\n * (never joined at `services/<id>/`): the model is opened in exactly one state —\n * an element with no edge — to name the calls the map most likely owes. The\n * WHOLE record travels rather than the model path alone, because the model is\n * now read through its own shape and an extending one is read as a project of\n * the service's directory plus the map. It is required rather than optional\n * because the edgeless instruction states what that file attests, and a caller\n * that omitted it would get a brief asserting a fact about a document loam never\n * opened.\n */\nexport async function landscapeContext(\n docsDir: DocsDir,\n service: string,\n paths: ServicePaths,\n): Promise<LandscapeContext> {\n const path = landscapePath(docsDir);\n // The service's own directory, spelled from the enumeration rather than joined\n // at the root: this text is handed to an AGENT that will go and edit files, and\n // a filed service named at `services/<id>/` sends it to create a second\n // directory beside the one that already exists.\n const servicePath = await serviceTreePathOf(docsDir, service);\n const bare = { service, expects: [], servicePath, attested: [], foreignTags: [] };\n const empty: LandscapeContext = {\n present: existsSync(path),\n parses: false,\n callersKnown: false,\n modelled: null,\n elements: [],\n inbound: [],\n outbound: [],\n expects: [],\n touched: null,\n attested: [],\n instruction: null,\n };\n if (!empty.present) return { ...empty, modelled: false, instruction: instructionFor({ ...bare, state: \"absent\" }) };\n\n // The `architecture/` PROJECT, through the one loader every other reader\n // shares — never `loadFile(path)`. A fleet spreads its model over more than\n // the landscape (a second `model { }` block in a sibling, a use case, a\n // palette), and `landscape.service-isolated` grades `touched` over exactly\n // this set: reading the landscape alone made `adopt` report `touched: false`,\n // the attested calls and an edit target for a service the fleet run on the\n // same tree called matched, with nothing to tell an agent which was right.\n const known = await knownServices(docsDir);\n const land = await loadArchitecture(docsDir);\n if (land.errors.length > 0) {\n // Since W4 this arm answers for the whole `architecture/` PROJECT, so the\n // document that broke is very often a SIBLING beside a landscape that parses\n // perfectly — and two things followed from not saying so:\n // - the instruction told an agent to go and fix parse errors in\n // `architecture/landscape.likec4`, which has none, contradicting\n // `validate --all` run one second later on the same tree;\n // - the API question — who CALLS this service — was answered \"unknown\"\n // off a landscape file loam can read, so a broken palette flipped\n // `openapi.yaml` to required for a browser UI the map proves nobody\n // calls, while `list` and `context` (which read the FILE, see\n // core/pack/living.ts) answered the opposite about the same tree.\n // Nothing else is rescued: `modelled`, `elements` and `touched` stay\n // unknown, because those are claims about the project the renderer loads.\n const expects = await expectsFromFile(path, service, known);\n return {\n ...empty,\n callersKnown: expects !== null,\n expects: expects ?? [],\n instruction: instructionFor({\n ...bare,\n expects: expects ?? [],\n state: \"unparseable\",\n broken: brokenDocuments(docsDir, land),\n }),\n };\n }\n\n // One resolver for the whole scan, built with the real service ids, and\n // built BEFORE the element filter: \"an element resolves to this service\" is\n // the resolver's answer — a nearest-ancestor binding wins over a descendant's\n // title — and it is the answer `validate --all` grades `service-unmodelled`\n // on and `loam context` prints `modelled` from. This used to ask each\n // element for its OWN binding-or-title instead, so a container titled\n // 'payment-service' inside an element bound to order-service read as\n // payment-service's box, and the edgeless arm told an agent \"do not add a\n // second element\" for a service the fleet run reported unmodelled (error).\n // The same resolver serves the edges: one drawn into a modelled container\n // (`paymentService.api`) is an edge into the service, and `expects` — which\n // decides whether this service owes an OpenAPI contract at all — has to see\n // it. Building it once also stops the per-edge rebuild the old\n // `serviceOf(land.elements, id)` call did.\n const svcOf = serviceResolver(land.elements, known);\n const mine = (id: string): boolean => svcOf(id) === service;\n // The service-LEVEL elements only: one that resolves to the service while\n // an ancestor does too is a container of it, and listing it would show a\n // second box the map does not have.\n const elements: LandscapeElement[] = land.elements\n .filter((e) => mine(e.id) && !properAncestorIds(e.id).some(mine))\n .map((e) => ({\n id: e.id,\n title: e.title,\n kind: e.kind,\n tags: e.tags,\n bound: e.service !== undefined,\n }));\n\n const inbound: LandscapeEdge[] = [];\n const outbound: LandscapeEdge[] = [];\n // An edge whose BOTH endpoints resolve here is the service's own internal\n // wiring: filed as inbound it made the service its own caller (verification\n // 2026-09-04). It still TOUCHES the service, so `touched` below reads the\n // relationships rather than these lists — the isolation check counts either\n // endpoint, and the two predicates must not disagree.\n const self = (r: { source: string; target: string }): boolean => mine(r.source) && mine(r.target);\n for (const r of land.relationships) {\n if (self(r)) continue;\n const edge = { op: r.op ?? null, title: r.title ?? null };\n if (mine(r.target)) inbound.push({ from: svcOf(r.source), ...edge });\n else if (mine(r.source)) outbound.push({ to: svcOf(r.target), ...edge });\n }\n\n const expects = [...new Set(inbound.map((e) => e.op).filter((op): op is string => op !== null))];\n const modelled = land.elements.some((e) => mine(e.id));\n const touched = modelled ? land.relationships.some((r) => mine(r.source) || mine(r.target)) : null;\n const attested =\n touched === false && existsSync(paths.model) ? await attestedFrom(docsDir, paths, service, known) : [];\n const req = {\n service,\n expects,\n servicePath,\n attested,\n foreignTags: foreignTagsOf(elements),\n elementId: elements[0]?.id,\n // The one graded tag that changes what the brief may PROMISE: an `#external`\n // element is never `landscape.service-isolated`'s subject. Off the same\n // element `elementId` names, never the union.\n external: elements[0]?.tags.includes(EXTERNAL_TAG) ?? false,\n };\n const instruction = !modelled\n ? instructionFor({ ...req, state: \"unmodelled\" })\n : touched === false\n ? instructionFor({ ...req, state: \"edgeless\" })\n : null;\n return {\n present: true,\n parses: true,\n callersKnown: true,\n modelled,\n elements,\n inbound,\n outbound,\n expects,\n touched,\n attested,\n instruction,\n };\n}\n\n/**\n * The operations the fleet already calls on this service, read off the landscape\n * FILE alone — `null` when that file cannot be read either.\n *\n * The fallback for an `architecture/` project that does not parse, and the\n * narrowest one that can be justified: it answers ONE question, the one `loam\n * list` and `loam context` answer off the same bytes (`landscapeEvidence`,\n * core/vocabulary/maturity.ts). \"Nothing calls this service\" and \"loam could not\n * tell\" are different answers, and only the first may switch an API contract\n * off, which is why the empty list and the unreadable file are told apart here\n * rather than collapsed to `[]`.\n */\nasync function expectsFromFile(path: string, service: string, known: ReadonlySet<string>): Promise<string[] | null> {\n try {\n const doc = await loadFile(path);\n if (doc.errors.length > 0) return null;\n const svcOf = serviceResolver(doc.elements, known);\n const ops = doc.relationships\n .filter((r) => svcOf(r.target) === service)\n .map((r) => r.op)\n .filter((op): op is string => op !== undefined);\n return [...new Set(ops)];\n } catch {\n return null;\n }\n}\n"]}
1
+ {"version":3,"file":"landscape.js","sourceRoot":"","sources":["../../../src/core/brief/landscape.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The fleet-map half of the adoption brief, READ side: what the fleet map\n * already says about one service — its elements, its inbound and outbound\n * edges, the operations the fleet already calls on it — and which of the four\n * states the map is in for it. The write the service still owes the file — the\n * block, when nothing in it resolves to the service yet, or the EDGES, when an\n * element does and no edge touches it — is spelled in `./map/owed.ts`\n * (`landscapeArtifact`, `instructionFor`), which this module hands its findings\n * to. Both halves exist to stop an agent inventing a parallel model: the fleet\n * has already drawn some of these boxes and edges, and a baseline has to attach.\n *\n * \"The fleet map\" is the `architecture/` PROJECT — the landscape plus every\n * other `.likec4` under it, minus the generated views file — and not the\n * landscape FILE. The two are the same document set `validate --all` grades\n * `landscape.service-isolated` over, and reading one file here let the two\n * surfaces disagree about the same tree.\n */\nimport { existsSync } from \"node:fs\";\nimport { loadFile } from \"../c4/likec4.js\";\nimport { loadArchitecture } from \"../c4/project/architecture.js\";\nimport { attestedCalls, type AttestedCall } from \"../c4/resolve/attested.js\";\nimport { serviceResolver } from \"../c4/resolve/service.js\";\nimport { ServiceModels } from \"../c4/prefetch/fleet.js\";\nimport { OBLIGATION_TAG_PREFIX } from \"../obligations/obligations.js\";\nimport { landscapePath, type ServicePaths } from \"../repo/paths.js\";\nimport { enumeratedServiceIds, serviceTreePathOf } from \"../repo/service-target.js\";\nimport { EXTERNAL_TAG, GRADED_TAGS } from \"../vocabulary/maturity.js\";\nimport { brokenDocuments, instructionFor } from \"./map/owed.js\";\nimport { properAncestorIds } from \"../kernel/ids/fqn/ancestors.js\";\nimport type { DocsDir } from \"../kernel/ids/dirs.js\";\n\n/* ------------------------------------------------------------------ */\n/* What the fleet already says about this service */\n/* ------------------------------------------------------------------ */\n\nexport interface LandscapeElement {\n id: string;\n title: string;\n kind: string;\n tags: string[];\n /** True when the element binds explicitly with `metadata { service }`. */\n bound: boolean;\n}\n\nexport interface LandscapeEdge {\n from?: string;\n to?: string;\n op: string | null;\n title: string | null;\n}\n\nexport interface LandscapeContext {\n /** Whether `architecture/landscape.likec4` — the file the brief asks for edits to — is on disk. */\n present: boolean;\n /**\n * False when the landscape exists but the `architecture/` project does not\n * parse. A sibling under `architecture/` counts: a broken use case leaves the\n * map unusable for `validate --all` too, and a brief that read the landscape\n * alone would report a fleet nobody can render.\n */\n parses: boolean;\n /**\n * Whether loam could read WHO CALLS this service — the only evidence an EMPTY\n * `expects` may be read as \"nothing calls it\".\n *\n * A separate fact from `parses` because they answer about different documents.\n * `parses` is about the whole `architecture/` PROJECT (W4), and the callers\n * live in the landscape FILE: a broken palette or use case beside a landscape\n * that reads perfectly left `parses` false, and `apiExpected` then demanded an\n * OpenAPI contract of a browser UI the map proves nobody calls — while `loam\n * list` and `loam context`, which read the file (core/pack/living.ts), said the\n * opposite about the same tree.\n */\n callersKnown: boolean;\n /** Whether any element resolves to this service — null when nothing could be read. */\n modelled: boolean | null;\n /**\n * The service-LEVEL elements that resolve to this service: an element whose\n * ancestor also resolves to it is one of its containers, not a second box.\n */\n elements: LandscapeElement[];\n /**\n * The map's edges into and out of this service, INTERNAL edges excluded: an\n * edge whose two endpoints both resolve here (`svc.web -> svc.db`) is this\n * service's own wiring, and filed as inbound it named the service as its own\n * caller.\n */\n inbound: LandscapeEdge[];\n outbound: LandscapeEdge[];\n /** Operations other services already call on this one: the contract owes them. */\n expects: string[];\n /**\n * Whether at least one relationship in the map has an endpoint resolving to\n * this service — read off the relationships, NOT off `inbound`/`outbound`,\n * because an intra-service edge counts here (it is `landscape.service-\n * isolated`'s predicate) while those two lists drop it. `null` exactly when\n * `modelled` is `null` or `false`. It exists\n * because `instruction` used to go null on element existence alone, and\n * `loam seed` writes a bound, edgeless element for every service nobody\n * listed under `calls:` — so the brief declared the map finished for exactly\n * the services the map had not reached.\n *\n * Binary by design, and the fifth silent case of `landscape.service-isolated`\n * with it: ONE edge closes the state, so a map drawing one of five attested\n * calls reports `touched: true`, `attested: []` and no instruction. The check\n * is touched/untouched, not a set difference — loam cannot tell a call the map\n * omits from a call it deliberately does not draw at fleet level, and a\n * difference reported as an omission would be an invented edge with a code\n * beside it.\n */\n touched: boolean | null;\n /**\n * The calls `services/<…>/<svc>/model.likec4` declares ACROSS its boundary\n * (`attestedCalls`, core/c4/resolve/attested.ts). `[]` when the model is\n * absent, unparseable, or declares none — deliberately not told apart, since\n * the only thing the brief may do with the list is name edges ALREADY\n * ATTESTED, never invent one. Read only when `touched` is `false`: every\n * other state either has edges (nothing owed) or no element (the block, not\n * the edges, is owed), and a first adoption has no model to read anyway.\n */\n attested: AttestedCall[];\n /**\n * The write this service still owes the fleet map, in prose — null once an\n * element resolves to it AND an edge touches that element. It duplicates the\n * `landscape.likec4` target's shape rules on purpose: an agent driving off\n * `--json` reads `landscape` to learn what the fleet already says, and every\n * `targets[]` consumer walks the artifact list. Only one of those two readers\n * used to exist, so `adopt` briefed eight files and never once said the ninth\n * thing — that a documented service nobody drew is invisible to the whole\n * cross-service layer.\n */\n instruction: string | null;\n}\n\n/**\n * The tags loam reads on an ELEMENT. `GRADED_TAGS` is the vocabulary's own\n * list of the tags that change what loam concludes — spelled once there, so\n * the day a third tag is graded this set follows and the instruction below\n * does not tell an agent to clear it as unread. The obligation prefix is the\n * one tag PREFIX loam reads on an element; `#cap-`/`#req-` are view tags and\n * never sit on one, so they are not in this list.\n */\nconst LOAM_TAGS: ReadonlySet<string> = new Set(GRADED_TAGS);\n\n/**\n * The element's tags loam does NOT read — named in the instruction and never\n * interpreted. A placeholder convention (\"drawn ahead of adoption\") is the\n * fleet's own, `loam seed` writes none, and a view written `exclude\n * element.tag = #<that>` keeps an adopted service hidden until it is cleared.\n */\nfunction foreignTagsOf(elements: LandscapeElement[]): string[] {\n const unread = (t: string): boolean => !LOAM_TAGS.has(t) && !t.startsWith(OBLIGATION_TAG_PREFIX);\n return [...new Set(elements.flatMap((e) => e.tags))].filter(unread);\n}\n\n/**\n * The attested calls, or nothing off a model that could not be read: `c4.invalid`\n * is validate's finding, and an edge list read off half a document is invention.\n *\n * Through the ONE reader of `model.likec4` (`core/c4/service-model/load.ts`),\n * because the two shapes are read differently and this brief is handed to an\n * agent that will act on it: a model that EXTENDS the fleet map, opened as a\n * lone file, comes back as a pile of parse errors — so the brief would tell\n * every migrated service that its model attests nothing and quietly drop the\n * edges the map most likely owes. `mapUnreadable` is the same empty answer for\n * the same reason: an extending model whose map does not parse was never read.\n */\nasync function attestedFrom(\n docsDir: DocsDir,\n paths: ServicePaths,\n service: string,\n known: ReadonlySet<string>,\n): Promise<AttestedCall[]> {\n // The memo directly rather than through `core/fleet-context.ts`, and the\n // reason is the package graph: this module already depends on `core/c4/`, and\n // reaching for the read index would add a `brief -> core-root` edge for one\n // read in a command that grades a single service. The three things the memo\n // cannot know for itself are supplied here — the enumeration this function\n // was already handed, the per-file loader, and a counter nobody is keeping.\n const models = new ServiceModels({\n onProjectLoad: () => {\n // `adopt` reports no read counters, so there is nothing to increment. The\n // hook is the memo's way of letting a caller own its own statistics, and\n // an empty one is that answer, not a forgotten line.\n },\n known: async () => known,\n standalone: (path) => loadFile(path),\n });\n const model = await models.model(docsDir, paths);\n if (model.mapUnreadable || model.doc.errors.length > 0) return [];\n return attestedCalls(model.doc, service, known);\n}\n\n/**\n * What the living fleet map already says about this service — the half of the\n * brief that stops an agent inventing a parallel model: the fleet has already\n * drawn some of these boxes and edges, and the baseline has to attach to them.\n * An `architecture/` project that does not parse yields `modelled: null` —\n * \"nothing models it\" would be a claim about documents nobody could read; the\n * landscape FILE parsing on its own does not rescue it, because the map the\n * renderer and `validate --all` see is the whole project. `paths` is the\n * service's own artifact set, located by the caller through the enumeration\n * (never joined at `services/<id>/`): the model is opened in exactly one state —\n * an element with no edge — to name the calls the map most likely owes. The\n * WHOLE record travels rather than the model path alone, because the model is\n * now read through its own shape and an extending one is read as a project of\n * the service's directory plus the map. It is required rather than optional\n * because the edgeless instruction states what that file attests, and a caller\n * that omitted it would get a brief asserting a fact about a document loam never\n * opened.\n */\nexport async function landscapeContext(\n docsDir: DocsDir,\n service: string,\n paths: ServicePaths,\n): Promise<LandscapeContext> {\n const path = landscapePath(docsDir);\n // The service's own directory, spelled from the enumeration rather than joined\n // at the root: this text is handed to an AGENT that will go and edit files, and\n // a filed service named at `services/<id>/` sends it to create a second\n // directory beside the one that already exists.\n const servicePath = await serviceTreePathOf(docsDir, service);\n const bare = { service, expects: [], servicePath, attested: [], foreignTags: [] };\n const empty: LandscapeContext = {\n present: existsSync(path),\n parses: false,\n callersKnown: false,\n modelled: null,\n elements: [],\n inbound: [],\n outbound: [],\n expects: [],\n touched: null,\n attested: [],\n instruction: null,\n };\n if (!empty.present) return { ...empty, modelled: false, instruction: instructionFor({ ...bare, state: \"absent\" }) };\n\n // The `architecture/` PROJECT, through the one loader every other reader\n // shares — never `loadFile(path)`. A fleet spreads its model over more than\n // the landscape (a second `model { }` block in a sibling, a use case, a\n // palette), and `landscape.service-isolated` grades `touched` over exactly\n // this set: reading the landscape alone made `adopt` report `touched: false`,\n // the attested calls and an edit target for a service the fleet run on the\n // same tree called matched, with nothing to tell an agent which was right.\n //\n // `known`: the service ids the resolver may fall back on when an element\n // carries no `metadata { service }` binding — the same positive evidence\n // `validate` and `list` hand it. A docs repo with no `services/` yet (`adopt`\n // runs there) leaves the resolver with bindings alone, which is what it had\n // before.\n const known: ReadonlySet<string> = new Set(await enumeratedServiceIds(docsDir));\n const land = await loadArchitecture(docsDir);\n if (land.errors.length > 0) {\n // Since W4 this arm answers for the whole `architecture/` PROJECT, so the\n // document that broke is very often a SIBLING beside a landscape that parses\n // perfectly — and two things followed from not saying so:\n // - the instruction told an agent to go and fix parse errors in\n // `architecture/landscape.likec4`, which has none, contradicting\n // `validate --all` run one second later on the same tree;\n // - the API question — who CALLS this service — was answered \"unknown\"\n // off a landscape file loam can read, so a broken palette flipped\n // `openapi.yaml` to required for a browser UI the map proves nobody\n // calls, while `list` and `context` (which read the FILE, see\n // core/pack/living.ts) answered the opposite about the same tree.\n // Nothing else is rescued: `modelled`, `elements` and `touched` stay\n // unknown, because those are claims about the project the renderer loads.\n const expects = await expectsFromFile(path, service, known);\n return {\n ...empty,\n callersKnown: expects !== null,\n expects: expects ?? [],\n instruction: instructionFor({\n ...bare,\n expects: expects ?? [],\n state: \"unparseable\",\n broken: brokenDocuments(docsDir, land),\n }),\n };\n }\n\n // One resolver for the whole scan, built with the real service ids, and\n // built BEFORE the element filter: \"an element resolves to this service\" is\n // the resolver's answer — a nearest-ancestor binding wins over a descendant's\n // title — and it is the answer `validate --all` grades `service-unmodelled`\n // on and `loam context` prints `modelled` from. This used to ask each\n // element for its OWN binding-or-title instead, so a container titled\n // 'payment-service' inside an element bound to order-service read as\n // payment-service's box, and the edgeless arm told an agent \"do not add a\n // second element\" for a service the fleet run reported unmodelled (error).\n // The same resolver serves the edges: one drawn into a modelled container\n // (`paymentService.api`) is an edge into the service, and `expects` — which\n // decides whether this service owes an OpenAPI contract at all — has to see\n // it. Building it once also stops the per-edge rebuild the old\n // `serviceOf(land.elements, id)` call did.\n const svcOf = serviceResolver(land.elements, known);\n const mine = (id: string): boolean => svcOf(id) === service;\n // The service-LEVEL elements only: one that resolves to the service while\n // an ancestor does too is a container of it, and listing it would show a\n // second box the map does not have.\n const elements: LandscapeElement[] = land.elements\n .filter((e) => mine(e.id) && !properAncestorIds(e.id).some(mine))\n .map((e) => ({\n id: e.id,\n title: e.title,\n kind: e.kind,\n tags: e.tags,\n bound: e.service !== undefined,\n }));\n\n const inbound: LandscapeEdge[] = [];\n const outbound: LandscapeEdge[] = [];\n // An edge whose BOTH endpoints resolve here is the service's own internal\n // wiring: filed as inbound it made the service its own caller (verification\n // 2026-09-04). It still TOUCHES the service, so `touched` below reads the\n // relationships rather than these lists — the isolation check counts either\n // endpoint, and the two predicates must not disagree.\n const self = (r: { source: string; target: string }): boolean => mine(r.source) && mine(r.target);\n for (const r of land.relationships) {\n if (self(r)) continue;\n const edge = { op: r.op ?? null, title: r.title ?? null };\n if (mine(r.target)) inbound.push({ from: svcOf(r.source), ...edge });\n else if (mine(r.source)) outbound.push({ to: svcOf(r.target), ...edge });\n }\n\n const expects = [...new Set(inbound.map((e) => e.op).filter((op): op is string => op !== null))];\n const modelled = land.elements.some((e) => mine(e.id));\n const touched = modelled ? land.relationships.some((r) => mine(r.source) || mine(r.target)) : null;\n const attested =\n touched === false && existsSync(paths.model) ? await attestedFrom(docsDir, paths, service, known) : [];\n const req = {\n service,\n expects,\n servicePath,\n attested,\n foreignTags: foreignTagsOf(elements),\n elementId: elements[0]?.id,\n // The one graded tag that changes what the brief may PROMISE: an `#external`\n // element is never `landscape.service-isolated`'s subject. Off the same\n // element `elementId` names, never the union.\n external: elements[0]?.tags.includes(EXTERNAL_TAG) ?? false,\n };\n const instruction = !modelled\n ? instructionFor({ ...req, state: \"unmodelled\" })\n : touched === false\n ? instructionFor({ ...req, state: \"edgeless\" })\n : null;\n return {\n present: true,\n parses: true,\n callersKnown: true,\n modelled,\n elements,\n inbound,\n outbound,\n expects,\n touched,\n attested,\n instruction,\n };\n}\n\n/**\n * The operations the fleet already calls on this service, read off the landscape\n * FILE alone — `null` when that file cannot be read either.\n *\n * The fallback for an `architecture/` project that does not parse, and the\n * narrowest one that can be justified: it answers ONE question, the one `loam\n * list` and `loam context` answer off the same bytes (`landscapeEvidence`,\n * core/vocabulary/maturity.ts). \"Nothing calls this service\" and \"loam could not\n * tell\" are different answers, and only the first may switch an API contract\n * off, which is why the empty list and the unreadable file are told apart here\n * rather than collapsed to `[]`.\n */\nasync function expectsFromFile(path: string, service: string, known: ReadonlySet<string>): Promise<string[] | null> {\n try {\n const doc = await loadFile(path);\n if (doc.errors.length > 0) return null;\n const svcOf = serviceResolver(doc.elements, known);\n const ops = doc.relationships\n .filter((r) => svcOf(r.target) === service)\n .map((r) => r.op)\n .filter((op): op is string => op !== undefined);\n return [...new Set(ops)];\n } catch {\n return null;\n }\n}\n"]}
@@ -1,11 +1,14 @@
1
1
  /** The literals `targets.ts` leaves in the model target for this module to fill in. */
2
2
  export const FQN = "<fqn>";
3
3
  /**
4
- * The service being adopted, in the model example's header. It is a placeholder
5
- * for the same reason `<fqn>` is: the example is handed to an agent that copies
6
- * it, and a header naming payment-service travelled verbatim into every other
7
- * service's model — the more convincingly for sitting one line above an
8
- * `extend` the brief HAD substituted.
4
+ * The service being adopted, in the model example's header and in the spec
5
+ * examples' frontmatter and title. It is a placeholder for the same reason
6
+ * `<fqn>` is: the example is handed to an agent that copies it, and a header
7
+ * naming payment-service travelled verbatim into every other service's model —
8
+ * the more convincingly for sitting one line above an `extend` the brief HAD
9
+ * substituted. In spec.md and arch.spec.md the literal was worse than
10
+ * misleading: `service:` must name the directory the file sits in, so a
11
+ * verbatim paste failed `frontmatter.field-mismatch` everywhere but one service.
9
12
  */
10
13
  export const SVC = "<svc>";
11
14
  /** The token an example carries where loam has no id to substitute. Short on purpose — see `ExtendPoint`. */