@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 +1 @@
1
- {"version":3,"file":"stale.js","sourceRoot":"","sources":["../../../src/core/gherkin/stale.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What `loam validate` reports about a generated suite: which scenarios moved\n * out from under it, and which were never generated at all.\n *\n * Service-repo-scoped, and silent until a suite exists — a service that never\n * ran `loam gherkin` has not opted into the axis and is not failing it. The\n * whole comparison is by digest rather than by name, so a reworded scenario\n * reads as stale rather than as missing plus new.\n */\nimport { existsSync, statSync, type Dirent } from \"node:fs\";\nimport { readdir, readFile, realpath } from \"node:fs/promises\";\nimport { join, relative, sep } from \"node:path\";\nimport { parseRequirements } from \"../document/parse.js\";\nimport type { PathableService } from \"../kernel/ids/service.js\";\nimport { SPEC_AXES } from \"../repo/paths.js\";\nimport { locateServicePaths } from \"../repo/service-target.js\";\nimport { listFeatures } from \"../repo/repo.js\";\nimport type { FleetContext } from \"../fleet-context.js\";\nimport type { Finding } from \"../vocabulary/report.js\";\nimport { UnsafePathError } from \"../kernel/path-safety.js\";\nimport { gherkinRoot, scenarioDigest } from \"./stamp.js\";\nimport { axisLabel } from \"./emit.js\";\nimport { parseStampedFeature, type StampedFeature } from \"./read.js\";\nimport type { DocsDir } from \"../kernel/ids/dirs.js\";\n\n/**\n * The gherkin freshness chain, service-repo-scoped like `sources.*`: it needs\n * the repo (the suite lives there), and it stays quiet until\n * `<gherkinDir>/loam/` exists — a service that never generated has not opted\n * in, and silence there is honest, not vacuous.\n *\n * The LIVING specs are the reference, with one carve-out. A stamped file\n * tagged with a feature still in flight answers to that feature's delta, not\n * to the living spec its requirements have not merged into yet — so it is\n * skipped here (its regeneration is `loam gherkin <FEAT>`), and the moment the\n * feature archives, its requirements ARE living requirements, digests\n * unchanged, and the same file grades current with no rewrite. A tag naming\n * no active feature (the feature was archived, or abandoned) gets no\n * exemption: the living spec is all there is to answer to.\n *\n * Three warnings, one per way the suite and the spec can disagree, judged\n * digest-first and by requirement name second:\n *\n * - `gherkin.missing` — a living scenario whose digest no stamped scenario of\n * its axis carries: the suite has no test for these words.\n * - `gherkin.stale` — a stamped scenario whose digest matches no living\n * scenario while its file's requirement still exists: the spec moved under\n * the generated file. (A reworded scenario therefore reports both: the old\n * words are stale, the new words are missing — one regeneration clears\n * both.)\n * - `gherkin.orphaned` — a whole file whose requirement no longer exists in\n * its axis's living spec; reported once per file, because every scenario in\n * it is moot together.\n *\n * Digests are content identity within one service and one axis, deliberately:\n * two identically-worded scenarios of one service's one axis share one digest,\n * so one stamped copy covers both — while the axes stay separate namespaces\n * twice over (every comparison here is axis-scoped, and the recipe salts the\n * arch axis), and the services stay separate because the recipe salts the\n * service too. That last salt is why a stamped file copied out of another\n * service's repository grades `gherkin.orphaned`/`gherkin.stale` here instead\n * of quietly counting as this service's coverage.\n *\n * All warnings — `--strict` is the CI escalation — plus one `ok` confirmation\n * (`gherkin.current`, the `sources.current` pattern) when a generated suite\n * exists and nothing disagrees.\n */\nexport async function gherkinFindings(ctx: {\n docsDir: DocsDir;\n service: PathableService;\n /** The service repo, when loam is standing in it. Undefined disables the chain, like sources.*. */\n repoDir?: string;\n gherkinDir?: string;\n fleet?: FleetContext;\n}): Promise<Finding[]> {\n if (ctx.repoDir === undefined) return [];\n let root: string;\n try {\n root = gherkinRoot(ctx.repoDir, ctx.gherkinDir);\n } catch (err) {\n if (!(err instanceof UnsafePathError)) throw err;\n return [\n {\n severity: \"error\",\n code: \"gherkin.path-outside\",\n subject: ctx.service,\n message: `The configured Gherkin output is unsafe: ${err.message}. It must stay inside the service repo.`,\n },\n ];\n }\n if (!existsSync(root)) return [];\n\n // The living reference, per axis: requirement names, and scenario digests.\n interface AxisState {\n file: string;\n reqNames: Set<string>;\n digests: Set<string>;\n scenarios: Array<{ req: string; name: string; digest: string }>;\n }\n const paths = await locateServicePaths(ctx.docsDir, ctx.service, ctx.fleet);\n const axes = new Map<\"business\" | \"arch\", AxisState>();\n for (const axis of SPEC_AXES) {\n const label = axisLabel(axis);\n const state: AxisState = { file: axis.file, reqNames: new Set(), digests: new Set(), scenarios: [] };\n const path = paths[axis.key];\n if (existsSync(path)) {\n const reqs = ctx.fleet === undefined\n ? parseRequirements(await readFile(path, \"utf8\"))\n : await ctx.fleet.readRequirements(path);\n for (const r of reqs) {\n if (r.kind === \"REMOVED\") continue;\n state.reqNames.add(r.name);\n for (const s of r.scenarios) {\n const digest = scenarioDigest(ctx.service, s.lines, label);\n state.digests.add(digest);\n state.scenarios.push({ req: r.name, name: s.name, digest });\n }\n }\n }\n axes.set(label, state);\n }\n\n // The stamped side. Every stamped digest counts toward coverage — an\n // in-flight file whose scenario matches the living words IS a test for them.\n // `Set<string>`, not the ids' own brand: this set answers membership\n // questions for tags read out of .feature files — document text.\n const active = new Set<string>((await listFeatures(ctx.docsDir, {}, ctx.fleet)).map((f) => f.id));\n const stampedDigests = { business: new Set<string>(), arch: new Set<string>() };\n const parsed: Array<{ rel: string; axis: \"business\" | \"arch\"; file: StampedFeature }> = [];\n let stampedScenarios = 0;\n for (const abs of await featureFilesUnder(root)) {\n const file = parseStampedFeature(await readFile(abs, \"utf8\"));\n if (file === null) continue;\n const axis = file.tags.includes(\"architecture\") ? \"arch\" : \"business\";\n for (const s of file.scenarios) stampedDigests[axis].add(s.digest);\n stampedScenarios += file.scenarios.length;\n parsed.push({ rel: relative(ctx.repoDir, abs).split(sep).join(\"/\"), axis, file });\n }\n\n const findings: Finding[] = [];\n const regen = `\\`loam gherkin --service ${ctx.service}\\``;\n for (const [axis, state] of axes) {\n for (const sc of state.scenarios) {\n if (stampedDigests[axis].has(sc.digest)) continue;\n findings.push({\n severity: \"warn\",\n code: \"gherkin.missing\",\n subject: ctx.service,\n message: `${ctx.service}: scenario '${sc.name}' of requirement '${sc.req}' (${state.file}) has no generated .feature scenario — the suite has no test for these words; ${regen} regenerates it`,\n });\n }\n }\n for (const { rel, axis, file } of parsed) {\n if (file.tags.some((t) => active.has(t))) continue;\n const state = axes.get(axis)!;\n if (file.featureName === null || !state.reqNames.has(file.featureName)) {\n findings.push({\n severity: \"warn\",\n code: \"gherkin.orphaned\",\n subject: ctx.service,\n message: `${ctx.service}: ${rel}: requirement '${file.featureName ?? \"(unnamed)\"}' no longer exists in the living ${state.file} — the whole file is orphaned; ${regen} removes it`,\n });\n continue;\n }\n for (const s of file.scenarios) {\n if (state.digests.has(s.digest)) continue;\n findings.push({\n severity: \"warn\",\n code: \"gherkin.stale\",\n subject: ctx.service,\n message: `${ctx.service}: ${rel}: scenario '${s.name}' matches no scenario in the living ${state.file} — the spec moved under the generated file; ${regen} regenerates it`,\n });\n }\n }\n if (findings.length === 0) {\n findings.push({\n severity: \"ok\",\n code: \"gherkin.current\",\n message: `${ctx.service}: generated gherkin current (${parsed.length} file(s), ${stampedScenarios} stamped scenario(s) agree with the living specs)`,\n });\n }\n return findings;\n}\n\n/**\n * Every `.feature` file at or under `root`, sorted by repo-relative path —\n * enumeration is part of the output contract, so the order cannot depend on\n * the filesystem's. Dot-entries are skipped, like every other walk in loam.\n *\n * Symlinks are FOLLOWED — repo.ts's `entryIs` and brief/brief.ts's `hasMarkdown` are\n * the same rule for the same reason. A `Dirent` from `readdir(withFileTypes)`\n * never follows one, so `isFile()` and `isDirectory()` are both false for a\n * symlinked `.feature`: it was invisible to the orphan scan (`loam gherkin`\n * left it behind forever) and invisible to the staleness scan (it graded as no\n * coverage at all). Composing a suite out of links is legitimate, so they are\n * followed rather than refused, and a dangling one is skipped exactly as an\n * absent file is.\n */\nexport async function featureFilesUnder(root: string): Promise<string[]> {\n const out: string[] = [];\n // Following links means a cycle is now possible; every directory is entered\n // once by its resolved path, so a loop terminates instead of hanging.\n const entered = new Set<string>();\n const walk = async (dir: string): Promise<void> => {\n if (!existsSync(dir)) return;\n let resolved: string;\n try {\n resolved = await realpath(dir);\n } catch {\n return;\n }\n if (entered.has(resolved)) return;\n entered.add(resolved);\n for (const entry of await readdir(dir, { withFileTypes: true })) {\n if (entry.name.startsWith(\".\")) continue;\n const abs = join(dir, entry.name);\n const what = entryKind(abs, entry);\n if (what === \"dir\") await walk(abs);\n else if (what === \"file\" && entry.name.endsWith(\".feature\")) out.push(abs);\n }\n };\n await walk(root);\n return out.sort((a, b) => {\n const ra = a.split(sep).join(\"/\");\n const rb = b.split(sep).join(\"/\");\n return ra < rb ? -1 : ra > rb ? 1 : 0;\n });\n}\n\n/** What an entry IS, resolving a symlink to find out. Nothing when it dangles. */\nfunction entryKind(abs: string, e: Dirent): \"dir\" | \"file\" | null {\n if (!e.isSymbolicLink()) return e.isDirectory() ? \"dir\" : e.isFile() ? \"file\" : null;\n try {\n const s = statSync(abs);\n return s.isDirectory() ? \"dir\" : s.isFile() ? \"file\" : null;\n } catch {\n return null;\n }\n}\n"]}
1
+ {"version":3,"file":"stale.js","sourceRoot":"","sources":["../../../src/core/gherkin/stale.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What `loam validate` reports about a generated suite: which scenarios moved\n * out from under it, and which were never generated at all.\n *\n * Service-repo-scoped, and silent until a suite exists — a service that never\n * ran `loam gherkin` has not opted into the axis and is not failing it. The\n * whole comparison is by digest rather than by name, so a reworded scenario\n * reads as stale rather than as missing plus new.\n */\nimport { existsSync } from \"node:fs\";\nimport { readdir, readFile } from \"node:fs/promises\";\nimport { join, relative, sep } from \"node:path\";\nimport type { PathableService } from \"../kernel/ids/service.js\";\nimport { SPEC_AXES } from \"../repo/paths.js\";\nimport { locateServicePaths } from \"../repo/service-target.js\";\nimport { listFeatures } from \"../repo/repo.js\";\nimport { entryIs, markDescended } from \"../repo/tree/fs.js\";\nimport type { FleetContext } from \"../fleet-context.js\";\nimport type { Finding } from \"../vocabulary/report.js\";\nimport { UnsafePathError } from \"../kernel/path-safety.js\";\nimport { gherkinRoot, scenarioDigest } from \"./stamp.js\";\nimport { axisLabel } from \"./emit.js\";\nimport { parseStampedFeature, type StampedFeature } from \"./read.js\";\nimport type { DocsDir } from \"../kernel/ids/dirs.js\";\n\n/**\n * The gherkin freshness chain, service-repo-scoped like `sources.*`: it needs\n * the repo (the suite lives there), and it stays quiet until\n * `<gherkinDir>/loam/` exists — a service that never generated has not opted\n * in, and silence there is honest, not vacuous.\n *\n * The LIVING specs are the reference, with one carve-out. A stamped file\n * tagged with a feature still in flight answers to that feature's delta, not\n * to the living spec its requirements have not merged into yet — so it is\n * skipped here (its regeneration is `loam gherkin <FEAT>`), and the moment the\n * feature archives, its requirements ARE living requirements, digests\n * unchanged, and the same file grades current with no rewrite. A tag naming\n * no active feature (the feature was archived, or abandoned) gets no\n * exemption: the living spec is all there is to answer to.\n *\n * Three warnings, one per way the suite and the spec can disagree, judged\n * digest-first and by requirement name second:\n *\n * - `gherkin.missing` — a living scenario whose digest no stamped scenario of\n * its axis carries: the suite has no test for these words.\n * - `gherkin.stale` — a stamped scenario whose digest matches no living\n * scenario while its file's requirement still exists: the spec moved under\n * the generated file. (A reworded scenario therefore reports both: the old\n * words are stale, the new words are missing — one regeneration clears\n * both.)\n * - `gherkin.orphaned` — a whole file whose requirement no longer exists in\n * its axis's living spec; reported once per file, because every scenario in\n * it is moot together.\n *\n * Digests are content identity within one service and one axis, deliberately:\n * two identically-worded scenarios of one service's one axis share one digest,\n * so one stamped copy covers both — while the axes stay separate namespaces\n * twice over (every comparison here is axis-scoped, and the recipe salts the\n * arch axis), and the services stay separate because the recipe salts the\n * service too. That last salt is why a stamped file copied out of another\n * service's repository grades `gherkin.orphaned`/`gherkin.stale` here instead\n * of quietly counting as this service's coverage.\n *\n * All warnings — `--strict` is the CI escalation — plus one `ok` confirmation\n * (`gherkin.current`, the `sources.current` pattern) when a generated suite\n * exists and nothing disagrees.\n */\nexport async function gherkinFindings(ctx: {\n docsDir: DocsDir;\n service: PathableService;\n /** The service repo, when loam is standing in it. Undefined disables the chain, like sources.*. */\n repoDir?: string;\n gherkinDir?: string;\n fleet: FleetContext;\n}): Promise<Finding[]> {\n if (ctx.repoDir === undefined) return [];\n let root: string;\n try {\n root = gherkinRoot(ctx.repoDir, ctx.gherkinDir);\n } catch (err) {\n if (!(err instanceof UnsafePathError)) throw err;\n return [\n {\n severity: \"error\",\n code: \"gherkin.path-outside\",\n subject: ctx.service,\n message: `The configured Gherkin output is unsafe: ${err.message}. It must stay inside the service repo.`,\n },\n ];\n }\n if (!existsSync(root)) return [];\n\n // The living reference, per axis: requirement names, and scenario digests.\n interface AxisState {\n file: string;\n reqNames: Set<string>;\n digests: Set<string>;\n scenarios: Array<{ req: string; name: string; digest: string }>;\n }\n const paths = await locateServicePaths(ctx.docsDir, ctx.service, ctx.fleet);\n const axes = new Map<\"business\" | \"arch\", AxisState>();\n for (const axis of SPEC_AXES) {\n const label = axisLabel(axis);\n const state: AxisState = { file: axis.file, reqNames: new Set(), digests: new Set(), scenarios: [] };\n const path = paths[axis.key];\n if (existsSync(path)) {\n for (const r of await ctx.fleet.readRequirements(path)) {\n if (r.kind === \"REMOVED\") continue;\n state.reqNames.add(r.name);\n for (const s of r.scenarios) {\n const digest = scenarioDigest(ctx.service, s.lines, label);\n state.digests.add(digest);\n state.scenarios.push({ req: r.name, name: s.name, digest });\n }\n }\n }\n axes.set(label, state);\n }\n\n // The stamped side. Every stamped digest counts toward coverage — an\n // in-flight file whose scenario matches the living words IS a test for them.\n // `Set<string>`, not the ids' own brand: this set answers membership\n // questions for tags read out of .feature files — document text.\n const active = new Set<string>((await listFeatures(ctx.docsDir, {}, ctx.fleet)).map((f) => f.id));\n const stampedDigests = { business: new Set<string>(), arch: new Set<string>() };\n const parsed: Array<{ rel: string; axis: \"business\" | \"arch\"; file: StampedFeature }> = [];\n let stampedScenarios = 0;\n for (const abs of await featureFilesUnder(root)) {\n const file = parseStampedFeature(await readFile(abs, \"utf8\"));\n if (file === null) continue;\n const axis = file.tags.includes(\"architecture\") ? \"arch\" : \"business\";\n for (const s of file.scenarios) stampedDigests[axis].add(s.digest);\n stampedScenarios += file.scenarios.length;\n parsed.push({ rel: relative(ctx.repoDir, abs).split(sep).join(\"/\"), axis, file });\n }\n\n const findings: Finding[] = [];\n const regen = `\\`loam gherkin --service ${ctx.service}\\``;\n for (const [axis, state] of axes) {\n for (const sc of state.scenarios) {\n if (stampedDigests[axis].has(sc.digest)) continue;\n findings.push({\n severity: \"warn\",\n code: \"gherkin.missing\",\n subject: ctx.service,\n message: `${ctx.service}: scenario '${sc.name}' of requirement '${sc.req}' (${state.file}) has no generated .feature scenario — the suite has no test for these words; ${regen} regenerates it`,\n });\n }\n }\n for (const { rel, axis, file } of parsed) {\n if (file.tags.some((t) => active.has(t))) continue;\n const state = axes.get(axis)!;\n if (file.featureName === null || !state.reqNames.has(file.featureName)) {\n findings.push({\n severity: \"warn\",\n code: \"gherkin.orphaned\",\n subject: ctx.service,\n message: `${ctx.service}: ${rel}: requirement '${file.featureName ?? \"(unnamed)\"}' no longer exists in the living ${state.file} — the whole file is orphaned; ${regen} removes it`,\n });\n continue;\n }\n for (const s of file.scenarios) {\n if (state.digests.has(s.digest)) continue;\n findings.push({\n severity: \"warn\",\n code: \"gherkin.stale\",\n subject: ctx.service,\n message: `${ctx.service}: ${rel}: scenario '${s.name}' matches no scenario in the living ${state.file} — the spec moved under the generated file; ${regen} regenerates it`,\n });\n }\n }\n if (findings.length === 0) {\n findings.push({\n severity: \"ok\",\n code: \"gherkin.current\",\n message: `${ctx.service}: generated gherkin current (${parsed.length} file(s), ${stampedScenarios} stamped scenario(s) agree with the living specs)`,\n });\n }\n return findings;\n}\n\n/**\n * Every `.feature` file at or under `root`, sorted by repo-relative path —\n * enumeration is part of the output contract, so the order cannot depend on\n * the filesystem's. Dot-entries are skipped, like every other walk in loam.\n *\n * Symlinks are FOLLOWED, through `repo/tree/fs.ts`'s `entryIs` — the rule\n * every enumeration shares (brief/brief.ts's `hasEntry` too), for the same\n * reason. A `Dirent` from `readdir(withFileTypes)` never follows one, so\n * `isFile()` and `isDirectory()` are both false for a\n * symlinked `.feature`: it was invisible to the orphan scan (`loam gherkin`\n * left it behind forever) and invisible to the staleness scan (it graded as no\n * coverage at all). Composing a suite out of links is legitimate, so they are\n * followed rather than refused, and a dangling one is skipped exactly as an\n * absent file is.\n */\nexport async function featureFilesUnder(root: string): Promise<string[]> {\n const out: string[] = [];\n // Following links means a cycle is now possible; every directory is entered\n // once by its resolved path (`markDescended`), so a loop terminates instead\n // of hanging.\n const entered = new Set<string>();\n const walk = async (dir: string): Promise<void> => {\n if (!markDescended(dir, entered)) return;\n for (const entry of await readdir(dir, { withFileTypes: true })) {\n if (entry.name.startsWith(\".\")) continue;\n const abs = join(dir, entry.name);\n if (entryIs(dir, entry, \"dir\")) await walk(abs);\n else if (entry.name.endsWith(\".feature\") && entryIs(dir, entry, \"file\")) out.push(abs);\n }\n };\n await walk(root);\n return out.sort((a, b) => {\n const ra = a.split(sep).join(\"/\");\n const rb = b.split(sep).join(\"/\");\n return ra < rb ? -1 : ra > rb ? 1 : 0;\n });\n}\n"]}
@@ -49,13 +49,16 @@ export function featureGlossary(featureDir) {
49
49
  * Where each of this feature's terms WILL live once the archive merges — the
50
50
  * overlay a feature's own documents are allowed to cite.
51
51
  *
52
- * A feature that introduces a word cites it at its future living path
53
- * (`../../glossary/order.md`), because that is where the link has to point once
54
- * the feature ships; citing the delta path instead would resolve today and rot
55
- * on the day it lands. So while a feature is in flight, `link.unresolved` treats
56
- * these paths as resolved — the same overlay the capability axis needed for
57
- * `Realizes:`, and for the same reason: an index built from the LIVING tree
58
- * alone refuses the feature's own headline flow.
52
+ * A feature that introduces a word may cite it at its future living path
53
+ * (`../../glossary/order.md` from `intent.md`), because that is where the link
54
+ * points once the feature ships. So while a feature is in flight,
55
+ * `link.unresolved` treats these paths as resolved — the same overlay the
56
+ * capability axis needed for `Realizes:`, and for the same reason: an index
57
+ * built from the LIVING tree alone refuses the feature's own headline flow.
58
+ * Citing the term at its path INSIDE the feature is the other legal spelling,
59
+ * and it no longer rots on the day the feature lands: archive re-points every
60
+ * merged link into the feature at the file's new home
61
+ * (`commands/archive/plan/document/links.ts`).
59
62
  */
60
63
  export async function featureGlossaryOverlay(docsDir, featureDir) {
61
64
  const glossary = await featureGlossary(featureDir);
@@ -1 +1 @@
1
- {"version":3,"file":"delta.js","sourceRoot":"","sources":["../../../src/core/glossary/delta.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAC5E,OAAO,EAAc,MAAM,wBAAwB,CAAC;AAEpD,OAAO,EAAE,YAAY,EAAiB,MAAM,WAAW,CAAC;AAExD;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,UAAsB;IACpD,OAAO,YAAY,CAAC,kBAAkB,CAAC,UAAU,CAAC,CAAC,CAAC;AACtD,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,OAAgB,EAAE,UAAsB;IACnF,MAAM,QAAQ,GAAG,MAAM,eAAe,CAAC,UAAU,CAAC,CAAC;IACnD,OAAO,IAAI,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,cAAc,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;AACpF,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,OAAgB,EAAE,UAAsB;IAChF,MAAM,QAAQ,GAAG,MAAM,eAAe,CAAC,UAAU,CAAC,CAAC;IACnD,IAAI,CAAC,QAAQ,CAAC,OAAO;QAAE,OAAO,EAAE,CAAC;IACjC,OAAO,QAAQ,CAAC,KAAK;SAClB,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,UAAU,CAAC,cAAc,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;SAC9D,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QACd,QAAQ,EAAE,OAAgB;QAC1B,IAAI,EAAE,sBAA+B;QACrC,OAAO,EAAE,IAAI,CAAC,EAAE;QAChB,OAAO,EACL,YAAY,IAAI,CAAC,EAAE,wDAAwD,IAAI,CAAC,EAAE,8CAA8C;YAChI,gFAAgF;YAChF,mCAAmC,IAAI,CAAC,EAAE,yBAAyB,IAAI,CAAC,EAAE,oCAAoC;YAC9G,6EAA6E;KAChF,CAAC,CAAC,CAAC;AACR,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,cAAc,CAAC,OAAgB,EAAE,EAAU;IACzD,OAAO,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;AAChD,CAAC","sourcesContent":["/**\n * A feature's own glossary deltas: `features/<FEAT>/glossary/<term>.md`.\n *\n * A change that needs a new word brings the word with it. The archive copies the\n * definition into `glossary/`, `loam unarchive` takes it back out with\n * everything else the feature landed, and until then the term is visible in\n * review beside the requirements that use it — which is the whole reason this\n * route exists rather than \"remember to edit the glossary too\".\n *\n * CREATE-ONLY, and this is the axis's one real design decision rather than an\n * unfinished half. A feature-local capability document is a DELTA — `## ADDED\n * Requirements`, `Requirement-ID:` identity, `Based-On:` pins — because it\n * merges INTO a living document and two features editing one requirement would\n * otherwise silently overwrite each other. A term has none of that: the file is\n * one definition, the merge is a whole-file copy, and there is nothing to merge\n * partially. So rewriting a living definition through a feature would be a\n * silent whole-file replacement with no pin to collide on, where editing\n * `glossary/<term>.md` directly in the same pull request produces an ordinary\n * git conflict that a human resolves. `glossary.term-exists` refuses the first\n * and points at the second.\n *\n * TWO FEATURES INTRODUCING THE SAME WORD is the case that makes the refusal\n * load-bearing rather than pedantic: whichever archives first creates the living\n * definition, and the second is told — loudly, before it merges — that the word\n * now exists and its own file is the one to delete.\n *\n * ONE CALL for the walk, because it is already written: `readGlossary`\n * implements the file-is-a-term rule, the `README.md` exclusion, the nesting and\n * the symlink guard over any root. A second walk for the feature side would be\n * two implementations free to disagree about what a term is.\n */\nimport { existsSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport { featureGlossaryDir, glossaryDir } from \"../repo/authored/paths.js\";\nimport { type Issue } from \"../vocabulary/issue.js\";\nimport type { DocsDir, FeatureDir } from \"../kernel/ids/dirs.js\";\nimport { readGlossary, type Glossary } from \"./tree.js\";\n\n/**\n * The definitions one feature carries, ordered by id.\n *\n * `present: false` — and zero readdirs — when the feature has no `glossary/`\n * directory, which is every feature in a fleet that has not adopted the axis.\n * That short-circuit is `readGlossary`'s own `existsSync` and it is the whole\n * cost such a fleet pays.\n */\nexport function featureGlossary(featureDir: FeatureDir): Promise<Glossary> {\n return readGlossary(featureGlossaryDir(featureDir));\n}\n\n/**\n * Where each of this feature's terms WILL live once the archive merges — the\n * overlay a feature's own documents are allowed to cite.\n *\n * A feature that introduces a word cites it at its future living path\n * (`../../glossary/order.md`), because that is where the link has to point once\n * the feature ships; citing the delta path instead would resolve today and rot\n * on the day it lands. So while a feature is in flight, `link.unresolved` treats\n * these paths as resolved — the same overlay the capability axis needed for\n * `Realizes:`, and for the same reason: an index built from the LIVING tree\n * alone refuses the feature's own headline flow.\n */\nexport async function featureGlossaryOverlay(docsDir: DocsDir, featureDir: FeatureDir): Promise<Set<string>> {\n const glossary = await featureGlossary(featureDir);\n return new Set(glossary.terms.map((t) => resolve(livingTermPath(docsDir, t.id))));\n}\n\n/**\n * `glossary.term-exists` — a term this feature introduces that the living\n * glossary already defines.\n *\n * An ERROR and never a warning-that-gates: unlike `capability.uncovered`, there\n * is no legal reading of it. The merge would replace an authored definition\n * with another one, and no `--approve` makes that a different act than it is.\n * The fix is in the message and is the one a reviewer would give anyway.\n */\nexport async function glossaryDeltaIssues(docsDir: DocsDir, featureDir: FeatureDir): Promise<Issue[]> {\n const glossary = await featureGlossary(featureDir);\n if (!glossary.present) return [];\n return glossary.terms\n .filter((term) => existsSync(livingTermPath(docsDir, term.id)))\n .map((term) => ({\n severity: \"error\" as const,\n code: \"glossary.term-exists\" as const,\n subject: term.id,\n message:\n `glossary/${term.id}.md already exists, so this feature's definition of '${term.id}' would replace an authored one wholesale — ` +\n \"a feature-local glossary document INTRODUCES a term, it does not rewrite one. \" +\n `Delete features/<FEAT>/glossary/${term.id}.md and edit glossary/${term.id}.md directly in this same change, ` +\n \"where git produces an ordinary conflict if somebody else is editing it too.\",\n }));\n}\n\n/**\n * The LIVING path a feature-local term id addresses. Spelled here because three\n * readers need it — the refusal above, the overlay, and the archive merge — and\n * a reader that resolved it differently would refuse a term the merge then\n * created anyway.\n *\n * The id is joined un-split, which `node:path` normalizes: `join(dir, \"a/b\")`\n * and `join(dir, \"a\", \"b\")` are the same string. What holds this join is\n * PROVENANCE, exactly as it holds `livingCapabilityPaths` — every id here came\n * out of a `readGlossary` walk, where each component is a `readdir` entry name\n * and therefore a directory or file that exists. No caller takes one from argv;\n * the day one does, it needs the treatment `core/kernel/ids/capability.ts` gave\n * the same hazard one axis over.\n */\nexport function livingTermPath(docsDir: DocsDir, id: string): string {\n return join(glossaryDir(docsDir), `${id}.md`);\n}\n"]}
1
+ {"version":3,"file":"delta.js","sourceRoot":"","sources":["../../../src/core/glossary/delta.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAC5E,OAAO,EAAc,MAAM,wBAAwB,CAAC;AAEpD,OAAO,EAAE,YAAY,EAAiB,MAAM,WAAW,CAAC;AAExD;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,UAAsB;IACpD,OAAO,YAAY,CAAC,kBAAkB,CAAC,UAAU,CAAC,CAAC,CAAC;AACtD,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,OAAgB,EAAE,UAAsB;IACnF,MAAM,QAAQ,GAAG,MAAM,eAAe,CAAC,UAAU,CAAC,CAAC;IACnD,OAAO,IAAI,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,cAAc,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;AACpF,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,OAAgB,EAAE,UAAsB;IAChF,MAAM,QAAQ,GAAG,MAAM,eAAe,CAAC,UAAU,CAAC,CAAC;IACnD,IAAI,CAAC,QAAQ,CAAC,OAAO;QAAE,OAAO,EAAE,CAAC;IACjC,OAAO,QAAQ,CAAC,KAAK;SAClB,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,UAAU,CAAC,cAAc,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;SAC9D,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QACd,QAAQ,EAAE,OAAgB;QAC1B,IAAI,EAAE,sBAA+B;QACrC,OAAO,EAAE,IAAI,CAAC,EAAE;QAChB,OAAO,EACL,YAAY,IAAI,CAAC,EAAE,wDAAwD,IAAI,CAAC,EAAE,8CAA8C;YAChI,gFAAgF;YAChF,mCAAmC,IAAI,CAAC,EAAE,yBAAyB,IAAI,CAAC,EAAE,oCAAoC;YAC9G,6EAA6E;KAChF,CAAC,CAAC,CAAC;AACR,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,cAAc,CAAC,OAAgB,EAAE,EAAU;IACzD,OAAO,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;AAChD,CAAC","sourcesContent":["/**\n * A feature's own glossary deltas: `features/<FEAT>/glossary/<term>.md`.\n *\n * A change that needs a new word brings the word with it. The archive copies the\n * definition into `glossary/`, `loam unarchive` takes it back out with\n * everything else the feature landed, and until then the term is visible in\n * review beside the requirements that use it — which is the whole reason this\n * route exists rather than \"remember to edit the glossary too\".\n *\n * CREATE-ONLY, and this is the axis's one real design decision rather than an\n * unfinished half. A feature-local capability document is a DELTA — `## ADDED\n * Requirements`, `Requirement-ID:` identity, `Based-On:` pins — because it\n * merges INTO a living document and two features editing one requirement would\n * otherwise silently overwrite each other. A term has none of that: the file is\n * one definition, the merge is a whole-file copy, and there is nothing to merge\n * partially. So rewriting a living definition through a feature would be a\n * silent whole-file replacement with no pin to collide on, where editing\n * `glossary/<term>.md` directly in the same pull request produces an ordinary\n * git conflict that a human resolves. `glossary.term-exists` refuses the first\n * and points at the second.\n *\n * TWO FEATURES INTRODUCING THE SAME WORD is the case that makes the refusal\n * load-bearing rather than pedantic: whichever archives first creates the living\n * definition, and the second is told — loudly, before it merges — that the word\n * now exists and its own file is the one to delete.\n *\n * ONE CALL for the walk, because it is already written: `readGlossary`\n * implements the file-is-a-term rule, the `README.md` exclusion, the nesting and\n * the symlink guard over any root. A second walk for the feature side would be\n * two implementations free to disagree about what a term is.\n */\nimport { existsSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport { featureGlossaryDir, glossaryDir } from \"../repo/authored/paths.js\";\nimport { type Issue } from \"../vocabulary/issue.js\";\nimport type { DocsDir, FeatureDir } from \"../kernel/ids/dirs.js\";\nimport { readGlossary, type Glossary } from \"./tree.js\";\n\n/**\n * The definitions one feature carries, ordered by id.\n *\n * `present: false` — and zero readdirs — when the feature has no `glossary/`\n * directory, which is every feature in a fleet that has not adopted the axis.\n * That short-circuit is `readGlossary`'s own `existsSync` and it is the whole\n * cost such a fleet pays.\n */\nexport function featureGlossary(featureDir: FeatureDir): Promise<Glossary> {\n return readGlossary(featureGlossaryDir(featureDir));\n}\n\n/**\n * Where each of this feature's terms WILL live once the archive merges — the\n * overlay a feature's own documents are allowed to cite.\n *\n * A feature that introduces a word may cite it at its future living path\n * (`../../glossary/order.md` from `intent.md`), because that is where the link\n * points once the feature ships. So while a feature is in flight,\n * `link.unresolved` treats these paths as resolved — the same overlay the\n * capability axis needed for `Realizes:`, and for the same reason: an index\n * built from the LIVING tree alone refuses the feature's own headline flow.\n * Citing the term at its path INSIDE the feature is the other legal spelling,\n * and it no longer rots on the day the feature lands: archive re-points every\n * merged link into the feature at the file's new home\n * (`commands/archive/plan/document/links.ts`).\n */\nexport async function featureGlossaryOverlay(docsDir: DocsDir, featureDir: FeatureDir): Promise<Set<string>> {\n const glossary = await featureGlossary(featureDir);\n return new Set(glossary.terms.map((t) => resolve(livingTermPath(docsDir, t.id))));\n}\n\n/**\n * `glossary.term-exists` — a term this feature introduces that the living\n * glossary already defines.\n *\n * An ERROR and never a warning-that-gates: unlike `capability.uncovered`, there\n * is no legal reading of it. The merge would replace an authored definition\n * with another one, and no `--approve` makes that a different act than it is.\n * The fix is in the message and is the one a reviewer would give anyway.\n */\nexport async function glossaryDeltaIssues(docsDir: DocsDir, featureDir: FeatureDir): Promise<Issue[]> {\n const glossary = await featureGlossary(featureDir);\n if (!glossary.present) return [];\n return glossary.terms\n .filter((term) => existsSync(livingTermPath(docsDir, term.id)))\n .map((term) => ({\n severity: \"error\" as const,\n code: \"glossary.term-exists\" as const,\n subject: term.id,\n message:\n `glossary/${term.id}.md already exists, so this feature's definition of '${term.id}' would replace an authored one wholesale — ` +\n \"a feature-local glossary document INTRODUCES a term, it does not rewrite one. \" +\n `Delete features/<FEAT>/glossary/${term.id}.md and edit glossary/${term.id}.md directly in this same change, ` +\n \"where git produces an ordinary conflict if somebody else is editing it too.\",\n }));\n}\n\n/**\n * The LIVING path a feature-local term id addresses. Spelled here because three\n * readers need it — the refusal above, the overlay, and the archive merge — and\n * a reader that resolved it differently would refuse a term the merge then\n * created anyway.\n *\n * The id is joined un-split, which `node:path` normalizes: `join(dir, \"a/b\")`\n * and `join(dir, \"a\", \"b\")` are the same string. What holds this join is\n * PROVENANCE, exactly as it holds `livingCapabilityPaths` — every id here came\n * out of a `readGlossary` walk, where each component is a `readdir` entry name\n * and therefore a directory or file that exists. No caller takes one from argv;\n * the day one does, it needs the treatment `core/kernel/ids/capability.ts` gave\n * the same hazard one axis over.\n */\nexport function livingTermPath(docsDir: DocsDir, id: string): string {\n return join(glossaryDir(docsDir), `${id}.md`);\n}\n"]}
@@ -24,11 +24,11 @@
24
24
  * document is prose for a human, and the one thing loam reads from it — the
25
25
  * links it writes — is read by `core/links/`, where every other document's are.
26
26
  */
27
- import { existsSync, realpathSync } from "node:fs";
27
+ import { existsSync } from "node:fs";
28
28
  import { readdir } from "node:fs/promises";
29
29
  import { join } from "node:path";
30
30
  import { compareIds } from "../repo/entries.js";
31
- import { entryIs } from "../repo/tree/fs.js";
31
+ import { entryIs, markDescended } from "../repo/tree/fs.js";
32
32
  /** The one filename under `glossary/` that is not a term. */
33
33
  const INDEX_PAGE = "README.md";
34
34
  /**
@@ -37,8 +37,9 @@ const INDEX_PAGE = "README.md";
37
37
  * A fleet is expected to hold tens to low hundreds of these — a domain's
38
38
  * vocabulary, not its data — so the cost is one `readdir` per directory and the
39
39
  * walk's expense is a non-question. The symlink-cycle guard is the one
40
- * `core/repo/tree/walk.ts` and the capability walk both carry, and for the same
41
- * reason: `entryIs` follows symlinks on purpose, and a followed cycle without a
40
+ * `core/repo/tree/walk.ts` and the capability walk both carry
41
+ * (`core/repo/tree/fs.ts`'s `markDescended`), and for the same reason:
42
+ * `entryIs` follows symlinks on purpose, and a followed cycle without a
42
43
  * visited set recurses until the stack ends.
43
44
  */
44
45
  export async function readGlossary(root) {
@@ -51,7 +52,7 @@ export async function readGlossary(root) {
51
52
  }
52
53
  /** Collect the terms in one directory and walk what is beneath it. */
53
54
  async function visit(spot, state) {
54
- if (!descended(spot.dir, state))
55
+ if (!markDescended(spot.dir, state.visited))
55
56
  return;
56
57
  const entries = (await readdir(spot.dir, { withFileTypes: true })).filter((e) => !e.name.startsWith("."));
57
58
  for (const entry of entries.filter((e) => e.name.endsWith(".md")).sort((a, b) => compareIds(a.name, b.name))) {
@@ -69,18 +70,4 @@ async function visit(spot, state) {
69
70
  await visit({ dir: join(spot.dir, child), chain: [...spot.chain, child] }, state);
70
71
  }
71
72
  }
72
- /** Mark a directory as descended-into; false when it (or another name for it) already was. */
73
- function descended(dir, state) {
74
- let real;
75
- try {
76
- real = realpathSync(dir);
77
- }
78
- catch {
79
- return false;
80
- }
81
- if (state.visited.has(real))
82
- return false;
83
- state.visited.add(real);
84
- return true;
85
- }
86
73
  //# sourceMappingURL=tree.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"tree.js","sourceRoot":"","sources":["../../../src/core/glossary/tree.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The glossary tree: `glossary/<term>.md`, and the rule that a markdown file\n * under it IS a declared term.\n *\n * WHY A TREE AND NOT A YAML VOCABULARY, at length in `repo/authored/paths.ts`'s\n * `glossaryDir`: a single vocabulary file at fleet scale is unworkable, and a\n * second list is the drift `loam init`'s removed service manifest was removed\n * for. A term is prose — that is what a definition is — so by this codebase's\n * own rule it gets a file.\n *\n * PRESENCE CLASSIFIES, one tree over from `capabilities/`: a `.md` file is a\n * term, a directory is a group the walk descends through. There is no\n * \"half-created term\" state to name, which is why this walk emits nothing like\n * `capability.doc-missing`: creating a term is writing one file, and an empty\n * grouping directory declares nothing and claims nothing.\n *\n * `README.md` IS NOT A TERM, and it is the one name excluded. A glossary big\n * enough to want an index page is normal, and reporting that page as a word\n * nobody uses would make the axis's only warning fire on the document that\n * explains the axis. Excluded by name rather than by a marker field: the\n * alternative is frontmatter nobody would remember to write.\n *\n * The walk reads directories only and never opens a file. What is IN a term\n * document is prose for a human, and the one thing loam reads from it — the\n * links it writes — is read by `core/links/`, where every other document's are.\n */\nimport { existsSync, realpathSync } from \"node:fs\";\nimport { readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { compareIds } from \"../repo/entries.js\";\nimport { entryIs } from \"../repo/tree/fs.js\";\n\n/** The one filename under `glossary/` that is not a term. */\nconst INDEX_PAGE = \"README.md\";\n\n/** One defined term: what it is called, and where its definition is. */\nexport interface GlossaryTerm {\n /** The path from `glossary/` to the file, without `.md` — `order`, or `payments/order`. */\n id: string;\n /** Absolute path of the definition. */\n path: string;\n}\n\nexport interface Glossary {\n /**\n * False when `glossary/` does not exist. The directory's existence is the\n * axis's opt-in, so a fleet without one produces nothing here — not an empty\n * glossary, which would read as \"the terms went missing\" to anything counting.\n */\n present: boolean;\n /** Every term found, at any depth, ordered by id. */\n terms: GlossaryTerm[];\n}\n\n/** One directory the walk is looking at: where it is, and how it was reached. */\ninterface Spot {\n dir: string;\n /** Directory names between `glossary/` and this one, outermost first. */\n chain: string[];\n}\n\ninterface WalkState {\n terms: GlossaryTerm[];\n /** Realpaths already descended into — the symlink-cycle guard. */\n visited: Set<string>;\n}\n\n/**\n * Read the tree under `root` (`glossaryDir(docsDir)`).\n *\n * A fleet is expected to hold tens to low hundreds of these — a domain's\n * vocabulary, not its data — so the cost is one `readdir` per directory and the\n * walk's expense is a non-question. The symlink-cycle guard is the one\n * `core/repo/tree/walk.ts` and the capability walk both carry, and for the same\n * reason: `entryIs` follows symlinks on purpose, and a followed cycle without a\n * visited set recurses until the stack ends.\n */\nexport async function readGlossary(root: string): Promise<Glossary> {\n if (!existsSync(root)) return { present: false, terms: [] };\n const state: WalkState = { terms: [], visited: new Set() };\n await visit({ dir: root, chain: [] }, state);\n state.terms.sort((a, b) => compareIds(a.id, b.id));\n return { present: true, terms: state.terms };\n}\n\n/** Collect the terms in one directory and walk what is beneath it. */\nasync function visit(spot: Spot, state: WalkState): Promise<void> {\n if (!descended(spot.dir, state)) return;\n const entries = (await readdir(spot.dir, { withFileTypes: true })).filter((e) => !e.name.startsWith(\".\"));\n for (const entry of entries.filter((e) => e.name.endsWith(\".md\")).sort((a, b) => compareIds(a.name, b.name))) {\n if (entry.name === INDEX_PAGE || !entryIs(spot.dir, entry, \"file\")) continue;\n state.terms.push({\n id: [...spot.chain, entry.name.slice(0, -\".md\".length)].join(\"/\"),\n path: join(spot.dir, entry.name),\n });\n }\n // Sequential, not Promise.all: the visited set is shared mutable state and\n // the walk's correctness under symlinks rests on a child being marked before\n // its sibling asks. These trees are tens of directories at most.\n for (const child of entries.filter((e) => entryIs(spot.dir, e, \"dir\")).map((e) => e.name).sort()) {\n await visit({ dir: join(spot.dir, child), chain: [...spot.chain, child] }, state);\n }\n}\n\n/** Mark a directory as descended-into; false when it (or another name for it) already was. */\nfunction descended(dir: string, state: WalkState): boolean {\n let real: string;\n try {\n real = realpathSync(dir);\n } catch {\n return false;\n }\n if (state.visited.has(real)) return false;\n state.visited.add(real);\n return true;\n}\n"]}
1
+ {"version":3,"file":"tree.js","sourceRoot":"","sources":["../../../src/core/glossary/tree.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The glossary tree: `glossary/<term>.md`, and the rule that a markdown file\n * under it IS a declared term.\n *\n * WHY A TREE AND NOT A YAML VOCABULARY, at length in `repo/authored/paths.ts`'s\n * `glossaryDir`: a single vocabulary file at fleet scale is unworkable, and a\n * second list is the drift `loam init`'s removed service manifest was removed\n * for. A term is prose — that is what a definition is — so by this codebase's\n * own rule it gets a file.\n *\n * PRESENCE CLASSIFIES, one tree over from `capabilities/`: a `.md` file is a\n * term, a directory is a group the walk descends through. There is no\n * \"half-created term\" state to name, which is why this walk emits nothing like\n * `capability.doc-missing`: creating a term is writing one file, and an empty\n * grouping directory declares nothing and claims nothing.\n *\n * `README.md` IS NOT A TERM, and it is the one name excluded. A glossary big\n * enough to want an index page is normal, and reporting that page as a word\n * nobody uses would make the axis's only warning fire on the document that\n * explains the axis. Excluded by name rather than by a marker field: the\n * alternative is frontmatter nobody would remember to write.\n *\n * The walk reads directories only and never opens a file. What is IN a term\n * document is prose for a human, and the one thing loam reads from it — the\n * links it writes — is read by `core/links/`, where every other document's are.\n */\nimport { existsSync } from \"node:fs\";\nimport { readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { compareIds } from \"../repo/entries.js\";\nimport { entryIs, markDescended } from \"../repo/tree/fs.js\";\n\n/** The one filename under `glossary/` that is not a term. */\nconst INDEX_PAGE = \"README.md\";\n\n/** One defined term: what it is called, and where its definition is. */\nexport interface GlossaryTerm {\n /** The path from `glossary/` to the file, without `.md` — `order`, or `payments/order`. */\n id: string;\n /** Absolute path of the definition. */\n path: string;\n}\n\nexport interface Glossary {\n /**\n * False when `glossary/` does not exist. The directory's existence is the\n * axis's opt-in, so a fleet without one produces nothing here — not an empty\n * glossary, which would read as \"the terms went missing\" to anything counting.\n */\n present: boolean;\n /** Every term found, at any depth, ordered by id. */\n terms: GlossaryTerm[];\n}\n\n/** One directory the walk is looking at: where it is, and how it was reached. */\ninterface Spot {\n dir: string;\n /** Directory names between `glossary/` and this one, outermost first. */\n chain: string[];\n}\n\ninterface WalkState {\n terms: GlossaryTerm[];\n /** Realpaths already descended into — the symlink-cycle guard. */\n visited: Set<string>;\n}\n\n/**\n * Read the tree under `root` (`glossaryDir(docsDir)`).\n *\n * A fleet is expected to hold tens to low hundreds of these — a domain's\n * vocabulary, not its data — so the cost is one `readdir` per directory and the\n * walk's expense is a non-question. The symlink-cycle guard is the one\n * `core/repo/tree/walk.ts` and the capability walk both carry\n * (`core/repo/tree/fs.ts`'s `markDescended`), and for the same reason:\n * `entryIs` follows symlinks on purpose, and a followed cycle without a\n * visited set recurses until the stack ends.\n */\nexport async function readGlossary(root: string): Promise<Glossary> {\n if (!existsSync(root)) return { present: false, terms: [] };\n const state: WalkState = { terms: [], visited: new Set() };\n await visit({ dir: root, chain: [] }, state);\n state.terms.sort((a, b) => compareIds(a.id, b.id));\n return { present: true, terms: state.terms };\n}\n\n/** Collect the terms in one directory and walk what is beneath it. */\nasync function visit(spot: Spot, state: WalkState): Promise<void> {\n if (!markDescended(spot.dir, state.visited)) return;\n const entries = (await readdir(spot.dir, { withFileTypes: true })).filter((e) => !e.name.startsWith(\".\"));\n for (const entry of entries.filter((e) => e.name.endsWith(\".md\")).sort((a, b) => compareIds(a.name, b.name))) {\n if (entry.name === INDEX_PAGE || !entryIs(spot.dir, entry, \"file\")) continue;\n state.terms.push({\n id: [...spot.chain, entry.name.slice(0, -\".md\".length)].join(\"/\"),\n path: join(spot.dir, entry.name),\n });\n }\n // Sequential, not Promise.all: the visited set is shared mutable state and\n // the walk's correctness under symlinks rests on a child being marked before\n // its sibling asks. These trees are tens of directories at most.\n for (const child of entries.filter((e) => entryIs(spot.dir, e, \"dir\")).map((e) => e.name).sort()) {\n await visit({ dir: join(spot.dir, child), chain: [...spot.chain, child] }, state);\n }\n}\n"]}
@@ -7,6 +7,16 @@
7
7
  * either opens all of them — on a big fleet that is not a faster run, it is a
8
8
  * bigger one. The cap and the ordering guarantee travel together because a
9
9
  * caller that wants the first cannot afford to lose the second.
10
+ *
11
+ * The third answer to "how much may happen at once" is `memo`: the same read
12
+ * asked for twice in one invocation happens once, both askers sharing the one
13
+ * in-flight promise. It lives here, in a leaf, because both read indexes —
14
+ * `core/fleet-context.ts` and the service-model memos it owns
15
+ * (`core/c4/prefetch/fleet.ts`) — spell every cache they keep with it, and
16
+ * neither should own the other's idiom. The read index could borrow it from
17
+ * the package it already imports, but then a rule about caching in general
18
+ * would live inside the LikeC4 adapter, and the next memo outside `c4/` would
19
+ * have to import that adapter to obey it.
10
20
  */
11
21
  import { cpus } from "node:os";
12
22
  /**
@@ -48,4 +58,13 @@ export async function inOrder(items, work) {
48
58
  }));
49
59
  return out;
50
60
  }
61
+ /** One in-flight promise per key, created on first ask — the memo idiom, once. */
62
+ export function memo(store, k, make) {
63
+ let pending = store.get(k);
64
+ if (pending === undefined) {
65
+ pending = make();
66
+ store.set(k, pending);
67
+ }
68
+ return pending;
69
+ }
51
70
  //# sourceMappingURL=concurrency.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"concurrency.js","sourceRoot":"","sources":["../../../src/core/kernel/concurrency.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAE/B;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC;AAE1E;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAC3B,KAAmB,EACnB,IAA6B;IAE7B,MAAM,GAAG,GAAQ,EAAE,CAAC;IACpB,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,kBAAkB,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC3D,MAAM,OAAO,CAAC,GAAG,CACf,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,KAAK,IAAI,EAAE;QACzC,KAAK,IAAI,CAAC,GAAG,IAAI,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE;YAAE,GAAG,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,CAAC;IACpF,CAAC,CAAC,CACH,CAAC;IACF,OAAO,GAAG,CAAC;AACb,CAAC","sourcesContent":["/**\n * How much of loam may happen at once, and in what order the answers come back.\n *\n * One rule, because there is one resource being rationed: a LikeC4/Langium\n * workspace. `validate --all` opens one per service and per feature, the\n * dependency graph opens one per feature, and an unbounded `Promise.all` over\n * either opens all of them — on a big fleet that is not a faster run, it is a\n * bigger one. The cap and the ordering guarantee travel together because a\n * caller that wants the first cannot afford to lose the second.\n */\nimport { cpus } from \"node:os\";\n\n/**\n * How many units of work may be in flight at once.\n *\n * Capped as well as scaled: each in-flight unit holds a whole LikeC4/Langium\n * workspace, so a 64-core box would open sixty-four of them at once and buy\n * memory pressure rather than throughput (peak RSS on an 80-service fleet:\n * 385 MB serial, ~500 MB pooled). That measurement is the entire justification\n * for the cap, and it is why the cap lives with the work rather than inside one\n * command: `loam dependencies` and the fleet form of `loam status` fan out over\n * the same workspaces `validate --all` does, and used to do it uncapped.\n *\n * Do not quote a speedup for this. The measured cost is CPU inside the Langium\n * parse, which one thread cannot divide however the loop is shaped; what the\n * pool overlaps is the I/O half — the spec, contract and health reads around\n * each parse. On an 80-service fleet it measures as a wash (11–13 s either way,\n * on a box that was itself loaded). The number that moves these commands is\n * workspaces per run, not awaits per loop.\n */\nexport const TARGET_CONCURRENCY = Math.max(1, Math.min(cpus().length, 8));\n\n/**\n * Run `work` over `items` with a bounded number in flight, returning the\n * results in INPUT order.\n *\n * The order is the contract, not an accident: the text report and the `--json`\n * envelope are diffed between runs and between machines, so a concurrent run\n * has to be byte-identical to the serial one for the same input. That is why\n * results are written by index — nothing is appended as it finishes, and a slow\n * item cannot reorder the fleet around it.\n */\nexport async function inOrder<T, R>(\n items: readonly T[],\n work: (item: T) => Promise<R>,\n): Promise<R[]> {\n const out: R[] = [];\n let next = 0;\n const workers = Math.min(TARGET_CONCURRENCY, items.length);\n await Promise.all(\n Array.from({ length: workers }, async () => {\n for (let i = next++; i < items.length; i = next++) out[i] = await work(items[i]!);\n }),\n );\n return out;\n}\n"]}
1
+ {"version":3,"file":"concurrency.js","sourceRoot":"","sources":["../../../src/core/kernel/concurrency.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAE/B;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC;AAE1E;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAC3B,KAAmB,EACnB,IAA6B;IAE7B,MAAM,GAAG,GAAQ,EAAE,CAAC;IACpB,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,kBAAkB,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC3D,MAAM,OAAO,CAAC,GAAG,CACf,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,KAAK,IAAI,EAAE;QACzC,KAAK,IAAI,CAAC,GAAG,IAAI,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE;YAAE,GAAG,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,CAAC;IACpF,CAAC,CAAC,CACH,CAAC;IACF,OAAO,GAAG,CAAC;AACb,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,IAAI,CAAI,KAA8B,EAAE,CAAS,EAAE,IAAsB;IACvF,IAAI,OAAO,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAC3B,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,OAAO,GAAG,IAAI,EAAE,CAAC;QACjB,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IACxB,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC","sourcesContent":["/**\n * How much of loam may happen at once, and in what order the answers come back.\n *\n * One rule, because there is one resource being rationed: a LikeC4/Langium\n * workspace. `validate --all` opens one per service and per feature, the\n * dependency graph opens one per feature, and an unbounded `Promise.all` over\n * either opens all of them — on a big fleet that is not a faster run, it is a\n * bigger one. The cap and the ordering guarantee travel together because a\n * caller that wants the first cannot afford to lose the second.\n *\n * The third answer to \"how much may happen at once\" is `memo`: the same read\n * asked for twice in one invocation happens once, both askers sharing the one\n * in-flight promise. It lives here, in a leaf, because both read indexes —\n * `core/fleet-context.ts` and the service-model memos it owns\n * (`core/c4/prefetch/fleet.ts`) — spell every cache they keep with it, and\n * neither should own the other's idiom. The read index could borrow it from\n * the package it already imports, but then a rule about caching in general\n * would live inside the LikeC4 adapter, and the next memo outside `c4/` would\n * have to import that adapter to obey it.\n */\nimport { cpus } from \"node:os\";\n\n/**\n * How many units of work may be in flight at once.\n *\n * Capped as well as scaled: each in-flight unit holds a whole LikeC4/Langium\n * workspace, so a 64-core box would open sixty-four of them at once and buy\n * memory pressure rather than throughput (peak RSS on an 80-service fleet:\n * 385 MB serial, ~500 MB pooled). That measurement is the entire justification\n * for the cap, and it is why the cap lives with the work rather than inside one\n * command: `loam dependencies` and the fleet form of `loam status` fan out over\n * the same workspaces `validate --all` does, and used to do it uncapped.\n *\n * Do not quote a speedup for this. The measured cost is CPU inside the Langium\n * parse, which one thread cannot divide however the loop is shaped; what the\n * pool overlaps is the I/O half — the spec, contract and health reads around\n * each parse. On an 80-service fleet it measures as a wash (11–13 s either way,\n * on a box that was itself loaded). The number that moves these commands is\n * workspaces per run, not awaits per loop.\n */\nexport const TARGET_CONCURRENCY = Math.max(1, Math.min(cpus().length, 8));\n\n/**\n * Run `work` over `items` with a bounded number in flight, returning the\n * results in INPUT order.\n *\n * The order is the contract, not an accident: the text report and the `--json`\n * envelope are diffed between runs and between machines, so a concurrent run\n * has to be byte-identical to the serial one for the same input. That is why\n * results are written by index — nothing is appended as it finishes, and a slow\n * item cannot reorder the fleet around it.\n */\nexport async function inOrder<T, R>(\n items: readonly T[],\n work: (item: T) => Promise<R>,\n): Promise<R[]> {\n const out: R[] = [];\n let next = 0;\n const workers = Math.min(TARGET_CONCURRENCY, items.length);\n await Promise.all(\n Array.from({ length: workers }, async () => {\n for (let i = next++; i < items.length; i = next++) out[i] = await work(items[i]!);\n }),\n );\n return out;\n}\n\n/** One in-flight promise per key, created on first ask — the memo idiom, once. */\nexport function memo<T>(store: Map<string, Promise<T>>, k: string, make: () => Promise<T>): Promise<T> {\n let pending = store.get(k);\n if (pending === undefined) {\n pending = make();\n store.set(k, pending);\n }\n return pending;\n}\n"]}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The ancestor chain of a dotted element id — `a.b.c` yields `a.b`, then `a`.
3
+ *
4
+ * A leaf of its own because five modules were each walking it privately, in two
5
+ * spellings, and the walk is not incidental to any of them: it is the rule that
6
+ * a landscape does not have to draw a service as one opaque box. The moment
7
+ * somebody models its containers — `paymentService.api`, `paymentService.worker`
8
+ * — an edge drawn INTO a container is still an edge into the service, and every
9
+ * join that groups by service has to know that. Resolving only the exact id is
10
+ * what made those edges invisible: the spine check silently skipped them, and
11
+ * the no-openapi grace treated a service with a dozen inbound container calls as
12
+ * one nobody calls at all.
13
+ *
14
+ * The same walk answers a second question one altitude down, which is why a
15
+ * fifth copy appeared. A service model draws an edge at `kafka.orderEvents`
16
+ * while the fleet's health.yaml names the dependency `kafka`, so the slice of a
17
+ * per-service project has to carry the ANCESTOR of a partner endpoint as well as
18
+ * the endpoint (`core/c4/service-model/slice.ts`). Dropping it would turn a
19
+ * fleet's dependency declarations into `health.dependency-unmodelled` findings
20
+ * on the day it migrated its models, with nothing in the docs having changed.
21
+ *
22
+ * TWO SPELLINGS, both exported, because both are real and picking one would make
23
+ * every second caller write `[id, ...properAncestorIds(id)]` or `.slice(1)` — a
24
+ * per-call-site adjustment is exactly how a shared walk drifts back into five.
25
+ * A resolver that also looks the element itself up wants `ancestorIds`; a walk
26
+ * that has already handled the element wants `properAncestorIds`.
27
+ *
28
+ * No brand and no validation: this is string arithmetic over an id a DOCUMENT
29
+ * spells, and `core/kernel/ids/service.ts`'s provenance rules are about names
30
+ * that have to answer for a directory. An id with no dot in it has no ancestors
31
+ * and yields the empty list, which is the correct answer rather than an edge
32
+ * case — a top-level element belongs to nothing above it.
33
+ */
34
+ /** Every dotted ancestor of an id, NEAREST FIRST and excluding the id itself. */
35
+ export function properAncestorIds(id) {
36
+ const out = [];
37
+ // Walking right-to-left from the last dot rather than splitting and rejoining:
38
+ // the ids are already strings, the answer is a set of prefixes of one of them,
39
+ // and a split/join round trip would invent a rule about what a segment may
40
+ // contain that nothing else here has.
41
+ for (let dot = id.lastIndexOf("."); dot !== -1; dot = id.lastIndexOf(".", dot - 1)) {
42
+ out.push(id.slice(0, dot));
43
+ }
44
+ return out;
45
+ }
46
+ /** The id itself, then every dotted ancestor of it, nearest first. */
47
+ export function ancestorIds(id) {
48
+ return [id, ...properAncestorIds(id)];
49
+ }
50
+ /**
51
+ * The id a declaration nests directly under — `""` for a top-level one, which
52
+ * nests under nothing.
53
+ *
54
+ * The nearest ancestor, named because that is the question the callers ask:
55
+ * `properAncestorIds(id)[0] ?? ""` is the same walk spelled as an array index,
56
+ * and every reader has to re-derive that the first entry is the parent. It is
57
+ * here rather than beside its callers for the reason the file exists at all —
58
+ * the archive's two merges had each grown a private copy, and a third was about
59
+ * to appear the moment the routing gained a module.
60
+ */
61
+ export function parentIdOf(id) {
62
+ const dot = id.lastIndexOf(".");
63
+ return dot === -1 ? "" : id.slice(0, dot);
64
+ }
65
+ //# sourceMappingURL=ancestors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ancestors.js","sourceRoot":"","sources":["../../../../../src/core/kernel/ids/fqn/ancestors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,iFAAiF;AACjF,MAAM,UAAU,iBAAiB,CAAC,EAAU;IAC1C,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,+EAA+E;IAC/E,+EAA+E;IAC/E,2EAA2E;IAC3E,sCAAsC;IACtC,KAAK,IAAI,GAAG,GAAG,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,GAAG,KAAK,CAAC,CAAC,EAAE,GAAG,GAAG,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE,GAAG,GAAG,CAAC,CAAC,EAAE,CAAC;QACnF,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;IAC7B,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,WAAW,CAAC,EAAU;IACpC,OAAO,CAAC,EAAE,EAAE,GAAG,iBAAiB,CAAC,EAAE,CAAC,CAAC,CAAC;AACxC,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,UAAU,CAAC,EAAU;IACnC,MAAM,GAAG,GAAG,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IAChC,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AAC5C,CAAC","sourcesContent":["/**\n * The ancestor chain of a dotted element id — `a.b.c` yields `a.b`, then `a`.\n *\n * A leaf of its own because five modules were each walking it privately, in two\n * spellings, and the walk is not incidental to any of them: it is the rule that\n * a landscape does not have to draw a service as one opaque box. The moment\n * somebody models its containers — `paymentService.api`, `paymentService.worker`\n * — an edge drawn INTO a container is still an edge into the service, and every\n * join that groups by service has to know that. Resolving only the exact id is\n * what made those edges invisible: the spine check silently skipped them, and\n * the no-openapi grace treated a service with a dozen inbound container calls as\n * one nobody calls at all.\n *\n * The same walk answers a second question one altitude down, which is why a\n * fifth copy appeared. A service model draws an edge at `kafka.orderEvents`\n * while the fleet's health.yaml names the dependency `kafka`, so the slice of a\n * per-service project has to carry the ANCESTOR of a partner endpoint as well as\n * the endpoint (`core/c4/service-model/slice.ts`). Dropping it would turn a\n * fleet's dependency declarations into `health.dependency-unmodelled` findings\n * on the day it migrated its models, with nothing in the docs having changed.\n *\n * TWO SPELLINGS, both exported, because both are real and picking one would make\n * every second caller write `[id, ...properAncestorIds(id)]` or `.slice(1)` — a\n * per-call-site adjustment is exactly how a shared walk drifts back into five.\n * A resolver that also looks the element itself up wants `ancestorIds`; a walk\n * that has already handled the element wants `properAncestorIds`.\n *\n * No brand and no validation: this is string arithmetic over an id a DOCUMENT\n * spells, and `core/kernel/ids/service.ts`'s provenance rules are about names\n * that have to answer for a directory. An id with no dot in it has no ancestors\n * and yields the empty list, which is the correct answer rather than an edge\n * case — a top-level element belongs to nothing above it.\n */\n\n/** Every dotted ancestor of an id, NEAREST FIRST and excluding the id itself. */\nexport function properAncestorIds(id: string): string[] {\n const out: string[] = [];\n // Walking right-to-left from the last dot rather than splitting and rejoining:\n // the ids are already strings, the answer is a set of prefixes of one of them,\n // and a split/join round trip would invent a rule about what a segment may\n // contain that nothing else here has.\n for (let dot = id.lastIndexOf(\".\"); dot !== -1; dot = id.lastIndexOf(\".\", dot - 1)) {\n out.push(id.slice(0, dot));\n }\n return out;\n}\n\n/** The id itself, then every dotted ancestor of it, nearest first. */\nexport function ancestorIds(id: string): string[] {\n return [id, ...properAncestorIds(id)];\n}\n\n/**\n * The id a declaration nests directly under — `\"\"` for a top-level one, which\n * nests under nothing.\n *\n * The nearest ancestor, named because that is the question the callers ask:\n * `properAncestorIds(id)[0] ?? \"\"` is the same walk spelled as an array index,\n * and every reader has to re-derive that the first entry is the parent. It is\n * here rather than beside its callers for the reason the file exists at all —\n * the archive's two merges had each grown a private copy, and a third was about\n * to appear the moment the routing gained a module.\n */\nexport function parentIdOf(id: string): string {\n const dot = id.lastIndexOf(\".\");\n return dot === -1 ? \"\" : id.slice(0, dot);\n}\n"]}
@@ -22,6 +22,44 @@ export function isPathInside(root, target) {
22
22
  const rel = relative(resolve(root), resolve(target));
23
23
  return rel === "" || (!isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${sep}`));
24
24
  }
25
+ /**
26
+ * Where `target` is once the directory `from` has been renamed to `to`: its
27
+ * image under `to` when it is inside `from`, `target` itself when it is not.
28
+ * Absolute paths in and out. The question every reader of a directory move
29
+ * asks of one path: the link rewrites of both renames loam makes (`subsystem
30
+ * move`, and `archive` for the feature's own documents), and the archive's
31
+ * snapshot, crash repair and `unarchive`'s report, which meet those documents
32
+ * on either side of the move.
33
+ */
34
+ export function acrossRename(target, from, to) {
35
+ return isPathInside(from, target) ? resolve(to, relative(resolve(from), resolve(target))) : target;
36
+ }
37
+ /**
38
+ * `path` in the spelling every other path here has: the Win32 file-namespace
39
+ * prefix taken off a drive or UNC path (`\\?\C:\x` → `C:\x`,
40
+ * `\\?\UNC\host\share\x` → `\\host\share\x`), anything else unchanged.
41
+ *
42
+ * Measured 2026-10-21 on Node 24.13.0, Windows 11: `mkdir(dir, { recursive:
43
+ * true })` — and `mkdirSync` — reports the first directory it created as
44
+ * `\\?\C:\…\features\archive` for a `dir` of `C:\…\features\archive`, at short
45
+ * and long path lengths alike; `mkdtemp` and `realpath` return the plain
46
+ * spelling. `resolve()` keeps the prefix and `relative()` between the two
47
+ * spellings is an absolute path, so `isPathInside` judged
48
+ * `C:\…\features\archive` to lie outside `\\?\C:\…\features\archive` — and every
49
+ * rollback that pruned "up to the directory mkdir created" pruned nothing,
50
+ * leaving an empty `features/archive/` or `services/<new-service>/` behind at a
51
+ * clean `merge-failed`. Linux never reports the prefix, so CI could not see it.
52
+ *
53
+ * Only the drive and UNC forms are taken off: `\\?\Volume{…}\` and
54
+ * `\\?\GLOBALROOT\…` have no plain spelling, and stripping theirs would leave a
55
+ * relative path. A POSIX absolute path starts with `/` and never matches.
56
+ */
57
+ export function withoutNamespacePrefix(path) {
58
+ const unc = /^\\\\\?\\UNC\\/i.exec(path);
59
+ if (unc !== null)
60
+ return `\\\\${path.slice(unc[0].length)}`;
61
+ return /^\\\\\?\\[A-Za-z]:(?:\\|$)/.test(path) ? path.slice(4) : path;
62
+ }
25
63
  /**
26
64
  * Resolve a relative path beneath `root`, rejecting absolute paths, explicit
27
65
  * parent traversal, and symlink escape. The final path may be absent: its
@@ -1 +1 @@
1
- {"version":3,"file":"path-safety.js","sourceRoot":"","sources":["../../../src/core/kernel/path-safety.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Resolve caller-controlled paths without letting them leave an owned tree.\n *\n * A lexical `relative()` check is not enough: an existing component may be a\n * symlink whose target lives elsewhere. Conversely, checking only the final\n * path misses destinations that do not exist yet but whose nearest existing\n * parent is a symlink. This helper checks both views before a caller reads,\n * writes, walks, or removes anything.\n */\nimport { lstatSync, realpathSync } from \"node:fs\";\nimport { dirname, isAbsolute, relative, resolve, sep, win32 } from \"node:path\";\n\nexport class UnsafePathError extends Error {\n constructor(\n readonly candidate: string,\n message: string,\n ) {\n super(message);\n this.name = \"UnsafePathError\";\n }\n}\n\n/** True when `target` is `root` or one of its descendants. */\nexport function isPathInside(root: string, target: string): boolean {\n const rel = relative(resolve(root), resolve(target));\n return rel === \"\" || (!isAbsolute(rel) && rel !== \"..\" && !rel.startsWith(`..${sep}`));\n}\n\n/**\n * Resolve a relative path beneath `root`, rejecting absolute paths, explicit\n * parent traversal, and symlink escape. The final path may be absent: its\n * nearest existing ancestor is checked in that case.\n */\nexport function resolveInside(root: string, candidate: string, label = \"path\"): string {\n if (candidate.length === 0) {\n throw new UnsafePathError(candidate, `${label} must not be empty`);\n }\n // win32.isAbsolute is intentional even on POSIX. Persisted/configured paths\n // may be moved between operating systems, and `C:\\\\...` must never turn into\n // an innocuous relative filename merely because validation ran on Linux.\n if (isAbsolute(candidate) || win32.isAbsolute(candidate)) {\n throw new UnsafePathError(candidate, `${label} must be relative to its owning directory`);\n }\n if (candidate.split(/[\\\\/]/).includes(\"..\")) {\n throw new UnsafePathError(candidate, `${label} must not contain '..' path segments`);\n }\n\n const rootAbs = resolve(root);\n const target = resolve(rootAbs, candidate);\n if (!isPathInside(rootAbs, target)) {\n throw new UnsafePathError(candidate, `${label} resolves outside its owning directory`);\n }\n\n let rootReal: string;\n let existing: string;\n try {\n rootReal = realpathSync(rootAbs);\n existing = nearestExisting(target, rootAbs);\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n throw new UnsafePathError(candidate, `${label} cannot be resolved safely: ${detail}`);\n }\n\n let existingReal: string;\n try {\n existingReal = realpathSync(existing);\n } catch (err) {\n // A broken symlink is an existing path according to lstat, but realpath\n // cannot prove where a later write through it would land. Refuse it.\n const detail = err instanceof Error ? err.message : String(err);\n throw new UnsafePathError(candidate, `${label} crosses an unreadable or broken symlink: ${detail}`);\n }\n if (!isPathInside(rootReal, existingReal)) {\n throw new UnsafePathError(candidate, `${label} crosses a symlink outside its owning directory`);\n }\n return target;\n}\n\n/**\n * A relative file path in the portable spelling — forward slashes, no\n * redundant components. The brand's provenance is `portablePathOf` below: the\n * spelling checks there are the only way to construct one, so a value carrying\n * it has demonstrably passed them. Deliberately thin: it says nothing about\n * containment (that stays `resolveInside`'s question, asked per root), and it\n * is consumed by `resolvePortableInside` below — the one signature that\n * demands it — and every boundary (verify's evidence, unarchive's manifests)\n * reaches it through `resolvePortableFileInside`, the pair's one spelling.\n * The staging manifests keep their plain strings until the writer work that\n * owns those formats re-keys them.\n */\ndeclare const portable: unique symbol;\nexport type PortablePath = string & { readonly [portable]: true };\n\n/**\n * Snapshot manifests are portable data, so their path spelling is stricter\n * than an interactive path: forward slashes, no redundant components, and a\n * file (not directory) name at the end. The one constructor of `PortablePath`,\n * because these checks are what the brand asserts.\n */\nexport function portablePathOf(candidate: string, label = \"path\"): PortablePath {\n if (candidate.includes(\"\\\\\")) {\n throw new UnsafePathError(candidate, `${label} must use forward slashes`);\n }\n const parts = candidate.split(\"/\");\n if (parts.some((part) => part === \"\" || part === \".\" || part === \"..\")) {\n throw new UnsafePathError(candidate, `${label} must be a canonical relative file path`);\n }\n // The cast, on the line immediately after the checks that earn it.\n return candidate as PortablePath;\n}\n\n/**\n * Containment for a path that already EARNED the portable spelling — the one\n * signature that demands the brand, so `PortablePath` has a consumer the\n * compiler checks rather than a comment claiming the order of two calls.\n */\nfunction resolvePortableInside(root: string, portable: PortablePath, label: string): string {\n return resolveInside(root, portable, label);\n}\n\n/** The portable spelling checks, then `resolveInside`'s containment proof — the ONE spelling of the pair. */\nexport function resolvePortableFileInside(root: string, candidate: string, label = \"path\"): string {\n return resolvePortableInside(root, portablePathOf(candidate, label), label);\n}\n\n/**\n * The same portable spelling rules, with containment judged LEXICALLY — for a\n * path that names a file the docs repo itself mounts.\n *\n * Every spelling defence above is kept, and for its reason: a snapshot manifest\n * is loam's own file, but a corrupted or hostile one could carry `..` segments,\n * an absolute path or a `C:\\` spelling, and what comes out of this join is a\n * path a caller then goes and reads.\n *\n * What is deliberately not applied is the realpath test. Composing a docs repo\n * out of symlinks — a worktree, a submodule, one service's directory shared\n * between two checkouts — is a supported layout that every walk of the repo\n * FOLLOWS rather than refuses (repo.ts's `entryIs` says so in full), and that\n * `archive` already writes through: its living-document writes are planned from\n * `unfiledServicePaths()`/`servicePathsAt()` and never see this file. With the realpath test in force on\n * the manifest's `path` field, a `services/<svc>/` mounted that way made an\n * interrupted archive permanently unrepairable — the retry was told the\n * snapshot could not be read when it was perfectly intact, and the half-merge it\n * was the only record of could never be repaired.\n *\n * This is only ever safe on a READ. A symlink that leaves the repo is\n * lexically indistinguishable from a service directory the operator mounted on\n * purpose — `escape/owned.txt` and `services/<svc>/spec.md` are both \"inside\n * docsDir\" until something resolves them — so the realpath test is the only\n * check that can refuse the first, at the price of also refusing the second.\n * A caller that goes on to WRITE through the path must pay that price and use\n * `resolvePortableFileInside`; `unarchive` does, and says so where it does.\n * A caller that only reads a file and compares a digest it never discloses\n * gains nothing from refusing, and loses the operator's ability to repair an\n * interrupted archive.\n */\nexport function resolvePortableFileInsideLexically(root: string, candidate: string, label = \"path\"): string {\n if (candidate.length === 0) {\n throw new UnsafePathError(candidate, `${label} must not be empty`);\n }\n if (candidate.includes(\"\\\\\")) {\n throw new UnsafePathError(candidate, `${label} must use forward slashes`);\n }\n const parts = candidate.split(\"/\");\n if (parts.some((part) => part === \"\" || part === \".\" || part === \"..\")) {\n throw new UnsafePathError(candidate, `${label} must be a canonical relative file path`);\n }\n // win32.isAbsolute is intentional even on POSIX, for the reason `resolveInside`\n // gives: a snapshot travels with its feature directory between operating\n // systems, and `C:\\...` must never turn into an innocuous relative filename\n // merely because the question was asked on Linux.\n if (isAbsolute(candidate) || win32.isAbsolute(candidate)) {\n throw new UnsafePathError(candidate, `${label} must be relative to its owning directory`);\n }\n const rootAbs = resolve(root);\n const target = resolve(rootAbs, ...parts);\n // Already implied by the segment test above, and asked anyway: this line is\n // the containment claim itself, so a later loosening of the spelling rules\n // cannot quietly stop making it.\n if (!isPathInside(rootAbs, target)) {\n throw new UnsafePathError(candidate, `${label} resolves outside its owning directory`);\n }\n return target;\n}\n\n/** Find the closest lexically contained path that exists, including symlinks. */\nfunction nearestExisting(target: string, root: string): string {\n let current = target;\n for (;;) {\n try {\n lstatSync(current);\n return current;\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code !== \"ENOENT\" && code !== \"ENOTDIR\") throw err;\n }\n if (current === root) return root;\n const parent = dirname(current);\n if (parent === current || !isPathInside(root, parent)) return root;\n current = parent;\n }\n}\n"]}
1
+ {"version":3,"file":"path-safety.js","sourceRoot":"","sources":["../../../src/core/kernel/path-safety.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Resolve caller-controlled paths without letting them leave an owned tree.\n *\n * A lexical `relative()` check is not enough: an existing component may be a\n * symlink whose target lives elsewhere. Conversely, checking only the final\n * path misses destinations that do not exist yet but whose nearest existing\n * parent is a symlink. This helper checks both views before a caller reads,\n * writes, walks, or removes anything.\n */\nimport { lstatSync, realpathSync } from \"node:fs\";\nimport { dirname, isAbsolute, relative, resolve, sep, win32 } from \"node:path\";\n\nexport class UnsafePathError extends Error {\n constructor(\n readonly candidate: string,\n message: string,\n ) {\n super(message);\n this.name = \"UnsafePathError\";\n }\n}\n\n/** True when `target` is `root` or one of its descendants. */\nexport function isPathInside(root: string, target: string): boolean {\n const rel = relative(resolve(root), resolve(target));\n return rel === \"\" || (!isAbsolute(rel) && rel !== \"..\" && !rel.startsWith(`..${sep}`));\n}\n\n/**\n * Where `target` is once the directory `from` has been renamed to `to`: its\n * image under `to` when it is inside `from`, `target` itself when it is not.\n * Absolute paths in and out. The question every reader of a directory move\n * asks of one path: the link rewrites of both renames loam makes (`subsystem\n * move`, and `archive` for the feature's own documents), and the archive's\n * snapshot, crash repair and `unarchive`'s report, which meet those documents\n * on either side of the move.\n */\nexport function acrossRename(target: string, from: string, to: string): string {\n return isPathInside(from, target) ? resolve(to, relative(resolve(from), resolve(target))) : target;\n}\n\n/**\n * `path` in the spelling every other path here has: the Win32 file-namespace\n * prefix taken off a drive or UNC path (`\\\\?\\C:\\x` → `C:\\x`,\n * `\\\\?\\UNC\\host\\share\\x` → `\\\\host\\share\\x`), anything else unchanged.\n *\n * Measured 2026-10-21 on Node 24.13.0, Windows 11: `mkdir(dir, { recursive:\n * true })` — and `mkdirSync` — reports the first directory it created as\n * `\\\\?\\C:\\…\\features\\archive` for a `dir` of `C:\\…\\features\\archive`, at short\n * and long path lengths alike; `mkdtemp` and `realpath` return the plain\n * spelling. `resolve()` keeps the prefix and `relative()` between the two\n * spellings is an absolute path, so `isPathInside` judged\n * `C:\\…\\features\\archive` to lie outside `\\\\?\\C:\\…\\features\\archive` — and every\n * rollback that pruned \"up to the directory mkdir created\" pruned nothing,\n * leaving an empty `features/archive/` or `services/<new-service>/` behind at a\n * clean `merge-failed`. Linux never reports the prefix, so CI could not see it.\n *\n * Only the drive and UNC forms are taken off: `\\\\?\\Volume{…}\\` and\n * `\\\\?\\GLOBALROOT\\…` have no plain spelling, and stripping theirs would leave a\n * relative path. A POSIX absolute path starts with `/` and never matches.\n */\nexport function withoutNamespacePrefix(path: string): string {\n const unc = /^\\\\\\\\\\?\\\\UNC\\\\/i.exec(path);\n if (unc !== null) return `\\\\\\\\${path.slice(unc[0].length)}`;\n return /^\\\\\\\\\\?\\\\[A-Za-z]:(?:\\\\|$)/.test(path) ? path.slice(4) : path;\n}\n\n/**\n * Resolve a relative path beneath `root`, rejecting absolute paths, explicit\n * parent traversal, and symlink escape. The final path may be absent: its\n * nearest existing ancestor is checked in that case.\n */\nexport function resolveInside(root: string, candidate: string, label = \"path\"): string {\n if (candidate.length === 0) {\n throw new UnsafePathError(candidate, `${label} must not be empty`);\n }\n // win32.isAbsolute is intentional even on POSIX. Persisted/configured paths\n // may be moved between operating systems, and `C:\\\\...` must never turn into\n // an innocuous relative filename merely because validation ran on Linux.\n if (isAbsolute(candidate) || win32.isAbsolute(candidate)) {\n throw new UnsafePathError(candidate, `${label} must be relative to its owning directory`);\n }\n if (candidate.split(/[\\\\/]/).includes(\"..\")) {\n throw new UnsafePathError(candidate, `${label} must not contain '..' path segments`);\n }\n\n const rootAbs = resolve(root);\n const target = resolve(rootAbs, candidate);\n if (!isPathInside(rootAbs, target)) {\n throw new UnsafePathError(candidate, `${label} resolves outside its owning directory`);\n }\n\n let rootReal: string;\n let existing: string;\n try {\n rootReal = realpathSync(rootAbs);\n existing = nearestExisting(target, rootAbs);\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n throw new UnsafePathError(candidate, `${label} cannot be resolved safely: ${detail}`);\n }\n\n let existingReal: string;\n try {\n existingReal = realpathSync(existing);\n } catch (err) {\n // A broken symlink is an existing path according to lstat, but realpath\n // cannot prove where a later write through it would land. Refuse it.\n const detail = err instanceof Error ? err.message : String(err);\n throw new UnsafePathError(candidate, `${label} crosses an unreadable or broken symlink: ${detail}`);\n }\n if (!isPathInside(rootReal, existingReal)) {\n throw new UnsafePathError(candidate, `${label} crosses a symlink outside its owning directory`);\n }\n return target;\n}\n\n/**\n * A relative file path in the portable spelling — forward slashes, no\n * redundant components. The brand's provenance is `portablePathOf` below: the\n * spelling checks there are the only way to construct one, so a value carrying\n * it has demonstrably passed them. Deliberately thin: it says nothing about\n * containment (that stays `resolveInside`'s question, asked per root), and it\n * is consumed by `resolvePortableInside` below — the one signature that\n * demands it — and every boundary (verify's evidence, unarchive's manifests)\n * reaches it through `resolvePortableFileInside`, the pair's one spelling.\n * The staging manifests keep their plain strings until the writer work that\n * owns those formats re-keys them.\n */\ndeclare const portable: unique symbol;\nexport type PortablePath = string & { readonly [portable]: true };\n\n/**\n * Snapshot manifests are portable data, so their path spelling is stricter\n * than an interactive path: forward slashes, no redundant components, and a\n * file (not directory) name at the end. The one constructor of `PortablePath`,\n * because these checks are what the brand asserts.\n */\nexport function portablePathOf(candidate: string, label = \"path\"): PortablePath {\n if (candidate.includes(\"\\\\\")) {\n throw new UnsafePathError(candidate, `${label} must use forward slashes`);\n }\n const parts = candidate.split(\"/\");\n if (parts.some((part) => part === \"\" || part === \".\" || part === \"..\")) {\n throw new UnsafePathError(candidate, `${label} must be a canonical relative file path`);\n }\n // The cast, on the line immediately after the checks that earn it.\n return candidate as PortablePath;\n}\n\n/**\n * Containment for a path that already EARNED the portable spelling — the one\n * signature that demands the brand, so `PortablePath` has a consumer the\n * compiler checks rather than a comment claiming the order of two calls.\n */\nfunction resolvePortableInside(root: string, portable: PortablePath, label: string): string {\n return resolveInside(root, portable, label);\n}\n\n/** The portable spelling checks, then `resolveInside`'s containment proof — the ONE spelling of the pair. */\nexport function resolvePortableFileInside(root: string, candidate: string, label = \"path\"): string {\n return resolvePortableInside(root, portablePathOf(candidate, label), label);\n}\n\n/**\n * The same portable spelling rules, with containment judged LEXICALLY — for a\n * path that names a file the docs repo itself mounts.\n *\n * Every spelling defence above is kept, and for its reason: a snapshot manifest\n * is loam's own file, but a corrupted or hostile one could carry `..` segments,\n * an absolute path or a `C:\\` spelling, and what comes out of this join is a\n * path a caller then goes and reads.\n *\n * What is deliberately not applied is the realpath test. Composing a docs repo\n * out of symlinks — a worktree, a submodule, one service's directory shared\n * between two checkouts — is a supported layout that every walk of the repo\n * FOLLOWS rather than refuses (repo.ts's `entryIs` says so in full), and that\n * `archive` already writes through: its living-document writes are planned from\n * `unfiledServicePaths()`/`servicePathsAt()` and never see this file. With the realpath test in force on\n * the manifest's `path` field, a `services/<svc>/` mounted that way made an\n * interrupted archive permanently unrepairable — the retry was told the\n * snapshot could not be read when it was perfectly intact, and the half-merge it\n * was the only record of could never be repaired.\n *\n * This is only ever safe on a READ. A symlink that leaves the repo is\n * lexically indistinguishable from a service directory the operator mounted on\n * purpose — `escape/owned.txt` and `services/<svc>/spec.md` are both \"inside\n * docsDir\" until something resolves them — so the realpath test is the only\n * check that can refuse the first, at the price of also refusing the second.\n * A caller that goes on to WRITE through the path must pay that price and use\n * `resolvePortableFileInside`; `unarchive` does, and says so where it does.\n * A caller that only reads a file and compares a digest it never discloses\n * gains nothing from refusing, and loses the operator's ability to repair an\n * interrupted archive.\n */\nexport function resolvePortableFileInsideLexically(root: string, candidate: string, label = \"path\"): string {\n if (candidate.length === 0) {\n throw new UnsafePathError(candidate, `${label} must not be empty`);\n }\n if (candidate.includes(\"\\\\\")) {\n throw new UnsafePathError(candidate, `${label} must use forward slashes`);\n }\n const parts = candidate.split(\"/\");\n if (parts.some((part) => part === \"\" || part === \".\" || part === \"..\")) {\n throw new UnsafePathError(candidate, `${label} must be a canonical relative file path`);\n }\n // win32.isAbsolute is intentional even on POSIX, for the reason `resolveInside`\n // gives: a snapshot travels with its feature directory between operating\n // systems, and `C:\\...` must never turn into an innocuous relative filename\n // merely because the question was asked on Linux.\n if (isAbsolute(candidate) || win32.isAbsolute(candidate)) {\n throw new UnsafePathError(candidate, `${label} must be relative to its owning directory`);\n }\n const rootAbs = resolve(root);\n const target = resolve(rootAbs, ...parts);\n // Already implied by the segment test above, and asked anyway: this line is\n // the containment claim itself, so a later loosening of the spelling rules\n // cannot quietly stop making it.\n if (!isPathInside(rootAbs, target)) {\n throw new UnsafePathError(candidate, `${label} resolves outside its owning directory`);\n }\n return target;\n}\n\n/** Find the closest lexically contained path that exists, including symlinks. */\nfunction nearestExisting(target: string, root: string): string {\n let current = target;\n for (;;) {\n try {\n lstatSync(current);\n return current;\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code !== \"ENOENT\" && code !== \"ENOTDIR\") throw err;\n }\n if (current === root) return root;\n const parent = dirname(current);\n if (parent === current || !isPathInside(root, parent)) return root;\n current = parent;\n }\n}\n"]}
@@ -16,6 +16,17 @@ export function isRecord(value) {
16
16
  export function asRecord(value) {
17
17
  return isRecord(value) ? value : null;
18
18
  }
19
+ /**
20
+ * A list of strings. `Array.isArray` narrows an `unknown` to `any[]`, so an
21
+ * element check written below it proved nothing to tsc and the elements had to
22
+ * be asserted back to `string` at the point of use — how a checked element
23
+ * type went on being asserted back in a line later. Deciding both halves in
24
+ * one predicate makes the narrowing carry the check. (It was written twice,
25
+ * once in the OpenSpec scan and once in the migration's mapping reader.)
26
+ */
27
+ export function isStringArray(value) {
28
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
29
+ }
19
30
  /**
20
31
  * A map whose keys come from a document somebody else wrote.
21
32
  *
@@ -1 +1 @@
1
- {"version":3,"file":"records.js","sourceRoot":"","sources":["../../../src/core/kernel/records.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,+DAA+D;AAC/D,MAAM,UAAU,QAAQ,CAAC,KAAc;IACrC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,QAAQ,CAAC,KAAc;IACrC,OAAO,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;AACxC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,UAAU;IACxB,OAAO,MAAM,CAAC,MAAM,CAAC,IAAI,CAAsB,CAAC;AAClD,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,QAAQ,CAAI,MAAyB,EAAE,GAAW;IAChE,OAAO,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AACrF,CAAC","sourcesContent":["/**\n * The shapes an unknown parsed document is asked about, in one place.\n *\n * Every YAML and JSON file loam reads arrives as `unknown`, and every reader\n * has to answer the same first question before it can look at a key: is this an\n * object at all? Six modules each carried their own copy of the answer, two of\n * them spelled with a cast — and a cast is exactly what this test exists to\n * make unnecessary, since the predicate form hands the caller a narrowed type\n * that tsc keeps checking. A leaf with no imports, so any layer can ask.\n */\n\n/** An object with string keys — not null, and not an array. */\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/** The same question where the answer is used rather than branched on. */\nexport function asRecord(value: unknown): Record<string, unknown> | null {\n return isRecord(value) ? value : null;\n}\n\n/**\n * A map whose keys come from a document somebody else wrote.\n *\n * `Object.create(null)` rather than `{}` because a key read out of untrusted\n * YAML can be `__proto__` or `constructor`, and on a normal object those either\n * mutate the prototype or read back a function that was never in the file.\n */\nexport function dictionary<T>(): Record<string, T> {\n return Object.create(null) as Record<string, T>;\n}\n\n/** The value a record actually holds for `key`, never one it inherited. */\nexport function ownValue<T>(record: Record<string, T>, key: string): T | undefined {\n return Object.prototype.hasOwnProperty.call(record, key) ? record[key] : undefined;\n}\n"]}
1
+ {"version":3,"file":"records.js","sourceRoot":"","sources":["../../../src/core/kernel/records.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,+DAA+D;AAC/D,MAAM,UAAU,QAAQ,CAAC,KAAc;IACrC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,QAAQ,CAAC,KAAc;IACrC,OAAO,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;AACxC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,KAAc;IAC1C,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC;AACjF,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,UAAU;IACxB,OAAO,MAAM,CAAC,MAAM,CAAC,IAAI,CAAsB,CAAC;AAClD,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,QAAQ,CAAI,MAAyB,EAAE,GAAW;IAChE,OAAO,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AACrF,CAAC","sourcesContent":["/**\n * The shapes an unknown parsed document is asked about, in one place.\n *\n * Every YAML and JSON file loam reads arrives as `unknown`, and every reader\n * has to answer the same first question before it can look at a key: is this an\n * object at all? Six modules each carried their own copy of the answer, two of\n * them spelled with a cast — and a cast is exactly what this test exists to\n * make unnecessary, since the predicate form hands the caller a narrowed type\n * that tsc keeps checking. A leaf with no imports, so any layer can ask.\n */\n\n/** An object with string keys — not null, and not an array. */\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/** The same question where the answer is used rather than branched on. */\nexport function asRecord(value: unknown): Record<string, unknown> | null {\n return isRecord(value) ? value : null;\n}\n\n/**\n * A list of strings. `Array.isArray` narrows an `unknown` to `any[]`, so an\n * element check written below it proved nothing to tsc and the elements had to\n * be asserted back to `string` at the point of use — how a checked element\n * type went on being asserted back in a line later. Deciding both halves in\n * one predicate makes the narrowing carry the check. (It was written twice,\n * once in the OpenSpec scan and once in the migration's mapping reader.)\n */\nexport function isStringArray(value: unknown): value is string[] {\n return Array.isArray(value) && value.every((item) => typeof item === \"string\");\n}\n\n/**\n * A map whose keys come from a document somebody else wrote.\n *\n * `Object.create(null)` rather than `{}` because a key read out of untrusted\n * YAML can be `__proto__` or `constructor`, and on a normal object those either\n * mutate the prototype or read back a function that was never in the file.\n */\nexport function dictionary<T>(): Record<string, T> {\n return Object.create(null) as Record<string, T>;\n}\n\n/** The value a record actually holds for `key`, never one it inherited. */\nexport function ownValue<T>(record: Record<string, T>, key: string): T | undefined {\n return Object.prototype.hasOwnProperty.call(record, key) ? record[key] : undefined;\n}\n"]}
@@ -20,11 +20,9 @@
20
20
  * file already earns `link.unreadable`; `loam list` has no such finding, so it
21
21
  * needs the list to say what it did not read.
22
22
  */
23
- import { readFile } from "node:fs/promises";
24
23
  import { existsSync } from "node:fs";
25
24
  import { resolve } from "node:path";
26
25
  import { repoPath } from "../envelope/json.js";
27
- import { decodeDocument } from "../kernel/document-bytes.js";
28
26
  import { pathCaseIndex } from "./case.js";
29
27
  import { allAuthoredDocuments } from "./corpus.js";
30
28
  import { resolveLinks } from "./findings.js";
@@ -44,7 +42,7 @@ export async function backlinkIndex(docsDir, fleet) {
44
42
  continue;
45
43
  let text;
46
44
  try {
47
- text = fleet === undefined ? decodeDocument(await readFile(path), path) : await fleet.readText(path);
45
+ text = await fleet.readText(path);
48
46
  }
49
47
  catch {
50
48
  unreadable.push(repoPath(docsDir, path));
@@ -1 +1 @@
1
- {"version":3,"file":"backlinks.js","sourceRoot":"","sources":["../../../src/core/links/backlinks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAE/C,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAE7D,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAS7C;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,OAAgB,EAAE,KAAoB;IACxE,MAAM,OAAO,GAAG,IAAI,GAAG,EAAuB,CAAC;IAC/C,MAAM,UAAU,GAAa,EAAE,CAAC;IAChC,MAAM,KAAK,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,EAAE,CAAC;IAClD,KAAK,MAAM,IAAI,IAAI,MAAM,oBAAoB,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,CAAC;QAC9D,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,SAAS;QAChC,IAAI,IAAY,CAAC;QACjB,IAAI,CAAC;YACH,IAAI,GAAG,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,cAAc,CAAC,MAAM,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACvG,CAAC;QAAC,MAAM,CAAC;YACP,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC;YACzC,SAAS;QACX,CAAC;QACD,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QACrC,KAAK,MAAM,EAAE,MAAM,EAAE,IAAI,YAAY,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,KAAK,CAAC,CAAC,QAAQ,EAAE,CAAC;YACtE,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;YAC5B,IAAI,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAC/B,IAAI,OAAO,KAAK,SAAS;gBAAE,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,OAAO,GAAG,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC;YACnE,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACpB,CAAC;IACH,CAAC;IACD,UAAU,CAAC,IAAI,EAAE,CAAC;IAClB,OAAO;QACL,SAAS,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE;QACvE,UAAU;KACX,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Who links to what, across the whole repository — the link graph read\n * backwards.\n *\n * `link.unresolved` asks each document about its own links. This asks the\n * opposite question, and it is the one the glossary is built on: \"which\n * documents use this term\" is exactly \"which documents link to `glossary/<term>.md`\",\n * and no single target can answer it. A term is cited from a service's spec, a\n * capability document and a feature's intent, so the index is fleet-scope by\n * construction.\n *\n * IT IS THE SAME WALK AS THE FINDING, deliberately — `resolveLinks` produces\n * both halves of one answer, and the corpus is `core/links/corpus.ts`'s single\n * definition. An index built from a second enumeration would report a term as\n * unused while the document citing it was being graded one target over.\n *\n * AN UNREADABLE DOCUMENT IS RETURNED, not swallowed. A term can only be shown as\n * cited by nobody if loam actually read everybody, and a UTF-16 spec.md would\n * otherwise turn into a silently missing citation. Under `validate` the same\n * file already earns `link.unreadable`; `loam list` has no such finding, so it\n * needs the list to say what it did not read.\n */\nimport { readFile } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { repoPath } from \"../envelope/json.js\";\nimport type { FleetContext } from \"../fleet-context.js\";\nimport { decodeDocument } from \"../kernel/document-bytes.js\";\nimport type { DocsDir } from \"../kernel/ids/dirs.js\";\nimport { pathCaseIndex } from \"./case.js\";\nimport { allAuthoredDocuments } from \"./corpus.js\";\nimport { resolveLinks } from \"./findings.js\";\n\nexport interface BacklinkIndex {\n /** Repo-relative paths of the documents linking to `target`, ordered, without duplicates. */\n linkersOf(target: string): string[];\n /** Repo-relative paths of documents whose links could not be read at all. */\n unreadable: string[];\n}\n\n/**\n * Build the index over every authored document in the repository.\n *\n * One `pathCaseIndex` for the whole walk, because this one really is fleet-wide\n * and the same directories are asked about hundreds of times. Still not a\n * module-level cache: it lives exactly as long as this call.\n */\nexport async function backlinkIndex(docsDir: DocsDir, fleet?: FleetContext): Promise<BacklinkIndex> {\n const linkers = new Map<string, Set<string>>();\n const unreadable: string[] = [];\n const scope = { docsDir, cases: pathCaseIndex() };\n for (const path of await allAuthoredDocuments(docsDir, fleet)) {\n if (!existsSync(path)) continue;\n let text: string;\n try {\n text = fleet === undefined ? decodeDocument(await readFile(path), path) : await fleet.readText(path);\n } catch {\n unreadable.push(repoPath(docsDir, path));\n continue;\n }\n const from = repoPath(docsDir, path);\n for (const { target } of resolveLinks({ path, text }, scope).resolved) {\n const key = resolve(target);\n let sources = linkers.get(key);\n if (sources === undefined) linkers.set(key, (sources = new Set()));\n sources.add(from);\n }\n }\n unreadable.sort();\n return {\n linkersOf: (target) => [...(linkers.get(resolve(target)) ?? [])].sort(),\n unreadable,\n };\n}\n"]}
1
+ {"version":3,"file":"backlinks.js","sourceRoot":"","sources":["../../../src/core/links/backlinks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAG/C,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAS7C;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,OAAgB,EAAE,KAAmB;IACvE,MAAM,OAAO,GAAG,IAAI,GAAG,EAAuB,CAAC;IAC/C,MAAM,UAAU,GAAa,EAAE,CAAC;IAChC,MAAM,KAAK,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,EAAE,CAAC;IAClD,KAAK,MAAM,IAAI,IAAI,MAAM,oBAAoB,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,CAAC;QAC9D,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,SAAS;QAChC,IAAI,IAAY,CAAC;QACjB,IAAI,CAAC;YACH,IAAI,GAAG,MAAM,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACpC,CAAC;QAAC,MAAM,CAAC;YACP,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC;YACzC,SAAS;QACX,CAAC;QACD,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QACrC,KAAK,MAAM,EAAE,MAAM,EAAE,IAAI,YAAY,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,KAAK,CAAC,CAAC,QAAQ,EAAE,CAAC;YACtE,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;YAC5B,IAAI,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAC/B,IAAI,OAAO,KAAK,SAAS;gBAAE,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,OAAO,GAAG,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC;YACnE,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACpB,CAAC;IACH,CAAC;IACD,UAAU,CAAC,IAAI,EAAE,CAAC;IAClB,OAAO;QACL,SAAS,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE;QACvE,UAAU;KACX,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Who links to what, across the whole repository — the link graph read\n * backwards.\n *\n * `link.unresolved` asks each document about its own links. This asks the\n * opposite question, and it is the one the glossary is built on: \"which\n * documents use this term\" is exactly \"which documents link to `glossary/<term>.md`\",\n * and no single target can answer it. A term is cited from a service's spec, a\n * capability document and a feature's intent, so the index is fleet-scope by\n * construction.\n *\n * IT IS THE SAME WALK AS THE FINDING, deliberately — `resolveLinks` produces\n * both halves of one answer, and the corpus is `core/links/corpus.ts`'s single\n * definition. An index built from a second enumeration would report a term as\n * unused while the document citing it was being graded one target over.\n *\n * AN UNREADABLE DOCUMENT IS RETURNED, not swallowed. A term can only be shown as\n * cited by nobody if loam actually read everybody, and a UTF-16 spec.md would\n * otherwise turn into a silently missing citation. Under `validate` the same\n * file already earns `link.unreadable`; `loam list` has no such finding, so it\n * needs the list to say what it did not read.\n */\nimport { existsSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { repoPath } from \"../envelope/json.js\";\nimport type { FleetContext } from \"../fleet-context.js\";\nimport type { DocsDir } from \"../kernel/ids/dirs.js\";\nimport { pathCaseIndex } from \"./case.js\";\nimport { allAuthoredDocuments } from \"./corpus.js\";\nimport { resolveLinks } from \"./findings.js\";\n\nexport interface BacklinkIndex {\n /** Repo-relative paths of the documents linking to `target`, ordered, without duplicates. */\n linkersOf(target: string): string[];\n /** Repo-relative paths of documents whose links could not be read at all. */\n unreadable: string[];\n}\n\n/**\n * Build the index over every authored document in the repository.\n *\n * One `pathCaseIndex` for the whole walk, because this one really is fleet-wide\n * and the same directories are asked about hundreds of times. Still not a\n * module-level cache: it lives exactly as long as this call.\n */\nexport async function backlinkIndex(docsDir: DocsDir, fleet: FleetContext): Promise<BacklinkIndex> {\n const linkers = new Map<string, Set<string>>();\n const unreadable: string[] = [];\n const scope = { docsDir, cases: pathCaseIndex() };\n for (const path of await allAuthoredDocuments(docsDir, fleet)) {\n if (!existsSync(path)) continue;\n let text: string;\n try {\n text = await fleet.readText(path);\n } catch {\n unreadable.push(repoPath(docsDir, path));\n continue;\n }\n const from = repoPath(docsDir, path);\n for (const { target } of resolveLinks({ path, text }, scope).resolved) {\n const key = resolve(target);\n let sources = linkers.get(key);\n if (sources === undefined) linkers.set(key, (sources = new Set()));\n sources.add(from);\n }\n }\n unreadable.sort();\n return {\n linkersOf: (target) => [...(linkers.get(resolve(target)) ?? [])].sort(),\n unreadable,\n };\n}\n"]}