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

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 (901) hide show
  1. package/CHANGELOG.md +3448 -0
  2. package/COMPARISON.md +91 -3
  3. package/CONTRIBUTING.md +37 -0
  4. package/MIGRATING-from-OpenSpec.md +47 -23
  5. package/QUICKSTART.md +189 -12
  6. package/README.md +274 -65
  7. package/ROADMAP.md +572 -31
  8. package/SCHEMA.md +1709 -155
  9. package/WORKFLOW.md +298 -72
  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/adopt/render.js +50 -9
  15. package/dist/commands/adopt/render.js.map +1 -1
  16. package/dist/commands/archive/archive.js +16 -19
  17. package/dist/commands/archive/archive.js.map +1 -1
  18. package/dist/commands/archive/closing.js +42 -0
  19. package/dist/commands/archive/closing.js.map +1 -0
  20. package/dist/commands/archive/plan/contracts/asyncapi.js +36 -42
  21. package/dist/commands/archive/plan/contracts/asyncapi.js.map +1 -1
  22. package/dist/commands/archive/plan/contracts/consumers.js +56 -0
  23. package/dist/commands/archive/plan/contracts/consumers.js.map +1 -0
  24. package/dist/commands/archive/plan/contracts/openapi.js +77 -43
  25. package/dist/commands/archive/plan/contracts/openapi.js.map +1 -1
  26. package/dist/commands/archive/plan/contracts/verdict-lines.js +103 -0
  27. package/dist/commands/archive/plan/contracts/verdict-lines.js.map +1 -0
  28. package/dist/commands/archive/plan/document/head.js +151 -0
  29. package/dist/commands/archive/plan/document/head.js.map +1 -0
  30. package/dist/commands/archive/plan/document/links.js +284 -0
  31. package/dist/commands/archive/plan/document/links.js.map +1 -0
  32. package/dist/commands/archive/plan/document/run.js +136 -0
  33. package/dist/commands/archive/plan/document/run.js.map +1 -0
  34. package/dist/commands/archive/plan/gate.js +21 -19
  35. package/dist/commands/archive/plan/gate.js.map +1 -1
  36. package/dist/commands/archive/plan/landscape.js +167 -30
  37. package/dist/commands/archive/plan/landscape.js.map +1 -1
  38. package/dist/commands/archive/plan/model/extending.js +85 -0
  39. package/dist/commands/archive/plan/model/extending.js.map +1 -0
  40. package/dist/commands/archive/plan/refusal.js +14 -19
  41. package/dist/commands/archive/plan/refusal.js.map +1 -1
  42. package/dist/commands/archive/plan/specs.js +137 -56
  43. package/dist/commands/archive/plan/specs.js.map +1 -1
  44. package/dist/commands/archive/plan/state.js +3 -0
  45. package/dist/commands/archive/plan/state.js.map +1 -1
  46. package/dist/commands/archive/run.js +134 -30
  47. package/dist/commands/archive/run.js.map +1 -1
  48. package/dist/commands/archive/shipped.js +49 -0
  49. package/dist/commands/archive/shipped.js.map +1 -0
  50. package/dist/commands/context/context.js +85 -18
  51. package/dist/commands/context/context.js.map +1 -1
  52. package/dist/commands/context/print.js +29 -7
  53. package/dist/commands/context/print.js.map +1 -1
  54. package/dist/commands/delta/delta.js +102 -14
  55. package/dist/commands/delta/delta.js.map +1 -1
  56. package/dist/commands/delta/print.js +17 -1
  57. package/dist/commands/delta/print.js.map +1 -1
  58. package/dist/commands/dependencies.js +3 -7
  59. package/dist/commands/dependencies.js.map +1 -1
  60. package/dist/commands/diff/diff.js +20 -27
  61. package/dist/commands/diff/diff.js.map +1 -1
  62. package/dist/commands/doctor.js +60 -1
  63. package/dist/commands/doctor.js.map +1 -1
  64. package/dist/commands/explain/explain.js +89 -13
  65. package/dist/commands/explain/explain.js.map +1 -1
  66. package/dist/commands/explore.js +16 -10
  67. package/dist/commands/explore.js.map +1 -1
  68. package/dist/commands/gate/gate.js +45 -8
  69. package/dist/commands/gate/gate.js.map +1 -1
  70. package/dist/commands/gate/print.js +15 -3
  71. package/dist/commands/gate/print.js.map +1 -1
  72. package/dist/commands/gherkin/commit.js +13 -54
  73. package/dist/commands/gherkin/commit.js.map +1 -1
  74. package/dist/commands/gherkin/gherkin.js +36 -20
  75. package/dist/commands/gherkin/gherkin.js.map +1 -1
  76. package/dist/commands/gherkin/reconcile.js +10 -13
  77. package/dist/commands/gherkin/reconcile.js.map +1 -1
  78. package/dist/commands/init/first-hour.js +44 -0
  79. package/dist/commands/init/first-hour.js.map +1 -1
  80. package/dist/commands/init/init.js +18 -22
  81. package/dist/commands/init/init.js.map +1 -1
  82. package/dist/commands/list/campaign/campaign.js +4 -6
  83. package/dist/commands/list/campaign/campaign.js.map +1 -1
  84. package/dist/commands/list/json.js +5 -7
  85. package/dist/commands/list/json.js.map +1 -1
  86. package/dist/commands/list/list.js +44 -29
  87. package/dist/commands/list/list.js.map +1 -1
  88. package/dist/commands/list/print.js +52 -9
  89. package/dist/commands/list/print.js.map +1 -1
  90. package/dist/commands/list/suites/suites.js +140 -0
  91. package/dist/commands/list/suites/suites.js.map +1 -0
  92. package/dist/commands/list/views.js +37 -34
  93. package/dist/commands/list/views.js.map +1 -1
  94. package/dist/commands/mcp/dispatch.js +91 -36
  95. package/dist/commands/mcp/dispatch.js.map +1 -1
  96. package/dist/commands/mcp/mcp.js +56 -15
  97. package/dist/commands/mcp/mcp.js.map +1 -1
  98. package/dist/commands/mcp/serve.js.map +1 -1
  99. package/dist/commands/migrate-openspec/materialize/active.js +27 -22
  100. package/dist/commands/migrate-openspec/materialize/active.js.map +1 -1
  101. package/dist/commands/migrate-openspec/materialize/stage.js +4 -3
  102. package/dist/commands/migrate-openspec/materialize/stage.js.map +1 -1
  103. package/dist/commands/migrate-openspec/materialize/target.js +2 -2
  104. package/dist/commands/migrate-openspec/materialize/target.js.map +1 -1
  105. package/dist/commands/migrate-openspec/openspec/decisions.js +9 -14
  106. package/dist/commands/migrate-openspec/openspec/decisions.js.map +1 -1
  107. package/dist/commands/migrate-openspec/openspec/error.js +7 -8
  108. package/dist/commands/migrate-openspec/openspec/error.js.map +1 -1
  109. package/dist/commands/migrate-openspec/openspec/mapping.js +1 -9
  110. package/dist/commands/migrate-openspec/openspec/mapping.js.map +1 -1
  111. package/dist/commands/migrate-openspec/openspec/paths.js +3 -6
  112. package/dist/commands/migrate-openspec/openspec/paths.js.map +1 -1
  113. package/dist/commands/new/capability-template.js +118 -0
  114. package/dist/commands/new/capability-template.js.map +1 -0
  115. package/dist/commands/new/commit.js +8 -22
  116. package/dist/commands/new/commit.js.map +1 -1
  117. package/dist/commands/new/modifies.js +206 -0
  118. package/dist/commands/new/modifies.js.map +1 -0
  119. package/dist/commands/new/new.js +59 -14
  120. package/dist/commands/new/new.js.map +1 -1
  121. package/dist/commands/new/templates.js +48 -124
  122. package/dist/commands/new/templates.js.map +1 -1
  123. package/dist/commands/open.js +3 -7
  124. package/dist/commands/open.js.map +1 -1
  125. package/dist/commands/policy/gate.js +27 -0
  126. package/dist/commands/policy/gate.js.map +1 -1
  127. package/dist/commands/rebase/plan.js +2 -2
  128. package/dist/commands/rebase/plan.js.map +1 -1
  129. package/dist/commands/rebase/rebase.js +60 -35
  130. package/dist/commands/rebase/rebase.js.map +1 -1
  131. package/dist/commands/seed/commit.js +55 -26
  132. package/dist/commands/seed/commit.js.map +1 -1
  133. package/dist/commands/seed/plan.js +158 -13
  134. package/dist/commands/seed/plan.js.map +1 -1
  135. package/dist/commands/seed/seed.js +13 -11
  136. package/dist/commands/seed/seed.js.map +1 -1
  137. package/dist/commands/show/feature.js +9 -5
  138. package/dist/commands/show/feature.js.map +1 -1
  139. package/dist/commands/show/introduced.js +157 -0
  140. package/dist/commands/show/introduced.js.map +1 -0
  141. package/dist/commands/show/marks.js +35 -6
  142. package/dist/commands/show/marks.js.map +1 -1
  143. package/dist/commands/show/service.js +41 -33
  144. package/dist/commands/show/service.js.map +1 -1
  145. package/dist/commands/show/show.js +17 -7
  146. package/dist/commands/show/show.js.map +1 -1
  147. package/dist/commands/status/fleet-next.js +90 -0
  148. package/dist/commands/status/fleet-next.js.map +1 -0
  149. package/dist/commands/status/json.js +2 -0
  150. package/dist/commands/status/json.js.map +1 -1
  151. package/dist/commands/status/print.js +89 -37
  152. package/dist/commands/status/print.js.map +1 -1
  153. package/dist/commands/status/status.js +3 -7
  154. package/dist/commands/status/status.js.map +1 -1
  155. package/dist/commands/steps/steps.js +103 -23
  156. package/dist/commands/steps/steps.js.map +1 -1
  157. package/dist/commands/subsystem/edit.js +7 -7
  158. package/dist/commands/subsystem/edit.js.map +1 -1
  159. package/dist/commands/subsystem/move.js +106 -46
  160. package/dist/commands/subsystem/move.js.map +1 -1
  161. package/dist/commands/subsystem/query.js +2 -4
  162. package/dist/commands/subsystem/query.js.map +1 -1
  163. package/dist/commands/subsystem/subsystem.js +4 -7
  164. package/dist/commands/subsystem/subsystem.js.map +1 -1
  165. package/dist/commands/subsystem/sync.js +289 -74
  166. package/dist/commands/subsystem/sync.js.map +1 -1
  167. package/dist/commands/subsystem/txn/exclude.js +132 -0
  168. package/dist/commands/subsystem/txn/exclude.js.map +1 -0
  169. package/dist/commands/subsystem/txn/links.js +128 -0
  170. package/dist/commands/subsystem/txn/links.js.map +1 -0
  171. package/dist/commands/subsystem/txn/report.js +174 -0
  172. package/dist/commands/subsystem/txn/report.js.map +1 -0
  173. package/dist/commands/subsystem/txn/txn.js +147 -45
  174. package/dist/commands/subsystem/txn/txn.js.map +1 -1
  175. package/dist/commands/subsystem/txn/views.js +200 -21
  176. package/dist/commands/subsystem/txn/views.js.map +1 -1
  177. package/dist/commands/tasks/actions.js +107 -0
  178. package/dist/commands/tasks/actions.js.map +1 -0
  179. package/dist/commands/tasks/render.js +253 -0
  180. package/dist/commands/tasks/render.js.map +1 -0
  181. package/dist/commands/tasks/tasks.js +119 -0
  182. package/dist/commands/tasks/tasks.js.map +1 -0
  183. package/dist/commands/unarchive/manifest.js +42 -26
  184. package/dist/commands/unarchive/manifest.js.map +1 -1
  185. package/dist/commands/unarchive/restore.js +83 -33
  186. package/dist/commands/unarchive/restore.js.map +1 -1
  187. package/dist/commands/unarchive/unarchive.js +22 -30
  188. package/dist/commands/unarchive/unarchive.js.map +1 -1
  189. package/dist/commands/validate/arch-coverage.js +78 -27
  190. package/dist/commands/validate/arch-coverage.js.map +1 -1
  191. package/dist/commands/validate/checks/fleet-shape.js +34 -126
  192. package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
  193. package/dist/commands/validate/checks/health-deps.js +9 -6
  194. package/dist/commands/validate/checks/health-deps.js.map +1 -1
  195. package/dist/commands/validate/checks/introduced.js +102 -0
  196. package/dist/commands/validate/checks/introduced.js.map +1 -0
  197. package/dist/commands/validate/checks/requirements.js +18 -8
  198. package/dist/commands/validate/checks/requirements.js.map +1 -1
  199. package/dist/commands/validate/checks/sources.js +11 -2
  200. package/dist/commands/validate/checks/sources.js.map +1 -1
  201. package/dist/commands/validate/coverage/covers-after.js +105 -0
  202. package/dist/commands/validate/coverage/covers-after.js.map +1 -0
  203. package/dist/commands/validate/feature.js +26 -20
  204. package/dist/commands/validate/feature.js.map +1 -1
  205. package/dist/commands/validate/fleet/census.js +37 -13
  206. package/dist/commands/validate/fleet/census.js.map +1 -1
  207. package/dist/commands/validate/fleet/landscape.js +144 -126
  208. package/dist/commands/validate/fleet/landscape.js.map +1 -1
  209. package/dist/commands/validate/fleet/load.js +66 -0
  210. package/dist/commands/validate/fleet/load.js.map +1 -1
  211. package/dist/commands/validate/fleet/map/attest.js +98 -0
  212. package/dist/commands/validate/fleet/map/attest.js.map +1 -0
  213. package/dist/commands/validate/fleet/map/bindings.js +107 -0
  214. package/dist/commands/validate/fleet/map/bindings.js.map +1 -0
  215. package/dist/commands/validate/fleet/map/consumers.js +244 -0
  216. package/dist/commands/validate/fleet/map/consumers.js.map +1 -0
  217. package/dist/commands/validate/fleet/map/isolation.js +116 -0
  218. package/dist/commands/validate/fleet/map/isolation.js.map +1 -0
  219. package/dist/commands/validate/fleet/map/usecases.js +54 -0
  220. package/dist/commands/validate/fleet/map/usecases.js.map +1 -0
  221. package/dist/commands/validate/fleet/obligations.js +44 -14
  222. package/dist/commands/validate/fleet/obligations.js.map +1 -1
  223. package/dist/commands/validate/fleet/scorecard/print.js +10 -2
  224. package/dist/commands/validate/fleet/scorecard/print.js.map +1 -1
  225. package/dist/commands/validate/fleet/scorecard/scorecard.js +17 -11
  226. package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -1
  227. package/dist/commands/validate/fleet/usecases/place.js.map +1 -1
  228. package/dist/commands/validate/fleet/views/fleet-project.js +217 -0
  229. package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -0
  230. package/dist/commands/validate/fleet/views/ids.js +50 -15
  231. package/dist/commands/validate/fleet/views/ids.js.map +1 -1
  232. package/dist/commands/validate/fleet/views/projects.js +278 -0
  233. package/dist/commands/validate/fleet/views/projects.js.map +1 -0
  234. package/dist/commands/validate/fleet/views/root-documents.js +83 -0
  235. package/dist/commands/validate/fleet/views/root-documents.js.map +1 -0
  236. package/dist/commands/validate/fleet/views/stale.js +40 -47
  237. package/dist/commands/validate/fleet/views/stale.js.map +1 -1
  238. package/dist/commands/validate/links/corpus.js +1 -4
  239. package/dist/commands/validate/links/corpus.js.map +1 -1
  240. package/dist/commands/validate/links/glossary.js.map +1 -1
  241. package/dist/commands/validate/render/repeats.js +63 -0
  242. package/dist/commands/validate/render/repeats.js.map +1 -0
  243. package/dist/commands/validate/render/rollups.js +115 -0
  244. package/dist/commands/validate/render/rollups.js.map +1 -0
  245. package/dist/commands/validate/render/text.js +199 -0
  246. package/dist/commands/validate/render/text.js.map +1 -0
  247. package/dist/commands/validate/report.js +36 -160
  248. package/dist/commands/validate/report.js.map +1 -1
  249. package/dist/commands/validate/scope.js +113 -0
  250. package/dist/commands/validate/scope.js.map +1 -0
  251. package/dist/commands/validate/service/api.js +21 -107
  252. package/dist/commands/validate/service/api.js.map +1 -1
  253. package/dist/commands/validate/service/contracts/generated.js +134 -0
  254. package/dist/commands/validate/service/contracts/generated.js.map +1 -0
  255. package/dist/commands/validate/service/events/events.js +36 -14
  256. package/dist/commands/validate/service/events/events.js.map +1 -1
  257. package/dist/commands/validate/service/events/message-producers.js +1 -1
  258. package/dist/commands/validate/service/events/message-producers.js.map +1 -1
  259. package/dist/commands/validate/service/evidence-pins.js +56 -10
  260. package/dist/commands/validate/service/evidence-pins.js.map +1 -1
  261. package/dist/commands/validate/service/model/diverged.js +226 -0
  262. package/dist/commands/validate/service/model/diverged.js.map +1 -0
  263. package/dist/commands/validate/service/model/grade.js +163 -0
  264. package/dist/commands/validate/service/model/grade.js.map +1 -0
  265. package/dist/commands/validate/service/service.js +121 -61
  266. package/dist/commands/validate/service/service.js.map +1 -1
  267. package/dist/commands/validate/service/specs.js +88 -28
  268. package/dist/commands/validate/service/specs.js.map +1 -1
  269. package/dist/commands/validate/service/spine.js +38 -3
  270. package/dist/commands/validate/service/spine.js.map +1 -1
  271. package/dist/commands/validate/service/usecases/tags.js +113 -0
  272. package/dist/commands/validate/service/usecases/tags.js.map +1 -0
  273. package/dist/commands/validate/service/usecases/usecases.js +209 -0
  274. package/dist/commands/validate/service/usecases/usecases.js.map +1 -0
  275. package/dist/commands/validate/validate.js +85 -60
  276. package/dist/commands/validate/validate.js.map +1 -1
  277. package/dist/commands/verify/cross/diff.js +2 -6
  278. package/dist/commands/verify/cross/diff.js.map +1 -1
  279. package/dist/commands/verify/evidence/contract.js +1 -1
  280. package/dist/commands/verify/evidence/contract.js.map +1 -1
  281. package/dist/commands/verify/evidence/read.js +12 -3
  282. package/dist/commands/verify/evidence/read.js.map +1 -1
  283. package/dist/commands/verify/frozen.js +45 -23
  284. package/dist/commands/verify/frozen.js.map +1 -1
  285. package/dist/commands/verify/record.js +69 -17
  286. package/dist/commands/verify/record.js.map +1 -1
  287. package/dist/commands/verify/report.js +27 -44
  288. package/dist/commands/verify/report.js.map +1 -1
  289. package/dist/commands/verify/results.js +112 -30
  290. package/dist/commands/verify/results.js.map +1 -1
  291. package/dist/commands/verify/verify.js +55 -52
  292. package/dist/commands/verify/verify.js.map +1 -1
  293. package/dist/commands/vouch/pack/pack.js +2 -2
  294. package/dist/commands/vouch/pack/pack.js.map +1 -1
  295. package/dist/commands/vouch/pack/print.js +1 -6
  296. package/dist/commands/vouch/pack/print.js.map +1 -1
  297. package/dist/commands/vouch/pack/sections.js +1 -2
  298. package/dist/commands/vouch/pack/sections.js.map +1 -1
  299. package/dist/commands/vouch/run.js +26 -81
  300. package/dist/commands/vouch/run.js.map +1 -1
  301. package/dist/commands/vouch/sample/plan.js +20 -34
  302. package/dist/commands/vouch/sample/plan.js.map +1 -1
  303. package/dist/commands/vouch/vet/verify.js +39 -7
  304. package/dist/commands/vouch/vet/verify.js.map +1 -1
  305. package/dist/commands/vouch/vouch.js +10 -17
  306. package/dist/commands/vouch/vouch.js.map +1 -1
  307. package/dist/core/agent/agents-md/command-map.js +81 -106
  308. package/dist/core/agent/agents-md/command-map.js.map +1 -1
  309. package/dist/core/agent/agents-md/map/explain.js +21 -9
  310. package/dist/core/agent/agents-md/map/explain.js.map +1 -1
  311. package/dist/core/agent/agents-md/map/lenses/context.js +17 -8
  312. package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -1
  313. package/dist/core/agent/agents-md/map/lenses/diff.js +33 -20
  314. package/dist/core/agent/agents-md/map/lenses/diff.js.map +1 -1
  315. package/dist/core/agent/agents-md/map/lenses/gate.js +13 -5
  316. package/dist/core/agent/agents-md/map/lenses/gate.js.map +1 -1
  317. package/dist/core/agent/agents-md/map/mcp.js +10 -7
  318. package/dist/core/agent/agents-md/map/mcp.js.map +1 -1
  319. package/dist/core/agent/agents-md/map/status.js +24 -8
  320. package/dist/core/agent/agents-md/map/status.js.map +1 -1
  321. package/dist/core/agent/agents-md/map/subsystem.js +80 -8
  322. package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
  323. package/dist/core/agent/agents-md/map/usecases.js +88 -10
  324. package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
  325. package/dist/core/agent/agents-md/refusals.js +49 -22
  326. package/dist/core/agent/agents-md/refusals.js.map +1 -1
  327. package/dist/core/agent/agents-md.js +39 -6
  328. package/dist/core/agent/agents-md.js.map +1 -1
  329. package/dist/core/agent/agents-stamp.js +10 -0
  330. package/dist/core/agent/agents-stamp.js.map +1 -1
  331. package/dist/core/agent/protocol.js +1 -1
  332. package/dist/core/agent/protocol.js.map +1 -1
  333. package/dist/core/agent/scaffold.js +10 -15
  334. package/dist/core/agent/scaffold.js.map +1 -1
  335. package/dist/core/agent/support/report.js +46 -5
  336. package/dist/core/agent/support/report.js.map +1 -1
  337. package/dist/core/agent/tools/dialects.js +1 -0
  338. package/dist/core/agent/tools/dialects.js.map +1 -1
  339. package/dist/core/agent/tools/registry.js +41 -76
  340. package/dist/core/agent/tools/registry.js.map +1 -1
  341. package/dist/core/agent/workflows/adopt.js +158 -11
  342. package/dist/core/agent/workflows/adopt.js.map +1 -1
  343. package/dist/core/agent/workflows/check.js +78 -39
  344. package/dist/core/agent/workflows/check.js.map +1 -1
  345. package/dist/core/agent/workflows/closing.js +88 -27
  346. package/dist/core/agent/workflows/closing.js.map +1 -1
  347. package/dist/core/agent/workflows/feature.js +128 -13
  348. package/dist/core/agent/workflows/feature.js.map +1 -1
  349. package/dist/core/agent/workflows/implement.js +96 -13
  350. package/dist/core/agent/workflows/implement.js.map +1 -1
  351. package/dist/core/agent/workflows/reference/authoring.js +106 -4
  352. package/dist/core/agent/workflows/reference/authoring.js.map +1 -1
  353. package/dist/core/agent/workflows/reference/codes.js +8 -6
  354. package/dist/core/agent/workflows/reference/codes.js.map +1 -1
  355. package/dist/core/agent/workflows/reference/done-check.js +37 -15
  356. package/dist/core/agent/workflows/reference/done-check.js.map +1 -1
  357. package/dist/core/agent/workflows/reference/drawing/drawing.js +253 -0
  358. package/dist/core/agent/workflows/reference/drawing/drawing.js.map +1 -0
  359. package/dist/core/agent/workflows/reference/reference.js +6 -3
  360. package/dist/core/agent/workflows/reference/reference.js.map +1 -1
  361. package/dist/core/agent/workflows/reference/spine.js +68 -27
  362. package/dist/core/agent/workflows/reference/spine.js.map +1 -1
  363. package/dist/core/asyncapi/baseline/plan.js +14 -81
  364. package/dist/core/asyncapi/baseline/plan.js.map +1 -1
  365. package/dist/core/asyncapi/digest.js +30 -25
  366. package/dist/core/asyncapi/digest.js.map +1 -1
  367. package/dist/core/asyncapi/merge/components.js +5 -4
  368. package/dist/core/asyncapi/merge/components.js.map +1 -1
  369. package/dist/core/asyncapi/merge/error.js +7 -2
  370. package/dist/core/asyncapi/merge/error.js.map +1 -1
  371. package/dist/core/asyncapi/merge/markers.js +32 -50
  372. package/dist/core/asyncapi/merge/markers.js.map +1 -1
  373. package/dist/core/asyncapi/merge/merge.js +33 -53
  374. package/dist/core/asyncapi/merge/merge.js.map +1 -1
  375. package/dist/core/asyncapi/merge/pin.js +12 -36
  376. package/dist/core/asyncapi/merge/pin.js.map +1 -1
  377. package/dist/core/asyncapi/model.js.map +1 -1
  378. package/dist/core/asyncapi/read.js +36 -12
  379. package/dist/core/asyncapi/read.js.map +1 -1
  380. package/dist/core/brief/brief.js +96 -36
  381. package/dist/core/brief/brief.js.map +1 -1
  382. package/dist/core/brief/checks.js +138 -3
  383. package/dist/core/brief/checks.js.map +1 -1
  384. package/dist/core/brief/landscape.js +209 -130
  385. package/dist/core/brief/landscape.js.map +1 -1
  386. package/dist/core/brief/map/extend.js +84 -0
  387. package/dist/core/brief/map/extend.js.map +1 -0
  388. package/dist/core/brief/map/owed.js +266 -0
  389. package/dist/core/brief/map/owed.js.map +1 -0
  390. package/dist/core/brief/targets.js +109 -31
  391. package/dist/core/brief/targets.js.map +1 -1
  392. package/dist/core/brief/unchecked.js +14 -3
  393. package/dist/core/brief/unchecked.js.map +1 -1
  394. package/dist/core/c4/arch.js +90 -182
  395. package/dist/core/c4/arch.js.map +1 -1
  396. package/dist/core/c4/direction/direction.js +114 -0
  397. package/dist/core/c4/direction/direction.js.map +1 -0
  398. package/dist/core/c4/likec4.js +169 -14
  399. package/dist/core/c4/likec4.js.map +1 -1
  400. package/dist/core/c4/locate/unlocated.js +150 -0
  401. package/dist/core/c4/locate/unlocated.js.map +1 -0
  402. package/dist/core/c4/parsed/deployment.js +160 -0
  403. package/dist/core/c4/parsed/deployment.js.map +1 -0
  404. package/dist/core/c4/parsed/dynamic-views.js.map +1 -1
  405. package/dist/core/c4/parsed/values.js +54 -1
  406. package/dist/core/c4/parsed/values.js.map +1 -1
  407. package/dist/core/c4/parsed/view-ids.js +39 -1
  408. package/dist/core/c4/parsed/view-ids.js.map +1 -1
  409. package/dist/core/c4/prefetch/fleet.js +134 -0
  410. package/dist/core/c4/prefetch/fleet.js.map +1 -0
  411. package/dist/core/c4/project/architecture.js +123 -7
  412. package/dist/core/c4/project/architecture.js.map +1 -1
  413. package/dist/core/c4/project/batch.js +140 -0
  414. package/dist/core/c4/project/batch.js.map +1 -0
  415. package/dist/core/c4/project/documents.js +43 -12
  416. package/dist/core/c4/project/documents.js.map +1 -1
  417. package/dist/core/c4/project/load.js +79 -43
  418. package/dist/core/c4/project/load.js.map +1 -1
  419. package/dist/core/c4/project/staged.js +277 -0
  420. package/dist/core/c4/project/staged.js.map +1 -0
  421. package/dist/core/c4/resolve/attested.js +0 -0
  422. package/dist/core/c4/resolve/attested.js.map +1 -0
  423. package/dist/core/c4/resolve/external.js +51 -0
  424. package/dist/core/c4/resolve/external.js.map +1 -0
  425. package/dist/core/c4/resolve/service.js +78 -17
  426. package/dist/core/c4/resolve/service.js.map +1 -1
  427. package/dist/core/c4/resolve/steps.js +290 -0
  428. package/dist/core/c4/resolve/steps.js.map +1 -0
  429. package/dist/core/c4/resolve/systems.js +154 -0
  430. package/dist/core/c4/resolve/systems.js.map +1 -0
  431. package/dist/core/c4/root-project/exclude.js +352 -0
  432. package/dist/core/c4/root-project/exclude.js.map +1 -0
  433. package/dist/core/c4/seed/template.js +65 -1
  434. package/dist/core/c4/seed/template.js.map +1 -1
  435. package/dist/core/c4/service-model/created/models.js +96 -0
  436. package/dist/core/c4/service-model/created/models.js.map +1 -0
  437. package/dist/core/c4/service-model/documents.js +47 -0
  438. package/dist/core/c4/service-model/documents.js.map +1 -0
  439. package/dist/core/c4/service-model/fleet/extending.js +82 -0
  440. package/dist/core/c4/service-model/fleet/extending.js.map +1 -0
  441. package/dist/core/c4/service-model/load.js +165 -0
  442. package/dist/core/c4/service-model/load.js.map +1 -0
  443. package/dist/core/c4/service-model/renderer.js +128 -0
  444. package/dist/core/c4/service-model/renderer.js.map +1 -0
  445. package/dist/core/c4/service-model/shape.js +106 -0
  446. package/dist/core/c4/service-model/shape.js.map +1 -0
  447. package/dist/core/c4/service-model/slice.js +188 -0
  448. package/dist/core/c4/service-model/slice.js.map +1 -0
  449. package/dist/core/c4/source-mask.js +297 -10
  450. package/dist/core/c4/source-mask.js.map +1 -1
  451. package/dist/core/c4/source-scan.js +222 -141
  452. package/dist/core/c4/source-scan.js.map +1 -1
  453. package/dist/core/c4/splice/authored-source.js +79 -13
  454. package/dist/core/c4/splice/authored-source.js.map +1 -1
  455. package/dist/core/c4/splice/contract.js +20 -0
  456. package/dist/core/c4/splice/contract.js.map +1 -1
  457. package/dist/core/c4/splice/delta-blocks.js +20 -20
  458. package/dist/core/c4/splice/delta-blocks.js.map +1 -1
  459. package/dist/core/c4/splice/identity/declared.js +87 -0
  460. package/dist/core/c4/splice/identity/declared.js.map +1 -0
  461. package/dist/core/c4/splice/identity/edges.js +37 -0
  462. package/dist/core/c4/splice/identity/edges.js.map +1 -1
  463. package/dist/core/c4/splice/identity/existing.js +80 -0
  464. package/dist/core/c4/splice/identity/existing.js.map +1 -0
  465. package/dist/core/c4/splice/identity/interior.js +80 -0
  466. package/dist/core/c4/splice/identity/interior.js.map +1 -0
  467. package/dist/core/c4/splice/identity/owner.js +50 -0
  468. package/dist/core/c4/splice/identity/owner.js.map +1 -0
  469. package/dist/core/c4/splice/landscape-merge.js +182 -114
  470. package/dist/core/c4/splice/landscape-merge.js.map +1 -1
  471. package/dist/core/c4/splice/model/merge.js +151 -0
  472. package/dist/core/c4/splice/model/merge.js.map +1 -0
  473. package/dist/core/c4/splice/placement.js +160 -56
  474. package/dist/core/c4/splice/placement.js.map +1 -1
  475. package/dist/core/c4/splice/text/spot.js +131 -0
  476. package/dist/core/c4/splice/text/spot.js.map +1 -0
  477. package/dist/core/c4/workspace.js +13 -19
  478. package/dist/core/c4/workspace.js.map +1 -1
  479. package/dist/core/capabilities/capabilities.js +6 -6
  480. package/dist/core/capabilities/capabilities.js.map +1 -1
  481. package/dist/core/capabilities/delta/doc.js +7 -10
  482. package/dist/core/capabilities/delta/doc.js.map +1 -1
  483. package/dist/core/capabilities/delta/uncovered.js +2 -2
  484. package/dist/core/capabilities/delta/uncovered.js.map +1 -1
  485. package/dist/core/capabilities/findings.js +18 -22
  486. package/dist/core/capabilities/findings.js.map +1 -1
  487. package/dist/core/capabilities/realizes/findings.js +12 -19
  488. package/dist/core/capabilities/realizes/findings.js.map +1 -1
  489. package/dist/core/capabilities/realizes/join.js +0 -20
  490. package/dist/core/capabilities/realizes/join.js.map +1 -1
  491. package/dist/core/capabilities/rollup.js +6 -0
  492. package/dist/core/capabilities/rollup.js.map +1 -1
  493. package/dist/core/capabilities/tree.js +6 -28
  494. package/dist/core/capabilities/tree.js.map +1 -1
  495. package/dist/core/capabilities/usecase-join.js +3 -13
  496. package/dist/core/capabilities/usecase-join.js.map +1 -1
  497. package/dist/core/coherence/authoring/scaffold.js +11 -3
  498. package/dist/core/coherence/authoring/scaffold.js.map +1 -1
  499. package/dist/core/coherence/authoring/sentinels.js +2 -1
  500. package/dist/core/coherence/authoring/sentinels.js.map +1 -1
  501. package/dist/core/coherence/calls/called.js +52 -0
  502. package/dist/core/coherence/calls/called.js.map +1 -0
  503. package/dist/core/coherence/coherence.js +103 -22
  504. package/dist/core/coherence/coherence.js.map +1 -1
  505. package/dist/core/coherence/consumed/operations.js +70 -0
  506. package/dist/core/coherence/consumed/operations.js.map +1 -0
  507. package/dist/core/coherence/consumed/refusal.js +38 -0
  508. package/dist/core/coherence/consumed/refusal.js.map +1 -0
  509. package/dist/core/coherence/covers/health.js +85 -0
  510. package/dist/core/coherence/covers/health.js.map +1 -0
  511. package/dist/core/coherence/declared.js +60 -34
  512. package/dist/core/coherence/declared.js.map +1 -1
  513. package/dist/core/coherence/events/declared.js +44 -59
  514. package/dist/core/coherence/events/declared.js.map +1 -1
  515. package/dist/core/coherence/events/events.js +147 -16
  516. package/dist/core/coherence/events/events.js.map +1 -1
  517. package/dist/core/coherence/events/grades.js +31 -8
  518. package/dist/core/coherence/events/grades.js.map +1 -1
  519. package/dist/core/coherence/events/lookups.js +28 -87
  520. package/dist/core/coherence/events/lookups.js.map +1 -1
  521. package/dist/core/coherence/events/produced.js +100 -0
  522. package/dist/core/coherence/events/produced.js.map +1 -0
  523. package/dist/core/coherence/lookups.js +18 -78
  524. package/dist/core/coherence/lookups.js.map +1 -1
  525. package/dist/core/coherence/retired/archived.js +285 -0
  526. package/dist/core/coherence/retired/archived.js.map +1 -0
  527. package/dist/core/coherence/retired/event-pair.js +62 -0
  528. package/dist/core/coherence/retired/event-pair.js.map +1 -0
  529. package/dist/core/coherence/retired/merged.js +79 -0
  530. package/dist/core/coherence/retired/merged.js.map +1 -0
  531. package/dist/core/coherence/retired/retirement.js +49 -0
  532. package/dist/core/coherence/retired/retirement.js.map +1 -0
  533. package/dist/core/coherence/retired/revival/arrivals.js +131 -0
  534. package/dist/core/coherence/retired/revival/arrivals.js.map +1 -0
  535. package/dist/core/coherence/retired/revival/revived.js +297 -0
  536. package/dist/core/coherence/retired/revival/revived.js.map +1 -0
  537. package/dist/core/consumers/after.js +113 -0
  538. package/dist/core/consumers/after.js.map +1 -0
  539. package/dist/core/consumers/consumers.js +296 -0
  540. package/dist/core/consumers/consumers.js.map +1 -0
  541. package/dist/core/consumers/current.js +211 -0
  542. package/dist/core/consumers/current.js.map +1 -0
  543. package/dist/core/consumers/scan.js +46 -0
  544. package/dist/core/consumers/scan.js.map +1 -0
  545. package/dist/core/consumers/state.js +27 -0
  546. package/dist/core/consumers/state.js.map +1 -0
  547. package/dist/core/delta/delta.js +51 -6
  548. package/dist/core/delta/delta.js.map +1 -1
  549. package/dist/core/dependencies/fanin.js +2 -2
  550. package/dist/core/dependencies/fanin.js.map +1 -1
  551. package/dist/core/deployment/delta.js +191 -0
  552. package/dist/core/deployment/delta.js.map +1 -0
  553. package/dist/core/deployment/objects.js +145 -0
  554. package/dist/core/deployment/objects.js.map +1 -0
  555. package/dist/core/diff/base-git.js +94 -12
  556. package/dist/core/diff/base-git.js.map +1 -1
  557. package/dist/core/diff/base-state.js +49 -21
  558. package/dist/core/diff/base-state.js.map +1 -1
  559. package/dist/core/diff/joins.js +136 -0
  560. package/dist/core/diff/joins.js.map +1 -0
  561. package/dist/core/diff/scope/changed-paths.js +22 -6
  562. package/dist/core/diff/scope/changed-paths.js.map +1 -1
  563. package/dist/core/diff/scope/edges.js +82 -0
  564. package/dist/core/diff/scope/edges.js.map +1 -0
  565. package/dist/core/diff/semantic.js +100 -31
  566. package/dist/core/diff/semantic.js.map +1 -1
  567. package/dist/core/docs.js +31 -22
  568. package/dist/core/docs.js.map +1 -1
  569. package/dist/core/doctor/agents.js +34 -18
  570. package/dist/core/doctor/agents.js.map +1 -1
  571. package/dist/core/doctor/config.js +7 -3
  572. package/dist/core/doctor/config.js.map +1 -1
  573. package/dist/core/doctor/doctor.js +91 -11
  574. package/dist/core/doctor/doctor.js.map +1 -1
  575. package/dist/core/doctor/report.js.map +1 -1
  576. package/dist/core/doctor/reports/scan.js +242 -0
  577. package/dist/core/doctor/reports/scan.js.map +1 -0
  578. package/dist/core/document/apply.js +158 -11
  579. package/dist/core/document/apply.js.map +1 -1
  580. package/dist/core/document/frontmatter.js +55 -0
  581. package/dist/core/document/frontmatter.js.map +1 -1
  582. package/dist/core/document/parse.js +29 -6
  583. package/dist/core/document/parse.js.map +1 -1
  584. package/dist/core/document/scenarios.js +17 -0
  585. package/dist/core/document/scenarios.js.map +1 -1
  586. package/dist/core/document/spec.js +4 -11
  587. package/dist/core/document/spec.js.map +1 -1
  588. package/dist/core/envelope/config.js +10 -0
  589. package/dist/core/envelope/config.js.map +1 -1
  590. package/dist/core/envelope/json.js +34 -0
  591. package/dist/core/envelope/json.js.map +1 -1
  592. package/dist/core/explain/families.js +26 -21
  593. package/dist/core/explain/families.js.map +1 -1
  594. package/dist/core/explain/lookup.js +14 -1
  595. package/dist/core/explain/lookup.js.map +1 -1
  596. package/dist/core/explain/refusals.js +5 -5
  597. package/dist/core/explain/refusals.js.map +1 -1
  598. package/dist/core/explain/terms/documents.js +227 -0
  599. package/dist/core/explain/terms/documents.js.map +1 -0
  600. package/dist/core/explain/terms/done-check.js +144 -0
  601. package/dist/core/explain/terms/done-check.js.map +1 -0
  602. package/dist/core/explain/terms/entry.js +14 -0
  603. package/dist/core/explain/terms/entry.js.map +1 -0
  604. package/dist/core/explain/terms/stages.js +71 -0
  605. package/dist/core/explain/terms/stages.js.map +1 -0
  606. package/dist/core/explain/terms/tasks.js +97 -0
  607. package/dist/core/explain/terms/tasks.js.map +1 -0
  608. package/dist/core/explain/terms.js +68 -109
  609. package/dist/core/explain/terms.js.map +1 -1
  610. package/dist/core/explore/explore.js +40 -10
  611. package/dist/core/explore/explore.js.map +1 -1
  612. package/dist/core/fleet-context.js +81 -95
  613. package/dist/core/fleet-context.js.map +1 -1
  614. package/dist/core/gate/checks.js +11 -4
  615. package/dist/core/gate/checks.js.map +1 -1
  616. package/dist/core/gate/partners.js +69 -28
  617. package/dist/core/gate/partners.js.map +1 -1
  618. package/dist/core/gate/report.js.map +1 -1
  619. package/dist/core/gate/verification.js +75 -12
  620. package/dist/core/gate/verification.js.map +1 -1
  621. package/dist/core/gherkin/stale.js +13 -37
  622. package/dist/core/gherkin/stale.js.map +1 -1
  623. package/dist/core/glossary/delta.js +10 -7
  624. package/dist/core/glossary/delta.js.map +1 -1
  625. package/dist/core/glossary/tree.js +6 -19
  626. package/dist/core/glossary/tree.js.map +1 -1
  627. package/dist/core/kernel/concurrency.js +19 -0
  628. package/dist/core/kernel/concurrency.js.map +1 -1
  629. package/dist/core/kernel/ids/fqn/ancestors.js +65 -0
  630. package/dist/core/kernel/ids/fqn/ancestors.js.map +1 -0
  631. package/dist/core/kernel/path-safety.js +38 -0
  632. package/dist/core/kernel/path-safety.js.map +1 -1
  633. package/dist/core/kernel/records.js +11 -0
  634. package/dist/core/kernel/records.js.map +1 -1
  635. package/dist/core/links/backlinks.js +1 -3
  636. package/dist/core/links/backlinks.js.map +1 -1
  637. package/dist/core/links/corpus.js +51 -37
  638. package/dist/core/links/corpus.js.map +1 -1
  639. package/dist/core/links/findings.js.map +1 -1
  640. package/dist/core/links/parse.js +38 -9
  641. package/dist/core/links/parse.js.map +1 -1
  642. package/dist/core/mcp/argv.js +101 -0
  643. package/dist/core/mcp/argv.js.map +1 -1
  644. package/dist/core/mcp/author-tools.js +2 -1
  645. package/dist/core/mcp/author-tools.js.map +1 -1
  646. package/dist/core/mcp/protocol.js +2 -2
  647. package/dist/core/mcp/protocol.js.map +1 -1
  648. package/dist/core/mcp/tools.js +43 -33
  649. package/dist/core/mcp/tools.js.map +1 -1
  650. package/dist/core/openapi/baseline/gate.js +53 -12
  651. package/dist/core/openapi/baseline/gate.js.map +1 -1
  652. package/dist/core/openapi/baseline/plan.js +40 -10
  653. package/dist/core/openapi/baseline/plan.js.map +1 -1
  654. package/dist/core/openapi/baseline/record.js +0 -1
  655. package/dist/core/openapi/baseline/record.js.map +1 -1
  656. package/dist/core/openapi/contract-text.js +33 -0
  657. package/dist/core/openapi/contract-text.js.map +1 -0
  658. package/dist/core/openapi/contract-tree.js +301 -0
  659. package/dist/core/openapi/contract-tree.js.map +1 -0
  660. package/dist/core/openapi/depth.js +38 -24
  661. package/dist/core/openapi/depth.js.map +1 -1
  662. package/dist/core/openapi/digest.js +52 -2
  663. package/dist/core/openapi/digest.js.map +1 -1
  664. package/dist/core/openapi/doc.js +12 -4
  665. package/dist/core/openapi/doc.js.map +1 -1
  666. package/dist/core/openapi/merge/components.js +25 -60
  667. package/dist/core/openapi/merge/components.js.map +1 -1
  668. package/dist/core/openapi/merge/error.js +3 -13
  669. package/dist/core/openapi/merge/error.js.map +1 -1
  670. package/dist/core/openapi/merge/markers.js +26 -30
  671. package/dist/core/openapi/merge/markers.js.map +1 -1
  672. package/dist/core/openapi/merge/merge.js +28 -52
  673. package/dist/core/openapi/merge/merge.js.map +1 -1
  674. package/dist/core/openapi/merge/pin.js +17 -35
  675. package/dist/core/openapi/merge/pin.js.map +1 -1
  676. package/dist/core/openspec/decide/verdict.js.map +1 -1
  677. package/dist/core/openspec/inventory.js +4 -5
  678. package/dist/core/openspec/inventory.js.map +1 -1
  679. package/dist/core/openspec/model/mapping.js +30 -28
  680. package/dist/core/openspec/model/mapping.js.map +1 -1
  681. package/dist/core/openspec/model/model.js +12 -7
  682. package/dist/core/openspec/model/model.js.map +1 -1
  683. package/dist/core/openspec/scan/changes.js +10 -4
  684. package/dist/core/openspec/scan/changes.js.map +1 -1
  685. package/dist/core/openspec/scan/config.js +7 -16
  686. package/dist/core/openspec/scan/config.js.map +1 -1
  687. package/dist/core/openspec/scan/shape.js +63 -19
  688. package/dist/core/openspec/scan/shape.js.map +1 -1
  689. package/dist/core/openspec/scan/walk.js +21 -38
  690. package/dist/core/openspec/scan/walk.js.map +1 -1
  691. package/dist/core/pack/living.js +43 -4
  692. package/dist/core/pack/living.js.map +1 -1
  693. package/dist/core/pack/pack.js +50 -6
  694. package/dist/core/pack/pack.js.map +1 -1
  695. package/dist/core/projection/arch-slice.js +2 -2
  696. package/dist/core/projection/arch-slice.js.map +1 -1
  697. package/dist/core/projection/introduced.js +14 -0
  698. package/dist/core/projection/introduced.js.map +1 -0
  699. package/dist/core/provenance/findings.js +48 -12
  700. package/dist/core/provenance/findings.js.map +1 -1
  701. package/dist/core/provenance/git.js +33 -57
  702. package/dist/core/provenance/git.js.map +1 -1
  703. package/dist/core/provenance/gitq/moves.js +71 -114
  704. package/dist/core/provenance/gitq/moves.js.map +1 -1
  705. package/dist/core/provenance/gitq/vouched-ancestor.js +2 -2
  706. package/dist/core/provenance/gitq/vouched-ancestor.js.map +1 -1
  707. package/dist/core/provenance/sources.js +23 -6
  708. package/dist/core/provenance/sources.js.map +1 -1
  709. package/dist/core/provenance/stamp.js +5 -17
  710. package/dist/core/provenance/stamp.js.map +1 -1
  711. package/dist/core/repo/authored/paths.js +18 -0
  712. package/dist/core/repo/authored/paths.js.map +1 -1
  713. package/dist/core/repo/entries.js.map +1 -1
  714. package/dist/core/repo/paths.js +92 -6
  715. package/dist/core/repo/paths.js.map +1 -1
  716. package/dist/core/repo/repo.js +74 -14
  717. package/dist/core/repo/repo.js.map +1 -1
  718. package/dist/core/repo/tree/find.js +12 -3
  719. package/dist/core/repo/tree/find.js.map +1 -1
  720. package/dist/core/repo/tree/fs.js +27 -1
  721. package/dist/core/repo/tree/fs.js.map +1 -1
  722. package/dist/core/repo/tree/render/body.js +258 -0
  723. package/dist/core/repo/tree/render/body.js.map +1 -0
  724. package/dist/core/repo/tree/render/projects.js +182 -0
  725. package/dist/core/repo/tree/render/projects.js.map +1 -0
  726. package/dist/core/repo/tree/render/service/header.js +125 -0
  727. package/dist/core/repo/tree/render/service/header.js.map +1 -0
  728. package/dist/core/repo/tree/render/service/peers.js +28 -0
  729. package/dist/core/repo/tree/render/service/peers.js.map +1 -0
  730. package/dist/core/repo/tree/render/service/view-id.js +67 -0
  731. package/dist/core/repo/tree/render/service/view-id.js.map +1 -0
  732. package/dist/core/repo/tree/render/service/views.js +78 -0
  733. package/dist/core/repo/tree/render/service/views.js.map +1 -0
  734. package/dist/core/repo/tree/render/stale.js +42 -0
  735. package/dist/core/repo/tree/render/stale.js.map +1 -0
  736. package/dist/core/repo/tree/render/text.js +84 -0
  737. package/dist/core/repo/tree/render/text.js.map +1 -0
  738. package/dist/core/repo/tree/render/views.js +293 -0
  739. package/dist/core/repo/tree/render/views.js.map +1 -0
  740. package/dist/core/repo/tree/walk.js +53 -32
  741. package/dist/core/repo/tree/walk.js.map +1 -1
  742. package/dist/core/results.js +47 -12
  743. package/dist/core/results.js.map +1 -1
  744. package/dist/core/scaffold/landscape.js +121 -13
  745. package/dist/core/scaffold/landscape.js.map +1 -1
  746. package/dist/core/scaffold/legacy.js +271 -0
  747. package/dist/core/scaffold/legacy.js.map +1 -0
  748. package/dist/core/staging/commit.js +26 -15
  749. package/dist/core/staging/commit.js.map +1 -1
  750. package/dist/core/staging/lock.js +74 -12
  751. package/dist/core/staging/lock.js.map +1 -1
  752. package/dist/core/staging/recovery/intent.js +2 -5
  753. package/dist/core/staging/recovery/intent.js.map +1 -1
  754. package/dist/core/staging/recovery/recover.js +84 -6
  755. package/dist/core/staging/recovery/recover.js.map +1 -1
  756. package/dist/core/staging/snapshot/manifest.js +188 -0
  757. package/dist/core/staging/snapshot/manifest.js.map +1 -0
  758. package/dist/core/staging/{snapshot.js → snapshot/snapshot.js} +39 -45
  759. package/dist/core/staging/snapshot/snapshot.js.map +1 -0
  760. package/dist/core/staging/txn/forward.js +24 -14
  761. package/dist/core/staging/txn/forward.js.map +1 -1
  762. package/dist/core/staging/writes.js.map +1 -1
  763. package/dist/core/status/actions/execution.js +11 -0
  764. package/dist/core/status/actions/execution.js.map +1 -1
  765. package/dist/core/status/contracts.js +39 -15
  766. package/dist/core/status/contracts.js.map +1 -1
  767. package/dist/core/status/feature/artifacts.js +33 -8
  768. package/dist/core/status/feature/artifacts.js.map +1 -1
  769. package/dist/core/status/feature/feature.js +24 -17
  770. package/dist/core/status/feature/feature.js.map +1 -1
  771. package/dist/core/status/feature/next.js +20 -7
  772. package/dist/core/status/feature/next.js.map +1 -1
  773. package/dist/core/status/feature/words.js +96 -0
  774. package/dist/core/status/feature/words.js.map +1 -0
  775. package/dist/core/status/fleet/fleet.js +43 -14
  776. package/dist/core/status/fleet/fleet.js.map +1 -1
  777. package/dist/core/status/fleet/next.js +153 -58
  778. package/dist/core/status/fleet/next.js.map +1 -1
  779. package/dist/core/status/report.js.map +1 -1
  780. package/dist/core/status/scan.js.map +1 -1
  781. package/dist/core/status/verification.js +30 -15
  782. package/dist/core/status/verification.js.map +1 -1
  783. package/dist/core/tasks/packet.js +199 -0
  784. package/dist/core/tasks/packet.js.map +1 -0
  785. package/dist/core/tasks/record.js +43 -0
  786. package/dist/core/tasks/record.js.map +1 -0
  787. package/dist/core/tasks/sources.js +181 -0
  788. package/dist/core/tasks/sources.js.map +1 -0
  789. package/dist/core/tasks/stamps.js +76 -0
  790. package/dist/core/tasks/stamps.js.map +1 -0
  791. package/dist/core/tasks/unit.js +0 -0
  792. package/dist/core/tasks/unit.js.map +1 -0
  793. package/dist/core/usecases/capability.js +11 -14
  794. package/dist/core/usecases/capability.js.map +1 -1
  795. package/dist/core/usecases/delta/claims.js +31 -7
  796. package/dist/core/usecases/delta/claims.js.map +1 -1
  797. package/dist/core/usecases/delta/flows.js +5 -7
  798. package/dist/core/usecases/delta/flows.js.map +1 -1
  799. package/dist/core/usecases/delta/modified.js +315 -0
  800. package/dist/core/usecases/delta/modified.js.map +1 -0
  801. package/dist/core/usecases/delta/overlay.js +32 -191
  802. package/dist/core/usecases/delta/overlay.js.map +1 -1
  803. package/dist/core/usecases/fleet.js +195 -36
  804. package/dist/core/usecases/fleet.js.map +1 -1
  805. package/dist/{commands/validate/fleet/usecases → core/usecases/grade}/capability-tag.js +8 -9
  806. package/dist/core/usecases/grade/capability-tag.js.map +1 -0
  807. package/dist/core/usecases/grade/feature.js +88 -0
  808. package/dist/core/usecases/grade/feature.js.map +1 -0
  809. package/dist/{commands/validate/fleet/usecases → core/usecases/grade}/requirement-tag.js +3 -4
  810. package/dist/core/usecases/grade/requirement-tag.js.map +1 -0
  811. package/dist/{commands/validate/fleet/usecases → core/usecases/grade}/steps.js +102 -33
  812. package/dist/core/usecases/grade/steps.js.map +1 -0
  813. package/dist/{commands/validate/fleet/usecases → core/usecases/grade}/usecases.js +23 -10
  814. package/dist/core/usecases/grade/usecases.js.map +1 -0
  815. package/dist/core/usecases/operations.js +26 -14
  816. package/dist/core/usecases/operations.js.map +1 -1
  817. package/dist/core/usecases/place.js.map +1 -1
  818. package/dist/core/usecases/service/exclude.js +119 -0
  819. package/dist/core/usecases/service/exclude.js.map +1 -0
  820. package/dist/core/usecases/service/flows.js +164 -0
  821. package/dist/core/usecases/service/flows.js.map +1 -0
  822. package/dist/core/usecases/suites/participants.js +83 -0
  823. package/dist/core/usecases/suites/participants.js.map +1 -0
  824. package/dist/core/usecases/suites/suites.js +45 -0
  825. package/dist/core/usecases/suites/suites.js.map +1 -0
  826. package/dist/core/verify/answers.js +12 -5
  827. package/dist/core/verify/answers.js.map +1 -1
  828. package/dist/core/verify/build.js +102 -82
  829. package/dist/core/verify/build.js.map +1 -1
  830. package/dist/core/verify/checklist.js +70 -12
  831. package/dist/core/verify/checklist.js.map +1 -1
  832. package/dist/core/verify/location.js +67 -0
  833. package/dist/core/verify/location.js.map +1 -0
  834. package/dist/core/verify/pins/lint.js +3 -2
  835. package/dist/core/verify/pins/lint.js.map +1 -1
  836. package/dist/core/verify/pins/pin.js +1 -1
  837. package/dist/core/verify/pins/pin.js.map +1 -1
  838. package/dist/core/verify/read/file.js +224 -0
  839. package/dist/core/verify/read/file.js.map +1 -0
  840. package/dist/core/verify/{file.js → read/shape.js} +16 -104
  841. package/dist/core/verify/read/shape.js.map +1 -0
  842. package/dist/core/verify/read/union.js +105 -0
  843. package/dist/core/verify/read/union.js.map +1 -0
  844. package/dist/core/verify/record.js +35 -12
  845. package/dist/core/verify/record.js.map +1 -1
  846. package/dist/core/verify/store/commit.js +40 -30
  847. package/dist/core/verify/store/commit.js.map +1 -1
  848. package/dist/core/verify/store/render.js +8 -4
  849. package/dist/core/verify/store/render.js.map +1 -1
  850. package/dist/core/vocabulary/issue.js +4 -0
  851. package/dist/core/vocabulary/issue.js.map +1 -1
  852. package/dist/core/vocabulary/maturity.js +75 -4
  853. package/dist/core/vocabulary/maturity.js.map +1 -1
  854. package/dist/core/vocabulary/report.js +16 -1
  855. package/dist/core/vocabulary/report.js.map +1 -1
  856. package/examples/README.md +54 -11
  857. package/examples/docs/architecture/landscape.likec4 +62 -6
  858. package/examples/docs/architecture/services.likec4 +70 -0
  859. package/examples/docs/architecture/subsystems.likec4 +7 -0
  860. package/examples/docs/architecture/usecases/order-notification.likec4 +16 -7
  861. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/asyncapi.yaml +11 -8
  862. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/openapi.yaml +6 -6
  863. package/examples/docs/features/FEAT-112-retire-order-v1/specs/order-service/openapi.yaml +4 -1
  864. package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/openapi.yaml +13 -10
  865. package/examples/docs/likec4.config.json +0 -1
  866. package/examples/docs/services/checkout-web/model.likec4 +39 -56
  867. package/examples/docs/services/order-service/arch.spec.md +13 -6
  868. package/examples/docs/services/order-service/asyncapi.yaml +3 -2
  869. package/examples/docs/services/order-service/model.likec4 +58 -70
  870. package/examples/docs/services/payment-service/arch.spec.md +7 -4
  871. package/examples/docs/services/payment-service/model.likec4 +38 -37
  872. package/examples/docs/services/platform/identity-service/arch.spec.md +7 -4
  873. package/examples/docs/services/platform/identity-service/model.likec4 +35 -37
  874. package/examples/docs/services/platform/notification-service/asyncapi.yaml +2 -1
  875. package/examples/docs/services/platform/notification-service/health.yaml +8 -3
  876. package/examples/docs/services/platform/notification-service/model.likec4 +43 -64
  877. package/package.json +1 -1
  878. package/dist/commands/validate/checks/vocabulary.js +0 -31
  879. package/dist/commands/validate/checks/vocabulary.js.map +0 -1
  880. package/dist/commands/validate/fleet/usecases/capability-tag.js.map +0 -1
  881. package/dist/commands/validate/fleet/usecases/requirement-tag.js.map +0 -1
  882. package/dist/commands/validate/fleet/usecases/steps.js.map +0 -1
  883. package/dist/commands/validate/fleet/usecases/usecases.js.map +0 -1
  884. package/dist/core/agent/agents-md/artifacts.js +0 -220
  885. package/dist/core/agent/agents-md/artifacts.js.map +0 -1
  886. package/dist/core/agent/agents-md/cycle.js +0 -165
  887. package/dist/core/agent/agents-md/cycle.js.map +0 -1
  888. package/dist/core/agent/agents-md/shipped/archive-gate.js +0 -98
  889. package/dist/core/agent/agents-md/shipped/archive-gate.js.map +0 -1
  890. package/dist/core/agent/agents-md/spine.js +0 -40
  891. package/dist/core/agent/agents-md/spine.js.map +0 -1
  892. package/dist/core/coherence/events/merged.js +0 -40
  893. package/dist/core/coherence/events/merged.js.map +0 -1
  894. package/dist/core/diff/current-state.js +0 -96
  895. package/dist/core/diff/current-state.js.map +0 -1
  896. package/dist/core/diff/victims.js +0 -247
  897. package/dist/core/diff/victims.js.map +0 -1
  898. package/dist/core/repo/tree/views.js +0 -172
  899. package/dist/core/repo/tree/views.js.map +0 -1
  900. package/dist/core/staging/snapshot.js.map +0 -1
  901. package/dist/core/verify/file.js.map +0 -1
@@ -1,42 +1,15 @@
1
- /**
2
- * WHICH documents loam reads links out of — the corpus, spelled once.
3
- *
4
- * Two readers need it and must not disagree. `commands/validate/links/` grades
5
- * each target's own documents for `link.unresolved`; `core/glossary/backlinks.ts`
6
- * reads the WHOLE fleet's, because "which documents use this term" is a question
7
- * no single target can answer. A corpus that differed between them would report
8
- * a term as unused while the very document citing it was being graded one target
9
- * over.
10
- *
11
- * THE CORPUS IS EVERY AUTHORED MARKDOWN DOCUMENT, and the word doing the work is
12
- * authored. `AGENTS.md` and the scaffolded `README.md` are excluded because loam
13
- * WRITES them: a finding against generated prose names a defect its reader
14
- * cannot fix by editing, since the next `loam init` restores it. Their links are
15
- * loam's own problem, held on this side of the boundary by
16
- * `test/docs-facts.test.ts` and `test/package-docs.test.ts`.
17
- *
18
- * ADRs AND RUNBOOKS ARE IN IT, and they are the reason the convention was
19
- * written down at all: an ADR that supersedes another says which, by linking to
20
- * it. They are also the first documents loam reads that it previously only
21
- * counted, which is what `link.unreadable` exists for.
22
- *
23
- * `delta.likec4` IS NOT, and neither is any YAML: this is a markdown question.
24
- * The one thing a delta holds that addresses another file — `metadata { op }` —
25
- * has its own resolution and its own findings.
26
- */
27
- import { featureCapabilityDeltas } from "../capabilities/delta/tree.js";
28
- import { readCapabilityTree } from "../capabilities/tree.js";
1
+ import { readdir } from "node:fs/promises";
2
+ import { join } from "node:path";
29
3
  import { featureGlossary } from "../glossary/delta.js";
30
4
  import { readGlossary } from "../glossary/tree.js";
31
5
  import { featurePaths, featureSpecPaths, fleetAdrsDir, servicePathsAt } from "../repo/paths.js";
32
- import { capabilityDocsDir, glossaryDir } from "../repo/authored/paths.js";
6
+ import { glossaryDir } from "../repo/authored/paths.js";
33
7
  import { featureSpecServices, listFeatures, listServices } from "../repo/repo.js";
34
8
  import { markdownFiles } from "../repo/tree/fs.js";
35
9
  /**
36
10
  * One service's authored documents: the two requirement specs, the runbook, and
37
- * every ADR. The specs cost nothing extra when a `FleetContext` is in play — it
38
- * has already read both to parse their requirements, and the memo hands back the
39
- * same text.
11
+ * every ADR. The specs cost nothing extra: the run's `FleetContext` has already
12
+ * read both to parse their requirements, and the memo hands back the same text.
40
13
  */
41
14
  export async function serviceDocuments(paths) {
42
15
  return [paths.spec, paths.archSpec, paths.runbook, ...(await markdownFiles(paths.adrsDir))];
@@ -52,7 +25,7 @@ export async function featureDocuments(featureDir, fleet) {
52
25
  const spec = featureSpecPaths(featureDir, svc);
53
26
  docs.push(spec.spec, spec.archSpec);
54
27
  }
55
- const capabilities = fleet === undefined ? await featureCapabilityDeltas(featureDir) : await fleet.featureCapabilityDeltas(featureDir);
28
+ const capabilities = await fleet.featureCapabilityDeltas(featureDir);
56
29
  docs.push(...capabilities.docs.map((d) => d.spec));
57
30
  // The definitions this feature introduces. Their own links are graded like
58
31
  // any other document's — a term that cites a term the feature is not adding
@@ -70,9 +43,7 @@ export async function featureDocuments(featureDir, fleet) {
70
43
  * the fleet repeated on every service target is the report.
71
44
  */
72
45
  export async function fleetDocuments(docsDir, fleet) {
73
- const capabilities = fleet === undefined
74
- ? await readCapabilityTree(capabilityDocsDir(docsDir))
75
- : (await fleet.capabilities(docsDir)).tree;
46
+ const capabilities = (await fleet.capabilities(docsDir)).tree;
76
47
  const glossary = await readGlossary(glossaryDir(docsDir));
77
48
  return [
78
49
  ...(await markdownFiles(fleetAdrsDir(docsDir))),
@@ -89,7 +60,11 @@ export async function fleetDocuments(docsDir, fleet) {
89
60
  * describe a change that already shipped, its links point at whatever the tree
90
61
  * looked like then, and grading them would make every directory rename a
91
62
  * retroactive error against a record nobody may edit. `listFeatures` excludes
92
- * them by default, which is the same judgement one enumeration up.
63
+ * them by default, which is the same judgement one enumeration up. "Then" is
64
+ * the commit that archived the feature, and not the one before it: the archive
65
+ * re-expresses the links in the documents it moves (`movedMarkdown` below,
66
+ * `commands/archive/plan/document/links.ts`), because its own rename is the one
67
+ * change to the tree it knows exactly and makes in the same breath.
93
68
  */
94
69
  export async function allAuthoredDocuments(docsDir, fleet) {
95
70
  const docs = await fleetDocuments(docsDir, fleet);
@@ -101,4 +76,43 @@ export async function allAuthoredDocuments(docsDir, fleet) {
101
76
  }
102
77
  return docs;
103
78
  }
79
+ /**
80
+ * Every `.md` file under `dir`, at any depth, sorted — the documents a rename
81
+ * of `dir` carries with it, which is a different list from the corpus above:
82
+ * a rename moves a runbook, a subsystem's own notes, a feature's
83
+ * `legacy/proposal.md` exactly as it moves an ADR, and owes them all the same
84
+ * link correction whether or not anything grades them.
85
+ *
86
+ * Real directories and files only: a symlink is not followed — what it points
87
+ * at is not moved by the rename — and dot-directories and `node_modules` are
88
+ * machinery, never authored documents, exactly as every other walk in loam
89
+ * treats them. The archive's own `.loam-before/` is one of them: its
90
+ * pre-images are bytes to put back, not documents to correct.
91
+ *
92
+ * Two callers, the two directory renames loam makes: `subsystem move`
93
+ * (`commands/subsystem/txn/links.ts`) and `archive`
94
+ * (`commands/archive/plan/document/links.ts`). Here, below both, because the
95
+ * rule for "what does the rename carry" is one rule.
96
+ */
97
+ export async function movedMarkdown(dir) {
98
+ let entries;
99
+ try {
100
+ entries = await readdir(dir, { withFileTypes: true });
101
+ }
102
+ catch {
103
+ return [];
104
+ }
105
+ const out = [];
106
+ for (const entry of entries) {
107
+ const path = join(dir, entry.name);
108
+ if (entry.isDirectory()) {
109
+ if (!entry.name.startsWith(".") && entry.name !== "node_modules")
110
+ out.push(...(await movedMarkdown(path)));
111
+ }
112
+ else if (entry.isFile() && entry.name.endsWith(".md")) {
113
+ out.push(path);
114
+ }
115
+ }
116
+ return out.sort();
117
+ }
104
118
  //# sourceMappingURL=corpus.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"corpus.js","sourceRoot":"","sources":["../../../src/core/links/corpus.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * WHICH documents loam reads links out of — the corpus, spelled once.\n *\n * Two readers need it and must not disagree. `commands/validate/links/` grades\n * each target's own documents for `link.unresolved`; `core/glossary/backlinks.ts`\n * reads the WHOLE fleet's, because \"which documents use this term\" is a question\n * no single target can answer. A corpus that differed between them would report\n * a term as unused while the very document citing it was being graded one target\n * over.\n *\n * THE CORPUS IS EVERY AUTHORED MARKDOWN DOCUMENT, and the word doing the work is\n * authored. `AGENTS.md` and the scaffolded `README.md` are excluded because loam\n * WRITES them: a finding against generated prose names a defect its reader\n * cannot fix by editing, since the next `loam init` restores it. Their links are\n * loam's own problem, held on this side of the boundary by\n * `test/docs-facts.test.ts` and `test/package-docs.test.ts`.\n *\n * ADRs AND RUNBOOKS ARE IN IT, and they are the reason the convention was\n * written down at all: an ADR that supersedes another says which, by linking to\n * it. They are also the first documents loam reads that it previously only\n * counted, which is what `link.unreadable` exists for.\n *\n * `delta.likec4` IS NOT, and neither is any YAML: this is a markdown question.\n * The one thing a delta holds that addresses another file — `metadata { op }` —\n * has its own resolution and its own findings.\n */\nimport { featureCapabilityDeltas } from \"../capabilities/delta/tree.js\";\nimport { readCapabilityTree } from \"../capabilities/tree.js\";\nimport type { FleetContext } from \"../fleet-context.js\";\nimport { featureGlossary } from \"../glossary/delta.js\";\nimport { readGlossary } from \"../glossary/tree.js\";\nimport type { DocsDir, FeatureDir } from \"../kernel/ids/dirs.js\";\nimport { featurePaths, featureSpecPaths, fleetAdrsDir, servicePathsAt, type ServicePaths } from \"../repo/paths.js\";\nimport { capabilityDocsDir, glossaryDir } from \"../repo/authored/paths.js\";\nimport { featureSpecServices, listFeatures, listServices } from \"../repo/repo.js\";\nimport { markdownFiles } from \"../repo/tree/fs.js\";\n\n/**\n * One service's authored documents: the two requirement specs, the runbook, and\n * every ADR. The specs cost nothing extra when a `FleetContext` is in play — it\n * has already read both to parse their requirements, and the memo hands back the\n * same text.\n */\nexport async function serviceDocuments(paths: ServicePaths): Promise<string[]> {\n return [paths.spec, paths.archSpec, paths.runbook, ...(await markdownFiles(paths.adrsDir))];\n}\n\n/**\n * One feature's authored documents: the intent, each addressed service's two\n * spec deltas, every ADR, and each capability delta document.\n */\nexport async function featureDocuments(featureDir: FeatureDir, fleet?: FleetContext): Promise<string[]> {\n const paths = featurePaths(featureDir);\n const docs = [paths.intent, ...(await markdownFiles(paths.adrsDir))];\n for (const svc of await featureSpecServices(featureDir, fleet)) {\n const spec = featureSpecPaths(featureDir, svc);\n docs.push(spec.spec, spec.archSpec);\n }\n const capabilities =\n fleet === undefined ? await featureCapabilityDeltas(featureDir) : await fleet.featureCapabilityDeltas(featureDir);\n docs.push(...capabilities.docs.map((d) => d.spec));\n // The definitions this feature introduces. Their own links are graded like\n // any other document's — a term that cites a term the feature is not adding\n // is a broken join whether or not it has shipped yet.\n docs.push(...(await featureGlossary(featureDir)).terms.map((t) => t.path));\n return docs;\n}\n\n/**\n * The documents that belong to no service and no feature: the fleet's own\n * decision records, the living capability tree, and the glossary.\n *\n * Graded at fleet scope, so `validate --all` reports them once. A single-target\n * run says nothing about them — the rule `permissions.unenforced` and\n * `capability.invalid` already follow, and for the same reason: a finding about\n * the fleet repeated on every service target is the report.\n */\nexport async function fleetDocuments(docsDir: DocsDir, fleet?: FleetContext): Promise<string[]> {\n const capabilities =\n fleet === undefined\n ? await readCapabilityTree(capabilityDocsDir(docsDir))\n : (await fleet.capabilities(docsDir)).tree;\n const glossary = await readGlossary(glossaryDir(docsDir));\n return [\n ...(await markdownFiles(fleetAdrsDir(docsDir))),\n ...capabilities.docs.map((d) => d.spec),\n ...glossary.terms.map((t) => t.path),\n ];\n}\n\n/**\n * Every authored document in the repository — the union of the three above over\n * every service and every in-flight feature.\n *\n * ARCHIVED FEATURES ARE OUT, and the omission is the point rather than an\n * oversight. `features/archive/<FEAT>/` is the evolution history: its documents\n * describe a change that already shipped, its links point at whatever the tree\n * looked like then, and grading them would make every directory rename a\n * retroactive error against a record nobody may edit. `listFeatures` excludes\n * them by default, which is the same judgement one enumeration up.\n */\nexport async function allAuthoredDocuments(docsDir: DocsDir, fleet?: FleetContext): Promise<string[]> {\n const docs = await fleetDocuments(docsDir, fleet);\n for (const service of await listServices(docsDir, fleet)) {\n docs.push(...(await serviceDocuments(servicePathsAt(service.dir))));\n }\n for (const feature of await listFeatures(docsDir, {}, fleet)) {\n docs.push(...(await featureDocuments(feature.dir, fleet)));\n }\n return docs;\n}\n"]}
1
+ {"version":3,"file":"corpus.js","sourceRoot":"","sources":["../../../src/core/links/corpus.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * WHICH documents loam reads links out of — the corpus, spelled once.\n *\n * Two readers need it and must not disagree. `commands/validate/links/` grades\n * each target's own documents for `link.unresolved`; `core/links/backlinks.ts`\n * reads the WHOLE fleet's, because \"which documents use this term\" is a question\n * no single target can answer. A corpus that differed between them would report\n * a term as unused while the very document citing it was being graded one target\n * over. The two directory renames read it too — `subsystem move`\n * (`commands/subsystem/txn/links.ts`) and `archive`\n * (`commands/archive/plan/document/links.ts`) rewrite the links INTO what they\n * move from every document here — for the same reason from the other side: a\n * document `validate` grades that a rename did not relink is `link.unresolved`\n * the commit after.\n *\n * THE CORPUS IS EVERY AUTHORED MARKDOWN DOCUMENT, and the word doing the work is\n * authored. `AGENTS.md` and the scaffolded `README.md` are excluded because loam\n * WRITES them: a finding against generated prose names a defect its reader\n * cannot fix by editing, since the next `loam init` restores it. Their links are\n * loam's own problem, held on this side of the boundary by\n * `test/docs-facts.test.ts` and `test/package-docs.test.ts`.\n *\n * ADRs AND RUNBOOKS ARE IN IT, and they are the reason the convention was\n * written down at all: an ADR that supersedes another says which, by linking to\n * it. They are also the first documents loam reads that it previously only\n * counted, which is what `link.unreadable` exists for.\n *\n * `delta.likec4` IS NOT, and neither is any YAML: this is a markdown question.\n * The one thing a delta holds that addresses another file — `metadata { op }` —\n * has its own resolution and its own findings.\n */\nimport type { Dirent } from \"node:fs\";\nimport { readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { FleetContext } from \"../fleet-context.js\";\nimport { featureGlossary } from \"../glossary/delta.js\";\nimport { readGlossary } from \"../glossary/tree.js\";\nimport type { DocsDir, FeatureDir } from \"../kernel/ids/dirs.js\";\nimport { featurePaths, featureSpecPaths, fleetAdrsDir, servicePathsAt, type ServicePaths } from \"../repo/paths.js\";\nimport { glossaryDir } from \"../repo/authored/paths.js\";\nimport { featureSpecServices, listFeatures, listServices } from \"../repo/repo.js\";\nimport { markdownFiles } from \"../repo/tree/fs.js\";\n\n/**\n * One service's authored documents: the two requirement specs, the runbook, and\n * every ADR. The specs cost nothing extra: the run's `FleetContext` has already\n * read both to parse their requirements, and the memo hands back the same text.\n */\nexport async function serviceDocuments(paths: ServicePaths): Promise<string[]> {\n return [paths.spec, paths.archSpec, paths.runbook, ...(await markdownFiles(paths.adrsDir))];\n}\n\n/**\n * One feature's authored documents: the intent, each addressed service's two\n * spec deltas, every ADR, and each capability delta document.\n */\nexport async function featureDocuments(featureDir: FeatureDir, fleet: FleetContext): Promise<string[]> {\n const paths = featurePaths(featureDir);\n const docs = [paths.intent, ...(await markdownFiles(paths.adrsDir))];\n for (const svc of await featureSpecServices(featureDir, fleet)) {\n const spec = featureSpecPaths(featureDir, svc);\n docs.push(spec.spec, spec.archSpec);\n }\n const capabilities = await fleet.featureCapabilityDeltas(featureDir);\n docs.push(...capabilities.docs.map((d) => d.spec));\n // The definitions this feature introduces. Their own links are graded like\n // any other document's — a term that cites a term the feature is not adding\n // is a broken join whether or not it has shipped yet.\n docs.push(...(await featureGlossary(featureDir)).terms.map((t) => t.path));\n return docs;\n}\n\n/**\n * The documents that belong to no service and no feature: the fleet's own\n * decision records, the living capability tree, and the glossary.\n *\n * Graded at fleet scope, so `validate --all` reports them once. A single-target\n * run says nothing about them — the rule `permissions.unenforced` and\n * `capability.invalid` already follow, and for the same reason: a finding about\n * the fleet repeated on every service target is the report.\n */\nexport async function fleetDocuments(docsDir: DocsDir, fleet: FleetContext): Promise<string[]> {\n const capabilities = (await fleet.capabilities(docsDir)).tree;\n const glossary = await readGlossary(glossaryDir(docsDir));\n return [\n ...(await markdownFiles(fleetAdrsDir(docsDir))),\n ...capabilities.docs.map((d) => d.spec),\n ...glossary.terms.map((t) => t.path),\n ];\n}\n\n/**\n * Every authored document in the repository — the union of the three above over\n * every service and every in-flight feature.\n *\n * ARCHIVED FEATURES ARE OUT, and the omission is the point rather than an\n * oversight. `features/archive/<FEAT>/` is the evolution history: its documents\n * describe a change that already shipped, its links point at whatever the tree\n * looked like then, and grading them would make every directory rename a\n * retroactive error against a record nobody may edit. `listFeatures` excludes\n * them by default, which is the same judgement one enumeration up. \"Then\" is\n * the commit that archived the feature, and not the one before it: the archive\n * re-expresses the links in the documents it moves (`movedMarkdown` below,\n * `commands/archive/plan/document/links.ts`), because its own rename is the one\n * change to the tree it knows exactly and makes in the same breath.\n */\nexport async function allAuthoredDocuments(docsDir: DocsDir, fleet: FleetContext): Promise<string[]> {\n const docs = await fleetDocuments(docsDir, fleet);\n for (const service of await listServices(docsDir, fleet)) {\n docs.push(...(await serviceDocuments(servicePathsAt(service.dir))));\n }\n for (const feature of await listFeatures(docsDir, {}, fleet)) {\n docs.push(...(await featureDocuments(feature.dir, fleet)));\n }\n return docs;\n}\n\n/**\n * Every `.md` file under `dir`, at any depth, sorted — the documents a rename\n * of `dir` carries with it, which is a different list from the corpus above:\n * a rename moves a runbook, a subsystem's own notes, a feature's\n * `legacy/proposal.md` exactly as it moves an ADR, and owes them all the same\n * link correction whether or not anything grades them.\n *\n * Real directories and files only: a symlink is not followed — what it points\n * at is not moved by the rename — and dot-directories and `node_modules` are\n * machinery, never authored documents, exactly as every other walk in loam\n * treats them. The archive's own `.loam-before/` is one of them: its\n * pre-images are bytes to put back, not documents to correct.\n *\n * Two callers, the two directory renames loam makes: `subsystem move`\n * (`commands/subsystem/txn/links.ts`) and `archive`\n * (`commands/archive/plan/document/links.ts`). Here, below both, because the\n * rule for \"what does the rename carry\" is one rule.\n */\nexport async function movedMarkdown(dir: string): Promise<string[]> {\n let entries: Dirent[];\n try {\n entries = await readdir(dir, { withFileTypes: true });\n } catch {\n return [];\n }\n const out: string[] = [];\n for (const entry of entries) {\n const path = join(dir, entry.name);\n if (entry.isDirectory()) {\n if (!entry.name.startsWith(\".\") && entry.name !== \"node_modules\") out.push(...(await movedMarkdown(path)));\n } else if (entry.isFile() && entry.name.endsWith(\".md\")) {\n out.push(path);\n }\n }\n return out.sort();\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"findings.js","sourceRoot":"","sources":["../../../src/core/links/findings.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `link.unresolved` — a markdown link in an authored document whose target is\n * not there, or is there under a different spelling.\n *\n * The convention this grades has been written down since the fleet-ADR work and\n * said, in both `SCHEMA.md` and the generated `AGENTS.md`, that nothing\n * validated it. This module is what makes that sentence false, and the reason\n * the convention was chosen is the reason it can: the target of a standard\n * markdown link is a real relative path, so \"does this resolve\" has a\n * filesystem answer, where a wikilink would need Obsidian's\n * shortest-unique-path search reimplemented — guessing, which loam does not do.\n *\n * ERROR, and the severity is the decision the maintainer took against the\n * alternative of warning outside `glossary/`. A link is a join of the same kind\n * as `Operations:` and `Covers:`, both of which error when they resolve to\n * nothing; the fact that this one is written in prose changes who typed it, not\n * whether it points at something. A fleet adopting loam with rotted links sees\n * them on its first run — a user-visible widening of the same class as the two\n * the use-case and business axes shipped, and recorded in CHANGELOG as one.\n *\n * ONE FINDING PER DOCUMENT, with every broken link in `details`. A document\n * whose links have rotted is one editing session and one reviewer, not ten\n * separate breaches, and per-link findings would let a single stale directory\n * rename fill a `validate --all` report with the same fix restated.\n *\n * THE RESOLUTION IS EXPORTED SEPARATELY from the finding, because a second\n * reader needs the other half of the answer: the glossary grades a term by who\n * links TO it, which is this same walk read forwards instead of backwards.\n */\nimport { existsSync } from \"node:fs\";\nimport { basename, dirname, resolve } from \"node:path\";\nimport { repoPath } from \"../envelope/json.js\";\nimport { isPathInside } from \"../kernel/path-safety.js\";\nimport type { DocsDir } from \"../kernel/ids/dirs.js\";\nimport type { Finding } from \"../vocabulary/report.js\";\nimport { pathCaseIndex, type PathCaseIndex } from \"./case.js\";\nimport { documentLinks, type DocumentLink } from \"./parse.js\";\n\n/** One authored document, and the text somebody already read out of it. */\nexport interface LinkedDocument {\n /** Absolute path of the document — the directory every relative target resolves against. */\n path: string;\n text: string;\n}\n\n/** What the finding is filed under, and the tree it may look inside. */\nexport interface LinkScope {\n docsDir: DocsDir;\n /** The service or feature the report is about; absent at fleet scope. */\n subject?: string;\n /**\n * The directory-listing memo behind the case check, shared across a whole\n * run by whoever owns the walk. Omitted, each call builds its own — correct,\n * just not shared. Never a module-level cache: a listing that outlived one\n * command would answer for a directory that has since changed.\n */\n cases?: PathCaseIndex;\n /**\n * Absolute paths that do not exist yet but WILL once an in-flight feature\n * archives — today, the glossary terms that feature introduces.\n *\n * A feature cites the word it is adding at the word's FUTURE living path,\n * because that is where the link has to point once the change lands; citing\n * the delta's own path would resolve now and rot on the day it ships. Without\n * this set, the axis would refuse its own headline case — which is exactly\n * the overlay the capability axis had to add to `Realizes:` after shipping\n * without one.\n */\n overlay?: ReadonlySet<string>;\n}\n\n/** A link that points at a real file inside the docs repo, and the file it points at. */\nexport interface ResolvedLink {\n link: DocumentLink;\n /** The absolute path the link names, as `node:path` resolves it. */\n target: string;\n}\n\n/** One document's links, split by whether the docs repo answers for them. */\nexport interface DocumentLinkage {\n resolved: ResolvedLink[];\n /** Links inside the docs repo that name nothing — with the stored spelling, where there is one. */\n broken: Array<{ link: DocumentLink; storedAs?: string }>;\n}\n\n/**\n * Every link in one document, resolved against the docs repo.\n *\n * TARGETS OUTSIDE THE DOCS REPO ARE NEITHER RESOLVED NOR BROKEN, and the\n * exclusion is honesty rather than leniency. A docs repo is routinely checked\n * out beside nothing else, so `../../payments-svc/README.md` is a path loam\n * cannot answer for: the file's absence here is evidence about this checkout,\n * not about the link. Only a target that stays inside `docsDir` gets a verdict,\n * which is the same boundary every other reader in loam observes and the reason\n * the finding below can afford to be an error at all.\n *\n * CASE COUNTS, and it must, because the alternative is a check that passes on\n * the author's machine and fails where the work is read. `existsSync` is\n * case-insensitive on Windows and macOS; GitHub's renderer and every Linux CI\n * runner are not. So a resolving link is one that exists AND is spelled the way\n * the filesystem stores it — and when it is not, `storedAs` carries the real\n * name into the message, on every platform alike, so the diagnosis does not\n * depend on which operating system ran the check.\n */\nexport function resolveLinks(doc: LinkedDocument, scope: LinkScope): DocumentLinkage {\n const cases = scope.cases ?? pathCaseIndex();\n const dir = dirname(doc.path);\n const linkage: DocumentLinkage = { resolved: [], broken: [] };\n for (const link of documentLinks(doc.text)) {\n const target = resolve(dir, link.path);\n if (!isPathInside(scope.docsDir, target)) continue;\n // The overlay is checked FIRST and without the spelling test: the file is\n // not on disk, so there is no stored name to compare against, and the id it\n // came from was a `readdir` entry in the feature's own tree.\n if (scope.overlay?.has(resolve(target)) === true) {\n linkage.resolved.push({ link, target });\n continue;\n }\n if (existsSync(target) && cases.spelledExactly(scope.docsDir, target)) {\n linkage.resolved.push({ link, target });\n continue;\n }\n const storedAs = cases.storedAs(dirname(target), basename(target));\n linkage.broken.push({ link, ...(storedAs === undefined ? {} : { storedAs }) });\n }\n return linkage;\n}\n\n/** The links in one document that resolve to nothing, as at most one finding. */\nexport function unresolvedLinkFindings(doc: LinkedDocument, scope: LinkScope): Finding[] {\n const where = repoPath(scope.docsDir, doc.path);\n const { broken } = resolveLinks(doc, scope);\n if (broken.length === 0) return [];\n return [\n {\n severity: \"error\",\n code: \"link.unresolved\",\n ...(scope.subject === undefined ? {} : { subject: scope.subject }),\n message:\n `${where}: ${broken.length} markdown link(s) resolve to nothing. ` +\n \"A link between documents here is a join — fix the path, or write the document it points at. \" +\n \"Targets outside the docs repo and links to a heading (`#section`) are not graded.\",\n details: broken.map((one) => detail(where, one)),\n },\n ];\n}\n\n/**\n * One broken link, spelled so the author can find it: the file and line first,\n * then the link exactly as written. `raw` rather than the decoded path, because\n * the decoded form is not the string that is in the document.\n *\n * The stored spelling is appended when the only thing wrong is case, because\n * without it the message reads as a lie — the file the author is being told to\n * write is already sitting beside the one they linked from.\n */\nfunction detail(where: string, one: { link: DocumentLink; storedAs?: string }): string {\n const line = `${where}:${one.link.line}: [${one.link.text}](${one.link.raw})`;\n return one.storedAs === undefined ? line : `${line} — stored as '${one.storedAs}'`;\n}\n"]}
1
+ {"version":3,"file":"findings.js","sourceRoot":"","sources":["../../../src/core/links/findings.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `link.unresolved` — a markdown link in an authored document whose target is\n * not there, or is there under a different spelling.\n *\n * The convention this grades has been written down since the fleet-ADR work and\n * said, in both `SCHEMA.md` and the generated `AGENTS.md`, that nothing\n * validated it. This module is what makes that sentence false, and the reason\n * the convention was chosen is the reason it can: the target of a standard\n * markdown link is a real relative path, so \"does this resolve\" has a\n * filesystem answer, where a wikilink would need Obsidian's\n * shortest-unique-path search reimplemented — guessing, which loam does not do.\n *\n * ERROR, and the severity is the decision the maintainer took against the\n * alternative of warning outside `glossary/`. A link is a join of the same kind\n * as `Operations:` and `Covers:`, both of which error when they resolve to\n * nothing; the fact that this one is written in prose changes who typed it, not\n * whether it points at something. A fleet adopting loam with rotted links sees\n * them on its first run — a user-visible widening of the same class as the two\n * the use-case and business axes shipped, and recorded in CHANGELOG as one.\n *\n * ONE FINDING PER DOCUMENT, with every broken link in `details`. A document\n * whose links have rotted is one editing session and one reviewer, not ten\n * separate breaches, and per-link findings would let a single stale directory\n * rename fill a `validate --all` report with the same fix restated.\n *\n * THE RESOLUTION IS EXPORTED SEPARATELY from the finding, because a second\n * reader needs the other half of the answer: the glossary grades a term by who\n * links TO it, which is this same walk read forwards instead of backwards.\n */\nimport { existsSync } from \"node:fs\";\nimport { basename, dirname, resolve } from \"node:path\";\nimport { repoPath } from \"../envelope/json.js\";\nimport { isPathInside } from \"../kernel/path-safety.js\";\nimport type { DocsDir } from \"../kernel/ids/dirs.js\";\nimport type { Finding } from \"../vocabulary/report.js\";\nimport { pathCaseIndex, type PathCaseIndex } from \"./case.js\";\nimport { documentLinks, type DocumentLink } from \"./parse.js\";\n\n/** One authored document, and the text somebody already read out of it. */\nexport interface LinkedDocument {\n /** Absolute path of the document — the directory every relative target resolves against. */\n path: string;\n text: string;\n}\n\n/** What the finding is filed under, and the tree it may look inside. */\nexport interface LinkScope {\n docsDir: DocsDir;\n /** The service or feature the report is about; absent at fleet scope. */\n subject?: string;\n /**\n * The directory-listing memo behind the case check, shared across a whole\n * run by whoever owns the walk. Omitted, each call builds its own — correct,\n * just not shared. Never a module-level cache: a listing that outlived one\n * command would answer for a directory that has since changed.\n */\n cases?: PathCaseIndex;\n /**\n * Absolute paths that do not exist yet but WILL once an in-flight feature\n * archives — today, the glossary terms that feature introduces.\n *\n * A feature may cite the word it is adding at the word's FUTURE living path,\n * because that is where the link points once the change lands. Without this\n * set, the axis would refuse its own headline case — which is exactly the\n * overlay the capability axis had to add to `Realizes:` after shipping\n * without one. (Citing the term inside the feature resolves without it, and\n * archive re-points that spelling when it merges the text that carries it —\n * SCHEMA \"Conventions\" states the rule.)\n */\n overlay?: ReadonlySet<string>;\n}\n\n/** A link that points at a real file inside the docs repo, and the file it points at. */\nexport interface ResolvedLink {\n link: DocumentLink;\n /** The absolute path the link names, as `node:path` resolves it. */\n target: string;\n}\n\n/** One document's links, split by whether the docs repo answers for them. */\nexport interface DocumentLinkage {\n resolved: ResolvedLink[];\n /** Links inside the docs repo that name nothing — with the stored spelling, where there is one. */\n broken: Array<{ link: DocumentLink; storedAs?: string }>;\n}\n\n/**\n * Every link in one document, resolved against the docs repo.\n *\n * TARGETS OUTSIDE THE DOCS REPO ARE NEITHER RESOLVED NOR BROKEN, and the\n * exclusion is honesty rather than leniency. A docs repo is routinely checked\n * out beside nothing else, so `../../payments-svc/README.md` is a path loam\n * cannot answer for: the file's absence here is evidence about this checkout,\n * not about the link. Only a target that stays inside `docsDir` gets a verdict,\n * which is the same boundary every other reader in loam observes and the reason\n * the finding below can afford to be an error at all.\n *\n * CASE COUNTS, and it must, because the alternative is a check that passes on\n * the author's machine and fails where the work is read. `existsSync` is\n * case-insensitive on Windows and macOS; GitHub's renderer and every Linux CI\n * runner are not. So a resolving link is one that exists AND is spelled the way\n * the filesystem stores it — and when it is not, `storedAs` carries the real\n * name into the message, on every platform alike, so the diagnosis does not\n * depend on which operating system ran the check.\n */\nexport function resolveLinks(doc: LinkedDocument, scope: LinkScope): DocumentLinkage {\n const cases = scope.cases ?? pathCaseIndex();\n const dir = dirname(doc.path);\n const linkage: DocumentLinkage = { resolved: [], broken: [] };\n for (const link of documentLinks(doc.text)) {\n const target = resolve(dir, link.path);\n if (!isPathInside(scope.docsDir, target)) continue;\n // The overlay is checked FIRST and without the spelling test: the file is\n // not on disk, so there is no stored name to compare against, and the id it\n // came from was a `readdir` entry in the feature's own tree.\n if (scope.overlay?.has(resolve(target)) === true) {\n linkage.resolved.push({ link, target });\n continue;\n }\n if (existsSync(target) && cases.spelledExactly(scope.docsDir, target)) {\n linkage.resolved.push({ link, target });\n continue;\n }\n const storedAs = cases.storedAs(dirname(target), basename(target));\n linkage.broken.push({ link, ...(storedAs === undefined ? {} : { storedAs }) });\n }\n return linkage;\n}\n\n/** The links in one document that resolve to nothing, as at most one finding. */\nexport function unresolvedLinkFindings(doc: LinkedDocument, scope: LinkScope): Finding[] {\n const where = repoPath(scope.docsDir, doc.path);\n const { broken } = resolveLinks(doc, scope);\n if (broken.length === 0) return [];\n return [\n {\n severity: \"error\",\n code: \"link.unresolved\",\n ...(scope.subject === undefined ? {} : { subject: scope.subject }),\n message:\n `${where}: ${broken.length} markdown link(s) resolve to nothing. ` +\n \"A link between documents here is a join — fix the path, or write the document it points at. \" +\n \"Targets outside the docs repo and links to a heading (`#section`) are not graded.\",\n details: broken.map((one) => detail(where, one)),\n },\n ];\n}\n\n/**\n * One broken link, spelled so the author can find it: the file and line first,\n * then the link exactly as written. `raw` rather than the decoded path, because\n * the decoded form is not the string that is in the document.\n *\n * The stored spelling is appended when the only thing wrong is case, because\n * without it the message reads as a lie — the file the author is being told to\n * write is already sitting beside the one they linked from.\n */\nfunction detail(where: string, one: { link: DocumentLink; storedAs?: string }): string {\n const line = `${where}:${one.link.line}: [${one.link.text}](${one.link.raw})`;\n return one.storedAs === undefined ? line : `${line} — stored as '${one.storedAs}'`;\n}\n"]}
@@ -153,12 +153,29 @@ function decodePath(raw) {
153
153
  * It is a positional rewrite, never a re-render: the matched target's own span
154
154
  * is replaced and every other byte of the line — the link text, the title, the
155
155
  * prose around it — is left exactly as written.
156
+ *
157
+ * WHEN THE TARGET MOVES TOO, `retarget` says where to. The same archive that
158
+ * moves a requirement two directories up moves the rest of the feature as
159
+ * well: a term the feature defines is copied into `glossary/`, and the feature
160
+ * directory itself — its ADRs, its intent — goes to `features/archive/<FEAT>/`.
161
+ * A link into the feature re-expressed against the target's OLD place was
162
+ * therefore re-expressed correctly and still rotted on the commit that made it.
163
+ * And a document that does not move at all can still need rewriting, when the
164
+ * directory it links into does (`subsystem move`) — which is why `from === to`
165
+ * is no longer a reason to stop when a `retarget` is given.
166
+ *
167
+ * A LINK WHOSE TEXT STILL REACHES ITS TARGET FROM THE NEW PLACE KEEPS ITS
168
+ * SPELLING: a sibling that moves with its document, or a target nothing moved
169
+ * under a document that stays, comes out of the rewrite exactly as the author
170
+ * wrote it rather than normalised into `./` form by a commit that had no
171
+ * reason to touch it.
156
172
  */
157
173
  export function rebaseLinks(md, move) {
158
174
  const from = resolve(move.from);
159
175
  const to = resolve(move.to);
160
- if (from === to)
176
+ if (from === to && move.retarget === undefined)
161
177
  return md;
178
+ const route = { from, to, retarget: move.retarget ?? ((target) => target) };
162
179
  const fenced = fenceTracker();
163
180
  // `split` with a capturing group keeps the separators, so a document's own
164
181
  // mixture of CRLF and LF survives a rewrite that touches neither.
@@ -167,19 +184,19 @@ export function rebaseLinks(md, move) {
167
184
  const rawLine = parts[i];
168
185
  if (fenced(rawLine))
169
186
  continue;
170
- parts[i] = rebaseLine(rawLine, from, to);
187
+ parts[i] = rebaseLine(rawLine, route);
171
188
  }
172
189
  return parts.join("");
173
190
  }
174
191
  /** One line's links re-expressed, right to left so earlier spans keep their offsets. */
175
- function rebaseLine(rawLine, from, to) {
192
+ function rebaseLine(rawLine, route) {
176
193
  const blanked = rawLine.replace(CODE_SPAN_RE, (m) => " ".repeat(m.length));
177
194
  const spans = [];
178
195
  const collect = (m) => {
179
196
  const at = m.indices?.[2];
180
197
  if (at === undefined)
181
198
  return;
182
- const rebased = rebaseTarget(m[2], from, to);
199
+ const rebased = rebaseTarget(m[2], route);
183
200
  if (rebased !== null)
184
201
  spans.push([at[0], at[1], rebased]);
185
202
  };
@@ -198,16 +215,18 @@ function rebaseLine(rawLine, from, to) {
198
215
  return out;
199
216
  }
200
217
  /**
201
- * One target, re-expressed from `to` instead of `from`, or null when there is
202
- * nothing to re-express.
218
+ * One target, re-expressed from `to` instead of `from` — and pointed at wherever
219
+ * `retarget` says the file itself will be — or null when there is nothing to
220
+ * re-express.
203
221
  *
204
222
  * The exclusions are `push`'s exactly — an absolute URL, a site-root path and a
205
223
  * bare fragment mean the same thing from every directory — and the ANGLE-BRACKET
206
224
  * and PERCENT-ESCAPE forms are preserved rather than normalised: a rewrite that
207
225
  * silently changed `<a b.md>` into `a%20b.md` would be editing the author's
208
- * spelling under cover of moving their file.
226
+ * spelling under cover of moving their file. A route that comes out unchanged
227
+ * is null for the same reason (`rebaseLinks` says when that happens).
209
228
  */
210
- function rebaseTarget(target, from, to) {
229
+ function rebaseTarget(target, route) {
211
230
  const angled = target.startsWith("<") && target.endsWith(">");
212
231
  const raw = angled ? target.slice(1, -1) : target;
213
232
  if (raw === "" || raw.startsWith("#") || raw.startsWith("/"))
@@ -220,7 +239,17 @@ function rebaseTarget(target, from, to) {
220
239
  if (pathPart === "")
221
240
  return null;
222
241
  const encoded = pathPart.includes("%");
223
- let rebased = relative(to, resolve(from, decodePath(pathPart))).split(sep).join("/");
242
+ const was = resolve(route.from, decodePath(pathPart));
243
+ const will = resolve(route.retarget(was));
244
+ // The author's TEXT, read from the new place, still reaching the target —
245
+ // not two computed routes compared. A link that climbs above a moved
246
+ // directory and comes back down through its old name
247
+ // (`../../../services/payment-service/spec.md` from inside that service)
248
+ // normalises to the same route either side of the move, and comparing
249
+ // routes kept a spelling that names a directory the move just removed.
250
+ if (resolve(route.to, decodePath(pathPart)) === will)
251
+ return null;
252
+ let rebased = relative(route.to, will).split(sep).join("/");
224
253
  // `relative` returns a bare name for a sibling, which markdown reads as a
225
254
  // path relative to the file anyway — but `./` is what an author writes and
226
255
  // what keeps the link distinguishable from a reference label.
@@ -1 +1 @@
1
- {"version":3,"file":"parse.js","sourceRoot":"","sources":["../../../src/core/links/parse.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Every markdown link a document writes, as data.\n *\n * A link between documents in a docs repo is a JOIN, not decoration — a\n * requirement that names a domain term points at that term's definition, and an\n * ADR that supersedes another says which. `SCHEMA.md` and the generated\n * `AGENTS.md` have stated the convention (standard markdown links, relative\n * path targets) since the fleet-ADR work; this module is what finally reads\n * one, so `./findings.ts` can answer the only question the convention was\n * chosen for: does the target exist.\n *\n * WHY A PARSER RATHER THAN A REGEX AT THE CALL SITE. Two classes of false\n * positive make the naive `\\[.*\\]\\(.*\\)` unusable on the documents loam owns,\n * and both occur in loam's OWN generated prose. A fenced block is where a\n * document SHOWS the convention — the `AGENTS.md` section that teaches the link\n * format contains a link nobody wrote to be followed — and an inline code span\n * is where prose quotes a path. Both must read as text. The fence rule is the\n * one `core/document/parse.ts` already applies to headings, and it is imported\n * rather than restated for the reason that module gives about its own sampler:\n * two readers disagreeing about what a fence encloses prescribe a document no\n * author can satisfy.\n */\nimport { relative, resolve, sep } from \"node:path\";\nimport { fenceTracker } from \"../kernel/fences.js\";\n\n/** One link written in a markdown document. */\nexport interface DocumentLink {\n /** The target exactly as written, angle brackets stripped, fragment included. */\n raw: string;\n /**\n * The FILE half — `raw` with any `#fragment` removed and percent-escapes\n * decoded. This is what resolves against the filesystem; `raw` is what the\n * finding quotes back, because that is the string the author has to find.\n */\n path: string;\n /** The link text, or the reference label for a definition. Quoted in the finding. */\n text: string;\n /** 1-based line the link was written on. */\n line: number;\n}\n\n/**\n * Inline links and images: `[text](target)`, `![alt](target)`,\n * `[text](<target with spaces>)`, `[text](target \"title\")`.\n *\n * The target alternation takes `<...>` first so an angle-bracketed path\n * containing spaces stays one token; the bare form stops at whitespace or `)`,\n * which gives up on the balanced-parens spelling markdown also allows\n * (`[x](a(1).md)`). That is a deliberate under-read: it costs a link nobody\n * writes, where over-reading would swallow the rest of a sentence and report a\n * target no author would recognise.\n */\nconst INLINE_RE = /!?\\[([^\\]]*)\\]\\(\\s*(<[^>\\n]*>|[^\\s)]*)(?:\\s+(?:\"[^\"]*\"|'[^']*'|\\([^)]*\\)))?\\s*\\)/gd;\n\n/**\n * A reference DEFINITION — `[label]: target \"title\"` on its own line. Included\n * because the target is the same join written in a different place: a document\n * that collects its links at the bottom is following the convention, and a\n * reader that only saw inline links would grade it as having none.\n */\nconst DEFINITION_RE = /^ {0,3}\\[([^\\]]+)\\]:\\s*(<[^>\\n]*>|\\S+)/d;\n\n/**\n * Code spans, blanked before matching. `\\`[a](b)\\`` is prose ABOUT a link and\n * resolving it would convict a document for explaining the convention. Blanked\n * to spaces rather than removed so every column after it — and therefore any\n * real link later on the same line — keeps its position.\n */\nconst CODE_SPAN_RE = /(`+)(?:[^`]|(?!\\1)`)*\\1/g;\n\n/**\n * Every link in `md`, in document order.\n *\n * The whole document is scanned, frontmatter included: a YAML `see: [x](y.md)`\n * is not a link loam wrote a field for, and the alternative — teaching this\n * module where frontmatter ends — would make it read the one part of a document\n * loam's own doctrine says is structured. A link that resolves is a link that\n * resolves wherever it is written.\n */\nexport function documentLinks(md: string): DocumentLink[] {\n const fenced = fenceTracker();\n const out: DocumentLink[] = [];\n md.split(/\\r?\\n/).forEach((rawLine, i) => {\n if (fenced(rawLine)) return;\n const line = rawLine.replace(CODE_SPAN_RE, (m) => \" \".repeat(m.length));\n const definition = DEFINITION_RE.exec(line);\n if (definition) {\n push(out, definition[2]!, definition[1]!, i + 1);\n return;\n }\n INLINE_RE.lastIndex = 0;\n for (let m = INLINE_RE.exec(line); m !== null; m = INLINE_RE.exec(line)) {\n push(out, m[2]!, m[1]!, i + 1);\n }\n });\n return out;\n}\n\n/**\n * Record one link, unless its target is not a path this repository can answer\n * for. Four exclusions, each of them a question with no filesystem answer\n * rather than a link loam declines to grade:\n *\n * - an EMPTY target (`[text]()`) addresses nothing;\n * - a SCHEME (`https:`, `mailto:`, and a Windows `C:` drive with it) addresses\n * something outside this repository;\n * - a ROOT-relative or protocol-relative target (`/x`, `//host/x`) is resolved\n * by whatever renders the document — a site root, not the file's directory —\n * so loam does not know what it points at;\n * - a PURE fragment (`#section`) addresses this same document, and grading it\n * means resolving heading anchors, which is a different question from \"does\n * this file exist\".\n *\n * The last two are DEFENSIVE rather than decisive, and a mutation run says so:\n * removing either leaves every test green, because a pure fragment leaves an\n * empty path and a site-root target resolves outside the docs repo, where\n * `./findings.ts` declines to answer anyway. They are kept because the reason\n * differs from that boundary's — loam is not silent about these because they\n * are elsewhere, it is silent because it does not know where they are — and a\n * later reader deleting them as dead would be removing an intention, not a\n * branch.\n */\nfunction push(out: DocumentLink[], target: string, text: string, line: number): void {\n const raw = target.startsWith(\"<\") && target.endsWith(\">\") ? target.slice(1, -1) : target;\n if (raw === \"\" || raw.startsWith(\"#\") || raw.startsWith(\"/\")) return;\n if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(raw)) return;\n const path = decodePath(raw.split(\"#\")[0]!);\n if (path === \"\") return;\n out.push({ raw, path, text, line });\n}\n\n/**\n * Percent-decode a link target, or keep it verbatim when it does not decode.\n *\n * `[Order line](order%20line.md)` is how every editor writes a path holding a\n * space, so a resolver that skipped this step would report the one spelling\n * authors do not type by hand. `decodeURIComponent` throws on a lone `%` — a\n * literal percent in a filename is legal and rarer than the escape — and the\n * honest answer there is the string as written, which then resolves or does not\n * on its own terms.\n */\nfunction decodePath(raw: string): string {\n try {\n return decodeURIComponent(raw);\n } catch {\n return raw;\n }\n}\n\n/**\n * Re-express every relative link in `md` from a new base directory — the merge's\n * half of the link convention.\n *\n * THE DEFECT THIS CLOSES is structural and predates the glossary. A feature's\n * `specs/<svc>/spec.md` sits four directories below the docs root; the living\n * `services/<svc>/spec.md` it merges into sits two. `loam archive` copies a\n * requirement's text verbatim, so `[Order](../../../../glossary/order.md)` —\n * correct where it was written, and clickable in the pull request that reviewed\n * it — lands in the living document pointing two directories above the\n * repository. The same is true of a delta requirement citing an ADR, which has\n * been possible since before this module existed.\n *\n * REWRITING IS THE ONLY ANSWER THAT KEEPS BOTH PROPERTIES. The alternative\n * considered was to have authors write the link relative to where the text will\n * END UP, leaving loam to touch nothing — but that link does not resolve in the\n * delta, and \"it renders as a link in pull-request review, which is where these\n * documents are actually read\" is the convention's own first argument. So the\n * author writes what is true where they write it, and the merge makes it true\n * where it lands. The target is unchanged: only the route to it is.\n *\n * It is a positional rewrite, never a re-render: the matched target's own span\n * is replaced and every other byte of the line — the link text, the title, the\n * prose around it — is left exactly as written.\n */\nexport function rebaseLinks(md: string, move: { from: string; to: string }): string {\n const from = resolve(move.from);\n const to = resolve(move.to);\n if (from === to) return md;\n const fenced = fenceTracker();\n // `split` with a capturing group keeps the separators, so a document's own\n // mixture of CRLF and LF survives a rewrite that touches neither.\n const parts = md.split(/(\\r?\\n)/);\n for (let i = 0; i < parts.length; i += 2) {\n const rawLine = parts[i]!;\n if (fenced(rawLine)) continue;\n parts[i] = rebaseLine(rawLine, from, to);\n }\n return parts.join(\"\");\n}\n\n/** One line's links re-expressed, right to left so earlier spans keep their offsets. */\nfunction rebaseLine(rawLine: string, from: string, to: string): string {\n const blanked = rawLine.replace(CODE_SPAN_RE, (m) => \" \".repeat(m.length));\n const spans: Array<[number, number, string]> = [];\n const collect = (m: RegExpExecArray): void => {\n const at = m.indices?.[2];\n if (at === undefined) return;\n const rebased = rebaseTarget(m[2]!, from, to);\n if (rebased !== null) spans.push([at[0], at[1], rebased]);\n };\n const definition = DEFINITION_RE.exec(blanked);\n if (definition) collect(definition);\n else {\n INLINE_RE.lastIndex = 0;\n for (let m = INLINE_RE.exec(blanked); m !== null; m = INLINE_RE.exec(blanked)) collect(m);\n }\n let out = rawLine;\n for (const [start, end, replacement] of spans.reverse()) {\n out = out.slice(0, start) + replacement + out.slice(end);\n }\n return out;\n}\n\n/**\n * One target, re-expressed from `to` instead of `from`, or null when there is\n * nothing to re-express.\n *\n * The exclusions are `push`'s exactly — an absolute URL, a site-root path and a\n * bare fragment mean the same thing from every directory — and the ANGLE-BRACKET\n * and PERCENT-ESCAPE forms are preserved rather than normalised: a rewrite that\n * silently changed `<a b.md>` into `a%20b.md` would be editing the author's\n * spelling under cover of moving their file.\n */\nfunction rebaseTarget(target: string, from: string, to: string): string | null {\n const angled = target.startsWith(\"<\") && target.endsWith(\">\");\n const raw = angled ? target.slice(1, -1) : target;\n if (raw === \"\" || raw.startsWith(\"#\") || raw.startsWith(\"/\")) return null;\n if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(raw)) return null;\n const hash = raw.indexOf(\"#\");\n const pathPart = hash === -1 ? raw : raw.slice(0, hash);\n const fragment = hash === -1 ? \"\" : raw.slice(hash);\n if (pathPart === \"\") return null;\n const encoded = pathPart.includes(\"%\");\n let rebased = relative(to, resolve(from, decodePath(pathPart))).split(sep).join(\"/\");\n // `relative` returns a bare name for a sibling, which markdown reads as a\n // path relative to the file anyway — but `./` is what an author writes and\n // what keeps the link distinguishable from a reference label.\n if (rebased !== \"\" && !rebased.startsWith(\".\")) rebased = `./${rebased}`;\n if (rebased === \"\") return null;\n if (!angled && (encoded || rebased.includes(\" \"))) rebased = rebased.split(\" \").join(\"%20\");\n const rewritten = rebased + fragment;\n return angled ? `<${rewritten}>` : rewritten;\n}\n"]}
1
+ {"version":3,"file":"parse.js","sourceRoot":"","sources":["../../../src/core/links/parse.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Every markdown link a document writes, as data.\n *\n * A link between documents in a docs repo is a JOIN, not decoration — a\n * requirement that names a domain term points at that term's definition, and an\n * ADR that supersedes another says which. `SCHEMA.md` and the generated\n * `AGENTS.md` have stated the convention (standard markdown links, relative\n * path targets) since the fleet-ADR work; this module is what finally reads\n * one, so `./findings.ts` can answer the only question the convention was\n * chosen for: does the target exist.\n *\n * WHY A PARSER RATHER THAN A REGEX AT THE CALL SITE. Two classes of false\n * positive make the naive `\\[.*\\]\\(.*\\)` unusable on the documents loam owns,\n * and both occur in loam's OWN generated prose. A fenced block is where a\n * document SHOWS the convention — the `AGENTS.md` section that teaches the link\n * format contains a link nobody wrote to be followed — and an inline code span\n * is where prose quotes a path. Both must read as text. The fence rule is the\n * one `core/document/parse.ts` already applies to headings, and it is imported\n * rather than restated for the reason that module gives about its own sampler:\n * two readers disagreeing about what a fence encloses prescribe a document no\n * author can satisfy.\n */\nimport { relative, resolve, sep } from \"node:path\";\nimport { fenceTracker } from \"../kernel/fences.js\";\n\n/** One link written in a markdown document. */\nexport interface DocumentLink {\n /** The target exactly as written, angle brackets stripped, fragment included. */\n raw: string;\n /**\n * The FILE half — `raw` with any `#fragment` removed and percent-escapes\n * decoded. This is what resolves against the filesystem; `raw` is what the\n * finding quotes back, because that is the string the author has to find.\n */\n path: string;\n /** The link text, or the reference label for a definition. Quoted in the finding. */\n text: string;\n /** 1-based line the link was written on. */\n line: number;\n}\n\n/**\n * Inline links and images: `[text](target)`, `![alt](target)`,\n * `[text](<target with spaces>)`, `[text](target \"title\")`.\n *\n * The target alternation takes `<...>` first so an angle-bracketed path\n * containing spaces stays one token; the bare form stops at whitespace or `)`,\n * which gives up on the balanced-parens spelling markdown also allows\n * (`[x](a(1).md)`). That is a deliberate under-read: it costs a link nobody\n * writes, where over-reading would swallow the rest of a sentence and report a\n * target no author would recognise.\n */\nconst INLINE_RE = /!?\\[([^\\]]*)\\]\\(\\s*(<[^>\\n]*>|[^\\s)]*)(?:\\s+(?:\"[^\"]*\"|'[^']*'|\\([^)]*\\)))?\\s*\\)/gd;\n\n/**\n * A reference DEFINITION — `[label]: target \"title\"` on its own line. Included\n * because the target is the same join written in a different place: a document\n * that collects its links at the bottom is following the convention, and a\n * reader that only saw inline links would grade it as having none.\n */\nconst DEFINITION_RE = /^ {0,3}\\[([^\\]]+)\\]:\\s*(<[^>\\n]*>|\\S+)/d;\n\n/**\n * Code spans, blanked before matching. `\\`[a](b)\\`` is prose ABOUT a link and\n * resolving it would convict a document for explaining the convention. Blanked\n * to spaces rather than removed so every column after it — and therefore any\n * real link later on the same line — keeps its position.\n */\nconst CODE_SPAN_RE = /(`+)(?:[^`]|(?!\\1)`)*\\1/g;\n\n/**\n * Every link in `md`, in document order.\n *\n * The whole document is scanned, frontmatter included: a YAML `see: [x](y.md)`\n * is not a link loam wrote a field for, and the alternative — teaching this\n * module where frontmatter ends — would make it read the one part of a document\n * loam's own doctrine says is structured. A link that resolves is a link that\n * resolves wherever it is written.\n */\nexport function documentLinks(md: string): DocumentLink[] {\n const fenced = fenceTracker();\n const out: DocumentLink[] = [];\n md.split(/\\r?\\n/).forEach((rawLine, i) => {\n if (fenced(rawLine)) return;\n const line = rawLine.replace(CODE_SPAN_RE, (m) => \" \".repeat(m.length));\n const definition = DEFINITION_RE.exec(line);\n if (definition) {\n push(out, definition[2]!, definition[1]!, i + 1);\n return;\n }\n INLINE_RE.lastIndex = 0;\n for (let m = INLINE_RE.exec(line); m !== null; m = INLINE_RE.exec(line)) {\n push(out, m[2]!, m[1]!, i + 1);\n }\n });\n return out;\n}\n\n/**\n * Record one link, unless its target is not a path this repository can answer\n * for. Four exclusions, each of them a question with no filesystem answer\n * rather than a link loam declines to grade:\n *\n * - an EMPTY target (`[text]()`) addresses nothing;\n * - a SCHEME (`https:`, `mailto:`, and a Windows `C:` drive with it) addresses\n * something outside this repository;\n * - a ROOT-relative or protocol-relative target (`/x`, `//host/x`) is resolved\n * by whatever renders the document — a site root, not the file's directory —\n * so loam does not know what it points at;\n * - a PURE fragment (`#section`) addresses this same document, and grading it\n * means resolving heading anchors, which is a different question from \"does\n * this file exist\".\n *\n * The last two are DEFENSIVE rather than decisive, and a mutation run says so:\n * removing either leaves every test green, because a pure fragment leaves an\n * empty path and a site-root target resolves outside the docs repo, where\n * `./findings.ts` declines to answer anyway. They are kept because the reason\n * differs from that boundary's — loam is not silent about these because they\n * are elsewhere, it is silent because it does not know where they are — and a\n * later reader deleting them as dead would be removing an intention, not a\n * branch.\n */\nfunction push(out: DocumentLink[], target: string, text: string, line: number): void {\n const raw = target.startsWith(\"<\") && target.endsWith(\">\") ? target.slice(1, -1) : target;\n if (raw === \"\" || raw.startsWith(\"#\") || raw.startsWith(\"/\")) return;\n if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(raw)) return;\n const path = decodePath(raw.split(\"#\")[0]!);\n if (path === \"\") return;\n out.push({ raw, path, text, line });\n}\n\n/**\n * Percent-decode a link target, or keep it verbatim when it does not decode.\n *\n * `[Order line](order%20line.md)` is how every editor writes a path holding a\n * space, so a resolver that skipped this step would report the one spelling\n * authors do not type by hand. `decodeURIComponent` throws on a lone `%` — a\n * literal percent in a filename is legal and rarer than the escape — and the\n * honest answer there is the string as written, which then resolves or does not\n * on its own terms.\n */\nfunction decodePath(raw: string): string {\n try {\n return decodeURIComponent(raw);\n } catch {\n return raw;\n }\n}\n\n/**\n * Re-express every relative link in `md` from a new base directory — the merge's\n * half of the link convention.\n *\n * THE DEFECT THIS CLOSES is structural and predates the glossary. A feature's\n * `specs/<svc>/spec.md` sits four directories below the docs root; the living\n * `services/<svc>/spec.md` it merges into sits two. `loam archive` copies a\n * requirement's text verbatim, so `[Order](../../../../glossary/order.md)` —\n * correct where it was written, and clickable in the pull request that reviewed\n * it — lands in the living document pointing two directories above the\n * repository. The same is true of a delta requirement citing an ADR, which has\n * been possible since before this module existed.\n *\n * REWRITING IS THE ONLY ANSWER THAT KEEPS BOTH PROPERTIES. The alternative\n * considered was to have authors write the link relative to where the text will\n * END UP, leaving loam to touch nothing — but that link does not resolve in the\n * delta, and \"it renders as a link in pull-request review, which is where these\n * documents are actually read\" is the convention's own first argument. So the\n * author writes what is true where they write it, and the merge makes it true\n * where it lands. The target is unchanged: only the route to it is.\n *\n * It is a positional rewrite, never a re-render: the matched target's own span\n * is replaced and every other byte of the line — the link text, the title, the\n * prose around it — is left exactly as written.\n *\n * WHEN THE TARGET MOVES TOO, `retarget` says where to. The same archive that\n * moves a requirement two directories up moves the rest of the feature as\n * well: a term the feature defines is copied into `glossary/`, and the feature\n * directory itself — its ADRs, its intent — goes to `features/archive/<FEAT>/`.\n * A link into the feature re-expressed against the target's OLD place was\n * therefore re-expressed correctly and still rotted on the commit that made it.\n * And a document that does not move at all can still need rewriting, when the\n * directory it links into does (`subsystem move`) — which is why `from === to`\n * is no longer a reason to stop when a `retarget` is given.\n *\n * A LINK WHOSE TEXT STILL REACHES ITS TARGET FROM THE NEW PLACE KEEPS ITS\n * SPELLING: a sibling that moves with its document, or a target nothing moved\n * under a document that stays, comes out of the rewrite exactly as the author\n * wrote it rather than normalised into `./` form by a commit that had no\n * reason to touch it.\n */\nexport function rebaseLinks(md: string, move: LinkMove): string {\n const from = resolve(move.from);\n const to = resolve(move.to);\n if (from === to && move.retarget === undefined) return md;\n const route: Route = { from, to, retarget: move.retarget ?? ((target) => target) };\n const fenced = fenceTracker();\n // `split` with a capturing group keeps the separators, so a document's own\n // mixture of CRLF and LF survives a rewrite that touches neither.\n const parts = md.split(/(\\r?\\n)/);\n for (let i = 0; i < parts.length; i += 2) {\n const rawLine = parts[i]!;\n if (fenced(rawLine)) continue;\n parts[i] = rebaseLine(rawLine, route);\n }\n return parts.join(\"\");\n}\n\n/** Where a document's text is moving, and where each file it links to will be. */\nexport interface LinkMove {\n /** The directory the text was written in. */\n from: string;\n /** The directory the text will be read from. */\n to: string;\n /**\n * Where a link's target will be once the same change lands: an absolute path\n * in, an absolute path out, the input itself for a target that stays put.\n * Omitted, every target stays put — the requirement merge's original case.\n */\n retarget?: (target: string) => string;\n}\n\n/** `LinkMove` with both directories resolved and the retarget defaulted — what each line is rewritten against. */\ninterface Route {\n from: string;\n to: string;\n retarget: (target: string) => string;\n}\n\n/** One line's links re-expressed, right to left so earlier spans keep their offsets. */\nfunction rebaseLine(rawLine: string, route: Route): string {\n const blanked = rawLine.replace(CODE_SPAN_RE, (m) => \" \".repeat(m.length));\n const spans: Array<[number, number, string]> = [];\n const collect = (m: RegExpExecArray): void => {\n const at = m.indices?.[2];\n if (at === undefined) return;\n const rebased = rebaseTarget(m[2]!, route);\n if (rebased !== null) spans.push([at[0], at[1], rebased]);\n };\n const definition = DEFINITION_RE.exec(blanked);\n if (definition) collect(definition);\n else {\n INLINE_RE.lastIndex = 0;\n for (let m = INLINE_RE.exec(blanked); m !== null; m = INLINE_RE.exec(blanked)) collect(m);\n }\n let out = rawLine;\n for (const [start, end, replacement] of spans.reverse()) {\n out = out.slice(0, start) + replacement + out.slice(end);\n }\n return out;\n}\n\n/**\n * One target, re-expressed from `to` instead of `from` — and pointed at wherever\n * `retarget` says the file itself will be — or null when there is nothing to\n * re-express.\n *\n * The exclusions are `push`'s exactly — an absolute URL, a site-root path and a\n * bare fragment mean the same thing from every directory — and the ANGLE-BRACKET\n * and PERCENT-ESCAPE forms are preserved rather than normalised: a rewrite that\n * silently changed `<a b.md>` into `a%20b.md` would be editing the author's\n * spelling under cover of moving their file. A route that comes out unchanged\n * is null for the same reason (`rebaseLinks` says when that happens).\n */\nfunction rebaseTarget(target: string, route: Route): string | null {\n const angled = target.startsWith(\"<\") && target.endsWith(\">\");\n const raw = angled ? target.slice(1, -1) : target;\n if (raw === \"\" || raw.startsWith(\"#\") || raw.startsWith(\"/\")) return null;\n if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(raw)) return null;\n const hash = raw.indexOf(\"#\");\n const pathPart = hash === -1 ? raw : raw.slice(0, hash);\n const fragment = hash === -1 ? \"\" : raw.slice(hash);\n if (pathPart === \"\") return null;\n const encoded = pathPart.includes(\"%\");\n const was = resolve(route.from, decodePath(pathPart));\n const will = resolve(route.retarget(was));\n // The author's TEXT, read from the new place, still reaching the target —\n // not two computed routes compared. A link that climbs above a moved\n // directory and comes back down through its old name\n // (`../../../services/payment-service/spec.md` from inside that service)\n // normalises to the same route either side of the move, and comparing\n // routes kept a spelling that names a directory the move just removed.\n if (resolve(route.to, decodePath(pathPart)) === will) return null;\n let rebased = relative(route.to, will).split(sep).join(\"/\");\n // `relative` returns a bare name for a sibling, which markdown reads as a\n // path relative to the file anyway — but `./` is what an author writes and\n // what keeps the link distinguishable from a reference label.\n if (rebased !== \"\" && !rebased.startsWith(\".\")) rebased = `./${rebased}`;\n if (rebased === \"\") return null;\n if (!angled && (encoded || rebased.includes(\" \"))) rebased = rebased.split(\" \").join(\"%20\");\n const rewritten = rebased + fragment;\n return angled ? `<${rewritten}>` : rewritten;\n}\n"]}
@@ -1,3 +1,80 @@
1
+ /**
2
+ * The MCP inputSchema for one tool: a closed object, so a typo'd argument
3
+ * refuses instead of vanishing, with the required set and the positional order
4
+ * `toArgv` enforces declared beside it.
5
+ */
6
+ export function toInputSchema(tool) {
7
+ const properties = {};
8
+ for (const positional of tool.positionals) {
9
+ properties[positional.property] = positional.variadic
10
+ ? { type: "array", items: { type: "string" }, description: positional.description }
11
+ : { type: "string", description: positional.description };
12
+ }
13
+ for (const flag of tool.flags) {
14
+ properties[flag.property] =
15
+ flag.kind === "boolean"
16
+ ? { type: "boolean", description: flag.description }
17
+ : flag.kind === "strings"
18
+ ? { type: "array", items: { type: "string" }, description: flag.description }
19
+ : { type: "string", description: flag.description };
20
+ }
21
+ const required = [
22
+ ...tool.positionals.filter((p) => p.required).map((p) => p.property),
23
+ ...tool.flags.filter((f) => f.required === true).map((f) => f.property),
24
+ ];
25
+ return {
26
+ type: "object",
27
+ properties,
28
+ ...(required.length > 0 ? { required } : {}),
29
+ ...orderRule(tool),
30
+ additionalProperties: false,
31
+ };
32
+ }
33
+ /**
34
+ * The rule `positionalTokens` enforces, declared: a positional after an
35
+ * optional one needs it, because positionals reach argv by position and the
36
+ * later value would otherwise be read in the earlier one's slot. Without it a
37
+ * client that validated `{name: "smoke"}` for `loam_list` against the schema
38
+ * it was given passed, and was then refused `-32602` by the server.
39
+ *
40
+ * Read off the declaration order rather than spelled per tool, so a tool that
41
+ * gains a positional after an optional one is declared the day it is refused.
42
+ * Each later positional needs every earlier OPTIONAL one; an earlier required
43
+ * one is in `required` already.
44
+ *
45
+ * Two keywords, because presence means two things. A scalar that is present
46
+ * always yields a token, so presence is the whole condition and
47
+ * `dependentRequired` says exactly that. A variadic yields tokens only when it
48
+ * has members: an empty one moves nothing into anybody's slot, and
49
+ * `positionalTokens` serves it — `{args: []}` with no `workflow` is the
50
+ * workflow list. `dependentRequired` there would forbid a call the server
51
+ * answers, the same disagreement turned the other way, so the variadic's rule
52
+ * sits under `dependentSchemas`: once it is present, either the earlier ones
53
+ * are too or it is empty. `anyOf` rather than `if`/`then`, which says the same
54
+ * but puts a `then` key on an object, and a thenable literal is one `await`
55
+ * away from being called.
56
+ */
57
+ function orderRule(tool) {
58
+ const dependentRequired = {};
59
+ const dependentSchemas = {};
60
+ const earlier = [];
61
+ for (const positional of tool.positionals) {
62
+ if (earlier.length > 0 && positional.variadic) {
63
+ dependentSchemas[positional.property] = {
64
+ anyOf: [{ required: [...earlier] }, { properties: { [positional.property]: { maxItems: 0 } } }],
65
+ };
66
+ }
67
+ else if (earlier.length > 0) {
68
+ dependentRequired[positional.property] = [...earlier];
69
+ }
70
+ if (!positional.required)
71
+ earlier.push(positional.property);
72
+ }
73
+ return {
74
+ ...(Object.keys(dependentRequired).length > 0 ? { dependentRequired } : {}),
75
+ ...(Object.keys(dependentSchemas).length > 0 ? { dependentSchemas } : {}),
76
+ };
77
+ }
1
78
  /**
2
79
  * A string value that argv would read as a flag is refused before any command
3
80
  * sees it: {"target": "--type"} must not become a parsed option. Commander
@@ -30,13 +107,30 @@ function checkedStrings(property, value) {
30
107
  }
31
108
  return { ok: true, tokens };
32
109
  }
110
+ /**
111
+ * The positionals, in declaration order — and refused when one would land in
112
+ * another's slot.
113
+ *
114
+ * A positional reaches argv by POSITION, so an optional one left out leaves no
115
+ * gap: whatever follows it moves up into its place. `loam_list` with `{name:
116
+ * "services"}` built `list services` and answered ok:true with the services
117
+ * listing, a wrong answer with nothing in it saying so; `loam_instructions`
118
+ * with `{args: [...]}` and no `workflow` read the first arg as the workflow.
119
+ * Refused here instead, as the invalid-params problem a missing REQUIRED
120
+ * positional already is, naming the slot the caller left empty. A later
121
+ * positional that yields no token — an empty variadic — moves nothing up, so
122
+ * it passes. `orderRule` above declares the same rule in the advertised
123
+ * schema; the two change together.
124
+ */
33
125
  function positionalTokens(tool, args) {
34
126
  const tokens = [];
127
+ let absent;
35
128
  for (const positional of tool.positionals) {
36
129
  const value = args[positional.property];
37
130
  if (value === undefined) {
38
131
  if (positional.required)
39
132
  return { ok: false, problem: `required argument '${positional.property}' is missing` };
133
+ absent ??= positional.property;
40
134
  continue;
41
135
  }
42
136
  const checked = positional.variadic
@@ -44,6 +138,13 @@ function positionalTokens(tool, args) {
44
138
  : checkedString(positional.property, value);
45
139
  if (!checked.ok)
46
140
  return checked;
141
+ if (absent !== undefined && checked.tokens.length > 0) {
142
+ return {
143
+ ok: false,
144
+ problem: `argument '${positional.property}' requires '${absent}' — positionals are read in order, ` +
145
+ `so without '${absent}' this value would be read as '${absent}'`,
146
+ };
147
+ }
47
148
  tokens.push(...checked.tokens);
48
149
  }
49
150
  return { ok: true, tokens };
@@ -1 +1 @@
1
- {"version":3,"file":"argv.js","sourceRoot":"","sources":["../../../src/core/mcp/argv.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * How one MCP tool call's JSON arguments become an argv the CLI already\n * trusts — the boundary where an argument value could otherwise smuggle a\n * flag. The tool TABLE (what the tools are) lives in `./tools.ts`; this\n * module owns the crossing.\n */\nimport type { McpTool } from \"./tools.js\";\n\n/** One checked step of argv construction: the tokens it yields, or the invalid-params problem. */\ntype Checked =\n | { readonly ok: true; readonly tokens: readonly string[] }\n | { readonly ok: false; readonly problem: string };\n\nexport type ArgvOutcome =\n | { readonly ok: true; readonly argv: readonly string[] }\n | { readonly ok: false; readonly problem: string };\n\n/**\n * A string value that argv would read as a flag is refused before any command\n * sees it: {\"target\": \"--type\"} must not become a parsed option. Commander\n * cannot make this distinction after the fact — by then the token IS a flag —\n * so the boundary is here, on every string that crosses, array members\n * included. No shell is ever involved, so this is the only smuggling path.\n */\nfunction smuggled(property: string, value: string): string | null {\n return value.startsWith(\"-\")\n ? `argument '${property}' value '${value}' begins with '-' — a value argv would read as a flag is refused`\n : null;\n}\n\n/** One string value checked for type and smuggling. */\nfunction checkedString(property: string, value: unknown): Checked {\n if (typeof value !== \"string\") return { ok: false, problem: `argument '${property}' must be a string` };\n const bad = smuggled(property, value);\n return bad === null ? { ok: true, tokens: [value] } : { ok: false, problem: bad };\n}\n\n/** An array value checked member by member — rebuilt, not cast, so the claim is earned. */\nfunction checkedStrings(property: string, value: unknown): Checked {\n if (!Array.isArray(value)) return { ok: false, problem: `argument '${property}' must be an array of strings` };\n const tokens: string[] = [];\n for (const entry of value) {\n const checked = checkedString(property, entry);\n if (!checked.ok) return checked;\n tokens.push(...checked.tokens);\n }\n return { ok: true, tokens };\n}\n\nfunction positionalTokens(tool: McpTool, args: Record<string, unknown>): Checked {\n const tokens: string[] = [];\n for (const positional of tool.positionals) {\n const value = args[positional.property];\n if (value === undefined) {\n if (positional.required) return { ok: false, problem: `required argument '${positional.property}' is missing` };\n continue;\n }\n const checked = positional.variadic\n ? checkedStrings(positional.property, value)\n : checkedString(positional.property, value);\n if (!checked.ok) return checked;\n tokens.push(...checked.tokens);\n }\n return { ok: true, tokens };\n}\n\nfunction flagTokens(tool: McpTool, args: Record<string, unknown>): Checked {\n const tokens: string[] = [];\n for (const flag of tool.flags) {\n const value = args[flag.property];\n if (value === undefined) {\n // The same refusal absent required positionals get: the client learns\n // \"-32602 missing argument\", not an invalid-option envelope after the\n // command already refused.\n if (flag.required === true) return { ok: false, problem: `required argument '${flag.property}' is missing` };\n continue;\n }\n if (flag.kind === \"boolean\") {\n if (typeof value !== \"boolean\") return { ok: false, problem: `argument '${flag.property}' must be a boolean` };\n if (value) tokens.push(flag.flag);\n continue;\n }\n const checked = flag.kind === \"strings\"\n ? checkedStrings(flag.property, value)\n : checkedString(flag.property, value);\n if (!checked.ok) return checked;\n // A repeatable flag repeats per value; a single-valued one appears once.\n for (const token of checked.tokens) tokens.push(flag.flag, token);\n }\n return { ok: true, tokens };\n}\n\n/**\n * The argv for one tool call — or the invalid-params problem that stops it.\n * `--json` is always appended: the envelope is the tool-result contract, and\n * a client cannot opt out of it into the human rendering. Unknown argument\n * names refuse rather than vanish — the schema says additionalProperties\n * false, and a server that enforced that only in the schema would silently\n * drop the typo'd argument of any client that skipped validation.\n */\nexport function toArgv(tool: McpTool, args: Record<string, unknown>): ArgvOutcome {\n const known = new Set([\n ...tool.positionals.map((p) => p.property),\n ...tool.flags.map((f) => f.property),\n ]);\n for (const property of Object.keys(args)) {\n if (!known.has(property)) {\n const allowed = known.size === 0 ? \"no arguments\" : [...known].join(\", \");\n return { ok: false, problem: `unknown argument '${property}' for ${tool.name} — expected ${allowed}` };\n }\n }\n const positionals = positionalTokens(tool, args);\n if (!positionals.ok) return { ok: false, problem: positionals.problem };\n const flags = flagTokens(tool, args);\n if (!flags.ok) return { ok: false, problem: flags.problem };\n return {\n ok: true,\n argv: [tool.command, ...positionals.tokens, ...flags.tokens, ...(tool.fixed ?? []), \"--json\"],\n };\n}\n"]}
1
+ {"version":3,"file":"argv.js","sourceRoot":"","sources":["../../../src/core/mcp/argv.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * How one MCP tool call's JSON arguments become an argv the CLI already\n * trusts — the boundary where an argument value could otherwise smuggle a\n * flag. The tool TABLE (what the tools are) lives in `./tools.ts`; this\n * module owns the crossing, and the inputSchema that declares it: the schema\n * a client validates against and the argv builder that refuses what it\n * forbids are one contract written twice, so they sit in one module.\n */\nimport type { McpTool } from \"./tools.js\";\n\n/**\n * The MCP inputSchema for one tool: a closed object, so a typo'd argument\n * refuses instead of vanishing, with the required set and the positional order\n * `toArgv` enforces declared beside it.\n */\nexport function toInputSchema(tool: McpTool): Record<string, unknown> {\n const properties: Record<string, unknown> = {};\n for (const positional of tool.positionals) {\n properties[positional.property] = positional.variadic\n ? { type: \"array\", items: { type: \"string\" }, description: positional.description }\n : { type: \"string\", description: positional.description };\n }\n for (const flag of tool.flags) {\n properties[flag.property] =\n flag.kind === \"boolean\"\n ? { type: \"boolean\", description: flag.description }\n : flag.kind === \"strings\"\n ? { type: \"array\", items: { type: \"string\" }, description: flag.description }\n : { type: \"string\", description: flag.description };\n }\n const required = [\n ...tool.positionals.filter((p) => p.required).map((p) => p.property),\n ...tool.flags.filter((f) => f.required === true).map((f) => f.property),\n ];\n return {\n type: \"object\",\n properties,\n ...(required.length > 0 ? { required } : {}),\n ...orderRule(tool),\n additionalProperties: false,\n };\n}\n\n/**\n * The rule `positionalTokens` enforces, declared: a positional after an\n * optional one needs it, because positionals reach argv by position and the\n * later value would otherwise be read in the earlier one's slot. Without it a\n * client that validated `{name: \"smoke\"}` for `loam_list` against the schema\n * it was given passed, and was then refused `-32602` by the server.\n *\n * Read off the declaration order rather than spelled per tool, so a tool that\n * gains a positional after an optional one is declared the day it is refused.\n * Each later positional needs every earlier OPTIONAL one; an earlier required\n * one is in `required` already.\n *\n * Two keywords, because presence means two things. A scalar that is present\n * always yields a token, so presence is the whole condition and\n * `dependentRequired` says exactly that. A variadic yields tokens only when it\n * has members: an empty one moves nothing into anybody's slot, and\n * `positionalTokens` serves it — `{args: []}` with no `workflow` is the\n * workflow list. `dependentRequired` there would forbid a call the server\n * answers, the same disagreement turned the other way, so the variadic's rule\n * sits under `dependentSchemas`: once it is present, either the earlier ones\n * are too or it is empty. `anyOf` rather than `if`/`then`, which says the same\n * but puts a `then` key on an object, and a thenable literal is one `await`\n * away from being called.\n */\nfunction orderRule(tool: McpTool): Record<string, unknown> {\n const dependentRequired: Record<string, string[]> = {};\n const dependentSchemas: Record<string, unknown> = {};\n const earlier: string[] = [];\n for (const positional of tool.positionals) {\n if (earlier.length > 0 && positional.variadic) {\n dependentSchemas[positional.property] = {\n anyOf: [{ required: [...earlier] }, { properties: { [positional.property]: { maxItems: 0 } } }],\n };\n } else if (earlier.length > 0) {\n dependentRequired[positional.property] = [...earlier];\n }\n if (!positional.required) earlier.push(positional.property);\n }\n return {\n ...(Object.keys(dependentRequired).length > 0 ? { dependentRequired } : {}),\n ...(Object.keys(dependentSchemas).length > 0 ? { dependentSchemas } : {}),\n };\n}\n\n/** One checked step of argv construction: the tokens it yields, or the invalid-params problem. */\ntype Checked =\n | { readonly ok: true; readonly tokens: readonly string[] }\n | { readonly ok: false; readonly problem: string };\n\nexport type ArgvOutcome =\n | { readonly ok: true; readonly argv: readonly string[] }\n | { readonly ok: false; readonly problem: string };\n\n/**\n * A string value that argv would read as a flag is refused before any command\n * sees it: {\"target\": \"--type\"} must not become a parsed option. Commander\n * cannot make this distinction after the fact — by then the token IS a flag —\n * so the boundary is here, on every string that crosses, array members\n * included. No shell is ever involved, so this is the only smuggling path.\n */\nfunction smuggled(property: string, value: string): string | null {\n return value.startsWith(\"-\")\n ? `argument '${property}' value '${value}' begins with '-' — a value argv would read as a flag is refused`\n : null;\n}\n\n/** One string value checked for type and smuggling. */\nfunction checkedString(property: string, value: unknown): Checked {\n if (typeof value !== \"string\") return { ok: false, problem: `argument '${property}' must be a string` };\n const bad = smuggled(property, value);\n return bad === null ? { ok: true, tokens: [value] } : { ok: false, problem: bad };\n}\n\n/** An array value checked member by member — rebuilt, not cast, so the claim is earned. */\nfunction checkedStrings(property: string, value: unknown): Checked {\n if (!Array.isArray(value)) return { ok: false, problem: `argument '${property}' must be an array of strings` };\n const tokens: string[] = [];\n for (const entry of value) {\n const checked = checkedString(property, entry);\n if (!checked.ok) return checked;\n tokens.push(...checked.tokens);\n }\n return { ok: true, tokens };\n}\n\n/**\n * The positionals, in declaration order — and refused when one would land in\n * another's slot.\n *\n * A positional reaches argv by POSITION, so an optional one left out leaves no\n * gap: whatever follows it moves up into its place. `loam_list` with `{name:\n * \"services\"}` built `list services` and answered ok:true with the services\n * listing, a wrong answer with nothing in it saying so; `loam_instructions`\n * with `{args: [...]}` and no `workflow` read the first arg as the workflow.\n * Refused here instead, as the invalid-params problem a missing REQUIRED\n * positional already is, naming the slot the caller left empty. A later\n * positional that yields no token — an empty variadic — moves nothing up, so\n * it passes. `orderRule` above declares the same rule in the advertised\n * schema; the two change together.\n */\nfunction positionalTokens(tool: McpTool, args: Record<string, unknown>): Checked {\n const tokens: string[] = [];\n let absent: string | undefined;\n for (const positional of tool.positionals) {\n const value = args[positional.property];\n if (value === undefined) {\n if (positional.required) return { ok: false, problem: `required argument '${positional.property}' is missing` };\n absent ??= positional.property;\n continue;\n }\n const checked = positional.variadic\n ? checkedStrings(positional.property, value)\n : checkedString(positional.property, value);\n if (!checked.ok) return checked;\n if (absent !== undefined && checked.tokens.length > 0) {\n return {\n ok: false,\n problem:\n `argument '${positional.property}' requires '${absent}' — positionals are read in order, ` +\n `so without '${absent}' this value would be read as '${absent}'`,\n };\n }\n tokens.push(...checked.tokens);\n }\n return { ok: true, tokens };\n}\n\nfunction flagTokens(tool: McpTool, args: Record<string, unknown>): Checked {\n const tokens: string[] = [];\n for (const flag of tool.flags) {\n const value = args[flag.property];\n if (value === undefined) {\n // The same refusal absent required positionals get: the client learns\n // \"-32602 missing argument\", not an invalid-option envelope after the\n // command already refused.\n if (flag.required === true) return { ok: false, problem: `required argument '${flag.property}' is missing` };\n continue;\n }\n if (flag.kind === \"boolean\") {\n if (typeof value !== \"boolean\") return { ok: false, problem: `argument '${flag.property}' must be a boolean` };\n if (value) tokens.push(flag.flag);\n continue;\n }\n const checked = flag.kind === \"strings\"\n ? checkedStrings(flag.property, value)\n : checkedString(flag.property, value);\n if (!checked.ok) return checked;\n // A repeatable flag repeats per value; a single-valued one appears once.\n for (const token of checked.tokens) tokens.push(flag.flag, token);\n }\n return { ok: true, tokens };\n}\n\n/**\n * The argv for one tool call — or the invalid-params problem that stops it.\n * `--json` is always appended: the envelope is the tool-result contract, and\n * a client cannot opt out of it into the human rendering. Unknown argument\n * names refuse rather than vanish — the schema says additionalProperties\n * false, and a server that enforced that only in the schema would silently\n * drop the typo'd argument of any client that skipped validation.\n */\nexport function toArgv(tool: McpTool, args: Record<string, unknown>): ArgvOutcome {\n const known = new Set([\n ...tool.positionals.map((p) => p.property),\n ...tool.flags.map((f) => f.property),\n ]);\n for (const property of Object.keys(args)) {\n if (!known.has(property)) {\n const allowed = known.size === 0 ? \"no arguments\" : [...known].join(\", \");\n return { ok: false, problem: `unknown argument '${property}' for ${tool.name} — expected ${allowed}` };\n }\n }\n const positionals = positionalTokens(tool, args);\n if (!positionals.ok) return { ok: false, problem: positionals.problem };\n const flags = flagTokens(tool, args);\n if (!flags.ok) return { ok: false, problem: flags.problem };\n return {\n ok: true,\n argv: [tool.command, ...positionals.tokens, ...flags.tokens, ...(tool.fixed ?? []), \"--json\"],\n };\n}\n"]}
@@ -13,7 +13,7 @@ export const MCP_AUTHOR_TOOLS = [
13
13
  name: "loam_new",
14
14
  command: "new",
15
15
  description: "Scaffold a feature in the docs repo. Opt-in writer; refuses an existing feature. " +
16
- "CLI equivalent: loam new <featureId> [--title <text>] [--touches <id>...] [--new-service <id>...] [--capability <id>...] --json",
16
+ "CLI equivalent: loam new <featureId> [--title <text>] [--touches <id>...] [--new-service <id>...] [--capability <id>...] [--modifies <Req-ID>...] --json",
17
17
  positionals: [
18
18
  { property: "featureId", required: true, variadic: false, description: "new feature id, e.g. FEAT-101" },
19
19
  ],
@@ -22,6 +22,7 @@ export const MCP_AUTHOR_TOOLS = [
22
22
  { property: "touches", flag: "--touches", kind: "strings", description: "existing services changed; repeatable" },
23
23
  { property: "newServices", flag: "--new-service", kind: "strings", description: "services introduced; repeatable" },
24
24
  { property: "capabilities", flag: "--capability", kind: "strings", description: "business capabilities changed; repeatable" },
25
+ { property: "modifies", flag: "--modifies", kind: "strings", description: "living Requirement-IDs of touched services this feature rewrites, scaffolded as pinned MODIFIED blocks; repeatable" },
25
26
  ],
26
27
  annotations: WRITE_ANNOTATIONS,
27
28
  },