@ybotok/loam 0.1.0-beta.3 → 0.2.0-alpha.2

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 (1036) hide show
  1. package/CHANGELOG.md +2635 -86
  2. package/COMPARISON.md +80 -63
  3. package/CONTRIBUTING.md +84 -0
  4. package/MIGRATING-from-OpenSpec.md +213 -54
  5. package/QUICKSTART.md +125 -0
  6. package/README.md +450 -150
  7. package/ROADMAP.md +599 -0
  8. package/SCHEMA.md +2355 -256
  9. package/SECURITY.md +21 -3
  10. package/WORKFLOW.md +701 -0
  11. package/dist/cli.js +113 -19
  12. package/dist/cli.js.map +1 -1
  13. package/dist/commands/adopt/adopt.js +209 -0
  14. package/dist/commands/adopt/adopt.js.map +1 -0
  15. package/dist/commands/adopt/render.js +268 -0
  16. package/dist/commands/adopt/render.js.map +1 -0
  17. package/dist/commands/archive/archive.js +77 -0
  18. package/dist/commands/archive/archive.js.map +1 -0
  19. package/dist/commands/archive/plan/contracts/asyncapi.js +178 -0
  20. package/dist/commands/archive/plan/contracts/asyncapi.js.map +1 -0
  21. package/dist/commands/archive/plan/contracts/openapi.js +159 -0
  22. package/dist/commands/archive/plan/contracts/openapi.js.map +1 -0
  23. package/dist/commands/archive/plan/gate.js +323 -0
  24. package/dist/commands/archive/plan/gate.js.map +1 -0
  25. package/dist/commands/archive/plan/landscape.js +180 -0
  26. package/dist/commands/archive/plan/landscape.js.map +1 -0
  27. package/dist/commands/archive/plan/refusal.js +111 -0
  28. package/dist/commands/archive/plan/refusal.js.map +1 -0
  29. package/dist/commands/archive/plan/specs.js +237 -0
  30. package/dist/commands/archive/plan/specs.js.map +1 -0
  31. package/dist/commands/archive/plan/state.js +27 -0
  32. package/dist/commands/archive/plan/state.js.map +1 -0
  33. package/dist/commands/archive/run.js +252 -0
  34. package/dist/commands/archive/run.js.map +1 -0
  35. package/dist/commands/context/context.js +99 -0
  36. package/dist/commands/context/context.js.map +1 -0
  37. package/dist/commands/context/print.js +283 -0
  38. package/dist/commands/context/print.js.map +1 -0
  39. package/dist/commands/delta/delta.js +279 -0
  40. package/dist/commands/delta/delta.js.map +1 -0
  41. package/dist/commands/delta/print.js +205 -0
  42. package/dist/commands/delta/print.js.map +1 -0
  43. package/dist/commands/dependencies.js +9 -7
  44. package/dist/commands/dependencies.js.map +1 -1
  45. package/dist/commands/diff/diff.js +93 -0
  46. package/dist/commands/diff/diff.js.map +1 -0
  47. package/dist/commands/diff/print.js +38 -0
  48. package/dist/commands/diff/print.js.map +1 -0
  49. package/dist/commands/doctor.js +5 -4
  50. package/dist/commands/doctor.js.map +1 -1
  51. package/dist/commands/explain/explain.js +151 -0
  52. package/dist/commands/explain/explain.js.map +1 -0
  53. package/dist/commands/explore.js +29 -24
  54. package/dist/commands/explore.js.map +1 -1
  55. package/dist/commands/gate/gate.js +115 -0
  56. package/dist/commands/gate/gate.js.map +1 -0
  57. package/dist/commands/gate/print.js +54 -0
  58. package/dist/commands/gate/print.js.map +1 -0
  59. package/dist/commands/gherkin/commit.js +146 -0
  60. package/dist/commands/gherkin/commit.js.map +1 -0
  61. package/dist/commands/gherkin/gherkin.js +246 -0
  62. package/dist/commands/gherkin/gherkin.js.map +1 -0
  63. package/dist/commands/gherkin/reconcile.js +149 -0
  64. package/dist/commands/gherkin/reconcile.js.map +1 -0
  65. package/dist/commands/gherkin/render.js +77 -0
  66. package/dist/commands/gherkin/render.js.map +1 -0
  67. package/dist/commands/init/example.js +204 -0
  68. package/dist/commands/init/example.js.map +1 -0
  69. package/dist/commands/init/first-hour.js +28 -0
  70. package/dist/commands/init/first-hour.js.map +1 -0
  71. package/dist/commands/{init.js → init/init.js} +146 -71
  72. package/dist/commands/init/init.js.map +1 -0
  73. package/dist/commands/init/mcp.js +131 -0
  74. package/dist/commands/init/mcp.js.map +1 -0
  75. package/dist/commands/init/options.js +78 -0
  76. package/dist/commands/init/options.js.map +1 -0
  77. package/dist/commands/instructions.js +72 -14
  78. package/dist/commands/instructions.js.map +1 -1
  79. package/dist/commands/list/campaign/campaign.js +65 -0
  80. package/dist/commands/list/campaign/campaign.js.map +1 -0
  81. package/dist/commands/list/campaign/owners.js +65 -0
  82. package/dist/commands/list/campaign/owners.js.map +1 -0
  83. package/dist/commands/list/json.js +157 -0
  84. package/dist/commands/list/json.js.map +1 -0
  85. package/dist/commands/list/list.js +343 -0
  86. package/dist/commands/list/list.js.map +1 -0
  87. package/dist/commands/list/print.js +301 -0
  88. package/dist/commands/list/print.js.map +1 -0
  89. package/dist/commands/list/review.js +120 -0
  90. package/dist/commands/list/review.js.map +1 -0
  91. package/dist/commands/list/views.js +105 -0
  92. package/dist/commands/list/views.js.map +1 -0
  93. package/dist/commands/mcp/dispatch.js +183 -0
  94. package/dist/commands/mcp/dispatch.js.map +1 -0
  95. package/dist/commands/mcp/mcp.js +34 -0
  96. package/dist/commands/mcp/mcp.js.map +1 -0
  97. package/dist/commands/mcp/serve.js +55 -0
  98. package/dist/commands/mcp/serve.js.map +1 -0
  99. package/dist/commands/migrate-openspec/materialize/active.js +170 -0
  100. package/dist/commands/migrate-openspec/materialize/active.js.map +1 -0
  101. package/dist/commands/migrate-openspec/materialize/feature.js +41 -0
  102. package/dist/commands/migrate-openspec/materialize/feature.js.map +1 -0
  103. package/dist/commands/migrate-openspec/materialize/living.js +62 -0
  104. package/dist/commands/migrate-openspec/materialize/living.js.map +1 -0
  105. package/dist/commands/migrate-openspec/materialize/stage.js +142 -0
  106. package/dist/commands/migrate-openspec/materialize/stage.js.map +1 -0
  107. package/dist/commands/migrate-openspec/materialize/target.js +219 -0
  108. package/dist/commands/migrate-openspec/materialize/target.js.map +1 -0
  109. package/dist/commands/migrate-openspec/migrate-openspec.js +142 -0
  110. package/dist/commands/migrate-openspec/migrate-openspec.js.map +1 -0
  111. package/dist/commands/migrate-openspec/openspec/decisions.js +91 -0
  112. package/dist/commands/migrate-openspec/openspec/decisions.js.map +1 -0
  113. package/dist/commands/migrate-openspec/openspec/error.js +19 -0
  114. package/dist/commands/migrate-openspec/openspec/error.js.map +1 -0
  115. package/dist/commands/migrate-openspec/openspec/mapping.js +170 -0
  116. package/dist/commands/migrate-openspec/openspec/mapping.js.map +1 -0
  117. package/dist/commands/migrate-openspec/openspec/paths.js +43 -0
  118. package/dist/commands/migrate-openspec/openspec/paths.js.map +1 -0
  119. package/dist/commands/migrate-openspec/openspec/read.js +49 -0
  120. package/dist/commands/migrate-openspec/openspec/read.js.map +1 -0
  121. package/dist/commands/migrate-openspec/print.js +89 -0
  122. package/dist/commands/migrate-openspec/print.js.map +1 -0
  123. package/dist/commands/new/commit.js +76 -0
  124. package/dist/commands/new/commit.js.map +1 -0
  125. package/dist/commands/new/new.js +283 -0
  126. package/dist/commands/new/new.js.map +1 -0
  127. package/dist/commands/new/templates.js +355 -0
  128. package/dist/commands/new/templates.js.map +1 -0
  129. package/dist/commands/open.js +203 -0
  130. package/dist/commands/open.js.map +1 -0
  131. package/dist/commands/policy/format.js +152 -0
  132. package/dist/commands/policy/format.js.map +1 -0
  133. package/dist/commands/{docs-repo-gate.js → policy/gate.js} +11 -11
  134. package/dist/commands/policy/gate.js.map +1 -0
  135. package/dist/commands/rebase/edit.js +106 -0
  136. package/dist/commands/rebase/edit.js.map +1 -0
  137. package/dist/commands/rebase/living.js +185 -0
  138. package/dist/commands/rebase/living.js.map +1 -0
  139. package/dist/commands/rebase/plan.js +245 -0
  140. package/dist/commands/rebase/plan.js.map +1 -0
  141. package/dist/commands/rebase/rebase.js +275 -0
  142. package/dist/commands/rebase/rebase.js.map +1 -0
  143. package/dist/commands/seed/commit.js +192 -0
  144. package/dist/commands/seed/commit.js.map +1 -0
  145. package/dist/commands/seed/plan.js +133 -0
  146. package/dist/commands/seed/plan.js.map +1 -0
  147. package/dist/commands/seed/seed.js +172 -0
  148. package/dist/commands/seed/seed.js.map +1 -0
  149. package/dist/commands/show/feature.js +178 -0
  150. package/dist/commands/show/feature.js.map +1 -0
  151. package/dist/commands/show/marks.js +24 -0
  152. package/dist/commands/show/marks.js.map +1 -0
  153. package/dist/commands/{show.js → show/service.js} +63 -152
  154. package/dist/commands/show/service.js.map +1 -0
  155. package/dist/commands/show/show.js +77 -0
  156. package/dist/commands/show/show.js.map +1 -0
  157. package/dist/commands/status/json.js +45 -0
  158. package/dist/commands/status/json.js.map +1 -0
  159. package/dist/commands/status/print.js +157 -0
  160. package/dist/commands/status/print.js.map +1 -0
  161. package/dist/commands/status/status.js +119 -0
  162. package/dist/commands/status/status.js.map +1 -0
  163. package/dist/commands/steps/steps.js +189 -0
  164. package/dist/commands/steps/steps.js.map +1 -0
  165. package/dist/commands/subsystem/edit.js +192 -0
  166. package/dist/commands/subsystem/edit.js.map +1 -0
  167. package/dist/commands/subsystem/move.js +265 -0
  168. package/dist/commands/subsystem/move.js.map +1 -0
  169. package/dist/commands/subsystem/query.js +115 -0
  170. package/dist/commands/subsystem/query.js.map +1 -0
  171. package/dist/commands/subsystem/subsystem.js +101 -0
  172. package/dist/commands/subsystem/subsystem.js.map +1 -0
  173. package/dist/commands/subsystem/sync.js +103 -0
  174. package/dist/commands/subsystem/sync.js.map +1 -0
  175. package/dist/commands/subsystem/txn/txn.js +94 -0
  176. package/dist/commands/subsystem/txn/txn.js.map +1 -0
  177. package/dist/commands/subsystem/txn/views.js +70 -0
  178. package/dist/commands/subsystem/txn/views.js.map +1 -0
  179. package/dist/commands/unarchive/manifest.js +160 -0
  180. package/dist/commands/unarchive/manifest.js.map +1 -0
  181. package/dist/commands/{unarchive.js → unarchive/restore.js} +47 -160
  182. package/dist/commands/unarchive/restore.js.map +1 -0
  183. package/dist/commands/unarchive/unarchive.js +67 -0
  184. package/dist/commands/unarchive/unarchive.js.map +1 -0
  185. package/dist/commands/validate/arch-coverage.js +204 -0
  186. package/dist/commands/validate/arch-coverage.js.map +1 -0
  187. package/dist/commands/validate/checks/fleet-shape.js +296 -0
  188. package/dist/commands/validate/checks/fleet-shape.js.map +1 -0
  189. package/dist/commands/validate/checks/health-deps.js +55 -0
  190. package/dist/commands/validate/checks/health-deps.js.map +1 -0
  191. package/dist/commands/validate/checks/requirements.js +254 -0
  192. package/dist/commands/validate/checks/requirements.js.map +1 -0
  193. package/dist/commands/validate/checks/sources.js +76 -0
  194. package/dist/commands/validate/checks/sources.js.map +1 -0
  195. package/dist/commands/validate/checks/vocabulary.js +31 -0
  196. package/dist/commands/validate/checks/vocabulary.js.map +1 -0
  197. package/dist/commands/validate/feature.js +216 -0
  198. package/dist/commands/validate/feature.js.map +1 -0
  199. package/dist/commands/validate/fleet/census.js +69 -0
  200. package/dist/commands/validate/fleet/census.js.map +1 -0
  201. package/dist/commands/validate/fleet/kind-tags.js +39 -0
  202. package/dist/commands/validate/fleet/kind-tags.js.map +1 -0
  203. package/dist/commands/validate/fleet/landscape.js +353 -0
  204. package/dist/commands/validate/fleet/landscape.js.map +1 -0
  205. package/dist/commands/validate/fleet/load.js +52 -0
  206. package/dist/commands/validate/fleet/load.js.map +1 -0
  207. package/dist/commands/validate/fleet/obligations.js +270 -0
  208. package/dist/commands/validate/fleet/obligations.js.map +1 -0
  209. package/dist/commands/validate/fleet/scorecard/adoption.js +102 -0
  210. package/dist/commands/validate/fleet/scorecard/adoption.js.map +1 -0
  211. package/dist/commands/validate/fleet/scorecard/contracts.js +98 -0
  212. package/dist/commands/validate/fleet/scorecard/contracts.js.map +1 -0
  213. package/dist/commands/validate/fleet/scorecard/print.js +63 -0
  214. package/dist/commands/validate/fleet/scorecard/print.js.map +1 -0
  215. package/dist/commands/validate/fleet/scorecard/scorecard.js +156 -0
  216. package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -0
  217. package/dist/commands/validate/fleet/usecases/capability-tag.js +94 -0
  218. package/dist/commands/validate/fleet/usecases/capability-tag.js.map +1 -0
  219. package/dist/commands/validate/fleet/usecases/place.js +13 -0
  220. package/dist/commands/validate/fleet/usecases/place.js.map +1 -0
  221. package/dist/commands/validate/fleet/usecases/requirement-tag.js +95 -0
  222. package/dist/commands/validate/fleet/usecases/requirement-tag.js.map +1 -0
  223. package/dist/commands/validate/fleet/usecases/steps.js +197 -0
  224. package/dist/commands/validate/fleet/usecases/steps.js.map +1 -0
  225. package/dist/commands/validate/fleet/usecases/usecases.js +99 -0
  226. package/dist/commands/validate/fleet/usecases/usecases.js.map +1 -0
  227. package/dist/commands/validate/fleet/views/ids.js +40 -0
  228. package/dist/commands/validate/fleet/views/ids.js.map +1 -0
  229. package/dist/commands/validate/fleet/views/stale.js +59 -0
  230. package/dist/commands/validate/fleet/views/stale.js.map +1 -0
  231. package/dist/commands/validate/links/corpus.js +92 -0
  232. package/dist/commands/validate/links/corpus.js.map +1 -0
  233. package/dist/commands/validate/links/glossary.js +75 -0
  234. package/dist/commands/validate/links/glossary.js.map +1 -0
  235. package/dist/commands/validate/report.js +269 -0
  236. package/dist/commands/validate/report.js.map +1 -0
  237. package/dist/commands/validate/service/api.js +338 -0
  238. package/dist/commands/validate/service/api.js.map +1 -0
  239. package/dist/commands/validate/service/events/events.js +219 -0
  240. package/dist/commands/validate/service/events/events.js.map +1 -0
  241. package/dist/commands/validate/service/events/message-producers.js +77 -0
  242. package/dist/commands/validate/service/events/message-producers.js.map +1 -0
  243. package/dist/commands/validate/service/evidence-pins.js +131 -0
  244. package/dist/commands/validate/service/evidence-pins.js.map +1 -0
  245. package/dist/commands/validate/service/service.js +251 -0
  246. package/dist/commands/validate/service/service.js.map +1 -0
  247. package/dist/commands/validate/service/specs.js +251 -0
  248. package/dist/commands/validate/service/specs.js.map +1 -0
  249. package/dist/commands/validate/service/spine.js +113 -0
  250. package/dist/commands/validate/service/spine.js.map +1 -0
  251. package/dist/commands/validate/validate.js +338 -0
  252. package/dist/commands/validate/validate.js.map +1 -0
  253. package/dist/commands/verify/cross/diff.js +182 -0
  254. package/dist/commands/verify/cross/diff.js.map +1 -0
  255. package/dist/commands/verify/evidence/contract.js +32 -0
  256. package/dist/commands/verify/evidence/contract.js.map +1 -0
  257. package/dist/commands/verify/evidence/pins.js +52 -0
  258. package/dist/commands/verify/evidence/pins.js.map +1 -0
  259. package/dist/commands/verify/evidence/read.js +72 -0
  260. package/dist/commands/verify/evidence/read.js.map +1 -0
  261. package/dist/commands/verify/frozen.js +241 -0
  262. package/dist/commands/verify/frozen.js.map +1 -0
  263. package/dist/commands/verify/record.js +226 -0
  264. package/dist/commands/verify/record.js.map +1 -0
  265. package/dist/commands/verify/report.js +223 -0
  266. package/dist/commands/verify/report.js.map +1 -0
  267. package/dist/commands/verify/results.js +246 -0
  268. package/dist/commands/verify/results.js.map +1 -0
  269. package/dist/commands/verify/verify.js +217 -0
  270. package/dist/commands/verify/verify.js.map +1 -0
  271. package/dist/commands/vouch/contract.js +11 -0
  272. package/dist/commands/vouch/contract.js.map +1 -0
  273. package/dist/commands/vouch/pack/contract.js +2 -0
  274. package/dist/commands/vouch/pack/contract.js.map +1 -0
  275. package/dist/commands/vouch/pack/pack.js +266 -0
  276. package/dist/commands/vouch/pack/pack.js.map +1 -0
  277. package/dist/commands/vouch/pack/print.js +271 -0
  278. package/dist/commands/vouch/pack/print.js.map +1 -0
  279. package/dist/commands/vouch/pack/sections.js +142 -0
  280. package/dist/commands/vouch/pack/sections.js.map +1 -0
  281. package/dist/commands/vouch/prompt/confirm.js +46 -0
  282. package/dist/commands/vouch/prompt/confirm.js.map +1 -0
  283. package/dist/commands/vouch/report.js +101 -0
  284. package/dist/commands/vouch/report.js.map +1 -0
  285. package/dist/commands/vouch/run.js +285 -0
  286. package/dist/commands/vouch/run.js.map +1 -0
  287. package/dist/commands/vouch/sample/plan.js +125 -0
  288. package/dist/commands/vouch/sample/plan.js.map +1 -0
  289. package/dist/commands/vouch/sample/print.js +109 -0
  290. package/dist/commands/vouch/sample/print.js.map +1 -0
  291. package/dist/commands/vouch/vet/verify.js +123 -0
  292. package/dist/commands/vouch/vet/verify.js.map +1 -0
  293. package/dist/commands/vouch/vouch.js +176 -0
  294. package/dist/commands/vouch/vouch.js.map +1 -0
  295. package/dist/core/agent/agents-md/artifacts.js +220 -0
  296. package/dist/core/agent/agents-md/artifacts.js.map +1 -0
  297. package/dist/core/agent/agents-md/command-map.js +324 -0
  298. package/dist/core/agent/agents-md/command-map.js.map +1 -0
  299. package/dist/core/agent/agents-md/cycle.js +165 -0
  300. package/dist/core/agent/agents-md/cycle.js.map +1 -0
  301. package/dist/core/agent/agents-md/map/explain.js +34 -0
  302. package/dist/core/agent/agents-md/map/explain.js.map +1 -0
  303. package/dist/core/agent/agents-md/map/lenses/context.js +41 -0
  304. package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -0
  305. package/dist/core/agent/agents-md/map/lenses/diff.js +46 -0
  306. package/dist/core/agent/agents-md/map/lenses/diff.js.map +1 -0
  307. package/dist/core/agent/agents-md/map/lenses/gate.js +57 -0
  308. package/dist/core/agent/agents-md/map/lenses/gate.js.map +1 -0
  309. package/dist/core/agent/agents-md/map/mcp.js +41 -0
  310. package/dist/core/agent/agents-md/map/mcp.js.map +1 -0
  311. package/dist/core/agent/agents-md/map/status.js +62 -0
  312. package/dist/core/agent/agents-md/map/status.js.map +1 -0
  313. package/dist/core/agent/agents-md/map/subsystem.js +32 -0
  314. package/dist/core/agent/agents-md/map/subsystem.js.map +1 -0
  315. package/dist/core/agent/agents-md/map/usecases.js +60 -0
  316. package/dist/core/agent/agents-md/map/usecases.js.map +1 -0
  317. package/dist/core/agent/agents-md/refusals.js +246 -0
  318. package/dist/core/agent/agents-md/refusals.js.map +1 -0
  319. package/dist/core/agent/agents-md/shipped/archive-gate.js +98 -0
  320. package/dist/core/agent/agents-md/shipped/archive-gate.js.map +1 -0
  321. package/dist/core/agent/agents-md/spine.js +40 -0
  322. package/dist/core/agent/agents-md/spine.js.map +1 -0
  323. package/dist/core/agent/agents-md.js +170 -0
  324. package/dist/core/agent/agents-md.js.map +1 -0
  325. package/dist/core/{agents-stamp.js → agent/agents-stamp.js} +50 -0
  326. package/dist/core/agent/agents-stamp.js.map +1 -0
  327. package/dist/core/agent/contract.js +2 -0
  328. package/dist/core/agent/contract.js.map +1 -0
  329. package/dist/core/agent/protocol.js +193 -0
  330. package/dist/core/agent/protocol.js.map +1 -0
  331. package/dist/core/agent/scaffold.js +259 -0
  332. package/dist/core/agent/scaffold.js.map +1 -0
  333. package/dist/core/agent/support/report.js +103 -0
  334. package/dist/core/agent/support/report.js.map +1 -0
  335. package/dist/core/agent/tools/dialects.js +150 -0
  336. package/dist/core/agent/tools/dialects.js.map +1 -0
  337. package/dist/core/agent/tools/registry.js +228 -0
  338. package/dist/core/agent/tools/registry.js.map +1 -0
  339. package/dist/core/agent/workflows/adopt.js +133 -0
  340. package/dist/core/agent/workflows/adopt.js.map +1 -0
  341. package/dist/core/agent/workflows/check.js +337 -0
  342. package/dist/core/agent/workflows/check.js.map +1 -0
  343. package/dist/core/agent/workflows/closing.js +262 -0
  344. package/dist/core/agent/workflows/closing.js.map +1 -0
  345. package/dist/core/agent/workflows/feature.js +105 -0
  346. package/dist/core/agent/workflows/feature.js.map +1 -0
  347. package/dist/core/agent/workflows/implement.js +105 -0
  348. package/dist/core/agent/workflows/implement.js.map +1 -0
  349. package/dist/core/agent/workflows/reference/authoring.js +209 -0
  350. package/dist/core/agent/workflows/reference/authoring.js.map +1 -0
  351. package/dist/core/agent/workflows/reference/codes.js +65 -0
  352. package/dist/core/agent/workflows/reference/codes.js.map +1 -0
  353. package/dist/core/agent/workflows/reference/done-check.js +188 -0
  354. package/dist/core/agent/workflows/reference/done-check.js.map +1 -0
  355. package/dist/core/agent/workflows/reference/reference.js +16 -0
  356. package/dist/core/agent/workflows/reference/reference.js.map +1 -0
  357. package/dist/core/agent/workflows/reference/spine.js +297 -0
  358. package/dist/core/agent/workflows/reference/spine.js.map +1 -0
  359. package/dist/core/asyncapi/baseline/plan.js +110 -0
  360. package/dist/core/asyncapi/baseline/plan.js.map +1 -0
  361. package/dist/core/asyncapi/baseline/surfaces.js +54 -0
  362. package/dist/core/asyncapi/baseline/surfaces.js.map +1 -0
  363. package/dist/core/asyncapi/depth.js +164 -0
  364. package/dist/core/asyncapi/depth.js.map +1 -0
  365. package/dist/core/asyncapi/digest.js +141 -0
  366. package/dist/core/asyncapi/digest.js.map +1 -0
  367. package/dist/core/asyncapi/merge/components.js +91 -0
  368. package/dist/core/asyncapi/merge/components.js.map +1 -0
  369. package/dist/core/asyncapi/merge/error.js +33 -0
  370. package/dist/core/asyncapi/merge/error.js.map +1 -0
  371. package/dist/core/asyncapi/merge/markers.js +308 -0
  372. package/dist/core/asyncapi/merge/markers.js.map +1 -0
  373. package/dist/core/asyncapi/merge/merge.js +215 -0
  374. package/dist/core/asyncapi/merge/merge.js.map +1 -0
  375. package/dist/core/asyncapi/merge/pin.js +107 -0
  376. package/dist/core/asyncapi/merge/pin.js.map +1 -0
  377. package/dist/core/asyncapi/model.js +11 -0
  378. package/dist/core/asyncapi/model.js.map +1 -0
  379. package/dist/core/asyncapi/producers.js +50 -0
  380. package/dist/core/asyncapi/producers.js.map +1 -0
  381. package/dist/core/{asyncapi.js → asyncapi/read.js} +95 -109
  382. package/dist/core/asyncapi/read.js.map +1 -0
  383. package/dist/core/brief/brief.js +225 -0
  384. package/dist/core/brief/brief.js.map +1 -0
  385. package/dist/core/brief/checks.js +250 -0
  386. package/dist/core/brief/checks.js.map +1 -0
  387. package/dist/core/brief/landscape.js +194 -0
  388. package/dist/core/brief/landscape.js.map +1 -0
  389. package/dist/core/brief/targets.js +226 -0
  390. package/dist/core/brief/targets.js.map +1 -0
  391. package/dist/core/brief/unchecked.js +48 -0
  392. package/dist/core/brief/unchecked.js.map +1 -0
  393. package/dist/core/c4/arch.js +174 -0
  394. package/dist/core/c4/arch.js.map +1 -0
  395. package/dist/core/c4/likec4.js +90 -0
  396. package/dist/core/c4/likec4.js.map +1 -0
  397. package/dist/core/c4/parsed/deployment.js +158 -0
  398. package/dist/core/c4/parsed/deployment.js.map +1 -0
  399. package/dist/core/c4/parsed/dynamic-views.js +132 -0
  400. package/dist/core/c4/parsed/dynamic-views.js.map +1 -0
  401. package/dist/core/c4/parsed/specification.js +78 -0
  402. package/dist/core/c4/parsed/specification.js.map +1 -0
  403. package/dist/core/c4/parsed/values.js +57 -0
  404. package/dist/core/c4/parsed/values.js.map +1 -0
  405. package/dist/core/c4/parsed/view-ids.js +75 -0
  406. package/dist/core/c4/parsed/view-ids.js.map +1 -0
  407. package/dist/core/c4/project/architecture.js +45 -0
  408. package/dist/core/c4/project/architecture.js.map +1 -0
  409. package/dist/core/c4/project/documents.js +58 -0
  410. package/dist/core/c4/project/documents.js.map +1 -0
  411. package/dist/core/c4/project/load.js +187 -0
  412. package/dist/core/c4/project/load.js.map +1 -0
  413. package/dist/core/c4/project/staged.js +220 -0
  414. package/dist/core/c4/project/staged.js.map +1 -0
  415. package/dist/core/c4/resolve/service.js +159 -0
  416. package/dist/core/c4/resolve/service.js.map +1 -0
  417. package/dist/core/c4/resolve/steps.js +144 -0
  418. package/dist/core/c4/resolve/steps.js.map +1 -0
  419. package/dist/core/c4/seed/fleet-file.js +201 -0
  420. package/dist/core/c4/seed/fleet-file.js.map +1 -0
  421. package/dist/core/c4/seed/items.js +91 -0
  422. package/dist/core/c4/seed/items.js.map +1 -0
  423. package/dist/core/c4/seed/stamp.js +48 -0
  424. package/dist/core/c4/seed/stamp.js.map +1 -0
  425. package/dist/core/c4/seed/template.js +109 -0
  426. package/dist/core/c4/seed/template.js.map +1 -0
  427. package/dist/core/c4/source-mask.js +82 -0
  428. package/dist/core/c4/source-mask.js.map +1 -0
  429. package/dist/core/c4/source-scan.js +227 -0
  430. package/dist/core/c4/source-scan.js.map +1 -0
  431. package/dist/core/c4/splice/authored-source.js +101 -0
  432. package/dist/core/c4/splice/authored-source.js.map +1 -0
  433. package/dist/core/c4/splice/contract.js +10 -0
  434. package/dist/core/c4/splice/contract.js.map +1 -0
  435. package/dist/core/c4/splice/delta-blocks.js +139 -0
  436. package/dist/core/c4/splice/delta-blocks.js.map +1 -0
  437. package/dist/core/c4/splice/identity/edges.js +36 -0
  438. package/dist/core/c4/splice/identity/edges.js.map +1 -0
  439. package/dist/core/c4/splice/landscape-merge.js +269 -0
  440. package/dist/core/c4/splice/landscape-merge.js.map +1 -0
  441. package/dist/core/c4/splice/placement.js +194 -0
  442. package/dist/core/c4/splice/placement.js.map +1 -0
  443. package/dist/core/c4/workspace.js +174 -0
  444. package/dist/core/c4/workspace.js.map +1 -0
  445. package/dist/core/capabilities/authoring/notes.js +141 -0
  446. package/dist/core/capabilities/authoring/notes.js.map +1 -0
  447. package/dist/core/capabilities/capabilities.js +124 -0
  448. package/dist/core/capabilities/capabilities.js.map +1 -0
  449. package/dist/core/capabilities/delta/doc.js +50 -0
  450. package/dist/core/capabilities/delta/doc.js.map +1 -0
  451. package/dist/core/capabilities/delta/overlay.js +129 -0
  452. package/dist/core/capabilities/delta/overlay.js.map +1 -0
  453. package/dist/core/capabilities/delta/summary.js +57 -0
  454. package/dist/core/capabilities/delta/summary.js.map +1 -0
  455. package/dist/core/capabilities/delta/tree.js +43 -0
  456. package/dist/core/capabilities/delta/tree.js.map +1 -0
  457. package/dist/core/capabilities/delta/uncovered.js +222 -0
  458. package/dist/core/capabilities/delta/uncovered.js.map +1 -0
  459. package/dist/core/capabilities/findings.js +342 -0
  460. package/dist/core/capabilities/findings.js.map +1 -0
  461. package/dist/core/capabilities/realizes/findings.js +181 -0
  462. package/dist/core/capabilities/realizes/findings.js.map +1 -0
  463. package/dist/core/capabilities/realizes/join.js +143 -0
  464. package/dist/core/capabilities/realizes/join.js.map +1 -0
  465. package/dist/core/capabilities/rollup.js +199 -0
  466. package/dist/core/capabilities/rollup.js.map +1 -0
  467. package/dist/core/capabilities/tree.js +126 -0
  468. package/dist/core/capabilities/tree.js.map +1 -0
  469. package/dist/core/capabilities/usecase-join.js +277 -0
  470. package/dist/core/capabilities/usecase-join.js.map +1 -0
  471. package/dist/core/coherence/authoring/scaffold.js +177 -0
  472. package/dist/core/coherence/authoring/scaffold.js.map +1 -0
  473. package/dist/core/coherence/authoring/sentinels.js +74 -0
  474. package/dist/core/coherence/authoring/sentinels.js.map +1 -0
  475. package/dist/core/coherence/coherence.js +287 -0
  476. package/dist/core/coherence/coherence.js.map +1 -0
  477. package/dist/core/coherence/declared.js +269 -0
  478. package/dist/core/coherence/declared.js.map +1 -0
  479. package/dist/core/coherence/events/declared.js +206 -0
  480. package/dist/core/coherence/events/declared.js.map +1 -0
  481. package/dist/core/coherence/events/events.js +204 -0
  482. package/dist/core/coherence/events/events.js.map +1 -0
  483. package/dist/core/coherence/events/grades.js +166 -0
  484. package/dist/core/coherence/events/grades.js.map +1 -0
  485. package/dist/core/coherence/events/lookups.js +122 -0
  486. package/dist/core/coherence/events/lookups.js.map +1 -0
  487. package/dist/core/coherence/events/merged.js +40 -0
  488. package/dist/core/coherence/events/merged.js.map +1 -0
  489. package/dist/core/coherence/living.js +198 -0
  490. package/dist/core/coherence/living.js.map +1 -0
  491. package/dist/core/coherence/lookups.js +155 -0
  492. package/dist/core/coherence/lookups.js.map +1 -0
  493. package/dist/core/coherence/pending.js +39 -0
  494. package/dist/core/coherence/pending.js.map +1 -0
  495. package/dist/core/conflict-markers.js +60 -0
  496. package/dist/core/conflict-markers.js.map +1 -0
  497. package/dist/core/delta/claims.js +73 -0
  498. package/dist/core/delta/claims.js.map +1 -0
  499. package/dist/core/delta/delta.js +296 -0
  500. package/dist/core/delta/delta.js.map +1 -0
  501. package/dist/core/delta/document.js +177 -0
  502. package/dist/core/delta/document.js.map +1 -0
  503. package/dist/core/delta/scope.js +48 -0
  504. package/dist/core/delta/scope.js.map +1 -0
  505. package/dist/core/delta/select.js +237 -0
  506. package/dist/core/delta/select.js.map +1 -0
  507. package/dist/core/dependencies/dependencies.js +141 -0
  508. package/dist/core/dependencies/dependencies.js.map +1 -0
  509. package/dist/core/dependencies/facts.js +165 -0
  510. package/dist/core/dependencies/facts.js.map +1 -0
  511. package/dist/core/dependencies/fanin.js +88 -0
  512. package/dist/core/dependencies/fanin.js.map +1 -0
  513. package/dist/core/dependencies/graph.js +106 -0
  514. package/dist/core/dependencies/graph.js.map +1 -0
  515. package/dist/core/deployment/delta.js +188 -0
  516. package/dist/core/deployment/delta.js.map +1 -0
  517. package/dist/core/deployment/objects.js +145 -0
  518. package/dist/core/deployment/objects.js.map +1 -0
  519. package/dist/core/diff/base-git.js +148 -0
  520. package/dist/core/diff/base-git.js.map +1 -0
  521. package/dist/core/diff/base-state.js +209 -0
  522. package/dist/core/diff/base-state.js.map +1 -0
  523. package/dist/core/diff/current-state.js +96 -0
  524. package/dist/core/diff/current-state.js.map +1 -0
  525. package/dist/core/diff/scope/changed-paths.js +188 -0
  526. package/dist/core/diff/scope/changed-paths.js.map +1 -0
  527. package/dist/core/diff/semantic.js +243 -0
  528. package/dist/core/diff/semantic.js.map +1 -0
  529. package/dist/core/diff/victims.js +247 -0
  530. package/dist/core/diff/victims.js.map +1 -0
  531. package/dist/core/docs.js +7 -64
  532. package/dist/core/docs.js.map +1 -1
  533. package/dist/core/doctor/agents.js +222 -0
  534. package/dist/core/doctor/agents.js.map +1 -0
  535. package/dist/core/doctor/config.js +135 -0
  536. package/dist/core/doctor/config.js.map +1 -0
  537. package/dist/core/doctor/doctor.js +275 -0
  538. package/dist/core/doctor/doctor.js.map +1 -0
  539. package/dist/core/doctor/report.js +13 -0
  540. package/dist/core/doctor/report.js.map +1 -0
  541. package/dist/core/doctor/residue.js +110 -0
  542. package/dist/core/doctor/residue.js.map +1 -0
  543. package/dist/core/document/apply.js +77 -0
  544. package/dist/core/document/apply.js.map +1 -0
  545. package/dist/core/{frontmatter.js → document/frontmatter.js} +48 -2
  546. package/dist/core/document/frontmatter.js.map +1 -0
  547. package/dist/core/document/grammar/directives.js +143 -0
  548. package/dist/core/document/grammar/directives.js.map +1 -0
  549. package/dist/core/document/parse.js +263 -0
  550. package/dist/core/document/parse.js.map +1 -0
  551. package/dist/core/document/scenarios.js +221 -0
  552. package/dist/core/document/scenarios.js.map +1 -0
  553. package/dist/core/document/spec.js +241 -0
  554. package/dist/core/document/spec.js.map +1 -0
  555. package/dist/core/{config.js → envelope/config.js} +79 -18
  556. package/dist/core/envelope/config.js.map +1 -0
  557. package/dist/core/envelope/json.js +175 -0
  558. package/dist/core/envelope/json.js.map +1 -0
  559. package/dist/core/{version.js → envelope/version.js} +3 -3
  560. package/dist/core/envelope/version.js.map +1 -0
  561. package/dist/core/explain/families.js +175 -0
  562. package/dist/core/explain/families.js.map +1 -0
  563. package/dist/core/explain/fix-tables.js +178 -0
  564. package/dist/core/explain/fix-tables.js.map +1 -0
  565. package/dist/core/explain/lookup.js +181 -0
  566. package/dist/core/explain/lookup.js.map +1 -0
  567. package/dist/core/explain/refusals.js +49 -0
  568. package/dist/core/explain/refusals.js.map +1 -0
  569. package/dist/core/explain/terms.js +144 -0
  570. package/dist/core/explain/terms.js.map +1 -0
  571. package/dist/core/explore/describe.js +105 -0
  572. package/dist/core/explore/describe.js.map +1 -0
  573. package/dist/core/{explore.js → explore/explore.js} +50 -101
  574. package/dist/core/explore/explore.js.map +1 -0
  575. package/dist/core/fleet-context.js +120 -78
  576. package/dist/core/fleet-context.js.map +1 -1
  577. package/dist/core/gate/checks.js +201 -0
  578. package/dist/core/gate/checks.js.map +1 -0
  579. package/dist/core/gate/partners.js +205 -0
  580. package/dist/core/gate/partners.js.map +1 -0
  581. package/dist/core/gate/report.js +47 -0
  582. package/dist/core/gate/report.js.map +1 -0
  583. package/dist/core/gate/verification.js +113 -0
  584. package/dist/core/gate/verification.js.map +1 -0
  585. package/dist/core/gherkin/digest.js +60 -0
  586. package/dist/core/gherkin/digest.js.map +1 -0
  587. package/dist/core/gherkin/emit.js +177 -0
  588. package/dist/core/gherkin/emit.js.map +1 -0
  589. package/dist/core/gherkin/read.js +76 -0
  590. package/dist/core/gherkin/read.js.map +1 -0
  591. package/dist/core/gherkin/stale.js +234 -0
  592. package/dist/core/gherkin/stale.js.map +1 -0
  593. package/dist/core/gherkin/stamp.js +77 -0
  594. package/dist/core/gherkin/stamp.js.map +1 -0
  595. package/dist/core/gherkin/steps/catalogue.js +154 -0
  596. package/dist/core/gherkin/steps/catalogue.js.map +1 -0
  597. package/dist/core/gherkin/steps/inventory.js +98 -0
  598. package/dist/core/gherkin/steps/inventory.js.map +1 -0
  599. package/dist/core/gherkin/steps/phrase.js +72 -0
  600. package/dist/core/gherkin/steps/phrase.js.map +1 -0
  601. package/dist/core/glossary/delta.js +106 -0
  602. package/dist/core/glossary/delta.js.map +1 -0
  603. package/dist/core/glossary/tree.js +86 -0
  604. package/dist/core/glossary/tree.js.map +1 -0
  605. package/dist/core/kernel/concurrency.js.map +1 -0
  606. package/dist/core/{document-bytes.js → kernel/document-bytes.js} +1 -1
  607. package/dist/core/kernel/document-bytes.js.map +1 -0
  608. package/dist/core/kernel/fences.js +42 -0
  609. package/dist/core/kernel/fences.js.map +1 -0
  610. package/dist/core/kernel/ids/capability.js +72 -0
  611. package/dist/core/kernel/ids/capability.js.map +1 -0
  612. package/dist/core/kernel/ids/dirs.js +33 -0
  613. package/dist/core/kernel/ids/dirs.js.map +1 -0
  614. package/dist/core/kernel/ids/feature.js +75 -0
  615. package/dist/core/kernel/ids/feature.js.map +1 -0
  616. package/dist/core/kernel/ids/service.js +156 -0
  617. package/dist/core/kernel/ids/service.js.map +1 -0
  618. package/dist/core/kernel/ids/subsystem.js +63 -0
  619. package/dist/core/kernel/ids/subsystem.js.map +1 -0
  620. package/dist/core/{path-safety.js → kernel/path-safety.js} +18 -4
  621. package/dist/core/kernel/path-safety.js.map +1 -0
  622. package/dist/core/kernel/records.js.map +1 -0
  623. package/dist/core/links/backlinks.js +68 -0
  624. package/dist/core/links/backlinks.js.map +1 -0
  625. package/dist/core/links/case.js +74 -0
  626. package/dist/core/links/case.js.map +1 -0
  627. package/dist/core/links/corpus.js +104 -0
  628. package/dist/core/links/corpus.js.map +1 -0
  629. package/dist/core/links/findings.js +110 -0
  630. package/dist/core/links/findings.js.map +1 -0
  631. package/dist/core/links/parse.js +236 -0
  632. package/dist/core/links/parse.js.map +1 -0
  633. package/dist/core/mcp/argv.js +111 -0
  634. package/dist/core/mcp/argv.js.map +1 -0
  635. package/dist/core/mcp/author-tools.js +72 -0
  636. package/dist/core/mcp/author-tools.js.map +1 -0
  637. package/dist/core/mcp/framing.js +46 -0
  638. package/dist/core/mcp/framing.js.map +1 -0
  639. package/dist/core/mcp/protocol.js +280 -0
  640. package/dist/core/mcp/protocol.js.map +1 -0
  641. package/dist/core/mcp/tools.js +314 -0
  642. package/dist/core/mcp/tools.js.map +1 -0
  643. package/dist/core/obligations/obligations.js +106 -0
  644. package/dist/core/obligations/obligations.js.map +1 -0
  645. package/dist/core/openapi/baseline/gate.js +166 -0
  646. package/dist/core/openapi/baseline/gate.js.map +1 -0
  647. package/dist/core/openapi/baseline/plan.js +112 -0
  648. package/dist/core/openapi/baseline/plan.js.map +1 -0
  649. package/dist/core/openapi/baseline/record.js +175 -0
  650. package/dist/core/openapi/baseline/record.js.map +1 -0
  651. package/dist/core/openapi/depth.js +122 -0
  652. package/dist/core/openapi/depth.js.map +1 -0
  653. package/dist/core/openapi/digest.js +189 -0
  654. package/dist/core/openapi/digest.js.map +1 -0
  655. package/dist/core/{openapi.js → openapi/doc.js} +44 -106
  656. package/dist/core/openapi/doc.js.map +1 -0
  657. package/dist/core/openapi/merge/components.js +234 -0
  658. package/dist/core/openapi/merge/components.js.map +1 -0
  659. package/dist/core/openapi/merge/error.js +26 -0
  660. package/dist/core/openapi/merge/error.js.map +1 -0
  661. package/dist/core/openapi/merge/markers.js +208 -0
  662. package/dist/core/openapi/merge/markers.js.map +1 -0
  663. package/dist/core/openapi/merge/merge.js +256 -0
  664. package/dist/core/openapi/merge/merge.js.map +1 -0
  665. package/dist/core/openapi/merge/pin.js +128 -0
  666. package/dist/core/openapi/merge/pin.js.map +1 -0
  667. package/dist/core/openspec/decide/capabilities.js +244 -0
  668. package/dist/core/openspec/decide/capabilities.js.map +1 -0
  669. package/dist/core/openspec/decide/dispositions.js +63 -0
  670. package/dist/core/openspec/decide/dispositions.js.map +1 -0
  671. package/dist/core/openspec/decide/renames.js +179 -0
  672. package/dist/core/openspec/decide/renames.js.map +1 -0
  673. package/dist/core/openspec/decide/verdict.js +84 -0
  674. package/dist/core/openspec/decide/verdict.js.map +1 -0
  675. package/dist/core/openspec/inventory.js +221 -0
  676. package/dist/core/openspec/inventory.js.map +1 -0
  677. package/dist/core/openspec/model/mapping.js +76 -0
  678. package/dist/core/openspec/model/mapping.js.map +1 -0
  679. package/dist/core/openspec/model/model.js +65 -0
  680. package/dist/core/openspec/model/model.js.map +1 -0
  681. package/dist/core/openspec/scan/changes.js +235 -0
  682. package/dist/core/openspec/scan/changes.js.map +1 -0
  683. package/dist/core/openspec/scan/config.js +200 -0
  684. package/dist/core/openspec/scan/config.js.map +1 -0
  685. package/dist/core/openspec/scan/shape.js +260 -0
  686. package/dist/core/openspec/scan/shape.js.map +1 -0
  687. package/dist/core/openspec/scan/walk.js +133 -0
  688. package/dist/core/openspec/scan/walk.js.map +1 -0
  689. package/dist/core/openspec/scan/workspace.js +71 -0
  690. package/dist/core/openspec/scan/workspace.js.map +1 -0
  691. package/dist/core/owners/codeowners.js +141 -0
  692. package/dist/core/owners/codeowners.js.map +1 -0
  693. package/dist/core/pack/features.js +120 -0
  694. package/dist/core/pack/features.js.map +1 -0
  695. package/dist/core/pack/joins.js +112 -0
  696. package/dist/core/pack/joins.js.map +1 -0
  697. package/dist/core/pack/living.js +137 -0
  698. package/dist/core/pack/living.js.map +1 -0
  699. package/dist/core/pack/pack.js +76 -0
  700. package/dist/core/pack/pack.js.map +1 -0
  701. package/dist/core/permissions/permissions.js +87 -0
  702. package/dist/core/permissions/permissions.js.map +1 -0
  703. package/dist/core/projection/api.js +84 -0
  704. package/dist/core/projection/api.js.map +1 -0
  705. package/dist/core/projection/arch-slice.js +63 -0
  706. package/dist/core/projection/arch-slice.js.map +1 -0
  707. package/dist/core/projection/events.js +34 -0
  708. package/dist/core/projection/events.js.map +1 -0
  709. package/dist/core/provenance/findings.js +208 -0
  710. package/dist/core/provenance/findings.js.map +1 -0
  711. package/dist/core/provenance/git.js +225 -0
  712. package/dist/core/provenance/git.js.map +1 -0
  713. package/dist/core/provenance/gitq/moves.js +182 -0
  714. package/dist/core/provenance/gitq/moves.js.map +1 -0
  715. package/dist/core/provenance/gitq/vouched-ancestor.js +179 -0
  716. package/dist/core/provenance/gitq/vouched-ancestor.js.map +1 -0
  717. package/dist/core/provenance/sample/scope.js +238 -0
  718. package/dist/core/provenance/sample/scope.js.map +1 -0
  719. package/dist/core/provenance/sample/sections.js +61 -0
  720. package/dist/core/provenance/sample/sections.js.map +1 -0
  721. package/dist/core/provenance/sources.js +263 -0
  722. package/dist/core/provenance/sources.js.map +1 -0
  723. package/dist/core/provenance/stamp.js +232 -0
  724. package/dist/core/provenance/stamp.js.map +1 -0
  725. package/dist/core/provenance/walk.js +157 -0
  726. package/dist/core/provenance/walk.js.map +1 -0
  727. package/dist/core/repo/authored/paths.js +210 -0
  728. package/dist/core/repo/authored/paths.js.map +1 -0
  729. package/dist/core/repo/entries.js +126 -0
  730. package/dist/core/repo/entries.js.map +1 -0
  731. package/dist/core/repo/paths.js +277 -0
  732. package/dist/core/repo/paths.js.map +1 -0
  733. package/dist/core/repo/repo.js +248 -0
  734. package/dist/core/repo/repo.js.map +1 -0
  735. package/dist/core/repo/service-target.js +153 -0
  736. package/dist/core/repo/service-target.js.map +1 -0
  737. package/dist/core/repo/state.js +58 -0
  738. package/dist/core/repo/state.js.map +1 -0
  739. package/dist/core/repo/tree/find.js +52 -0
  740. package/dist/core/repo/tree/find.js.map +1 -0
  741. package/dist/core/repo/tree/fs.js +64 -0
  742. package/dist/core/repo/tree/fs.js.map +1 -0
  743. package/dist/core/repo/tree/marker.js +61 -0
  744. package/dist/core/repo/tree/marker.js.map +1 -0
  745. package/dist/core/repo/tree/views.js +172 -0
  746. package/dist/core/repo/tree/views.js.map +1 -0
  747. package/dist/core/repo/tree/walk.js +234 -0
  748. package/dist/core/repo/tree/walk.js.map +1 -0
  749. package/dist/core/results.js +99 -2
  750. package/dist/core/results.js.map +1 -1
  751. package/dist/core/scaffold/landscape.js +131 -0
  752. package/dist/core/scaffold/landscape.js.map +1 -0
  753. package/dist/core/scaffold/readme.js +84 -0
  754. package/dist/core/scaffold/readme.js.map +1 -0
  755. package/dist/core/staging/commit.js +241 -0
  756. package/dist/core/staging/commit.js.map +1 -0
  757. package/dist/core/staging/interrupted.js +96 -0
  758. package/dist/core/staging/interrupted.js.map +1 -0
  759. package/dist/core/staging/lock.js +206 -0
  760. package/dist/core/staging/lock.js.map +1 -0
  761. package/dist/core/staging/recovery/finding.js +35 -0
  762. package/dist/core/staging/recovery/finding.js.map +1 -0
  763. package/dist/core/staging/recovery/intent.js +98 -0
  764. package/dist/core/staging/recovery/intent.js.map +1 -0
  765. package/dist/core/staging/recovery/recover.js +126 -0
  766. package/dist/core/staging/recovery/recover.js.map +1 -0
  767. package/dist/core/staging/recovery/residue.js +62 -0
  768. package/dist/core/staging/recovery/residue.js.map +1 -0
  769. package/dist/core/staging/snapshot.js +202 -0
  770. package/dist/core/staging/snapshot.js.map +1 -0
  771. package/dist/core/staging/txn/forward.js +171 -0
  772. package/dist/core/staging/txn/forward.js.map +1 -0
  773. package/dist/core/staging/txn/journal.js +139 -0
  774. package/dist/core/staging/txn/journal.js.map +1 -0
  775. package/dist/core/staging/txn/transaction.js +75 -0
  776. package/dist/core/staging/txn/transaction.js.map +1 -0
  777. package/dist/core/staging/writes.js +113 -0
  778. package/dist/core/staging/writes.js.map +1 -0
  779. package/dist/core/status/actions/execution.js +49 -0
  780. package/dist/core/status/actions/execution.js.map +1 -0
  781. package/dist/core/status/contracts.js +102 -0
  782. package/dist/core/status/contracts.js.map +1 -0
  783. package/dist/core/status/feature/artifacts.js +196 -0
  784. package/dist/core/status/feature/artifacts.js.map +1 -0
  785. package/dist/core/status/feature/feature.js +207 -0
  786. package/dist/core/status/feature/feature.js.map +1 -0
  787. package/dist/core/status/feature/next.js +150 -0
  788. package/dist/core/status/feature/next.js.map +1 -0
  789. package/dist/core/status/feature/state.js +21 -0
  790. package/dist/core/status/feature/state.js.map +1 -0
  791. package/dist/core/status/fleet/fleet.js +197 -0
  792. package/dist/core/status/fleet/fleet.js.map +1 -0
  793. package/dist/core/status/fleet/next.js +207 -0
  794. package/dist/core/status/fleet/next.js.map +1 -0
  795. package/dist/core/status/interrupted.js +97 -0
  796. package/dist/core/status/interrupted.js.map +1 -0
  797. package/dist/core/status/report.js +31 -0
  798. package/dist/core/status/report.js.map +1 -0
  799. package/dist/core/status/scan.js +41 -0
  800. package/dist/core/status/scan.js.map +1 -0
  801. package/dist/core/status/verification.js +201 -0
  802. package/dist/core/status/verification.js.map +1 -0
  803. package/dist/core/usecases/capability.js +169 -0
  804. package/dist/core/usecases/capability.js.map +1 -0
  805. package/dist/core/usecases/delta/claims.js +116 -0
  806. package/dist/core/usecases/delta/claims.js.map +1 -0
  807. package/dist/core/usecases/delta/flows.js +103 -0
  808. package/dist/core/usecases/delta/flows.js.map +1 -0
  809. package/dist/core/usecases/delta/overlay.js +72 -0
  810. package/dist/core/usecases/delta/overlay.js.map +1 -0
  811. package/dist/core/usecases/fleet.js +137 -0
  812. package/dist/core/usecases/fleet.js.map +1 -0
  813. package/dist/core/usecases/operations.js +111 -0
  814. package/dist/core/usecases/operations.js.map +1 -0
  815. package/dist/core/usecases/place.js +44 -0
  816. package/dist/core/usecases/place.js.map +1 -0
  817. package/dist/core/usecases/touch.js +101 -0
  818. package/dist/core/usecases/touch.js.map +1 -0
  819. package/dist/core/verify/answers.js +173 -0
  820. package/dist/core/verify/answers.js.map +1 -0
  821. package/dist/core/verify/build.js +150 -0
  822. package/dist/core/verify/build.js.map +1 -0
  823. package/dist/core/verify/checklist.js +201 -0
  824. package/dist/core/verify/checklist.js.map +1 -0
  825. package/dist/core/verify/claims/identity.js +61 -0
  826. package/dist/core/verify/claims/identity.js.map +1 -0
  827. package/dist/core/verify/cross/diff.js +97 -0
  828. package/dist/core/verify/cross/diff.js.map +1 -0
  829. package/dist/core/verify/evidence/contract.js +169 -0
  830. package/dist/core/verify/evidence/contract.js.map +1 -0
  831. package/dist/core/verify/file.js +257 -0
  832. package/dist/core/verify/file.js.map +1 -0
  833. package/dist/core/verify/pins/lint.js +157 -0
  834. package/dist/core/verify/pins/lint.js.map +1 -0
  835. package/dist/core/verify/pins/pin.js +95 -0
  836. package/dist/core/verify/pins/pin.js.map +1 -0
  837. package/dist/core/verify/record.js +142 -0
  838. package/dist/core/verify/record.js.map +1 -0
  839. package/dist/core/verify/store/commit.js +89 -0
  840. package/dist/core/verify/store/commit.js.map +1 -0
  841. package/dist/core/verify/store/render.js +57 -0
  842. package/dist/core/verify/store/render.js.map +1 -0
  843. package/dist/core/vocabulary/health.js +91 -0
  844. package/dist/core/vocabulary/health.js.map +1 -0
  845. package/dist/core/vocabulary/issue.js +58 -0
  846. package/dist/core/vocabulary/issue.js.map +1 -0
  847. package/dist/core/vocabulary/maturity.js +145 -0
  848. package/dist/core/vocabulary/maturity.js.map +1 -0
  849. package/dist/core/{report.js → vocabulary/report.js} +30 -2
  850. package/dist/core/vocabulary/report.js.map +1 -0
  851. package/dist/core/vocabulary/steps.js +266 -0
  852. package/dist/core/vocabulary/steps.js.map +1 -0
  853. package/dist/core/workspace/discover.js +239 -0
  854. package/dist/core/workspace/discover.js.map +1 -0
  855. package/dist/core/workspace/render.js +52 -0
  856. package/dist/core/workspace/render.js.map +1 -0
  857. package/examples/README.md +189 -0
  858. package/examples/docs/architecture/adrs/0001-transactional-outbox.md +33 -0
  859. package/examples/docs/architecture/capabilities.yaml +31 -0
  860. package/examples/docs/architecture/landscape.likec4 +280 -0
  861. package/examples/docs/architecture/obligations.yaml +23 -0
  862. package/examples/docs/architecture/permissions.yaml +50 -0
  863. package/examples/docs/architecture/subsystems.likec4 +11 -0
  864. package/examples/docs/architecture/usecases/order-notification.likec4 +65 -0
  865. package/examples/docs/capabilities/checkout/spec.md +55 -0
  866. package/examples/docs/capabilities/identity/tokens/spec.md +42 -0
  867. package/examples/docs/capabilities/order-notifications/spec.md +43 -0
  868. package/examples/docs/features/FEAT-101-payment-splitting/adrs/0001-dedicated-split-service.md +18 -0
  869. package/examples/docs/features/FEAT-101-payment-splitting/delta.likec4 +94 -0
  870. package/examples/docs/features/FEAT-101-payment-splitting/intent.md +44 -0
  871. package/examples/docs/features/FEAT-101-payment-splitting/specs/checkout-web/spec.md +21 -0
  872. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/asyncapi.yaml +129 -0
  873. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/spec.md +36 -0
  874. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/arch.spec.md +40 -0
  875. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/openapi.yaml +116 -0
  876. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/spec.md +55 -0
  877. package/examples/docs/features/FEAT-112-retire-order-v1/intent.md +35 -0
  878. package/examples/docs/features/FEAT-112-retire-order-v1/specs/order-service/openapi.yaml +28 -0
  879. package/examples/docs/features/FEAT-112-retire-order-v1/specs/order-service/spec.md +17 -0
  880. package/examples/docs/features/archive/FEAT-088-refunds/.loam-before/files/architecture/landscape.likec4 +253 -0
  881. package/examples/docs/features/archive/FEAT-088-refunds/.loam-before/files/services/order-service/spec.md +121 -0
  882. package/examples/docs/features/archive/FEAT-088-refunds/.loam-before/files/services/payment-service/openapi.yaml +111 -0
  883. package/examples/docs/features/archive/FEAT-088-refunds/.loam-before/files/services/payment-service/spec.md +58 -0
  884. package/examples/docs/features/archive/FEAT-088-refunds/.loam-before/manifest.json +32 -0
  885. package/examples/docs/features/archive/FEAT-088-refunds/adrs/0001-refunds-stay-in-payment-service.md +26 -0
  886. package/examples/docs/features/archive/FEAT-088-refunds/delta.likec4 +28 -0
  887. package/examples/docs/features/archive/FEAT-088-refunds/intent.md +26 -0
  888. package/examples/docs/features/archive/FEAT-088-refunds/specs/order-service/spec.md +37 -0
  889. package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/openapi.yaml +70 -0
  890. package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/spec.md +23 -0
  891. package/examples/docs/features/archive/FEAT-088-refunds/verification.yaml +76 -0
  892. package/examples/docs/features/archive/FEAT-120-refund-notification/.loam-before/files/architecture/landscape.likec4 +275 -0
  893. package/examples/docs/features/archive/FEAT-120-refund-notification/.loam-before/files/services/payment-service/asyncapi.yaml +96 -0
  894. package/examples/docs/features/archive/FEAT-120-refund-notification/.loam-before/files/services/payment-service/spec.md +82 -0
  895. package/examples/docs/features/archive/FEAT-120-refund-notification/.loam-before/files/services/platform/notification-service/asyncapi.yaml +164 -0
  896. package/examples/docs/features/archive/FEAT-120-refund-notification/.loam-before/files/services/platform/notification-service/spec.md +91 -0
  897. package/examples/docs/features/archive/FEAT-120-refund-notification/.loam-before/manifest.json +46 -0
  898. package/examples/docs/features/archive/FEAT-120-refund-notification/delta.likec4 +36 -0
  899. package/examples/docs/features/archive/FEAT-120-refund-notification/intent.md +22 -0
  900. package/examples/docs/features/archive/FEAT-120-refund-notification/scenario-report.json +10 -0
  901. package/examples/docs/features/archive/FEAT-120-refund-notification/specs/notification-service/asyncapi.yaml +208 -0
  902. package/examples/docs/features/archive/FEAT-120-refund-notification/specs/notification-service/spec.md +24 -0
  903. package/examples/docs/features/archive/FEAT-120-refund-notification/specs/payment-service/asyncapi.yaml +137 -0
  904. package/examples/docs/features/archive/FEAT-120-refund-notification/specs/payment-service/spec.md +31 -0
  905. package/examples/docs/features/archive/FEAT-120-refund-notification/verification.yaml +84 -0
  906. package/examples/docs/glossary/order.md +15 -0
  907. package/examples/docs/glossary/payments/authorization.md +14 -0
  908. package/examples/docs/likec4.config.json +9 -0
  909. package/examples/docs/loam.json +3 -0
  910. package/examples/docs/services/checkout-web/model.likec4 +109 -0
  911. package/examples/docs/services/checkout-web/spec.md +91 -0
  912. package/examples/docs/services/checkout-web/ui/pages/checkout.page.yaml +40 -0
  913. package/examples/docs/services/checkout-web/ui/pages/order-status.page.yaml +24 -0
  914. package/examples/docs/services/checkout-web/ui/pages/split-payment.page.yaml +26 -0
  915. package/examples/docs/services/order-service/adrs/0001-orchestrated-order-lifecycle.md +52 -0
  916. package/examples/docs/services/order-service/arch.spec.md +127 -0
  917. package/examples/docs/services/order-service/asyncapi.yaml +108 -0
  918. package/examples/docs/services/order-service/health.yaml +57 -0
  919. package/examples/docs/services/order-service/model.likec4 +138 -0
  920. package/examples/docs/services/order-service/openapi.yaml +257 -0
  921. package/examples/docs/services/order-service/runbook.md +62 -0
  922. package/examples/docs/services/order-service/spec.md +134 -0
  923. package/examples/docs/services/payment-service/adrs/0001-payments-outbox.md +18 -0
  924. package/examples/docs/services/payment-service/adrs/0002-idempotency-keys-on-authorization.md +27 -0
  925. package/examples/docs/services/payment-service/arch.spec.md +73 -0
  926. package/examples/docs/services/payment-service/asyncapi.yaml +130 -0
  927. package/examples/docs/services/payment-service/health.yaml +39 -0
  928. package/examples/docs/services/payment-service/model.likec4 +67 -0
  929. package/examples/docs/services/payment-service/openapi.yaml +151 -0
  930. package/examples/docs/services/payment-service/runbook.md +37 -0
  931. package/examples/docs/services/payment-service/spec.md +88 -0
  932. package/examples/docs/services/platform/identity-service/adrs/0001-token-introspection-over-local-validation.md +60 -0
  933. package/examples/docs/services/platform/identity-service/arch.spec.md +117 -0
  934. package/examples/docs/services/platform/identity-service/health.yaml +70 -0
  935. package/examples/docs/services/platform/identity-service/model.likec4 +83 -0
  936. package/examples/docs/services/platform/identity-service/openapi.yaml +197 -0
  937. package/examples/docs/services/platform/identity-service/runbook.md +95 -0
  938. package/examples/docs/services/platform/identity-service/spec.md +119 -0
  939. package/examples/docs/services/platform/notification-service/asyncapi.yaml +199 -0
  940. package/examples/docs/services/platform/notification-service/health.yaml +35 -0
  941. package/examples/docs/services/platform/notification-service/model.likec4 +118 -0
  942. package/examples/docs/services/platform/notification-service/runbook.md +48 -0
  943. package/examples/docs/services/platform/notification-service/spec.md +112 -0
  944. package/examples/docs/services/platform/subsystem.yaml +2 -0
  945. package/examples/fleet.yaml +57 -0
  946. package/package.json +19 -6
  947. package/dist/commands/adopt.js +0 -240
  948. package/dist/commands/adopt.js.map +0 -1
  949. package/dist/commands/archive.js +0 -1313
  950. package/dist/commands/archive.js.map +0 -1
  951. package/dist/commands/delta.js +0 -388
  952. package/dist/commands/delta.js.map +0 -1
  953. package/dist/commands/docs-repo-gate.js.map +0 -1
  954. package/dist/commands/format.js +0 -17
  955. package/dist/commands/format.js.map +0 -1
  956. package/dist/commands/gherkin.js +0 -309
  957. package/dist/commands/gherkin.js.map +0 -1
  958. package/dist/commands/init.js.map +0 -1
  959. package/dist/commands/list.js +0 -295
  960. package/dist/commands/list.js.map +0 -1
  961. package/dist/commands/migrate-openspec.js +0 -950
  962. package/dist/commands/migrate-openspec.js.map +0 -1
  963. package/dist/commands/new.js +0 -413
  964. package/dist/commands/new.js.map +0 -1
  965. package/dist/commands/rebase.js +0 -356
  966. package/dist/commands/rebase.js.map +0 -1
  967. package/dist/commands/show.js.map +0 -1
  968. package/dist/commands/status.js +0 -262
  969. package/dist/commands/status.js.map +0 -1
  970. package/dist/commands/unarchive.js.map +0 -1
  971. package/dist/commands/validate.js +0 -1773
  972. package/dist/commands/validate.js.map +0 -1
  973. package/dist/commands/verify.js +0 -775
  974. package/dist/commands/verify.js.map +0 -1
  975. package/dist/commands/vouch.js +0 -421
  976. package/dist/commands/vouch.js.map +0 -1
  977. package/dist/core/agent.js +0 -2173
  978. package/dist/core/agent.js.map +0 -1
  979. package/dist/core/agents-stamp.js.map +0 -1
  980. package/dist/core/arch.js +0 -130
  981. package/dist/core/arch.js.map +0 -1
  982. package/dist/core/asyncapi.js.map +0 -1
  983. package/dist/core/brief.js +0 -755
  984. package/dist/core/brief.js.map +0 -1
  985. package/dist/core/coherence.js +0 -577
  986. package/dist/core/coherence.js.map +0 -1
  987. package/dist/core/concurrency.js.map +0 -1
  988. package/dist/core/config.js.map +0 -1
  989. package/dist/core/delta.js +0 -476
  990. package/dist/core/delta.js.map +0 -1
  991. package/dist/core/dependencies.js +0 -356
  992. package/dist/core/dependencies.js.map +0 -1
  993. package/dist/core/doctor.js +0 -596
  994. package/dist/core/doctor.js.map +0 -1
  995. package/dist/core/document-bytes.js.map +0 -1
  996. package/dist/core/explore.js.map +0 -1
  997. package/dist/core/frontmatter.js.map +0 -1
  998. package/dist/core/gherkin.js +0 -463
  999. package/dist/core/gherkin.js.map +0 -1
  1000. package/dist/core/health.js +0 -67
  1001. package/dist/core/health.js.map +0 -1
  1002. package/dist/core/ids.js +0 -122
  1003. package/dist/core/ids.js.map +0 -1
  1004. package/dist/core/issue.js +0 -9
  1005. package/dist/core/issue.js.map +0 -1
  1006. package/dist/core/json.js +0 -74
  1007. package/dist/core/json.js.map +0 -1
  1008. package/dist/core/likec4.js +0 -489
  1009. package/dist/core/likec4.js.map +0 -1
  1010. package/dist/core/maturity.js +0 -66
  1011. package/dist/core/maturity.js.map +0 -1
  1012. package/dist/core/openapi-merge.js +0 -552
  1013. package/dist/core/openapi-merge.js.map +0 -1
  1014. package/dist/core/openapi.js.map +0 -1
  1015. package/dist/core/openspec-inventory.js +0 -1440
  1016. package/dist/core/openspec-inventory.js.map +0 -1
  1017. package/dist/core/path-safety.js.map +0 -1
  1018. package/dist/core/provenance.js +0 -787
  1019. package/dist/core/provenance.js.map +0 -1
  1020. package/dist/core/records.js.map +0 -1
  1021. package/dist/core/repo.js +0 -438
  1022. package/dist/core/repo.js.map +0 -1
  1023. package/dist/core/report.js.map +0 -1
  1024. package/dist/core/spec.js +0 -505
  1025. package/dist/core/spec.js.map +0 -1
  1026. package/dist/core/staging.js +0 -890
  1027. package/dist/core/staging.js.map +0 -1
  1028. package/dist/core/status.js +0 -1132
  1029. package/dist/core/status.js.map +0 -1
  1030. package/dist/core/steps.js +0 -72
  1031. package/dist/core/steps.js.map +0 -1
  1032. package/dist/core/verify.js +0 -807
  1033. package/dist/core/verify.js.map +0 -1
  1034. package/dist/core/version.js.map +0 -1
  1035. /package/dist/core/{concurrency.js → kernel/concurrency.js} +0 -0
  1036. /package/dist/core/{records.js → kernel/records.js} +0 -0
@@ -0,0 +1,75 @@
1
+ /**
2
+ * The spelling rule for feature ids, and the brands that carry its provenance.
3
+ *
4
+ * Split out of `service.ts` when the feature grammar grew the same brand
5
+ * lattice the service grammar has: the two rules share a package because both
6
+ * become directory names in the shared docs repo, but a feature id is not a
7
+ * service id — the brands below are deliberately disjoint from `RawServiceId`
8
+ * and `ServiceId`, so one grammar's checked value cannot stand in where the
9
+ * other's is demanded.
10
+ */
11
+ /**
12
+ * Feature ids are `<word>-<number>`: the id has to survive being read back off
13
+ * the directory name (`FEAT-101-payment-splitting` -> `FEAT-101`), or the
14
+ * feature would answer to a name it was never given.
15
+ *
16
+ * Here for the same reason the service grammar is — this was spelled twice,
17
+ * privately, in `commands/new.ts` and `core/openspec/`, and
18
+ * docs/DESIGN.md rule 7 recorded the pair as a hazard rather than a fact. The
19
+ * third caller is what made it one: `loam explore --as <FEAT>` interpolates its
20
+ * argument into a `loam new` line that loam PRINTS for an agent to run, so
21
+ * without this check `explore` cheerfully handed back a command `new` refuses.
22
+ * A guard test catches that class only for literal source strings — a command
23
+ * assembled at runtime from argv is invisible to it — which is exactly why the
24
+ * grammar has to be shared rather than re-derived by whoever needs it next.
25
+ */
26
+ const FEATURE_ID = /^[A-Za-z][A-Za-z0-9]*-\d+$/;
27
+ /** Prose form, shown verbatim in every refusal so the fix is obvious. */
28
+ export const FEATURE_ID_RULE = "Expected <word>-<number>, e.g. FEAT-101 or BUG-42.";
29
+ export function isFeatureId(id) {
30
+ return typeof id === "string" && FEATURE_ID.test(id);
31
+ }
32
+ /**
33
+ * Why `id` is not a usable feature id, or null when it is — the `serviceIdProblem`
34
+ * shape, so a caller already collecting findings reports the sentence a refusal
35
+ * would print.
36
+ */
37
+ export function featureIdProblem(id, label = "feature id") {
38
+ if (!isFeatureId(id)) {
39
+ const shown = typeof id === "string" ? `'${id}'` : String(id);
40
+ return `${shown} is not a usable ${label}. ${FEATURE_ID_RULE}`;
41
+ }
42
+ return null;
43
+ }
44
+ /* ---------------------------------------------------------------- */
45
+ /* The two constructors — the only place a brand is asserted */
46
+ /* ---------------------------------------------------------------- */
47
+ /**
48
+ * The one constructor that validates — `parseServiceId`'s shape, for the same
49
+ * reason: a discriminated pair rather than `FeatureId | string`, because that
50
+ * second shape compiles and is unusable, and the problem sentence is the one
51
+ * `featureIdProblem` already produces, so a caller that is collecting findings
52
+ * prints exactly what a refusal would.
53
+ */
54
+ export function parseFeatureId(raw, label = "feature id") {
55
+ const problem = featureIdProblem(raw, label);
56
+ if (problem !== null)
57
+ return { ok: false, problem };
58
+ // The cast, on the line immediately after the check that earns it.
59
+ return { ok: true, id: raw };
60
+ }
61
+ /**
62
+ * An id derived from a directory name a repository listed. Says nothing about
63
+ * whether it is a legal id — that is the point: the enumeration must be able to
64
+ * name the directory that is there, however it is spelled.
65
+ *
66
+ * This is a cast, and it has to be: the derivation in
67
+ * `core/repo/entries.ts`'s `featureIdFromDirName` starts from a plain `readdir`
68
+ * string, and a brand does not survive it. What the rule in
69
+ * `docs/CODE-STYLE.md` buys is that both of this file's casts live HERE and
70
+ * nowhere else — a third one somewhere in `src/` is a finding, not a shortcut.
71
+ */
72
+ export function rawFeatureId(derived) {
73
+ return derived;
74
+ }
75
+ //# sourceMappingURL=feature.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feature.js","sourceRoot":"","sources":["../../../../src/core/kernel/ids/feature.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AA2BH;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,GAAG,4BAA4B,CAAC;AAEhD,yEAAyE;AACzE,MAAM,CAAC,MAAM,eAAe,GAAG,oDAAoD,CAAC;AAEpF,MAAM,UAAU,WAAW,CAAC,EAAW;IACrC,OAAO,OAAO,EAAE,KAAK,QAAQ,IAAI,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AACvD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,EAAW,EAAE,KAAK,GAAG,YAAY;IAChE,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC,EAAE,CAAC;QACrB,MAAM,KAAK,GAAG,OAAO,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAC9D,OAAO,GAAG,KAAK,oBAAoB,KAAK,KAAK,eAAe,EAAE,CAAC;IACjE,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,sEAAsE;AACtE,uEAAuE;AACvE,sEAAsE;AAEtE;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAC5B,GAAW,EACX,KAAK,GAAG,YAAY;IAEpB,MAAM,OAAO,GAAG,gBAAgB,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAC7C,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;IACpD,mEAAmE;IACnE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,GAAgB,EAAE,CAAC;AAC5C,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,YAAY,CAAC,OAAe;IAC1C,OAAO,OAAuB,CAAC;AACjC,CAAC","sourcesContent":["/**\n * The spelling rule for feature ids, and the brands that carry its provenance.\n *\n * Split out of `service.ts` when the feature grammar grew the same brand\n * lattice the service grammar has: the two rules share a package because both\n * become directory names in the shared docs repo, but a feature id is not a\n * service id — the brands below are deliberately disjoint from `RawServiceId`\n * and `ServiceId`, so one grammar's checked value cannot stand in where the\n * other's is demanded.\n */\n\n/* ---------------------------------------------------------------- */\n/* The two provenances a feature name can have */\n/* ---------------------------------------------------------------- */\n\n/**\n * Same hierarchy as the service brands, for the same measured reason: the raw\n * form is the base — a name whose provenance is the repository — and the\n * checked form is a SUBTYPE, so `id === arg`, `Set<RawFeatureId>.has(id)` and\n * sorting a mixed list all stay the ordinary string business they are.\n */\ndeclare const provenance: unique symbol;\ndeclare const checked: unique symbol;\n\n/**\n * A feature id a repository listed this directory for: read back off a\n * `features/` directory name by the enumeration. It exists; it may still be an\n * illegal loam id — `core/repo/repo.ts`'s `listFeatures` deliberately returns\n * those, because `loam list` must show you the badly-named directory that is\n * there.\n */\nexport type RawFeatureId = string & { readonly [provenance]: \"loam\" };\n\n/** An id that passed `featureIdProblem`. The only form a caller may construct. */\nexport type FeatureId = RawFeatureId & { readonly [checked]: true };\n\n/**\n * Feature ids are `<word>-<number>`: the id has to survive being read back off\n * the directory name (`FEAT-101-payment-splitting` -> `FEAT-101`), or the\n * feature would answer to a name it was never given.\n *\n * Here for the same reason the service grammar is — this was spelled twice,\n * privately, in `commands/new.ts` and `core/openspec/`, and\n * docs/DESIGN.md rule 7 recorded the pair as a hazard rather than a fact. The\n * third caller is what made it one: `loam explore --as <FEAT>` interpolates its\n * argument into a `loam new` line that loam PRINTS for an agent to run, so\n * without this check `explore` cheerfully handed back a command `new` refuses.\n * A guard test catches that class only for literal source strings — a command\n * assembled at runtime from argv is invisible to it — which is exactly why the\n * grammar has to be shared rather than re-derived by whoever needs it next.\n */\nconst FEATURE_ID = /^[A-Za-z][A-Za-z0-9]*-\\d+$/;\n\n/** Prose form, shown verbatim in every refusal so the fix is obvious. */\nexport const FEATURE_ID_RULE = \"Expected <word>-<number>, e.g. FEAT-101 or BUG-42.\";\n\nexport function isFeatureId(id: unknown): id is string {\n return typeof id === \"string\" && FEATURE_ID.test(id);\n}\n\n/**\n * Why `id` is not a usable feature id, or null when it is — the `serviceIdProblem`\n * shape, so a caller already collecting findings reports the sentence a refusal\n * would print.\n */\nexport function featureIdProblem(id: unknown, label = \"feature id\"): string | null {\n if (!isFeatureId(id)) {\n const shown = typeof id === \"string\" ? `'${id}'` : String(id);\n return `${shown} is not a usable ${label}. ${FEATURE_ID_RULE}`;\n }\n return null;\n}\n\n/* ---------------------------------------------------------------- */\n/* The two constructors — the only place a brand is asserted */\n/* ---------------------------------------------------------------- */\n\n/**\n * The one constructor that validates — `parseServiceId`'s shape, for the same\n * reason: a discriminated pair rather than `FeatureId | string`, because that\n * second shape compiles and is unusable, and the problem sentence is the one\n * `featureIdProblem` already produces, so a caller that is collecting findings\n * prints exactly what a refusal would.\n */\nexport function parseFeatureId(\n raw: string,\n label = \"feature id\",\n): { ok: true; id: FeatureId } | { ok: false; problem: string } {\n const problem = featureIdProblem(raw, label);\n if (problem !== null) return { ok: false, problem };\n // The cast, on the line immediately after the check that earns it.\n return { ok: true, id: raw as FeatureId };\n}\n\n/**\n * An id derived from a directory name a repository listed. Says nothing about\n * whether it is a legal id — that is the point: the enumeration must be able to\n * name the directory that is there, however it is spelled.\n *\n * This is a cast, and it has to be: the derivation in\n * `core/repo/entries.ts`'s `featureIdFromDirName` starts from a plain `readdir`\n * string, and a brand does not survive it. What the rule in\n * `docs/CODE-STYLE.md` buys is that both of this file's casts live HERE and\n * nowhere else — a third one somewhere in `src/` is a finding, not a shortcut.\n */\nexport function rawFeatureId(derived: string): RawFeatureId {\n return derived as RawFeatureId;\n}\n"]}
@@ -0,0 +1,156 @@
1
+ /**
2
+ * The spelling rules for the ids loam turns into directory names.
3
+ *
4
+ * A service id is not a label: it becomes `services/<id>/` in the shared docs
5
+ * repo, and it is interpolated into paths by adopt, validate, vouch, gherkin and
6
+ * new. That makes it caller-controlled path input, so it is validated in exactly
7
+ * one place — here — rather than by each command inventing its own guard. The
8
+ * failure mode this closes is not theoretical: `--service ../../etc` would have
9
+ * had `unfiledServicePaths()` hand a writer a directory outside the docs repo, and
10
+ * `--service ''` would have made `services//spec.md` collapse to a file the
11
+ * enumeration in repo.ts can never see again.
12
+ *
13
+ * The rule is deliberately narrower than "a path segment that happens to work":
14
+ * ids are also compared, sorted and printed in tables, so shell metacharacters,
15
+ * spaces and leading dots are refused even where the filesystem would accept
16
+ * them. Widening the alphabet later is a compatible change; narrowing it is not.
17
+ *
18
+ * "Works" also means "works on Windows", which this project tests in CI: the
19
+ * two rules below are the ones POSIX accepts and Windows does not. They used to
20
+ * live in migrate-openspec.ts alone, as a second, stricter copy of this grammar
21
+ * — so the PRIMARY authoring path (`--service`, `--touches`, `adopt`) accepted
22
+ * ids the migration refused, and `services/CON/` or `services/payments./` got
23
+ * as far as a directory nobody can create or open. Narrowing is not a
24
+ * compatible change, which is exactly why it belongs here, once, rather than in
25
+ * whichever command happened to think of it.
26
+ */
27
+ /** The one grammar: alphanumeric head, then alphanumerics, dot, underscore, hyphen. */
28
+ const SERVICE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
29
+ /**
30
+ * The MS-DOS device names Windows still reserves. Reserved with any extension
31
+ * and in any case (`nul`, `NUL.txt`), so the test is on the stem — the part
32
+ * before the first dot — uppercased.
33
+ */
34
+ const WINDOWS_DEVICE = /^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/;
35
+ export function dirNameHazard(name) {
36
+ // `..` produces the same verdict the pattern would; it is not special-cased
37
+ // here — the CALLERS' prose is where "no '..'" gets its own words.
38
+ if (name.includes("..") || !SERVICE_ID.test(name))
39
+ return { kind: "grammar" };
40
+ const stem = name.split(".")[0];
41
+ if (WINDOWS_DEVICE.test(stem.toUpperCase()))
42
+ return { kind: "windows-device", stem };
43
+ if (/[. ]$/.test(name))
44
+ return { kind: "windows-trailing" };
45
+ return null;
46
+ }
47
+ /** Prose form of the rule, shown verbatim in every refusal so the fix is obvious. */
48
+ const SERVICE_ID_RULE = "a service id must start with a letter or digit and contain only letters, digits, '.', '_' or '-' " +
49
+ "(no slashes, no '..', no spaces) — it becomes the services/<id>/ directory name";
50
+ /** Refusal carrying the offending value, so a caller can quote it back. */
51
+ export class InvalidIdError extends Error {
52
+ value;
53
+ constructor(value, message) {
54
+ super(message);
55
+ this.value = value;
56
+ this.name = "InvalidIdError";
57
+ }
58
+ }
59
+ /**
60
+ * Why `id` is not a usable service id, or null when it is. Returning the reason
61
+ * instead of a boolean lets a caller that is already collecting findings report
62
+ * the same sentence a refusal would print.
63
+ */
64
+ export function serviceIdProblem(id, label = "--service") {
65
+ if (typeof id !== "string" || id === "") {
66
+ return `${label} must name a service: ${SERVICE_ID_RULE}.`;
67
+ }
68
+ const hazard = dirNameHazard(id);
69
+ if (hazard === null)
70
+ return null;
71
+ switch (hazard.kind) {
72
+ case "grammar":
73
+ return `Invalid ${label} '${id}': ${SERVICE_ID_RULE}.`;
74
+ // Both refusals below are about Windows, and both get their own sentence:
75
+ // "does not match the id grammar" is true and useless, because the id
76
+ // LOOKS fine and the problem is what the filesystem does with it.
77
+ case "windows-device":
78
+ return (`Invalid ${label} '${id}': '${hazard.stem}' is a reserved device name on Windows — ` +
79
+ `services/${id}/ cannot be created there at all, so the docs repo would only work on some of the machines that clone it.`);
80
+ case "windows-trailing":
81
+ return (`Invalid ${label} '${id}': a service id may not end with '.' or a space — ` +
82
+ `Windows strips both when creating a directory, so '${id}' and '${id.replace(/[. ]+$/, "")}' would silently become one directory there and two everywhere else.`);
83
+ }
84
+ }
85
+ /**
86
+ * Assert that `id` may be used as a `services/<id>/` directory name. Throws
87
+ * InvalidIdError; commands catch it and report `invalid-option`, because a bad
88
+ * id is always something the caller typed, never a state of the repository.
89
+ */
90
+ export function assertServiceId(id, label = "--service") {
91
+ const problem = serviceIdProblem(id, label);
92
+ if (problem !== null)
93
+ throw new InvalidIdError(typeof id === "string" ? id : String(id), problem);
94
+ }
95
+ /* ---------------------------------------------------------------- */
96
+ /* The three constructors — the only place a brand is asserted */
97
+ /* ---------------------------------------------------------------- */
98
+ /**
99
+ * The one constructor that validates. It takes a plain `string`, so a
100
+ * `DeclaredService` flows straight in — a brand is still a string — which is
101
+ * what makes this the single bridge out of document text.
102
+ *
103
+ * The result is a discriminated pair rather than `ServiceId | string`: that
104
+ * second shape compiles and is unusable, because nothing can narrow it back.
105
+ * The problem sentence is the one `serviceIdProblem` already produces, so a
106
+ * caller that is collecting findings prints exactly what a refusal would.
107
+ */
108
+ export function parseServiceId(raw, label = "--service") {
109
+ const problem = serviceIdProblem(raw, label);
110
+ if (problem !== null)
111
+ return { ok: false, problem };
112
+ // The cast, on the line immediately after the check that earns it.
113
+ return { ok: true, id: raw };
114
+ }
115
+ /**
116
+ * The plural form, for the two commands that take a LIST of ids.
117
+ *
118
+ * It exists because `for (const id of ids) assertServiceId(id)` narrows the
119
+ * ELEMENT and not the array: the loop leaves `string[]`, so the value that
120
+ * reaches core is unbranded no matter how thoroughly each item was checked.
121
+ * That is invisible in review and the compiler is the only thing that sees it.
122
+ *
123
+ * Returns the offending value alongside the problem so the caller prints the
124
+ * same sentence, with the same code, that the element-wise loop printed.
125
+ */
126
+ export function parseServiceIds(ids, label = "--service") {
127
+ const out = [];
128
+ for (const raw of ids) {
129
+ const parsed = parseServiceId(raw, label);
130
+ if (!parsed.ok)
131
+ return parsed;
132
+ out.push(parsed.id);
133
+ }
134
+ return { ok: true, ids: out };
135
+ }
136
+ /**
137
+ * A directory name off `readdir`. Says nothing about whether it is a legal id —
138
+ * that is the point, and `idProblem` is where the answer goes.
139
+ *
140
+ * This is a second cast, and it has to be: `readdir` and `basename` return plain
141
+ * `string`, and a brand does not survive them. What the rule in
142
+ * `docs/CODE-STYLE.md` actually buys is that all three casts live in THIS FILE
143
+ * and nowhere else. A fourth one somewhere in `src/` is a finding, not a shortcut.
144
+ */
145
+ export function rawServiceId(dirName) {
146
+ return dirName;
147
+ }
148
+ /**
149
+ * A name a document asserted. Third and last cast, for the crossing between
150
+ * LikeC4's parse output and loam's type system: `e.service ?? e.title` is a
151
+ * plain `string` and does not brand itself.
152
+ */
153
+ export function declaredService(text) {
154
+ return text;
155
+ }
156
+ //# sourceMappingURL=service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.js","sourceRoot":"","sources":["../../../../src/core/kernel/ids/service.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The spelling rules for the ids loam turns into directory names.\n *\n * A service id is not a label: it becomes `services/<id>/` in the shared docs\n * repo, and it is interpolated into paths by adopt, validate, vouch, gherkin and\n * new. That makes it caller-controlled path input, so it is validated in exactly\n * one place — here — rather than by each command inventing its own guard. The\n * failure mode this closes is not theoretical: `--service ../../etc` would have\n * had `unfiledServicePaths()` hand a writer a directory outside the docs repo, and\n * `--service ''` would have made `services//spec.md` collapse to a file the\n * enumeration in repo.ts can never see again.\n *\n * The rule is deliberately narrower than \"a path segment that happens to work\":\n * ids are also compared, sorted and printed in tables, so shell metacharacters,\n * spaces and leading dots are refused even where the filesystem would accept\n * them. Widening the alphabet later is a compatible change; narrowing it is not.\n *\n * \"Works\" also means \"works on Windows\", which this project tests in CI: the\n * two rules below are the ones POSIX accepts and Windows does not. They used to\n * live in migrate-openspec.ts alone, as a second, stricter copy of this grammar\n * — so the PRIMARY authoring path (`--service`, `--touches`, `adopt`) accepted\n * ids the migration refused, and `services/CON/` or `services/payments./` got\n * as far as a directory nobody can create or open. Narrowing is not a\n * compatible change, which is exactly why it belongs here, once, rather than in\n * whichever command happened to think of it.\n */\n\n/* ---------------------------------------------------------------- */\n/* The three provenances a service name can have */\n/* ---------------------------------------------------------------- */\n\n/**\n * A service name is one of three things, and until now all three were `string`.\n *\n * The distinction that matters is not \"valid\" versus \"invalid\" — it is WHERE the\n * name came from, because that is what decides whether loam may join it into a\n * path. A directory name read off disk is safe to join whether or not it is a\n * legal id: it demonstrably exists, `readdir` just returned it. A name a\n * *document* asserts is not, because nothing has checked it and `metadata {\n * service '../../../etc' }` parses in LikeC4 without a single error.\n *\n * So `RawServiceId` is the base — a name whose provenance is the repository —\n * and `ServiceId` is that plus the grammar check, which makes it a SUBTYPE\n * rather than a sibling. That hierarchy is deliberate and was measured: with\n * three disjoint brands, `id === dirName`, `Set<RawServiceId>.has(id)` and\n * `[...ids, ...raws].sort()` all stop compiling, and none of them is a safety\n * question — they are the ordinary business of comparing a validated id with a\n * directory that exists. Only `DeclaredService` is disjoint, because only there\n * is the comparison a real crossing.\n *\n * `path.join` is NOT the enforcement point and cannot be made one: node types it\n * `join(...paths: string[])`, so every brand satisfies it. The guarantee lives\n * in `unfiledServicePaths`' own parameter type — `PathableService`, declared below and\n * demanded by that signature — so a `DeclaredService` at the call site is a\n * compile error, not a review finding. Code that spells `services/<svc>/` with\n * a bare join is outside the guarantee; `repo/paths.ts`'s banner names it.\n */\ndeclare const provenance: unique symbol;\ndeclare const checked: unique symbol;\n\n/**\n * A directory name read off `services/` or `features/<id>/specs/`. It exists;\n * it may still be an illegal loam id — `core/repo/repo.ts`'s `listServices`\n * deliberately returns those, reporting the failure as an `idProblem` field,\n * because `loam list` must show you the badly-named directory that is there.\n */\nexport type RawServiceId = string & { readonly [provenance]: \"loam\" };\n\n/** A name that passed `serviceIdProblem`. The only form a caller may construct. */\nexport type ServiceId = RawServiceId & { readonly [checked]: true };\n\n/**\n * A name a DOCUMENT asserts — `metadata { service '...' }` in a `.likec4` file,\n * an element title standing in for one, or an element id. Arbitrary unchecked\n * text, and disjoint from the two above on purpose: it must not reach a path\n * join without going through `parseServiceId` first.\n */\nexport type DeclaredService = string & { readonly [provenance]: \"document\" };\n\n/** What `unfiledServicePaths` accepts: a name whose provenance is the repository. */\nexport type PathableService = RawServiceId;\n\n/** The one grammar: alphanumeric head, then alphanumerics, dot, underscore, hyphen. */\nconst SERVICE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;\n\n/**\n * The MS-DOS device names Windows still reserves. Reserved with any extension\n * and in any case (`nul`, `NUL.txt`), so the test is on the stem — the part\n * before the first dot — uppercased.\n */\nconst WINDOWS_DEVICE = /^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/;\n\n/**\n * Why `name` cannot be a directory name in the docs repo, as a discriminated\n * fact rather than a sentence. Split out of `serviceIdProblem` when subsystem\n * names arrived (`./subsystem.ts`): both kinds of name become directories in\n * the same tree and share one flat namespace, so they must share ONE grammar\n * and one set of Windows rules — a name legal as one and illegal as the other\n * would make the namespace-collision refusal incoherent. Each caller keeps its\n * own prose, because \"a service id\" and \"a subsystem name\" point the reader at\n * different fixes; the CHECKS are what may never fork.\n */\nexport type DirNameHazard =\n | { kind: \"grammar\" }\n | { kind: \"windows-device\"; stem: string }\n | { kind: \"windows-trailing\" };\n\nexport function dirNameHazard(name: string): DirNameHazard | null {\n // `..` produces the same verdict the pattern would; it is not special-cased\n // here — the CALLERS' prose is where \"no '..'\" gets its own words.\n if (name.includes(\"..\") || !SERVICE_ID.test(name)) return { kind: \"grammar\" };\n const stem = name.split(\".\")[0]!;\n if (WINDOWS_DEVICE.test(stem.toUpperCase())) return { kind: \"windows-device\", stem };\n if (/[. ]$/.test(name)) return { kind: \"windows-trailing\" };\n return null;\n}\n\n/** Prose form of the rule, shown verbatim in every refusal so the fix is obvious. */\nconst SERVICE_ID_RULE =\n \"a service id must start with a letter or digit and contain only letters, digits, '.', '_' or '-' \" +\n \"(no slashes, no '..', no spaces) — it becomes the services/<id>/ directory name\";\n\n/** Refusal carrying the offending value, so a caller can quote it back. */\nexport class InvalidIdError extends Error {\n constructor(\n readonly value: string,\n message: string,\n ) {\n super(message);\n this.name = \"InvalidIdError\";\n }\n}\n\n/**\n * Why `id` is not a usable service id, or null when it is. Returning the reason\n * instead of a boolean lets a caller that is already collecting findings report\n * the same sentence a refusal would print.\n */\nexport function serviceIdProblem(id: unknown, label = \"--service\"): string | null {\n if (typeof id !== \"string\" || id === \"\") {\n return `${label} must name a service: ${SERVICE_ID_RULE}.`;\n }\n const hazard = dirNameHazard(id);\n if (hazard === null) return null;\n switch (hazard.kind) {\n case \"grammar\":\n return `Invalid ${label} '${id}': ${SERVICE_ID_RULE}.`;\n // Both refusals below are about Windows, and both get their own sentence:\n // \"does not match the id grammar\" is true and useless, because the id\n // LOOKS fine and the problem is what the filesystem does with it.\n case \"windows-device\":\n return (\n `Invalid ${label} '${id}': '${hazard.stem}' is a reserved device name on Windows — ` +\n `services/${id}/ cannot be created there at all, so the docs repo would only work on some of the machines that clone it.`\n );\n case \"windows-trailing\":\n return (\n `Invalid ${label} '${id}': a service id may not end with '.' or a space — ` +\n `Windows strips both when creating a directory, so '${id}' and '${id.replace(/[. ]+$/, \"\")}' would silently become one directory there and two everywhere else.`\n );\n }\n}\n\n/**\n * Assert that `id` may be used as a `services/<id>/` directory name. Throws\n * InvalidIdError; commands catch it and report `invalid-option`, because a bad\n * id is always something the caller typed, never a state of the repository.\n */\nexport function assertServiceId(id: unknown, label = \"--service\"): asserts id is ServiceId {\n const problem = serviceIdProblem(id, label);\n if (problem !== null) throw new InvalidIdError(typeof id === \"string\" ? id : String(id), problem);\n}\n\n/* ---------------------------------------------------------------- */\n/* The three constructors — the only place a brand is asserted */\n/* ---------------------------------------------------------------- */\n\n/**\n * The one constructor that validates. It takes a plain `string`, so a\n * `DeclaredService` flows straight in — a brand is still a string — which is\n * what makes this the single bridge out of document text.\n *\n * The result is a discriminated pair rather than `ServiceId | string`: that\n * second shape compiles and is unusable, because nothing can narrow it back.\n * The problem sentence is the one `serviceIdProblem` already produces, so a\n * caller that is collecting findings prints exactly what a refusal would.\n */\nexport function parseServiceId(\n raw: string,\n label = \"--service\",\n): { ok: true; id: ServiceId } | { ok: false; problem: string } {\n const problem = serviceIdProblem(raw, label);\n if (problem !== null) return { ok: false, problem };\n // The cast, on the line immediately after the check that earns it.\n return { ok: true, id: raw as ServiceId };\n}\n\n/**\n * The plural form, for the two commands that take a LIST of ids.\n *\n * It exists because `for (const id of ids) assertServiceId(id)` narrows the\n * ELEMENT and not the array: the loop leaves `string[]`, so the value that\n * reaches core is unbranded no matter how thoroughly each item was checked.\n * That is invisible in review and the compiler is the only thing that sees it.\n *\n * Returns the offending value alongside the problem so the caller prints the\n * same sentence, with the same code, that the element-wise loop printed.\n */\nexport function parseServiceIds(\n ids: readonly string[],\n label = \"--service\",\n): { ok: true; ids: ServiceId[] } | { ok: false; problem: string } {\n const out: ServiceId[] = [];\n for (const raw of ids) {\n const parsed = parseServiceId(raw, label);\n if (!parsed.ok) return parsed;\n out.push(parsed.id);\n }\n return { ok: true, ids: out };\n}\n\n/**\n * A directory name off `readdir`. Says nothing about whether it is a legal id —\n * that is the point, and `idProblem` is where the answer goes.\n *\n * This is a second cast, and it has to be: `readdir` and `basename` return plain\n * `string`, and a brand does not survive them. What the rule in\n * `docs/CODE-STYLE.md` actually buys is that all three casts live in THIS FILE\n * and nowhere else. A fourth one somewhere in `src/` is a finding, not a shortcut.\n */\nexport function rawServiceId(dirName: string): RawServiceId {\n return dirName as RawServiceId;\n}\n\n/**\n * A name a document asserted. Third and last cast, for the crossing between\n * LikeC4's parse output and loam's type system: `e.service ?? e.title` is a\n * plain `string` and does not brand itself.\n */\nexport function declaredService(text: string): DeclaredService {\n return text as DeclaredService;\n}\n"]}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * The spelling rule for subsystem names — the directories that GROUP services
3
+ * under `services/`, marked by a `subsystem.yaml` (the walk that classifies
4
+ * them lives in `core/repo/tree/`).
5
+ *
6
+ * A subsystem name takes the service-id grammar and the same Windows rules,
7
+ * through the shared `dirNameHazard` in `./service.ts`, because both kinds of
8
+ * name become directory names in the same tree and share ONE flat namespace —
9
+ * unique across the whole tree at any depth, services and subsystems together.
10
+ * If the two grammars could drift, a name legal as one and illegal as the
11
+ * other would make the namespace-collision refusal incoherent.
12
+ *
13
+ * The BRAND is distinct on purpose, and deliberately not a subtype of
14
+ * `ServiceId`: a subsystem is a place services live, never a thing a `--service`
15
+ * flag may name or a path a spec's frontmatter may claim — `--into <name>` and
16
+ * `--service <id>` must not be satisfiable by each other's values. Placement is
17
+ * never part of any identity, and the type system is where that stops being a
18
+ * review comment — `repo/paths.ts`'s `subsystemPathUnder` is the consumer that
19
+ * DEMANDS the brand, so the joins `subsystem new` and `rename` build cannot be
20
+ * fed a raw argv string: a brand no signature requires is a validator with
21
+ * good intentions, which is exactly what this one briefly was.
22
+ */
23
+ import { dirNameHazard } from "./service.js";
24
+ /** Prose form of the rule, shown verbatim in every refusal so the fix is obvious. */
25
+ const SUBSYSTEM_NAME_RULE = "a subsystem name must start with a letter or digit and contain only letters, digits, '.', '_' or '-' " +
26
+ "(no slashes, no '..', no spaces) — it becomes a directory name under services/";
27
+ /**
28
+ * Why `name` is not a usable subsystem name, or null when it is. The same
29
+ * shape as `serviceIdProblem`, for the same reason: the tree walk reports the
30
+ * sentence as a finding (`subsystem.name-invalid`) while a command refusal
31
+ * prints it verbatim, and the two must not disagree.
32
+ */
33
+ export function subsystemNameProblem(name, label = "subsystem name") {
34
+ if (typeof name !== "string" || name === "") {
35
+ return `${label} must name a subsystem: ${SUBSYSTEM_NAME_RULE}.`;
36
+ }
37
+ const hazard = dirNameHazard(name);
38
+ if (hazard === null)
39
+ return null;
40
+ switch (hazard.kind) {
41
+ case "grammar":
42
+ return `Invalid ${label} '${name}': ${SUBSYSTEM_NAME_RULE}.`;
43
+ case "windows-device":
44
+ return (`Invalid ${label} '${name}': '${hazard.stem}' is a reserved device name on Windows — ` +
45
+ `a '${name}/' directory cannot be created there at all, so the docs repo would only work on some of the machines that clone it.`);
46
+ case "windows-trailing":
47
+ return (`Invalid ${label} '${name}': a subsystem name may not end with '.' or a space — ` +
48
+ `Windows strips both when creating a directory, so '${name}' and '${name.replace(/[. ]+$/, "")}' would silently become one directory there and two everywhere else.`);
49
+ }
50
+ }
51
+ /**
52
+ * The one constructor that validates. Discriminated pair rather than
53
+ * `SubsystemName | string` for `parseServiceId`'s reason: the union shape
54
+ * compiles and is unusable, because nothing can narrow it back.
55
+ */
56
+ export function parseSubsystemName(raw, label = "subsystem name") {
57
+ const problem = subsystemNameProblem(raw, label);
58
+ if (problem !== null)
59
+ return { ok: false, problem };
60
+ // The cast, on the line immediately after the check that earns it.
61
+ return { ok: true, name: raw };
62
+ }
63
+ //# sourceMappingURL=subsystem.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subsystem.js","sourceRoot":"","sources":["../../../../src/core/kernel/ids/subsystem.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAO7C,qFAAqF;AACrF,MAAM,mBAAmB,GACvB,uGAAuG;IACvG,gFAAgF,CAAC;AAEnF;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,IAAa,EAAE,KAAK,GAAG,gBAAgB;IAC1E,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,EAAE,EAAE,CAAC;QAC5C,OAAO,GAAG,KAAK,2BAA2B,mBAAmB,GAAG,CAAC;IACnE,CAAC;IACD,MAAM,MAAM,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;IACnC,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACjC,QAAQ,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,KAAK,SAAS;YACZ,OAAO,WAAW,KAAK,KAAK,IAAI,MAAM,mBAAmB,GAAG,CAAC;QAC/D,KAAK,gBAAgB;YACnB,OAAO,CACL,WAAW,KAAK,KAAK,IAAI,OAAO,MAAM,CAAC,IAAI,2CAA2C;gBACtF,MAAM,IAAI,sHAAsH,CACjI,CAAC;QACJ,KAAK,kBAAkB;YACrB,OAAO,CACL,WAAW,KAAK,KAAK,IAAI,wDAAwD;gBACjF,sDAAsD,IAAI,UAAU,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,sEAAsE,CACrK,CAAC;IACN,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAChC,GAAW,EACX,KAAK,GAAG,gBAAgB;IAExB,MAAM,OAAO,GAAG,oBAAoB,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACjD,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;IACpD,mEAAmE;IACnE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,GAAoB,EAAE,CAAC;AAClD,CAAC","sourcesContent":["/**\n * The spelling rule for subsystem names — the directories that GROUP services\n * under `services/`, marked by a `subsystem.yaml` (the walk that classifies\n * them lives in `core/repo/tree/`).\n *\n * A subsystem name takes the service-id grammar and the same Windows rules,\n * through the shared `dirNameHazard` in `./service.ts`, because both kinds of\n * name become directory names in the same tree and share ONE flat namespace —\n * unique across the whole tree at any depth, services and subsystems together.\n * If the two grammars could drift, a name legal as one and illegal as the\n * other would make the namespace-collision refusal incoherent.\n *\n * The BRAND is distinct on purpose, and deliberately not a subtype of\n * `ServiceId`: a subsystem is a place services live, never a thing a `--service`\n * flag may name or a path a spec's frontmatter may claim — `--into <name>` and\n * `--service <id>` must not be satisfiable by each other's values. Placement is\n * never part of any identity, and the type system is where that stops being a\n * review comment — `repo/paths.ts`'s `subsystemPathUnder` is the consumer that\n * DEMANDS the brand, so the joins `subsystem new` and `rename` build cannot be\n * fed a raw argv string: a brand no signature requires is a validator with\n * good intentions, which is exactly what this one briefly was.\n */\nimport { dirNameHazard } from \"./service.js\";\n\ndeclare const checked: unique symbol;\n\n/** A name that passed `subsystemNameProblem`. The only form a caller may construct. */\nexport type SubsystemName = string & { readonly [checked]: \"subsystem\" };\n\n/** Prose form of the rule, shown verbatim in every refusal so the fix is obvious. */\nconst SUBSYSTEM_NAME_RULE =\n \"a subsystem name must start with a letter or digit and contain only letters, digits, '.', '_' or '-' \" +\n \"(no slashes, no '..', no spaces) — it becomes a directory name under services/\";\n\n/**\n * Why `name` is not a usable subsystem name, or null when it is. The same\n * shape as `serviceIdProblem`, for the same reason: the tree walk reports the\n * sentence as a finding (`subsystem.name-invalid`) while a command refusal\n * prints it verbatim, and the two must not disagree.\n */\nexport function subsystemNameProblem(name: unknown, label = \"subsystem name\"): string | null {\n if (typeof name !== \"string\" || name === \"\") {\n return `${label} must name a subsystem: ${SUBSYSTEM_NAME_RULE}.`;\n }\n const hazard = dirNameHazard(name);\n if (hazard === null) return null;\n switch (hazard.kind) {\n case \"grammar\":\n return `Invalid ${label} '${name}': ${SUBSYSTEM_NAME_RULE}.`;\n case \"windows-device\":\n return (\n `Invalid ${label} '${name}': '${hazard.stem}' is a reserved device name on Windows — ` +\n `a '${name}/' directory cannot be created there at all, so the docs repo would only work on some of the machines that clone it.`\n );\n case \"windows-trailing\":\n return (\n `Invalid ${label} '${name}': a subsystem name may not end with '.' or a space — ` +\n `Windows strips both when creating a directory, so '${name}' and '${name.replace(/[. ]+$/, \"\")}' would silently become one directory there and two everywhere else.`\n );\n }\n}\n\n/**\n * The one constructor that validates. Discriminated pair rather than\n * `SubsystemName | string` for `parseServiceId`'s reason: the union shape\n * compiles and is unusable, because nothing can narrow it back.\n */\nexport function parseSubsystemName(\n raw: string,\n label = \"subsystem name\",\n): { ok: true; name: SubsystemName } | { ok: false; problem: string } {\n const problem = subsystemNameProblem(raw, label);\n if (problem !== null) return { ok: false, problem };\n // The cast, on the line immediately after the check that earns it.\n return { ok: true, name: raw as SubsystemName };\n}\n"]}
@@ -73,9 +73,10 @@ export function resolveInside(root, candidate, label = "path") {
73
73
  /**
74
74
  * Snapshot manifests are portable data, so their path spelling is stricter
75
75
  * than an interactive path: forward slashes, no redundant components, and a
76
- * file (not directory) name at the end.
76
+ * file (not directory) name at the end. The one constructor of `PortablePath`,
77
+ * because these checks are what the brand asserts.
77
78
  */
78
- export function resolvePortableFileInside(root, candidate, label = "path") {
79
+ export function portablePathOf(candidate, label = "path") {
79
80
  if (candidate.includes("\\")) {
80
81
  throw new UnsafePathError(candidate, `${label} must use forward slashes`);
81
82
  }
@@ -83,7 +84,20 @@ export function resolvePortableFileInside(root, candidate, label = "path") {
83
84
  if (parts.some((part) => part === "" || part === "." || part === "..")) {
84
85
  throw new UnsafePathError(candidate, `${label} must be a canonical relative file path`);
85
86
  }
86
- return resolveInside(root, candidate, label);
87
+ // The cast, on the line immediately after the checks that earn it.
88
+ return candidate;
89
+ }
90
+ /**
91
+ * Containment for a path that already EARNED the portable spelling — the one
92
+ * signature that demands the brand, so `PortablePath` has a consumer the
93
+ * compiler checks rather than a comment claiming the order of two calls.
94
+ */
95
+ function resolvePortableInside(root, portable, label) {
96
+ return resolveInside(root, portable, label);
97
+ }
98
+ /** The portable spelling checks, then `resolveInside`'s containment proof — the ONE spelling of the pair. */
99
+ export function resolvePortableFileInside(root, candidate, label = "path") {
100
+ return resolvePortableInside(root, portablePathOf(candidate, label), label);
87
101
  }
88
102
  /**
89
103
  * The same portable spelling rules, with containment judged LEXICALLY — for a
@@ -99,7 +113,7 @@ export function resolvePortableFileInside(root, candidate, label = "path") {
99
113
  * between two checkouts — is a supported layout that every walk of the repo
100
114
  * FOLLOWS rather than refuses (repo.ts's `entryIs` says so in full), and that
101
115
  * `archive` already writes through: its living-document writes are planned from
102
- * `servicePaths()` and never see this file. With the realpath test in force on
116
+ * `unfiledServicePaths()`/`servicePathsAt()` and never see this file. With the realpath test in force on
103
117
  * the manifest's `path` field, a `services/<svc>/` mounted that way made an
104
118
  * interrupted archive permanently unrepairable — the retry was told the
105
119
  * snapshot could not be read when it was perfectly intact, and the half-merge it
@@ -0,0 +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"]}
@@ -0,0 +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"]}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Who links to what, across the whole repository — the link graph read
3
+ * backwards.
4
+ *
5
+ * `link.unresolved` asks each document about its own links. This asks the
6
+ * opposite question, and it is the one the glossary is built on: "which
7
+ * documents use this term" is exactly "which documents link to `glossary/<term>.md`",
8
+ * and no single target can answer it. A term is cited from a service's spec, a
9
+ * capability document and a feature's intent, so the index is fleet-scope by
10
+ * construction.
11
+ *
12
+ * IT IS THE SAME WALK AS THE FINDING, deliberately — `resolveLinks` produces
13
+ * both halves of one answer, and the corpus is `core/links/corpus.ts`'s single
14
+ * definition. An index built from a second enumeration would report a term as
15
+ * unused while the document citing it was being graded one target over.
16
+ *
17
+ * AN UNREADABLE DOCUMENT IS RETURNED, not swallowed. A term can only be shown as
18
+ * cited by nobody if loam actually read everybody, and a UTF-16 spec.md would
19
+ * otherwise turn into a silently missing citation. Under `validate` the same
20
+ * file already earns `link.unreadable`; `loam list` has no such finding, so it
21
+ * needs the list to say what it did not read.
22
+ */
23
+ import { readFile } from "node:fs/promises";
24
+ import { existsSync } from "node:fs";
25
+ import { resolve } from "node:path";
26
+ import { repoPath } from "../envelope/json.js";
27
+ import { decodeDocument } from "../kernel/document-bytes.js";
28
+ import { pathCaseIndex } from "./case.js";
29
+ import { allAuthoredDocuments } from "./corpus.js";
30
+ import { resolveLinks } from "./findings.js";
31
+ /**
32
+ * Build the index over every authored document in the repository.
33
+ *
34
+ * One `pathCaseIndex` for the whole walk, because this one really is fleet-wide
35
+ * and the same directories are asked about hundreds of times. Still not a
36
+ * module-level cache: it lives exactly as long as this call.
37
+ */
38
+ export async function backlinkIndex(docsDir, fleet) {
39
+ const linkers = new Map();
40
+ const unreadable = [];
41
+ const scope = { docsDir, cases: pathCaseIndex() };
42
+ for (const path of await allAuthoredDocuments(docsDir, fleet)) {
43
+ if (!existsSync(path))
44
+ continue;
45
+ let text;
46
+ try {
47
+ text = fleet === undefined ? decodeDocument(await readFile(path), path) : await fleet.readText(path);
48
+ }
49
+ catch {
50
+ unreadable.push(repoPath(docsDir, path));
51
+ continue;
52
+ }
53
+ const from = repoPath(docsDir, path);
54
+ for (const { target } of resolveLinks({ path, text }, scope).resolved) {
55
+ const key = resolve(target);
56
+ let sources = linkers.get(key);
57
+ if (sources === undefined)
58
+ linkers.set(key, (sources = new Set()));
59
+ sources.add(from);
60
+ }
61
+ }
62
+ unreadable.sort();
63
+ return {
64
+ linkersOf: (target) => [...(linkers.get(resolve(target)) ?? [])].sort(),
65
+ unreadable,
66
+ };
67
+ }
68
+ //# sourceMappingURL=backlinks.js.map
@@ -0,0 +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"]}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Is a path spelled the way it is stored — the half of "does this link resolve"
3
+ * that `existsSync` cannot answer.
4
+ *
5
+ * `[Order](Order.md)` beside a file called `order.md` resolves on Windows and
6
+ * macOS and 404s on GitHub, which renders these documents and is
7
+ * case-sensitive, as is every Linux CI runner. That is the exact shape loam
8
+ * refuses everywhere else: a check that is green on the author's machine and red
9
+ * where the work is actually read. `link.unresolved` shipped without it as a
10
+ * recorded limit; this closes it.
11
+ *
12
+ * THE ANSWER IS A DIRECTORY LISTING, not a filesystem probe, because a probe is
13
+ * what is already wrong: on a case-insensitive volume every spelling of a name
14
+ * stats successfully, so the only place the true spelling exists is the parent's
15
+ * own listing. Each path segment between the docs root and the target is
16
+ * checked against that listing, verbatim.
17
+ *
18
+ * THE INDEX IS PER-INVOCATION AND PASSED, never module-level. A cached listing
19
+ * that outlived one command would answer for a directory that has since
20
+ * changed, and in this repository's forked test processes a module-level memo
21
+ * leaks between fixtures — `AGENTS.md` names that hazard directly. Cost is one
22
+ * `readdir` per distinct directory a link points into, which for a docs repo is
23
+ * a handful.
24
+ */
25
+ import { readdirSync } from "node:fs";
26
+ import { relative, resolve, sep } from "node:path";
27
+ /** A fresh index. One per command invocation — see the module note on why it is not shared. */
28
+ export function pathCaseIndex() {
29
+ const listings = new Map();
30
+ const namesIn = (dir) => {
31
+ let names = listings.get(dir);
32
+ if (names === undefined) {
33
+ try {
34
+ names = new Set(readdirSync(dir));
35
+ }
36
+ catch {
37
+ // An unreadable directory answers nothing, and the honest reading is
38
+ // "not disproved": the target stat'ed, so something is there. Refusing
39
+ // a link because its parent denied a listing would convict the author
40
+ // for a permission bit.
41
+ names = new Set();
42
+ }
43
+ listings.set(dir, names);
44
+ }
45
+ return names;
46
+ };
47
+ return {
48
+ spelledExactly(root, target) {
49
+ const rootAbs = resolve(root);
50
+ const rel = relative(rootAbs, resolve(target));
51
+ if (rel === "")
52
+ return true;
53
+ let dir = rootAbs;
54
+ for (const segment of rel.split(sep)) {
55
+ const names = namesIn(dir);
56
+ // The empty set is the unreadable-directory case above: stop asking
57
+ // rather than convict every segment beneath it.
58
+ if (names.size > 0 && !names.has(segment))
59
+ return false;
60
+ dir = resolve(dir, segment);
61
+ }
62
+ return true;
63
+ },
64
+ storedAs(dir, name) {
65
+ const wanted = name.toLowerCase();
66
+ for (const stored of namesIn(resolve(dir))) {
67
+ if (stored !== name && stored.toLowerCase() === wanted)
68
+ return stored;
69
+ }
70
+ return undefined;
71
+ },
72
+ };
73
+ }
74
+ //# sourceMappingURL=case.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"case.js","sourceRoot":"","sources":["../../../src/core/links/case.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AACtC,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAsBnD,+FAA+F;AAC/F,MAAM,UAAU,aAAa;IAC3B,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAuB,CAAC;IAChD,MAAM,OAAO,GAAG,CAAC,GAAW,EAAe,EAAE;QAC3C,IAAI,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC9B,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,IAAI,CAAC;gBACH,KAAK,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC;YACpC,CAAC;YAAC,MAAM,CAAC;gBACP,qEAAqE;gBACrE,uEAAuE;gBACvE,sEAAsE;gBACtE,wBAAwB;gBACxB,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;YAC5B,CAAC;YACD,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAC3B,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC,CAAC;IAEF,OAAO;QACL,cAAc,CAAC,IAAI,EAAE,MAAM;YACzB,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;YAC9B,MAAM,GAAG,GAAG,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;YAC/C,IAAI,GAAG,KAAK,EAAE;gBAAE,OAAO,IAAI,CAAC;YAC5B,IAAI,GAAG,GAAG,OAAO,CAAC;YAClB,KAAK,MAAM,OAAO,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;gBACrC,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;gBAC3B,oEAAoE;gBACpE,gDAAgD;gBAChD,IAAI,KAAK,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC;oBAAE,OAAO,KAAK,CAAC;gBACxD,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;YAC9B,CAAC;YACD,OAAO,IAAI,CAAC;QACd,CAAC;QACD,QAAQ,CAAC,GAAG,EAAE,IAAI;YAChB,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;YAClC,KAAK,MAAM,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;gBAC3C,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,CAAC,WAAW,EAAE,KAAK,MAAM;oBAAE,OAAO,MAAM,CAAC;YACxE,CAAC;YACD,OAAO,SAAS,CAAC;QACnB,CAAC;KACF,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Is a path spelled the way it is stored — the half of \"does this link resolve\"\n * that `existsSync` cannot answer.\n *\n * `[Order](Order.md)` beside a file called `order.md` resolves on Windows and\n * macOS and 404s on GitHub, which renders these documents and is\n * case-sensitive, as is every Linux CI runner. That is the exact shape loam\n * refuses everywhere else: a check that is green on the author's machine and red\n * where the work is actually read. `link.unresolved` shipped without it as a\n * recorded limit; this closes it.\n *\n * THE ANSWER IS A DIRECTORY LISTING, not a filesystem probe, because a probe is\n * what is already wrong: on a case-insensitive volume every spelling of a name\n * stats successfully, so the only place the true spelling exists is the parent's\n * own listing. Each path segment between the docs root and the target is\n * checked against that listing, verbatim.\n *\n * THE INDEX IS PER-INVOCATION AND PASSED, never module-level. A cached listing\n * that outlived one command would answer for a directory that has since\n * changed, and in this repository's forked test processes a module-level memo\n * leaks between fixtures — `AGENTS.md` names that hazard directly. Cost is one\n * `readdir` per distinct directory a link points into, which for a docs repo is\n * a handful.\n */\nimport { readdirSync } from \"node:fs\";\nimport { relative, resolve, sep } from \"node:path\";\n\nexport interface PathCaseIndex {\n /**\n * True when every segment of `target` below `root` is spelled exactly as the\n * filesystem stores it. `target` must already be known to lie inside `root`;\n * this asks only about spelling.\n */\n spelledExactly(root: string, target: string): boolean;\n /**\n * The name `dir` actually stores for `name`, when one differs from it only by\n * case — the hint that turns \"resolves to nothing\" into \"you spelled it\n * `Order.md` and it is stored as `order.md`\".\n *\n * Asked on both platforms and not only the case-insensitive one, which is the\n * whole point: on Linux the same link fails `existsSync` outright and on\n * Windows it fails `spelledExactly`, and a diagnosis that differed by\n * operating system would be a machine contract that does not hold.\n */\n storedAs(dir: string, name: string): string | undefined;\n}\n\n/** A fresh index. One per command invocation — see the module note on why it is not shared. */\nexport function pathCaseIndex(): PathCaseIndex {\n const listings = new Map<string, Set<string>>();\n const namesIn = (dir: string): Set<string> => {\n let names = listings.get(dir);\n if (names === undefined) {\n try {\n names = new Set(readdirSync(dir));\n } catch {\n // An unreadable directory answers nothing, and the honest reading is\n // \"not disproved\": the target stat'ed, so something is there. Refusing\n // a link because its parent denied a listing would convict the author\n // for a permission bit.\n names = new Set<string>();\n }\n listings.set(dir, names);\n }\n return names;\n };\n\n return {\n spelledExactly(root, target) {\n const rootAbs = resolve(root);\n const rel = relative(rootAbs, resolve(target));\n if (rel === \"\") return true;\n let dir = rootAbs;\n for (const segment of rel.split(sep)) {\n const names = namesIn(dir);\n // The empty set is the unreadable-directory case above: stop asking\n // rather than convict every segment beneath it.\n if (names.size > 0 && !names.has(segment)) return false;\n dir = resolve(dir, segment);\n }\n return true;\n },\n storedAs(dir, name) {\n const wanted = name.toLowerCase();\n for (const stored of namesIn(resolve(dir))) {\n if (stored !== name && stored.toLowerCase() === wanted) return stored;\n }\n return undefined;\n },\n };\n}\n"]}