@intentius/chant 0.79.0 → 0.81.0

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 (462) hide show
  1. package/dist/audit/core.d.ts.map +1 -1
  2. package/dist/audit/discover.d.ts +30 -0
  3. package/dist/audit/discover.d.ts.map +1 -1
  4. package/dist/audit/report-html.d.ts +5 -0
  5. package/dist/audit/report-html.d.ts.map +1 -1
  6. package/dist/audit/report-model.d.ts +13 -0
  7. package/dist/audit/report-model.d.ts.map +1 -1
  8. package/dist/audit/terraform-state.d.ts +13 -0
  9. package/dist/audit/terraform-state.d.ts.map +1 -1
  10. package/dist/cli/command-group.d.ts +6 -0
  11. package/dist/cli/command-group.d.ts.map +1 -1
  12. package/dist/cli/commands/audit.d.ts +44 -3
  13. package/dist/cli/commands/audit.d.ts.map +1 -1
  14. package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
  15. package/dist/cli/commands/check-lexicon.d.ts +8 -0
  16. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  17. package/dist/cli/commands/doctor.d.ts.map +1 -1
  18. package/dist/cli/commands/import-agents.d.ts.map +1 -1
  19. package/dist/cli/commands/init.d.ts +6 -0
  20. package/dist/cli/commands/init.d.ts.map +1 -1
  21. package/dist/cli/commands/lint.d.ts.map +1 -1
  22. package/dist/cli/commands/onboard.d.ts.map +1 -1
  23. package/dist/cli/commands/update.d.ts +6 -0
  24. package/dist/cli/commands/update.d.ts.map +1 -1
  25. package/dist/cli/commands/vendor.d.ts +5 -0
  26. package/dist/cli/commands/vendor.d.ts.map +1 -1
  27. package/dist/cli/conflict-check.d.ts +6 -0
  28. package/dist/cli/conflict-check.d.ts.map +1 -1
  29. package/dist/cli/handlers/build.d.ts.map +1 -1
  30. package/dist/cli/handlers/components.d.ts.map +1 -1
  31. package/dist/cli/handlers/dev.d.ts.map +1 -1
  32. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  33. package/dist/cli/handlers/init.d.ts.map +1 -1
  34. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  35. package/dist/cli/handlers/misc.d.ts.map +1 -1
  36. package/dist/cli/handlers/operator.d.ts +7 -0
  37. package/dist/cli/handlers/operator.d.ts.map +1 -1
  38. package/dist/cli/handlers/promote.d.ts +20 -0
  39. package/dist/cli/handlers/promote.d.ts.map +1 -0
  40. package/dist/cli/handlers/run-generate.d.ts +39 -0
  41. package/dist/cli/handlers/run-generate.d.ts.map +1 -0
  42. package/dist/cli/handlers/run.d.ts +4 -1
  43. package/dist/cli/handlers/run.d.ts.map +1 -1
  44. package/dist/cli/handlers/vendor.d.ts +8 -3
  45. package/dist/cli/handlers/vendor.d.ts.map +1 -1
  46. package/dist/cli/main.d.ts +12 -0
  47. package/dist/cli/main.d.ts.map +1 -1
  48. package/dist/cli/mcp/op-tools.d.ts.map +1 -1
  49. package/dist/cli/plugins.d.ts +14 -0
  50. package/dist/cli/plugins.d.ts.map +1 -1
  51. package/dist/cli/registry.d.ts +46 -1
  52. package/dist/cli/registry.d.ts.map +1 -1
  53. package/dist/codegen/fetch.d.ts +10 -0
  54. package/dist/codegen/fetch.d.ts.map +1 -1
  55. package/dist/components/capability-plugin-loader.d.ts.map +1 -1
  56. package/dist/components/cli-support.d.ts +3 -1
  57. package/dist/components/cli-support.d.ts.map +1 -1
  58. package/dist/components/discover.d.ts.map +1 -1
  59. package/dist/components/driver.d.ts +16 -2
  60. package/dist/components/driver.d.ts.map +1 -1
  61. package/dist/components/fan-out-support.d.ts.map +1 -1
  62. package/dist/components/gate-plan.d.ts +40 -0
  63. package/dist/components/gate-plan.d.ts.map +1 -0
  64. package/dist/components/pilots/alb-ecs.pilot.d.ts +7 -1
  65. package/dist/components/pilots/alb-ecs.pilot.d.ts.map +1 -1
  66. package/dist/components/presets/ecs-fargate.d.ts +10 -4
  67. package/dist/components/presets/ecs-fargate.d.ts.map +1 -1
  68. package/dist/components/promote.d.ts +264 -0
  69. package/dist/components/promote.d.ts.map +1 -0
  70. package/dist/components/sandbox/driver.d.ts.map +1 -1
  71. package/dist/components/sandbox/run.d.ts.map +1 -1
  72. package/dist/components/verbs/build-archive.d.ts +22 -11
  73. package/dist/components/verbs/build-archive.d.ts.map +1 -1
  74. package/dist/components/verbs/run-agent.d.ts.map +1 -1
  75. package/dist/config-static.d.ts +59 -0
  76. package/dist/config-static.d.ts.map +1 -0
  77. package/dist/config.d.ts +104 -5
  78. package/dist/config.d.ts.map +1 -1
  79. package/dist/content-digest.d.ts +17 -0
  80. package/dist/content-digest.d.ts.map +1 -0
  81. package/dist/discovery/files.d.ts +48 -4
  82. package/dist/discovery/files.d.ts.map +1 -1
  83. package/dist/discovery/fold-import.d.ts +32 -1
  84. package/dist/discovery/fold-import.d.ts.map +1 -1
  85. package/dist/discovery/index.d.ts.map +1 -1
  86. package/dist/discovery/sandbox/driver.d.ts +20 -0
  87. package/dist/discovery/sandbox/driver.d.ts.map +1 -1
  88. package/dist/discovery/sandbox/fork.d.ts +20 -0
  89. package/dist/discovery/sandbox/fork.d.ts.map +1 -1
  90. package/dist/discovery/walk.d.ts +119 -0
  91. package/dist/discovery/walk.d.ts.map +1 -0
  92. package/dist/graph-ir.d.ts +17 -1
  93. package/dist/graph-ir.d.ts.map +1 -1
  94. package/dist/join-key.d.ts +31 -0
  95. package/dist/join-key.d.ts.map +1 -0
  96. package/dist/lexicon-module.d.ts +110 -0
  97. package/dist/lexicon-module.d.ts.map +1 -0
  98. package/dist/lexicon.d.ts +82 -1
  99. package/dist/lexicon.d.ts.map +1 -1
  100. package/dist/lifecycle/build-ledger-store.d.ts.map +1 -1
  101. package/dist/lifecycle/digest.d.ts +5 -1
  102. package/dist/lifecycle/digest.d.ts.map +1 -1
  103. package/dist/lifecycle/gate-ledger.d.ts +20 -5
  104. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  105. package/dist/lifecycle/git.d.ts +64 -0
  106. package/dist/lifecycle/git.d.ts.map +1 -1
  107. package/dist/lifecycle/lease.d.ts +8 -1
  108. package/dist/lifecycle/lease.d.ts.map +1 -1
  109. package/dist/lifecycle/legacy-digest.d.ts +59 -0
  110. package/dist/lifecycle/legacy-digest.d.ts.map +1 -0
  111. package/dist/lifecycle/member-ledger.d.ts +67 -0
  112. package/dist/lifecycle/member-ledger.d.ts.map +1 -0
  113. package/dist/lifecycle/plan-digest.d.ts +4 -3
  114. package/dist/lifecycle/plan-digest.d.ts.map +1 -1
  115. package/dist/lifecycle/release-ledger.d.ts +42 -1
  116. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  117. package/dist/lifecycle/status.d.ts +90 -5
  118. package/dist/lifecycle/status.d.ts.map +1 -1
  119. package/dist/lint/rules/comp/comp003-mutating-no-rollback.d.ts +2 -3
  120. package/dist/lint/rules/comp/comp003-mutating-no-rollback.d.ts.map +1 -1
  121. package/dist/op/activities/activity-contracts.d.ts +32 -0
  122. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  123. package/dist/op/activities/index.d.ts +2 -0
  124. package/dist/op/activities/index.d.ts.map +1 -1
  125. package/dist/op/activities/propose-upgrade.d.ts +75 -0
  126. package/dist/op/activities/propose-upgrade.d.ts.map +1 -0
  127. package/dist/op/activity-contract-registry.d.ts.map +1 -1
  128. package/dist/op/activity-registry.d.ts.map +1 -1
  129. package/dist/op/discover.d.ts.map +1 -1
  130. package/dist/op/gate-approval.d.ts.map +1 -1
  131. package/dist/op/gate-name.d.ts +7 -0
  132. package/dist/op/gate-name.d.ts.map +1 -1
  133. package/dist/op/gate-summary.d.ts +16 -0
  134. package/dist/op/gate-summary.d.ts.map +1 -1
  135. package/dist/op/gate.d.ts +16 -2
  136. package/dist/op/gate.d.ts.map +1 -1
  137. package/dist/op/generate-pipeline.d.ts.map +1 -1
  138. package/dist/op/runtimes/local.d.ts.map +1 -1
  139. package/dist/project-root.d.ts +26 -0
  140. package/dist/project-root.d.ts.map +1 -1
  141. package/dist/workspace/__fixtures__/contract-repo.d.ts +37 -0
  142. package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -0
  143. package/dist/workspace/__fixtures__/two-member-workspace.d.ts +11 -0
  144. package/dist/workspace/__fixtures__/two-member-workspace.d.ts.map +1 -0
  145. package/dist/workspace/checks/generated.d.ts +156 -0
  146. package/dist/workspace/checks/generated.d.ts.map +1 -0
  147. package/dist/workspace/checks/ledgers.d.ts +78 -0
  148. package/dist/workspace/checks/ledgers.d.ts.map +1 -0
  149. package/dist/workspace/checks/links.d.ts +25 -0
  150. package/dist/workspace/checks/links.d.ts.map +1 -0
  151. package/dist/workspace/checks/pipelines.d.ts +76 -0
  152. package/dist/workspace/checks/pipelines.d.ts.map +1 -0
  153. package/dist/workspace/checks.d.ts +161 -0
  154. package/dist/workspace/checks.d.ts.map +1 -0
  155. package/dist/workspace/compose-graph.d.ts +115 -0
  156. package/dist/workspace/compose-graph.d.ts.map +1 -0
  157. package/dist/workspace/compose-reports.d.ts +68 -0
  158. package/dist/workspace/compose-reports.d.ts.map +1 -0
  159. package/dist/workspace/declaration.d.ts +220 -0
  160. package/dist/workspace/declaration.d.ts.map +1 -0
  161. package/dist/workspace/declaration.schema.json +434 -0
  162. package/dist/workspace/generated-files.d.ts +65 -0
  163. package/dist/workspace/generated-files.d.ts.map +1 -0
  164. package/dist/workspace/glob.d.ts +13 -0
  165. package/dist/workspace/glob.d.ts.map +1 -0
  166. package/dist/workspace/graph-cli.d.ts +75 -0
  167. package/dist/workspace/graph-cli.d.ts.map +1 -0
  168. package/dist/workspace/init.d.ts +96 -0
  169. package/dist/workspace/init.d.ts.map +1 -0
  170. package/dist/workspace/jsonc.d.ts +38 -0
  171. package/dist/workspace/jsonc.d.ts.map +1 -0
  172. package/dist/workspace/kinds.d.ts +192 -0
  173. package/dist/workspace/kinds.d.ts.map +1 -0
  174. package/dist/workspace/lineage-check.d.ts +84 -0
  175. package/dist/workspace/lineage-check.d.ts.map +1 -0
  176. package/dist/workspace/lineage-cli.d.ts +31 -0
  177. package/dist/workspace/lineage-cli.d.ts.map +1 -0
  178. package/dist/workspace/lineage-init.d.ts +101 -0
  179. package/dist/workspace/lineage-init.d.ts.map +1 -0
  180. package/dist/workspace/lineage-lock.d.ts +258 -0
  181. package/dist/workspace/lineage-lock.d.ts.map +1 -0
  182. package/dist/workspace/lineage-merge.d.ts +14 -0
  183. package/dist/workspace/lineage-merge.d.ts.map +1 -0
  184. package/dist/workspace/lineage-migrations.d.ts +178 -0
  185. package/dist/workspace/lineage-migrations.d.ts.map +1 -0
  186. package/dist/workspace/lineage-update.d.ts +59 -0
  187. package/dist/workspace/lineage-update.d.ts.map +1 -0
  188. package/dist/workspace/lineage-upgrade-cli.d.ts +40 -0
  189. package/dist/workspace/lineage-upgrade-cli.d.ts.map +1 -0
  190. package/dist/workspace/lineage-upgrade.d.ts +146 -0
  191. package/dist/workspace/lineage-upgrade.d.ts.map +1 -0
  192. package/dist/workspace/lineage-vendor.d.ts +63 -0
  193. package/dist/workspace/lineage-vendor.d.ts.map +1 -0
  194. package/dist/workspace/lineage-version.d.ts +32 -0
  195. package/dist/workspace/lineage-version.d.ts.map +1 -0
  196. package/dist/workspace/links.d.ts +150 -0
  197. package/dist/workspace/links.d.ts.map +1 -0
  198. package/dist/workspace/ls.d.ts +99 -0
  199. package/dist/workspace/ls.d.ts.map +1 -0
  200. package/dist/workspace/member-commands.d.ts +139 -0
  201. package/dist/workspace/member-commands.d.ts.map +1 -0
  202. package/dist/workspace/member-handles.d.ts +54 -0
  203. package/dist/workspace/member-handles.d.ts.map +1 -0
  204. package/dist/workspace/member-pipeline.d.ts +143 -0
  205. package/dist/workspace/member-pipeline.d.ts.map +1 -0
  206. package/dist/workspace/member-run.d.ts +67 -0
  207. package/dist/workspace/member-run.d.ts.map +1 -0
  208. package/dist/workspace/reason-codes.d.ts +65 -0
  209. package/dist/workspace/reason-codes.d.ts.map +1 -0
  210. package/dist/workspace/record-source.d.ts +22 -0
  211. package/dist/workspace/record-source.d.ts.map +1 -0
  212. package/dist/workspace/records-cli.d.ts +81 -0
  213. package/dist/workspace/records-cli.d.ts.map +1 -0
  214. package/dist/workspace/records.d.ts +118 -0
  215. package/dist/workspace/records.d.ts.map +1 -0
  216. package/dist/workspace/root-refusal.d.ts +53 -0
  217. package/dist/workspace/root-refusal.d.ts.map +1 -0
  218. package/dist/workspace/status.d.ts +150 -0
  219. package/dist/workspace/status.d.ts.map +1 -0
  220. package/dist/workspace/template-manifest.d.ts +90 -0
  221. package/dist/workspace/template-manifest.d.ts.map +1 -0
  222. package/dist/workspace/tree.d.ts +36 -0
  223. package/dist/workspace/tree.d.ts.map +1 -0
  224. package/dist/workspace/trust/attestor.d.ts +67 -0
  225. package/dist/workspace/trust/attestor.d.ts.map +1 -0
  226. package/dist/workspace/trust/policy.d.ts +111 -0
  227. package/dist/workspace/trust/policy.d.ts.map +1 -0
  228. package/dist/workspace/trust/provenance.d.ts +67 -0
  229. package/dist/workspace/trust/provenance.d.ts.map +1 -0
  230. package/dist/workspace/trust/ssh-commit.d.ts +43 -0
  231. package/dist/workspace/trust/ssh-commit.d.ts.map +1 -0
  232. package/dist/workspace/trust/test-repo.d.ts +42 -0
  233. package/dist/workspace/trust/test-repo.d.ts.map +1 -0
  234. package/dist/workspace/trust/verify-cli.d.ts +9 -0
  235. package/dist/workspace/trust/verify-cli.d.ts.map +1 -0
  236. package/dist/workspace/trust/verify.d.ts +66 -0
  237. package/dist/workspace/trust/verify.d.ts.map +1 -0
  238. package/dist/workspace/which-chant.d.ts +58 -0
  239. package/dist/workspace/which-chant.d.ts.map +1 -0
  240. package/dist/workspace/workspace-kinds.schema.json +133 -0
  241. package/package.json +4 -3
  242. package/src/audit/core.ts +13 -2
  243. package/src/audit/discover.ts +102 -36
  244. package/src/audit/report-html.ts +3 -1
  245. package/src/audit/report-model.ts +14 -1
  246. package/src/audit/terraform-state.test.ts +81 -1
  247. package/src/audit/terraform-state.ts +25 -0
  248. package/src/cli/command-group.ts +7 -0
  249. package/src/cli/commands/audit-truncation.test.ts +167 -0
  250. package/src/cli/commands/audit.ts +132 -29
  251. package/src/cli/commands/check-lexicon-examples.ts +2 -1
  252. package/src/cli/commands/check-lexicon.ts +24 -0
  253. package/src/cli/commands/doctor.test.ts +30 -0
  254. package/src/cli/commands/doctor.ts +21 -0
  255. package/src/cli/commands/import-agents.ts +15 -4
  256. package/src/cli/commands/init.ts +76 -25
  257. package/src/cli/commands/lint.test.ts +52 -1
  258. package/src/cli/commands/lint.ts +41 -71
  259. package/src/cli/commands/onboard.ts +15 -0
  260. package/src/cli/commands/update.ts +18 -5
  261. package/src/cli/commands/vendor.ts +11 -6
  262. package/src/cli/conflict-check.test.ts +18 -1
  263. package/src/cli/conflict-check.ts +13 -0
  264. package/src/cli/discovery-skip.test.ts +168 -0
  265. package/src/cli/handlers/__snapshots__/build-member.test.ts.snap +38 -0
  266. package/src/cli/handlers/build-member.test.ts +127 -0
  267. package/src/cli/handlers/build.test.ts +11 -0
  268. package/src/cli/handlers/build.ts +35 -1
  269. package/src/cli/handlers/components.test.ts +127 -0
  270. package/src/cli/handlers/components.ts +70 -8
  271. package/src/cli/handlers/dev.ts +6 -1
  272. package/src/cli/handlers/fan-out.ts +9 -2
  273. package/src/cli/handlers/graph.test.ts +20 -0
  274. package/src/cli/handlers/graph.ts +4 -2
  275. package/src/cli/handlers/init.ts +54 -1
  276. package/src/cli/handlers/lifecycle.ts +2 -1
  277. package/src/cli/handlers/misc.ts +16 -0
  278. package/src/cli/handlers/operator.test.ts +68 -0
  279. package/src/cli/handlers/operator.ts +39 -10
  280. package/src/cli/handlers/promote.test.ts +377 -0
  281. package/src/cli/handlers/promote.ts +391 -0
  282. package/src/cli/handlers/run-generate-member.test.ts +83 -0
  283. package/src/cli/handlers/run-generate.test.ts +158 -0
  284. package/src/cli/handlers/run-generate.ts +168 -0
  285. package/src/cli/handlers/run.test.ts +47 -0
  286. package/src/cli/handlers/run.ts +41 -6
  287. package/src/cli/handlers/vendor.ts +147 -43
  288. package/src/cli/main.test.ts +137 -1
  289. package/src/cli/main.ts +290 -24
  290. package/src/cli/mcp/op-tools.ts +2 -1
  291. package/src/cli/path-lexicon-messages.test.ts +152 -0
  292. package/src/cli/plugins.ts +43 -5
  293. package/src/cli/registry.ts +46 -1
  294. package/src/cli/static-config-read.test.ts +208 -0
  295. package/src/codegen/fetch.test.ts +63 -1
  296. package/src/codegen/fetch.ts +30 -1
  297. package/src/components/__fixtures__/alb-ecs-service.json +0 -8
  298. package/src/components/capability-plugin-loader.ts +3 -1
  299. package/src/components/cli-support.ts +9 -3
  300. package/src/components/discover.test.ts +27 -57
  301. package/src/components/discover.ts +20 -51
  302. package/src/components/driver.test.ts +90 -12
  303. package/src/components/driver.ts +43 -5
  304. package/src/components/fan-out-support.ts +2 -1
  305. package/src/components/gate-plan.ts +53 -0
  306. package/src/components/pilots/README.md +1 -1
  307. package/src/components/pilots/alb-ecs.pilot.ts +7 -7
  308. package/src/components/presets/ecs-fargate.ts +10 -5
  309. package/src/components/promote.test.ts +645 -0
  310. package/src/components/promote.ts +641 -0
  311. package/src/components/sandbox/driver.ts +8 -8
  312. package/src/components/sandbox/run.ts +4 -1
  313. package/src/components/verbs/build-archive.ts +27 -21
  314. package/src/components/verbs/run-agent.ts +3 -5
  315. package/src/config-static.test.ts +136 -0
  316. package/src/config-static.ts +232 -0
  317. package/src/config.test.ts +40 -0
  318. package/src/config.ts +161 -11
  319. package/src/content-digest.test.ts +120 -0
  320. package/src/content-digest.ts +21 -0
  321. package/src/discovery/files.test.ts +127 -1
  322. package/src/discovery/files.ts +91 -59
  323. package/src/discovery/fold-import.ts +127 -9
  324. package/src/discovery/index.ts +1 -0
  325. package/src/discovery/sandbox/driver.ts +73 -35
  326. package/src/discovery/sandbox/fork-diagnostic.test.ts +136 -13
  327. package/src/discovery/sandbox/fork.ts +83 -43
  328. package/src/discovery/sandbox/path-lexicon-multifile.test.ts +285 -0
  329. package/src/discovery/sandbox/path-lexicon-parity.test.ts +162 -0
  330. package/src/discovery/walk.test.ts +263 -0
  331. package/src/discovery/walk.ts +334 -0
  332. package/src/effect-receipt.ts +2 -2
  333. package/src/graph-ir.ts +18 -1
  334. package/src/join-key.test.ts +25 -0
  335. package/src/join-key.ts +39 -0
  336. package/src/lexicon-module.test.ts +203 -0
  337. package/src/lexicon-module.ts +215 -0
  338. package/src/lexicon.ts +105 -1
  339. package/src/lifecycle/build-ledger-store.ts +13 -2
  340. package/src/lifecycle/digest.ts +9 -5
  341. package/src/lifecycle/gate-ledger.test.ts +20 -0
  342. package/src/lifecycle/gate-ledger.ts +28 -6
  343. package/src/lifecycle/git.test.ts +13 -1
  344. package/src/lifecycle/git.ts +221 -176
  345. package/src/lifecycle/lease.ts +25 -10
  346. package/src/lifecycle/legacy-digest.test.ts +206 -0
  347. package/src/lifecycle/legacy-digest.ts +103 -0
  348. package/src/lifecycle/member-ledger.test.ts +319 -0
  349. package/src/lifecycle/member-ledger.ts +145 -0
  350. package/src/lifecycle/plan-digest.ts +7 -7
  351. package/src/lifecycle/release-ledger.test.ts +20 -0
  352. package/src/lifecycle/release-ledger.ts +53 -3
  353. package/src/lifecycle/status.test.ts +151 -1
  354. package/src/lifecycle/status.ts +228 -7
  355. package/src/lint/rules/comp/comp003-mutating-no-rollback.ts +2 -3
  356. package/src/op/activities/activity-contracts.ts +37 -0
  357. package/src/op/activities/index.ts +10 -0
  358. package/src/op/activities/propose-upgrade.ts +206 -0
  359. package/src/op/activity-contract-registry.ts +18 -4
  360. package/src/op/activity-registry.ts +26 -0
  361. package/src/op/discover.test.ts +14 -4
  362. package/src/op/discover.ts +19 -23
  363. package/src/op/gate-approval.ts +3 -1
  364. package/src/op/gate-name.ts +8 -0
  365. package/src/op/gate-summary.test.ts +14 -0
  366. package/src/op/gate-summary.ts +25 -2
  367. package/src/op/gate.ts +36 -7
  368. package/src/op/generate-pipeline.test.ts +14 -5
  369. package/src/op/generate-pipeline.ts +2 -1
  370. package/src/op/runtimes/local.ts +2 -1
  371. package/src/project-root.test.ts +66 -1
  372. package/src/project-root.ts +48 -0
  373. package/src/workspace/__fixtures__/contract-repo.ts +100 -0
  374. package/src/workspace/__fixtures__/two-member-workspace.ts +35 -0
  375. package/src/workspace/behold-kinds.test.ts +162 -0
  376. package/src/workspace/check-contract.test.ts +127 -0
  377. package/src/workspace/check.schema.json +347 -0
  378. package/src/workspace/checks/generated.test.ts +287 -0
  379. package/src/workspace/checks/generated.ts +475 -0
  380. package/src/workspace/checks/ledgers.test.ts +139 -0
  381. package/src/workspace/checks/ledgers.ts +246 -0
  382. package/src/workspace/checks/links.ts +183 -0
  383. package/src/workspace/checks/pipelines.test.ts +133 -0
  384. package/src/workspace/checks/pipelines.ts +225 -0
  385. package/src/workspace/checks-repo.test.ts +71 -0
  386. package/src/workspace/checks.test.ts +450 -0
  387. package/src/workspace/checks.ts +461 -0
  388. package/src/workspace/compose-graph.test.ts +146 -0
  389. package/src/workspace/compose-graph.ts +228 -0
  390. package/src/workspace/compose-reports.test.ts +109 -0
  391. package/src/workspace/compose-reports.ts +186 -0
  392. package/src/workspace/declaration.schema.json +434 -0
  393. package/src/workspace/declaration.test.ts +347 -0
  394. package/src/workspace/declaration.ts +623 -0
  395. package/src/workspace/generated-files.test.ts +114 -0
  396. package/src/workspace/generated-files.ts +94 -0
  397. package/src/workspace/glob.ts +49 -0
  398. package/src/workspace/graph-cli.ts +220 -0
  399. package/src/workspace/graph-contract.test.ts +204 -0
  400. package/src/workspace/graph.schema.json +662 -0
  401. package/src/workspace/init.test.ts +223 -0
  402. package/src/workspace/init.ts +585 -0
  403. package/src/workspace/jsonc.test.ts +65 -0
  404. package/src/workspace/jsonc.ts +231 -0
  405. package/src/workspace/kinds.test.ts +288 -0
  406. package/src/workspace/kinds.ts +506 -0
  407. package/src/workspace/lineage-check.ts +264 -0
  408. package/src/workspace/lineage-cli.ts +114 -0
  409. package/src/workspace/lineage-init.test.ts +231 -0
  410. package/src/workspace/lineage-init.ts +365 -0
  411. package/src/workspace/lineage-lock.test.ts +224 -0
  412. package/src/workspace/lineage-lock.ts +357 -0
  413. package/src/workspace/lineage-merge.ts +44 -0
  414. package/src/workspace/lineage-migrations.test.ts +178 -0
  415. package/src/workspace/lineage-migrations.ts +343 -0
  416. package/src/workspace/lineage-update.ts +160 -0
  417. package/src/workspace/lineage-upgrade-cli.ts +151 -0
  418. package/src/workspace/lineage-upgrade.test.ts +474 -0
  419. package/src/workspace/lineage-upgrade.ts +624 -0
  420. package/src/workspace/lineage-vendor.test.ts +126 -0
  421. package/src/workspace/lineage-vendor.ts +215 -0
  422. package/src/workspace/lineage-version.ts +127 -0
  423. package/src/workspace/links.test.ts +344 -0
  424. package/src/workspace/links.ts +331 -0
  425. package/src/workspace/ls-contract.test.ts +200 -0
  426. package/src/workspace/ls.schema.json +190 -0
  427. package/src/workspace/ls.ts +254 -0
  428. package/src/workspace/member-commands.e2e.test.ts +244 -0
  429. package/src/workspace/member-commands.test.ts +217 -0
  430. package/src/workspace/member-commands.ts +540 -0
  431. package/src/workspace/member-handles.test.ts +76 -0
  432. package/src/workspace/member-handles.ts +228 -0
  433. package/src/workspace/member-pipeline.test.ts +135 -0
  434. package/src/workspace/member-pipeline.ts +317 -0
  435. package/src/workspace/member-run.ts +146 -0
  436. package/src/workspace/read-contract.test.ts +129 -0
  437. package/src/workspace/reason-codes.test.ts +113 -0
  438. package/src/workspace/reason-codes.ts +81 -0
  439. package/src/workspace/record-source.ts +117 -0
  440. package/src/workspace/records-cli.ts +198 -0
  441. package/src/workspace/records-contract.test.ts +93 -0
  442. package/src/workspace/records.schema.json +150 -0
  443. package/src/workspace/records.test.ts +243 -0
  444. package/src/workspace/records.ts +384 -0
  445. package/src/workspace/root-refusal.ts +97 -0
  446. package/src/workspace/status-contract.test.ts +308 -0
  447. package/src/workspace/status.schema.json +210 -0
  448. package/src/workspace/status.ts +412 -0
  449. package/src/workspace/template-manifest.test.ts +79 -0
  450. package/src/workspace/template-manifest.ts +221 -0
  451. package/src/workspace/tree.ts +132 -0
  452. package/src/workspace/trust/attestor.ts +93 -0
  453. package/src/workspace/trust/policy.ts +306 -0
  454. package/src/workspace/trust/provenance.ts +181 -0
  455. package/src/workspace/trust/ssh-commit.ts +138 -0
  456. package/src/workspace/trust/test-repo.ts +138 -0
  457. package/src/workspace/trust/trust.test.ts +397 -0
  458. package/src/workspace/trust/verify-cli.ts +56 -0
  459. package/src/workspace/trust/verify.ts +194 -0
  460. package/src/workspace/which-chant.test.ts +117 -0
  461. package/src/workspace/which-chant.ts +151 -0
  462. package/src/workspace/workspace-kinds.schema.json +133 -0
@@ -0,0 +1,623 @@
1
+ /**
2
+ * The workspace declaration, `chant.workspace.json` or `.jsonc` (#2524 D1, D2;
3
+ * ws-051 for example groups).
4
+ *
5
+ * Every read goes through {@link parseDeclaration}: the text is parsed with
6
+ * positions, the declared `minReader` is checked, the result is validated
7
+ * against the JSON Schema shipped beside this file, and then the rules JSON
8
+ * Schema can't say are checked in code: unique names, and the placement rules.
9
+ * Any failure is a {@link WorkspaceReadError} naming the file, line and
10
+ * column. A declaration never reads as empty.
11
+ *
12
+ * Group matches depend on the tree, so their placement is checked when they
13
+ * are expanded ({@link resolveGroups}).
14
+ *
15
+ * Everything under `workspace/` loads only when a `chant workspace` command
16
+ * runs. The level-0 goldens (#2526) fail if a level-0 command loads it.
17
+ */
18
+
19
+ import { readFileSync } from "node:fs";
20
+ import { createRequire } from "node:module";
21
+ import { fileURLToPath } from "node:url";
22
+ import schema from "./declaration.schema.json";
23
+ import { expandGlob } from "./glob";
24
+ import { EXAMPLES_KIND, holdsChantProject } from "./kinds";
25
+ import { parseJsonText, pointerToken, type TextLocation } from "./jsonc";
26
+ import type { ReasonCode } from "./reason-codes";
27
+ import { joinPath, type WorkspaceTree } from "./tree";
28
+
29
+ export const DECLARATION_SCHEMA_ID = schema.$id;
30
+ export const DECLARATION_FILES = ["chant.workspace.json", "chant.workspace.jsonc"] as const;
31
+ export const DECLARATION_SCHEMA_VERSION = 1;
32
+
33
+ /** Names no member or group may take: ledger paths use them (#2524 D7, D17). */
34
+ export const RESERVED_NAMES = ["_workspace", "_members"] as const;
35
+ export const NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,39}$/;
36
+
37
+ // ── Errors ───────────────────────────────────────────────────────────────────
38
+
39
+ /**
40
+ * Why a workspace could not be read. The list is closed: a reader may switch
41
+ * on it, and a new code is a contract change (#2524 D15).
42
+ */
43
+ export const WORKSPACE_ERROR_CODES = [
44
+ /** No chant.workspace.json or .jsonc between the directory and the git root. */
45
+ "declaration-missing",
46
+ /** Both chant.workspace.json and chant.workspace.jsonc are present. */
47
+ "declaration-ambiguous",
48
+ /** The file is not valid JSON (or JSONC, for .jsonc). */
49
+ "declaration-unparseable",
50
+ /** The file does not match the declaration schema, or repeats a name. */
51
+ "declaration-invalid",
52
+ /** A member or group match breaks a placement rule (#2524 D2, ws-051). */
53
+ "placement-invalid",
54
+ /** The declaration's minReader is newer than this chant. */
55
+ "reader-too-old",
56
+ /** The declaration pins another chant, which is not installed at the root (#2524 D15, ws-021). */
57
+ "root-chant-required",
58
+ /** `--at` was given outside a git repository. */
59
+ "not-a-git-repository",
60
+ /** `--at` names no commit. */
61
+ "revision-unknown",
62
+ ] as const satisfies readonly ReasonCode[];
63
+ export type WorkspaceErrorCode = (typeof WORKSPACE_ERROR_CODES)[number];
64
+
65
+ export interface ErrorLocation extends TextLocation {
66
+ /** The declaration file, relative to the workspace root's tree. */
67
+ file: string;
68
+ }
69
+
70
+ export class WorkspaceReadError extends Error {
71
+ constructor(
72
+ readonly code: WorkspaceErrorCode,
73
+ message: string,
74
+ readonly location?: ErrorLocation,
75
+ ) {
76
+ super(message);
77
+ this.name = "WorkspaceReadError";
78
+ }
79
+
80
+ /** `file:line:column: message`, or the message alone. */
81
+ describe(): string {
82
+ const l = this.location;
83
+ return l ? `${l.file}:${l.line}:${l.column}: ${this.message}` : this.message;
84
+ }
85
+ }
86
+
87
+ // ── The shape ────────────────────────────────────────────────────────────────
88
+
89
+ /** A declaration check suppressed for one entry (#2535). */
90
+ export interface Suppression {
91
+ /** A `WSP` id. */
92
+ check: string;
93
+ because: string;
94
+ }
95
+
96
+ /** What `checks` may set a declaration check's severity to. */
97
+ export type CheckSeverity = "error" | "warning" | "info" | "off";
98
+
99
+ export interface MemberRole {
100
+ name: string;
101
+ /** Relative to the member's directory, or null for the whole member. */
102
+ path: string | null;
103
+ }
104
+
105
+ /** A file a member lists as written by a command (#2524 D14, #2541). */
106
+ export interface GeneratedFile {
107
+ /** Relative to the member's directory. */
108
+ path: string;
109
+ /** The command line that writes the file, run in the member's directory. */
110
+ generator: string;
111
+ /** What the generator reads, relative to the workspace root. */
112
+ sources: string[];
113
+ /** Set when the file is kept by hand instead of regenerated. */
114
+ handWritten: { because: string } | null;
115
+ /** The entry's JSON Pointer in the file, for messages. */
116
+ pointer: string;
117
+ }
118
+
119
+ /** A member link as the consumer states it (#2524 D6, #2539). */
120
+ export interface LinkDeclaration {
121
+ /** The producer member. */
122
+ member: string;
123
+ /** The producer's output, compared exactly. */
124
+ output: string;
125
+ /** The link kind as written, or null for the default (`output`). */
126
+ kind: string | null;
127
+ /** The link's JSON Pointer in the file, for messages. */
128
+ pointer: string;
129
+ }
130
+
131
+ export interface Member {
132
+ type: "member";
133
+ name: string;
134
+ /** Relative to the workspace root; `"."` is the root member. */
135
+ dir: string;
136
+ kind: string;
137
+ roles: MemberRole[];
138
+ /** Declared generated files, in file order. The implicit ones are not listed here (see `generated-files.ts`). */
139
+ generated: GeneratedFile[];
140
+ /** Outputs the entry lists as link targets, or null when it lists none (#2539). */
141
+ outputs: string[] | null;
142
+ /** The links this member states as a consumer, in file order (#2539). */
143
+ links: LinkDeclaration[];
144
+ upstream: string | null;
145
+ because: string | null;
146
+ suppress: Suppression[];
147
+ /** The entry's JSON Pointer in the file, for messages. */
148
+ pointer: string;
149
+ }
150
+
151
+ export interface Group {
152
+ type: "group";
153
+ name: string;
154
+ kind: typeof EXAMPLES_KIND;
155
+ globs: string[];
156
+ suppress: Suppression[];
157
+ pointer: string;
158
+ /** Where the entry's `glob` sits in the file, for placement errors. */
159
+ globLocation: TextLocation;
160
+ }
161
+
162
+ export type Entry = Member | Group;
163
+
164
+ export interface Pin {
165
+ package: string | null;
166
+ version: string | null;
167
+ path: string | null;
168
+ integrity: string | null;
169
+ }
170
+
171
+ export interface Declaration {
172
+ name: string;
173
+ schema: number;
174
+ minReader: string | null;
175
+ /** Members and groups, in file order. */
176
+ entries: Entry[];
177
+ members: Member[];
178
+ groups: Group[];
179
+ pins: Pin[];
180
+ /** Severities set for declaration checks, keyed by `WSP` id (#2535). */
181
+ checks: Record<string, CheckSeverity>;
182
+ /** The file, relative to the workspace root's tree (`chant.workspace.json` or `.jsonc`). */
183
+ file: string;
184
+ }
185
+
186
+ // ── This chant ───────────────────────────────────────────────────────────────
187
+
188
+ /** The package whose pin names the root's chant (#2524 D15, ws-021). */
189
+ export const CHANT_PACKAGE = "@intentius/chant";
190
+
191
+ let readerVersionCache: string | undefined;
192
+
193
+ /**
194
+ * The version of the chant doing the reading. Read as a file, not through
195
+ * `require`: discovery reads the declaration on every walk under a workspace
196
+ * root (#2527), and a CJS require of `package.json` took about a minute per
197
+ * call in a vitest worker once a build had run in it, where a file read takes
198
+ * well under a millisecond.
199
+ */
200
+ export function readerVersion(): string {
201
+ readerVersionCache ??= (
202
+ JSON.parse(readFileSync(fileURLToPath(new URL("../../package.json", import.meta.url)), "utf-8")) as { version: string }
203
+ ).version;
204
+ return readerVersionCache;
205
+ }
206
+
207
+ const SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?$/;
208
+
209
+ /** Negative when `a` is older than `b`. Build metadata is not allowed, and prereleases sort before their release. */
210
+ export function compareVersions(a: string, b: string): number {
211
+ const ma = SEMVER.exec(a);
212
+ const mb = SEMVER.exec(b);
213
+ if (!ma || !mb) throw new Error(`not a version: ${ma ? b : a}`);
214
+ for (let i = 1; i <= 3; i++) {
215
+ const d = Number(ma[i]) - Number(mb[i]);
216
+ if (d !== 0) return d;
217
+ }
218
+ if (ma[4] === mb[4]) return 0;
219
+ if (ma[4] === undefined) return 1;
220
+ if (mb[4] === undefined) return -1;
221
+ return ma[4] < mb[4] ? -1 : 1;
222
+ }
223
+
224
+ // ── Validation ───────────────────────────────────────────────────────────────
225
+
226
+ interface AjvError {
227
+ instancePath: string;
228
+ schemaPath: string;
229
+ keyword: string;
230
+ message?: string;
231
+ params: Record<string, unknown>;
232
+ }
233
+ type Validate = ((data: unknown) => boolean) & { errors?: AjvError[] | null };
234
+
235
+ let compiled: Validate | undefined;
236
+ function validator(): Validate {
237
+ if (compiled) return compiled;
238
+ const require = createRequire(import.meta.url);
239
+ // ajv is CommonJS; its class is the default export, or that export's own default.
240
+ const mod = require("ajv/dist/2020") as { default?: unknown };
241
+ const Ajv = (mod.default ?? mod) as new (opts: object) => { compile(s: object): Validate };
242
+ compiled = new Ajv({ allErrors: true, strict: true }).compile(schema);
243
+ return compiled;
244
+ }
245
+
246
+ /** Readable text for one schema error, keyed on the rule that failed. */
247
+ function explain(e: AjvError, value: unknown): string {
248
+ const def = /#\/\$defs\/([A-Za-z]+)\//.exec(e.schemaPath)?.[1];
249
+ if (e.keyword === "pattern") {
250
+ const shown = JSON.stringify(value);
251
+ if (def === "name" && typeof value === "string" && (RESERVED_NAMES as readonly string[]).includes(value)) {
252
+ return `${shown} is reserved; pick another name`;
253
+ }
254
+ if (def === "name") return `${shown} is not a valid name: use lowercase letters, digits and hyphens, starting with a letter or digit, at most 40 characters`;
255
+ if (def === "kindName") return `${shown} is not a valid kind or role name`;
256
+ if (def === "dir") return `${shown} is not a directory inside the workspace: use a relative path with / separators, and no . or .. segments`;
257
+ if (def === "path") return `${shown} is not a path inside the member or workspace: use a relative path with / separators, and no . or .. segments`;
258
+ if (def === "glob") return `${shown} is not a glob inside the workspace: use a relative pattern with / separators, and no . or .. segments`;
259
+ if (def === "version") return `${shown} is not a version such as 1.2.3`;
260
+ return `${shown} is not allowed here`;
261
+ }
262
+ if (e.keyword === "additionalProperties") {
263
+ return `unknown field ${JSON.stringify(e.params.additionalProperty)}; only fields named x-... may be added`;
264
+ }
265
+ if (e.keyword === "required") {
266
+ const missing = String(e.params.missingProperty);
267
+ if (missing === "because" && e.instancePath.endsWith("/handWritten")) return `a hand-written generated file needs "because", saying why it is kept by hand`;
268
+ if (missing === "because") return `a member of kind other needs "because", saying why it is there`;
269
+ return `missing required field ${JSON.stringify(missing)}`;
270
+ }
271
+ if (e.keyword === "const" && e.instancePath === "/schema") return `schema must be 1, the only declaration format this chant reads`;
272
+ return e.message ?? "is invalid";
273
+ }
274
+
275
+ /** Keywords that only summarise errors reported on their own. */
276
+ const SUMMARY_KEYWORDS = new Set(["if", "then", "else", "oneOf", "anyOf"]);
277
+
278
+ function valueAt(root: unknown, pointer: string): unknown {
279
+ let v: unknown = root;
280
+ for (const token of pointer.split("/").slice(1)) {
281
+ const key = token.replace(/~1/g, "/").replace(/~0/g, "~");
282
+ if (v === null || typeof v !== "object") return undefined;
283
+ v = (v as Record<string, unknown>)[key];
284
+ }
285
+ return v;
286
+ }
287
+
288
+ export interface ReadOptions {
289
+ /**
290
+ * Apply "which chant reads it" (#2524 D15, ws-021): when the declaration
291
+ * pins `@intentius/chant` at a version other than `reader`, that pinned
292
+ * chant, the root's, reads it, and this one refuses with
293
+ * `root-chant-required`. The read-contract commands (`ls`, `graph`,
294
+ * `check`, `status`) set it, and hand the command to the root's chant
295
+ * first when it is installed (`which-chant.ts`). Other commands leave it
296
+ * off: a member's own toolchain writes its ledger whatever the root pins.
297
+ */
298
+ rootChant?: boolean;
299
+ }
300
+
301
+ /** The version the declaration's `@intentius/chant` pin names, and where the pin is. Undefined without one. */
302
+ export function pinnedChant(obj: Record<string, unknown>): { version: string; index: number } | undefined {
303
+ if (!Array.isArray(obj.pins)) return undefined;
304
+ const index = obj.pins.findIndex(
305
+ (p) => p !== null && typeof p === "object" && (p as Record<string, unknown>).package === CHANT_PACKAGE && typeof (p as Record<string, unknown>).version === "string",
306
+ );
307
+ return index < 0 ? undefined : { version: (obj.pins[index] as { version: string }).version, index };
308
+ }
309
+
310
+ /**
311
+ * Parse and check a declaration's text. `file` is the name used in messages
312
+ * and decides the dialect: a name ending in `.jsonc` allows comments and
313
+ * trailing commas.
314
+ */
315
+ export function parseDeclaration(text: string, file: string, reader: string = readerVersion(), options: ReadOptions = {}): Declaration {
316
+ const parsed = parseJsonText(text, { jsonc: file.endsWith(".jsonc") });
317
+ if (!parsed.ok) {
318
+ throw new WorkspaceReadError("declaration-unparseable", parsed.message, { file, ...parsed.location });
319
+ }
320
+ const at = (pointer: string, key = false): ErrorLocation => ({ file, ...parsed.locate(pointer, key) });
321
+ const raw = parsed.value;
322
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
323
+ throw new WorkspaceReadError("declaration-invalid", "the declaration must be a JSON object", at(""));
324
+ }
325
+ const obj = raw as Record<string, unknown>;
326
+
327
+ // The root's chant reads the declaration (ws-021). A pin to another chant
328
+ // comes before minReader and the schema: that chant may know fields this
329
+ // one doesn't.
330
+ const pin = options.rootChant ? pinnedChant(obj) : undefined;
331
+ if (pin && pin.version !== reader) {
332
+ throw new WorkspaceReadError(
333
+ "root-chant-required",
334
+ `this declaration pins ${CHANT_PACKAGE} ${pin.version}, and this is chant ${reader}; the root's chant reads it, so install ${CHANT_PACKAGE}@${pin.version} at the workspace root`,
335
+ at(`/pins/${pin.index}/version`),
336
+ );
337
+ }
338
+
339
+ // minReader first: a declaration written for a newer chant may use fields
340
+ // this one doesn't know, and "too old" is the true reason it can't read it.
341
+ if (typeof obj.minReader === "string" && SEMVER.test(obj.minReader) && compareVersions(reader, obj.minReader) < 0) {
342
+ throw new WorkspaceReadError(
343
+ "reader-too-old",
344
+ `this declaration needs chant ${obj.minReader} or newer to read it, and this is chant ${reader}`,
345
+ at("/minReader"),
346
+ );
347
+ }
348
+
349
+ const validate = validator();
350
+ if (!validate(raw)) {
351
+ const errors = (validate.errors ?? []).filter((e) => !SUMMARY_KEYWORDS.has(e.keyword));
352
+ // The deepest error first: it is the one closest to what needs fixing.
353
+ errors.sort((a, b) => b.instancePath.split("/").length - a.instancePath.split("/").length);
354
+ const first = errors[0] ?? validate.errors![0];
355
+ const isKey = first.keyword === "additionalProperties";
356
+ const pointer = isKey ? `${first.instancePath}/${pointerToken(String(first.params.additionalProperty))}` : first.instancePath;
357
+ const where = first.instancePath === "" ? "" : ` (at ${first.instancePath})`;
358
+ const more = errors.length > 1 ? `; ${errors.length - 1} more problem${errors.length > 2 ? "s" : ""} after this one` : "";
359
+ throw new WorkspaceReadError("declaration-invalid", `${explain(first, valueAt(raw, first.instancePath))}${where}${more}`, at(pointer, isKey));
360
+ }
361
+
362
+ const entries: Entry[] = (obj.members as Record<string, unknown>[]).map((e, i): Entry => {
363
+ const pointer = `/members/${i}`;
364
+ const suppress = ((e.suppress as Suppression[] | undefined) ?? []).map((x) => ({ check: x.check, because: x.because }));
365
+ if (e.kind === EXAMPLES_KIND) {
366
+ const glob = e.glob as string | string[];
367
+ return {
368
+ type: "group",
369
+ name: e.name as string,
370
+ kind: EXAMPLES_KIND,
371
+ globs: typeof glob === "string" ? [glob] : [...glob],
372
+ suppress,
373
+ pointer,
374
+ globLocation: parsed.locate(`${pointer}/glob`),
375
+ };
376
+ }
377
+ const roles = ((e.roles as (string | { name: string; path?: string })[] | undefined) ?? []).map((r) =>
378
+ typeof r === "string" ? { name: r, path: null } : { name: r.name, path: r.path ?? null },
379
+ );
380
+ const generated = ((e.generated as Record<string, unknown>[] | undefined) ?? []).map((g, j): GeneratedFile => ({
381
+ path: g.path as string,
382
+ generator: g.generator as string,
383
+ sources: [...((g.sources as string[] | undefined) ?? [])],
384
+ handWritten: g.handWritten ? { because: (g.handWritten as { because: string }).because } : null,
385
+ pointer: `${pointer}/generated/${j}`,
386
+ }));
387
+ const links = ((e.links as { member: string; output: string; kind?: string }[] | undefined) ?? []).map((l, j) => ({
388
+ member: l.member,
389
+ output: l.output,
390
+ kind: l.kind ?? null,
391
+ pointer: `${pointer}/links/${j}`,
392
+ }));
393
+ return {
394
+ type: "member",
395
+ name: e.name as string,
396
+ dir: e.dir as string,
397
+ kind: e.kind as string,
398
+ roles,
399
+ generated,
400
+ outputs: e.outputs === undefined ? null : [...(e.outputs as string[])],
401
+ links,
402
+ upstream: (e.upstream as string | undefined) ?? null,
403
+ because: (e.because as string | undefined) ?? null,
404
+ suppress,
405
+ pointer,
406
+ };
407
+ });
408
+
409
+ // Names are unique across members and groups.
410
+ const names = new Map<string, Entry>();
411
+ for (const e of entries) {
412
+ const first = names.get(e.name);
413
+ if (first) {
414
+ throw new WorkspaceReadError(
415
+ "declaration-invalid",
416
+ `the name ${JSON.stringify(e.name)} is already used by the entry at ${first.pointer}`,
417
+ at(`${e.pointer}/name`),
418
+ );
419
+ }
420
+ names.set(e.name, e);
421
+ }
422
+
423
+ // Placement (#2524 D2): one root member at most, no two members in one
424
+ // directory, and only the root member contains other members.
425
+ const members = entries.filter((e): e is Member => e.type === "member");
426
+ const byDir = new Map<string, Member>();
427
+ for (const m of members) {
428
+ const other = byDir.get(m.dir);
429
+ if (other) {
430
+ throw new WorkspaceReadError(
431
+ "placement-invalid",
432
+ `member ${m.name} has the same directory as member ${other.name} (${m.dir})`,
433
+ at(`${m.pointer}/dir`),
434
+ );
435
+ }
436
+ byDir.set(m.dir, m);
437
+ }
438
+ for (const m of members) {
439
+ const outer = members.find((o) => o !== m && o.dir !== "." && isInside(m.dir, o.dir));
440
+ if (outer) {
441
+ throw new WorkspaceReadError(
442
+ "placement-invalid",
443
+ `member ${m.name} (${m.dir}) sits inside member ${outer.name} (${outer.dir}); only the root member "." may contain other members`,
444
+ at(`${m.pointer}/dir`),
445
+ );
446
+ }
447
+ }
448
+
449
+ // A member lists a generated file once, and never one inside another
450
+ // member's directory: that member owns the file and lists it (#2541).
451
+ for (const m of members) {
452
+ const seen = new Map<string, GeneratedFile>();
453
+ for (const g of m.generated) {
454
+ const first = seen.get(g.path);
455
+ if (first) {
456
+ throw new WorkspaceReadError("declaration-invalid", `member ${m.name} lists the generated file ${g.path} twice; the first is at ${first.pointer}`, at(`${g.pointer}/path`));
457
+ }
458
+ seen.set(g.path, g);
459
+ const full = m.dir === "." ? g.path : `${m.dir}/${g.path}`;
460
+ const inner = members.find((o) => o !== m && o.dir !== "." && isInside(o.dir, m.dir) && isInside(full, o.dir));
461
+ if (inner) {
462
+ throw new WorkspaceReadError(
463
+ "placement-invalid",
464
+ `member ${m.name} lists the generated file ${g.path}, which sits inside member ${inner.name} (${inner.dir}); list it there`,
465
+ at(`${g.pointer}/path`),
466
+ );
467
+ }
468
+ }
469
+ }
470
+
471
+ const pins = ((obj.pins as Record<string, string>[] | undefined) ?? []).map((p) => ({
472
+ package: p.package ?? null,
473
+ version: p.version ?? null,
474
+ path: p.path ?? null,
475
+ integrity: p.integrity ?? null,
476
+ }));
477
+
478
+ return {
479
+ name: obj.name as string,
480
+ schema: obj.schema as number,
481
+ minReader: (obj.minReader as string | undefined) ?? null,
482
+ entries,
483
+ members,
484
+ groups: entries.filter((e): e is Group => e.type === "group"),
485
+ pins,
486
+ checks: { ...((obj.checks as Record<string, CheckSeverity> | undefined) ?? {}) },
487
+ file,
488
+ };
489
+ }
490
+
491
+ /** Whether `path` is `dir` or sits under it. Both are tree-relative; `"."` is the root. */
492
+ export function isInside(path: string, dir: string): boolean {
493
+ if (dir === ".") return true;
494
+ return path === dir || path.startsWith(`${dir}/`);
495
+ }
496
+
497
+ // ── Reading from a tree ──────────────────────────────────────────────────────
498
+
499
+ /**
500
+ * Read the declaration in directory `dir` of `tree`. Throws
501
+ * `declaration-missing` when neither name is there and
502
+ * `declaration-ambiguous` when both are.
503
+ */
504
+ export function readDeclaration(tree: WorkspaceTree, dir = "", options: ReadOptions = {}): Declaration {
505
+ const present = DECLARATION_FILES.map((name) => joinPath(dir, name)).filter((p) => tree.stat(p) === "file");
506
+ if (present.length === 0) {
507
+ throw new WorkspaceReadError("declaration-missing", `no ${DECLARATION_FILES.join(" or ")} in ${dir || "the workspace root"}${tree.label}`);
508
+ }
509
+ if (present.length > 1) {
510
+ throw new WorkspaceReadError(
511
+ "declaration-ambiguous",
512
+ `both ${present.join(" and ")} exist${tree.label}; keep one of them`,
513
+ { file: present[1], line: 1, column: 1 },
514
+ );
515
+ }
516
+ return parseDeclaration(tree.read(present[0]), present[0], readerVersion(), options);
517
+ }
518
+
519
+ /**
520
+ * Walk up from `start` (tree-relative) to the nearest directory holding a
521
+ * declaration, within `tree`. The tree's root is the git root, so this is
522
+ * `findWorkspaceRoot` for a revision. Returns the directory, or undefined.
523
+ */
524
+ export function findDeclarationDir(tree: WorkspaceTree, start: string): string | undefined {
525
+ for (let dir = start; ; dir = dir.includes("/") ? dir.slice(0, dir.lastIndexOf("/")) : "") {
526
+ if (DECLARATION_FILES.some((name) => tree.stat(joinPath(dir, name)) === "file")) return dir;
527
+ if (dir === "") return undefined;
528
+ }
529
+ }
530
+
531
+ // ── Groups ───────────────────────────────────────────────────────────────────
532
+
533
+ export interface ResolvedGroup {
534
+ group: Group;
535
+ /** Directories the globs match that hold a chant project, sorted. */
536
+ matches: string[];
537
+ /** Directories the globs match that hold none; they are left alone. */
538
+ skipped: string[];
539
+ }
540
+
541
+ /**
542
+ * Expand every group's globs in `tree` (rooted at the workspace root) and
543
+ * check where the matches sit (ws-051): a match is never a member's directory
544
+ * and never contains one, it doesn't sit inside a nested workspace, and no
545
+ * two groups claim the same directory. A match may sit inside any other
546
+ * member's directory; that member leaves it to the group.
547
+ */
548
+ export function resolveGroups(declaration: Declaration, tree: WorkspaceTree): ResolvedGroup[] {
549
+ const at = (g: Group): ErrorLocation => ({ file: declaration.file, ...g.globLocation });
550
+ const claimed = new Map<string, Group>();
551
+ const resolved: ResolvedGroup[] = [];
552
+ for (const group of declaration.groups) {
553
+ const dirs = [...new Set(group.globs.flatMap((g) => expandGlob(tree, g)))].sort();
554
+ const matches: string[] = [];
555
+ const skipped: string[] = [];
556
+ for (const dir of dirs) {
557
+ if (!holdsChantProject(tree, dir)) {
558
+ skipped.push(dir);
559
+ continue;
560
+ }
561
+ const clash = (message: string) => new WorkspaceReadError("placement-invalid", `group ${group.name} matches ${dir}, ${message}`, at(group));
562
+ for (const m of declaration.members) {
563
+ if (m.dir === ".") continue;
564
+ if (m.dir === dir) throw clash(`which is member ${m.name}'s directory; a match is never a member`);
565
+ if (isInside(m.dir, dir)) throw clash(`which contains member ${m.name} (${m.dir})`);
566
+ if (m.kind === "workspace" && isInside(dir, m.dir)) {
567
+ throw clash(`which sits inside the nested workspace ${m.name}; that workspace declares its own examples`);
568
+ }
569
+ }
570
+ const other = claimed.get(dir);
571
+ if (other) throw clash(`which group ${other.name} also matches`);
572
+ claimed.set(dir, group);
573
+ matches.push(dir);
574
+ }
575
+ resolved.push({ group, matches, skipped });
576
+ }
577
+ return resolved;
578
+ }
579
+
580
+ // ── Ownership ────────────────────────────────────────────────────────────────
581
+
582
+ /**
583
+ * Which entry owns `path` (relative to the workspace root): the group whose
584
+ * match holds it, else the member with the deepest directory holding it, else
585
+ * none. A group match wins over the member whose directory it sits in
586
+ * (ws-051). Ledgers (#2538), telemetry attribution (#2558) and the root
587
+ * project's discovery (#2537) all ask this question.
588
+ */
589
+ export function ownerOf(
590
+ declaration: Declaration,
591
+ groups: ResolvedGroup[],
592
+ path: string,
593
+ ): { member: Member } | { group: Group; match: string } | undefined {
594
+ for (const g of groups) {
595
+ const match = g.matches.find((m) => isInside(path, m));
596
+ if (match) return { group: g.group, match };
597
+ }
598
+ let best: Member | undefined;
599
+ for (const m of declaration.members) {
600
+ if (!isInside(path, m.dir)) continue;
601
+ if (!best || best.dir === "." || (m.dir !== "." && m.dir.length > best.dir.length)) best = m;
602
+ }
603
+ return best ? { member: best } : undefined;
604
+ }
605
+
606
+ /**
607
+ * The directories that leave the root project once the declaration exists
608
+ * (#2524 D0, D2): every member's directory but the root's, and every group
609
+ * match. The root project's build, lint, Op and audit discovery skip them
610
+ * (#2537); `chant workspace init` prints them before it writes.
611
+ */
612
+ export function rootExclusions(declaration: Declaration, groups: ResolvedGroup[]): { dir: string; owner: string }[] {
613
+ const out: { dir: string; owner: string }[] = [];
614
+ for (const m of declaration.members) if (m.dir !== ".") out.push({ dir: m.dir, owner: m.name });
615
+ for (const g of groups) {
616
+ for (const dir of g.matches) {
617
+ // A match inside a member's directory already left with that member.
618
+ if (declaration.members.some((m) => m.dir !== "." && isInside(dir, m.dir))) continue;
619
+ out.push({ dir, owner: g.group.name });
620
+ }
621
+ }
622
+ return out.sort((a, b) => (a.dir < b.dir ? -1 : a.dir > b.dir ? 1 : 0));
623
+ }