@intentius/chant 0.80.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 (414) hide show
  1. package/dist/audit/discover.d.ts +8 -16
  2. package/dist/audit/discover.d.ts.map +1 -1
  3. package/dist/audit/report-html.d.ts +5 -0
  4. package/dist/audit/report-html.d.ts.map +1 -1
  5. package/dist/audit/report-model.d.ts +13 -0
  6. package/dist/audit/report-model.d.ts.map +1 -1
  7. package/dist/audit/terraform-state.d.ts +4 -5
  8. package/dist/audit/terraform-state.d.ts.map +1 -1
  9. package/dist/cli/commands/audit.d.ts +25 -9
  10. package/dist/cli/commands/audit.d.ts.map +1 -1
  11. package/dist/cli/commands/check-lexicon.d.ts +8 -0
  12. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  13. package/dist/cli/commands/doctor.d.ts.map +1 -1
  14. package/dist/cli/commands/import-agents.d.ts.map +1 -1
  15. package/dist/cli/commands/init.d.ts.map +1 -1
  16. package/dist/cli/commands/lint.d.ts.map +1 -1
  17. package/dist/cli/commands/update.d.ts +6 -0
  18. package/dist/cli/commands/update.d.ts.map +1 -1
  19. package/dist/cli/commands/vendor.d.ts +5 -0
  20. package/dist/cli/commands/vendor.d.ts.map +1 -1
  21. package/dist/cli/handlers/build.d.ts.map +1 -1
  22. package/dist/cli/handlers/components.d.ts.map +1 -1
  23. package/dist/cli/handlers/dev.d.ts.map +1 -1
  24. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  25. package/dist/cli/handlers/init.d.ts.map +1 -1
  26. package/dist/cli/handlers/operator.d.ts +7 -0
  27. package/dist/cli/handlers/operator.d.ts.map +1 -1
  28. package/dist/cli/handlers/promote.d.ts +7 -4
  29. package/dist/cli/handlers/promote.d.ts.map +1 -1
  30. package/dist/cli/handlers/run-generate.d.ts.map +1 -1
  31. package/dist/cli/handlers/run.d.ts +4 -1
  32. package/dist/cli/handlers/run.d.ts.map +1 -1
  33. package/dist/cli/handlers/vendor.d.ts +8 -3
  34. package/dist/cli/handlers/vendor.d.ts.map +1 -1
  35. package/dist/cli/main.d.ts +12 -0
  36. package/dist/cli/main.d.ts.map +1 -1
  37. package/dist/cli/plugins.d.ts +12 -7
  38. package/dist/cli/plugins.d.ts.map +1 -1
  39. package/dist/cli/registry.d.ts +32 -0
  40. package/dist/cli/registry.d.ts.map +1 -1
  41. package/dist/codegen/fetch.d.ts +10 -0
  42. package/dist/codegen/fetch.d.ts.map +1 -1
  43. package/dist/components/cli-support.d.ts +3 -1
  44. package/dist/components/cli-support.d.ts.map +1 -1
  45. package/dist/components/discover.d.ts.map +1 -1
  46. package/dist/components/driver.d.ts +16 -2
  47. package/dist/components/driver.d.ts.map +1 -1
  48. package/dist/components/gate-plan.d.ts +40 -0
  49. package/dist/components/gate-plan.d.ts.map +1 -0
  50. package/dist/components/promote.d.ts +60 -7
  51. package/dist/components/promote.d.ts.map +1 -1
  52. package/dist/components/sandbox/driver.d.ts.map +1 -1
  53. package/dist/components/sandbox/run.d.ts.map +1 -1
  54. package/dist/components/verbs/build-archive.d.ts +22 -11
  55. package/dist/components/verbs/build-archive.d.ts.map +1 -1
  56. package/dist/components/verbs/run-agent.d.ts.map +1 -1
  57. package/dist/config-static.d.ts +59 -0
  58. package/dist/config-static.d.ts.map +1 -0
  59. package/dist/config.d.ts +32 -10
  60. package/dist/config.d.ts.map +1 -1
  61. package/dist/content-digest.d.ts +17 -0
  62. package/dist/content-digest.d.ts.map +1 -0
  63. package/dist/discovery/files.d.ts +10 -5
  64. package/dist/discovery/files.d.ts.map +1 -1
  65. package/dist/discovery/fold-import.d.ts +20 -4
  66. package/dist/discovery/fold-import.d.ts.map +1 -1
  67. package/dist/discovery/index.d.ts.map +1 -1
  68. package/dist/discovery/sandbox/driver.d.ts +20 -0
  69. package/dist/discovery/sandbox/driver.d.ts.map +1 -1
  70. package/dist/discovery/sandbox/fork.d.ts +20 -0
  71. package/dist/discovery/sandbox/fork.d.ts.map +1 -1
  72. package/dist/discovery/walk.d.ts +119 -0
  73. package/dist/discovery/walk.d.ts.map +1 -0
  74. package/dist/graph-ir.d.ts +4 -1
  75. package/dist/graph-ir.d.ts.map +1 -1
  76. package/dist/join-key.d.ts +31 -0
  77. package/dist/join-key.d.ts.map +1 -0
  78. package/dist/lexicon-module.d.ts +51 -6
  79. package/dist/lexicon-module.d.ts.map +1 -1
  80. package/dist/lexicon.d.ts +58 -1
  81. package/dist/lexicon.d.ts.map +1 -1
  82. package/dist/lifecycle/build-ledger-store.d.ts.map +1 -1
  83. package/dist/lifecycle/digest.d.ts +5 -1
  84. package/dist/lifecycle/digest.d.ts.map +1 -1
  85. package/dist/lifecycle/gate-ledger.d.ts +20 -5
  86. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  87. package/dist/lifecycle/git.d.ts +64 -0
  88. package/dist/lifecycle/git.d.ts.map +1 -1
  89. package/dist/lifecycle/lease.d.ts +8 -1
  90. package/dist/lifecycle/lease.d.ts.map +1 -1
  91. package/dist/lifecycle/legacy-digest.d.ts +41 -8
  92. package/dist/lifecycle/legacy-digest.d.ts.map +1 -1
  93. package/dist/lifecycle/member-ledger.d.ts +67 -0
  94. package/dist/lifecycle/member-ledger.d.ts.map +1 -0
  95. package/dist/lifecycle/plan-digest.d.ts +4 -3
  96. package/dist/lifecycle/plan-digest.d.ts.map +1 -1
  97. package/dist/lifecycle/release-ledger.d.ts +18 -2
  98. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  99. package/dist/lifecycle/status.d.ts +90 -5
  100. package/dist/lifecycle/status.d.ts.map +1 -1
  101. package/dist/op/activities/activity-contracts.d.ts +32 -0
  102. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  103. package/dist/op/activities/index.d.ts +2 -0
  104. package/dist/op/activities/index.d.ts.map +1 -1
  105. package/dist/op/activities/propose-upgrade.d.ts +75 -0
  106. package/dist/op/activities/propose-upgrade.d.ts.map +1 -0
  107. package/dist/op/discover.d.ts.map +1 -1
  108. package/dist/op/gate-name.d.ts +7 -0
  109. package/dist/op/gate-name.d.ts.map +1 -1
  110. package/dist/op/gate-summary.d.ts +16 -0
  111. package/dist/op/gate-summary.d.ts.map +1 -1
  112. package/dist/op/gate.d.ts +16 -2
  113. package/dist/op/gate.d.ts.map +1 -1
  114. package/dist/project-root.d.ts +26 -0
  115. package/dist/project-root.d.ts.map +1 -1
  116. package/dist/workspace/__fixtures__/contract-repo.d.ts +37 -0
  117. package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -0
  118. package/dist/workspace/__fixtures__/two-member-workspace.d.ts +11 -0
  119. package/dist/workspace/__fixtures__/two-member-workspace.d.ts.map +1 -0
  120. package/dist/workspace/checks/generated.d.ts +156 -0
  121. package/dist/workspace/checks/generated.d.ts.map +1 -0
  122. package/dist/workspace/checks/ledgers.d.ts +78 -0
  123. package/dist/workspace/checks/ledgers.d.ts.map +1 -0
  124. package/dist/workspace/checks/links.d.ts +25 -0
  125. package/dist/workspace/checks/links.d.ts.map +1 -0
  126. package/dist/workspace/checks/pipelines.d.ts +76 -0
  127. package/dist/workspace/checks/pipelines.d.ts.map +1 -0
  128. package/dist/workspace/checks.d.ts +161 -0
  129. package/dist/workspace/checks.d.ts.map +1 -0
  130. package/dist/workspace/compose-graph.d.ts +115 -0
  131. package/dist/workspace/compose-graph.d.ts.map +1 -0
  132. package/dist/workspace/compose-reports.d.ts +68 -0
  133. package/dist/workspace/compose-reports.d.ts.map +1 -0
  134. package/dist/workspace/declaration.d.ts +220 -0
  135. package/dist/workspace/declaration.d.ts.map +1 -0
  136. package/dist/workspace/declaration.schema.json +434 -0
  137. package/dist/workspace/generated-files.d.ts +65 -0
  138. package/dist/workspace/generated-files.d.ts.map +1 -0
  139. package/dist/workspace/glob.d.ts +13 -0
  140. package/dist/workspace/glob.d.ts.map +1 -0
  141. package/dist/workspace/graph-cli.d.ts +75 -0
  142. package/dist/workspace/graph-cli.d.ts.map +1 -0
  143. package/dist/workspace/init.d.ts +96 -0
  144. package/dist/workspace/init.d.ts.map +1 -0
  145. package/dist/workspace/jsonc.d.ts +38 -0
  146. package/dist/workspace/jsonc.d.ts.map +1 -0
  147. package/dist/workspace/kinds.d.ts +192 -0
  148. package/dist/workspace/kinds.d.ts.map +1 -0
  149. package/dist/workspace/lineage-check.d.ts +84 -0
  150. package/dist/workspace/lineage-check.d.ts.map +1 -0
  151. package/dist/workspace/lineage-cli.d.ts +31 -0
  152. package/dist/workspace/lineage-cli.d.ts.map +1 -0
  153. package/dist/workspace/lineage-init.d.ts +101 -0
  154. package/dist/workspace/lineage-init.d.ts.map +1 -0
  155. package/dist/workspace/lineage-lock.d.ts +258 -0
  156. package/dist/workspace/lineage-lock.d.ts.map +1 -0
  157. package/dist/workspace/lineage-merge.d.ts +14 -0
  158. package/dist/workspace/lineage-merge.d.ts.map +1 -0
  159. package/dist/workspace/lineage-migrations.d.ts +178 -0
  160. package/dist/workspace/lineage-migrations.d.ts.map +1 -0
  161. package/dist/workspace/lineage-update.d.ts +59 -0
  162. package/dist/workspace/lineage-update.d.ts.map +1 -0
  163. package/dist/workspace/lineage-upgrade-cli.d.ts +40 -0
  164. package/dist/workspace/lineage-upgrade-cli.d.ts.map +1 -0
  165. package/dist/workspace/lineage-upgrade.d.ts +146 -0
  166. package/dist/workspace/lineage-upgrade.d.ts.map +1 -0
  167. package/dist/workspace/lineage-vendor.d.ts +63 -0
  168. package/dist/workspace/lineage-vendor.d.ts.map +1 -0
  169. package/dist/workspace/lineage-version.d.ts +32 -0
  170. package/dist/workspace/lineage-version.d.ts.map +1 -0
  171. package/dist/workspace/links.d.ts +150 -0
  172. package/dist/workspace/links.d.ts.map +1 -0
  173. package/dist/workspace/ls.d.ts +99 -0
  174. package/dist/workspace/ls.d.ts.map +1 -0
  175. package/dist/workspace/member-commands.d.ts +139 -0
  176. package/dist/workspace/member-commands.d.ts.map +1 -0
  177. package/dist/workspace/member-handles.d.ts +54 -0
  178. package/dist/workspace/member-handles.d.ts.map +1 -0
  179. package/dist/workspace/member-pipeline.d.ts +143 -0
  180. package/dist/workspace/member-pipeline.d.ts.map +1 -0
  181. package/dist/workspace/member-run.d.ts +67 -0
  182. package/dist/workspace/member-run.d.ts.map +1 -0
  183. package/dist/workspace/reason-codes.d.ts +65 -0
  184. package/dist/workspace/reason-codes.d.ts.map +1 -0
  185. package/dist/workspace/records-cli.d.ts +28 -4
  186. package/dist/workspace/records-cli.d.ts.map +1 -1
  187. package/dist/workspace/records.d.ts.map +1 -1
  188. package/dist/workspace/root-refusal.d.ts +53 -0
  189. package/dist/workspace/root-refusal.d.ts.map +1 -0
  190. package/dist/workspace/status.d.ts +150 -0
  191. package/dist/workspace/status.d.ts.map +1 -0
  192. package/dist/workspace/template-manifest.d.ts +90 -0
  193. package/dist/workspace/template-manifest.d.ts.map +1 -0
  194. package/dist/workspace/tree.d.ts +36 -0
  195. package/dist/workspace/tree.d.ts.map +1 -0
  196. package/dist/workspace/trust/attestor.d.ts +67 -0
  197. package/dist/workspace/trust/attestor.d.ts.map +1 -0
  198. package/dist/workspace/trust/policy.d.ts +111 -0
  199. package/dist/workspace/trust/policy.d.ts.map +1 -0
  200. package/dist/workspace/trust/provenance.d.ts +67 -0
  201. package/dist/workspace/trust/provenance.d.ts.map +1 -0
  202. package/dist/workspace/trust/ssh-commit.d.ts +43 -0
  203. package/dist/workspace/trust/ssh-commit.d.ts.map +1 -0
  204. package/dist/workspace/trust/test-repo.d.ts +42 -0
  205. package/dist/workspace/trust/test-repo.d.ts.map +1 -0
  206. package/dist/workspace/trust/verify-cli.d.ts +9 -0
  207. package/dist/workspace/trust/verify-cli.d.ts.map +1 -0
  208. package/dist/workspace/trust/verify.d.ts +66 -0
  209. package/dist/workspace/trust/verify.d.ts.map +1 -0
  210. package/dist/workspace/which-chant.d.ts +58 -0
  211. package/dist/workspace/which-chant.d.ts.map +1 -0
  212. package/dist/workspace/workspace-kinds.schema.json +133 -0
  213. package/package.json +2 -1
  214. package/src/audit/discover.ts +66 -78
  215. package/src/audit/report-html.ts +3 -1
  216. package/src/audit/report-model.ts +14 -1
  217. package/src/audit/terraform-state.test.ts +4 -9
  218. package/src/audit/terraform-state.ts +4 -5
  219. package/src/cli/commands/audit-truncation.test.ts +167 -0
  220. package/src/cli/commands/audit.ts +89 -60
  221. package/src/cli/commands/check-lexicon.ts +24 -0
  222. package/src/cli/commands/doctor.test.ts +30 -0
  223. package/src/cli/commands/doctor.ts +18 -0
  224. package/src/cli/commands/import-agents.ts +6 -2
  225. package/src/cli/commands/init.ts +22 -3
  226. package/src/cli/commands/lint.test.ts +17 -23
  227. package/src/cli/commands/lint.ts +24 -84
  228. package/src/cli/commands/update.ts +13 -4
  229. package/src/cli/commands/vendor.ts +11 -6
  230. package/src/cli/handlers/__snapshots__/build-member.test.ts.snap +38 -0
  231. package/src/cli/handlers/build-member.test.ts +127 -0
  232. package/src/cli/handlers/build.ts +35 -1
  233. package/src/cli/handlers/components.test.ts +127 -0
  234. package/src/cli/handlers/components.ts +70 -8
  235. package/src/cli/handlers/dev.ts +5 -3
  236. package/src/cli/handlers/fan-out.ts +9 -2
  237. package/src/cli/handlers/init.ts +54 -1
  238. package/src/cli/handlers/operator.test.ts +68 -0
  239. package/src/cli/handlers/operator.ts +37 -9
  240. package/src/cli/handlers/promote.test.ts +118 -2
  241. package/src/cli/handlers/promote.ts +82 -16
  242. package/src/cli/handlers/run-generate-member.test.ts +83 -0
  243. package/src/cli/handlers/run-generate.test.ts +6 -8
  244. package/src/cli/handlers/run-generate.ts +36 -1
  245. package/src/cli/handlers/run.test.ts +47 -0
  246. package/src/cli/handlers/run.ts +28 -5
  247. package/src/cli/handlers/vendor.ts +147 -43
  248. package/src/cli/main.test.ts +105 -1
  249. package/src/cli/main.ts +230 -17
  250. package/src/cli/plugins.ts +17 -17
  251. package/src/cli/registry.ts +32 -0
  252. package/src/cli/static-config-read.test.ts +208 -0
  253. package/src/codegen/fetch.test.ts +63 -1
  254. package/src/codegen/fetch.ts +30 -1
  255. package/src/components/cli-support.ts +6 -1
  256. package/src/components/discover.test.ts +27 -57
  257. package/src/components/discover.ts +13 -68
  258. package/src/components/driver.test.ts +78 -1
  259. package/src/components/driver.ts +43 -7
  260. package/src/components/gate-plan.ts +53 -0
  261. package/src/components/promote.test.ts +276 -2
  262. package/src/components/promote.ts +119 -15
  263. package/src/components/sandbox/driver.ts +8 -8
  264. package/src/components/sandbox/run.ts +4 -1
  265. package/src/components/verbs/build-archive.ts +27 -21
  266. package/src/components/verbs/run-agent.ts +3 -5
  267. package/src/config-static.test.ts +136 -0
  268. package/src/config-static.ts +232 -0
  269. package/src/config.test.ts +2 -2
  270. package/src/config.ts +48 -15
  271. package/src/content-digest.test.ts +120 -0
  272. package/src/content-digest.ts +21 -0
  273. package/src/discovery/files.test.ts +10 -10
  274. package/src/discovery/files.ts +25 -81
  275. package/src/discovery/fold-import.ts +49 -5
  276. package/src/discovery/index.ts +1 -0
  277. package/src/discovery/sandbox/driver.ts +73 -35
  278. package/src/discovery/sandbox/fork-diagnostic.test.ts +136 -13
  279. package/src/discovery/sandbox/fork.ts +83 -43
  280. package/src/discovery/sandbox/path-lexicon-multifile.test.ts +285 -0
  281. package/src/discovery/walk.test.ts +263 -0
  282. package/src/discovery/walk.ts +334 -0
  283. package/src/effect-receipt.ts +2 -2
  284. package/src/graph-ir.ts +4 -1
  285. package/src/join-key.test.ts +25 -0
  286. package/src/join-key.ts +39 -0
  287. package/src/lexicon-module.ts +108 -10
  288. package/src/lexicon.ts +79 -1
  289. package/src/lifecycle/build-ledger-store.ts +11 -10
  290. package/src/lifecycle/digest.ts +9 -5
  291. package/src/lifecycle/gate-ledger.test.ts +20 -0
  292. package/src/lifecycle/gate-ledger.ts +28 -6
  293. package/src/lifecycle/git.test.ts +13 -1
  294. package/src/lifecycle/git.ts +221 -176
  295. package/src/lifecycle/lease.ts +25 -10
  296. package/src/lifecycle/legacy-digest.test.ts +115 -23
  297. package/src/lifecycle/legacy-digest.ts +60 -19
  298. package/src/lifecycle/member-ledger.test.ts +319 -0
  299. package/src/lifecycle/member-ledger.ts +145 -0
  300. package/src/lifecycle/plan-digest.ts +7 -7
  301. package/src/lifecycle/release-ledger.test.ts +20 -0
  302. package/src/lifecycle/release-ledger.ts +23 -7
  303. package/src/lifecycle/status.test.ts +151 -1
  304. package/src/lifecycle/status.ts +228 -7
  305. package/src/op/activities/activity-contracts.ts +37 -0
  306. package/src/op/activities/index.ts +10 -0
  307. package/src/op/activities/propose-upgrade.ts +206 -0
  308. package/src/op/discover.test.ts +14 -4
  309. package/src/op/discover.ts +19 -27
  310. package/src/op/gate-name.ts +8 -0
  311. package/src/op/gate-summary.test.ts +14 -0
  312. package/src/op/gate-summary.ts +25 -2
  313. package/src/op/gate.ts +36 -7
  314. package/src/op/generate-pipeline.test.ts +14 -5
  315. package/src/project-root.test.ts +66 -1
  316. package/src/project-root.ts +48 -0
  317. package/src/workspace/__fixtures__/contract-repo.ts +100 -0
  318. package/src/workspace/__fixtures__/two-member-workspace.ts +35 -0
  319. package/src/workspace/behold-kinds.test.ts +162 -0
  320. package/src/workspace/check-contract.test.ts +127 -0
  321. package/src/workspace/check.schema.json +347 -0
  322. package/src/workspace/checks/generated.test.ts +287 -0
  323. package/src/workspace/checks/generated.ts +475 -0
  324. package/src/workspace/checks/ledgers.test.ts +139 -0
  325. package/src/workspace/checks/ledgers.ts +246 -0
  326. package/src/workspace/checks/links.ts +183 -0
  327. package/src/workspace/checks/pipelines.test.ts +133 -0
  328. package/src/workspace/checks/pipelines.ts +225 -0
  329. package/src/workspace/checks-repo.test.ts +71 -0
  330. package/src/workspace/checks.test.ts +450 -0
  331. package/src/workspace/checks.ts +461 -0
  332. package/src/workspace/compose-graph.test.ts +146 -0
  333. package/src/workspace/compose-graph.ts +228 -0
  334. package/src/workspace/compose-reports.test.ts +109 -0
  335. package/src/workspace/compose-reports.ts +186 -0
  336. package/src/workspace/declaration.schema.json +434 -0
  337. package/src/workspace/declaration.test.ts +347 -0
  338. package/src/workspace/declaration.ts +623 -0
  339. package/src/workspace/generated-files.test.ts +114 -0
  340. package/src/workspace/generated-files.ts +94 -0
  341. package/src/workspace/glob.ts +49 -0
  342. package/src/workspace/graph-cli.ts +220 -0
  343. package/src/workspace/graph-contract.test.ts +204 -0
  344. package/src/workspace/graph.schema.json +662 -0
  345. package/src/workspace/init.test.ts +223 -0
  346. package/src/workspace/init.ts +585 -0
  347. package/src/workspace/jsonc.test.ts +65 -0
  348. package/src/workspace/jsonc.ts +231 -0
  349. package/src/workspace/kinds.test.ts +288 -0
  350. package/src/workspace/kinds.ts +506 -0
  351. package/src/workspace/lineage-check.ts +264 -0
  352. package/src/workspace/lineage-cli.ts +114 -0
  353. package/src/workspace/lineage-init.test.ts +231 -0
  354. package/src/workspace/lineage-init.ts +365 -0
  355. package/src/workspace/lineage-lock.test.ts +224 -0
  356. package/src/workspace/lineage-lock.ts +357 -0
  357. package/src/workspace/lineage-merge.ts +44 -0
  358. package/src/workspace/lineage-migrations.test.ts +178 -0
  359. package/src/workspace/lineage-migrations.ts +343 -0
  360. package/src/workspace/lineage-update.ts +160 -0
  361. package/src/workspace/lineage-upgrade-cli.ts +151 -0
  362. package/src/workspace/lineage-upgrade.test.ts +474 -0
  363. package/src/workspace/lineage-upgrade.ts +624 -0
  364. package/src/workspace/lineage-vendor.test.ts +126 -0
  365. package/src/workspace/lineage-vendor.ts +215 -0
  366. package/src/workspace/lineage-version.ts +127 -0
  367. package/src/workspace/links.test.ts +344 -0
  368. package/src/workspace/links.ts +331 -0
  369. package/src/workspace/ls-contract.test.ts +200 -0
  370. package/src/workspace/ls.schema.json +190 -0
  371. package/src/workspace/ls.ts +254 -0
  372. package/src/workspace/member-commands.e2e.test.ts +244 -0
  373. package/src/workspace/member-commands.test.ts +217 -0
  374. package/src/workspace/member-commands.ts +540 -0
  375. package/src/workspace/member-handles.test.ts +76 -0
  376. package/src/workspace/member-handles.ts +228 -0
  377. package/src/workspace/member-pipeline.test.ts +135 -0
  378. package/src/workspace/member-pipeline.ts +317 -0
  379. package/src/workspace/member-run.ts +146 -0
  380. package/src/workspace/read-contract.test.ts +129 -0
  381. package/src/workspace/reason-codes.test.ts +113 -0
  382. package/src/workspace/reason-codes.ts +81 -0
  383. package/src/workspace/records-cli.ts +86 -14
  384. package/src/workspace/records-contract.test.ts +5 -0
  385. package/src/workspace/records.schema.json +32 -2
  386. package/src/workspace/records.ts +3 -2
  387. package/src/workspace/root-refusal.ts +97 -0
  388. package/src/workspace/status-contract.test.ts +308 -0
  389. package/src/workspace/status.schema.json +210 -0
  390. package/src/workspace/status.ts +412 -0
  391. package/src/workspace/template-manifest.test.ts +79 -0
  392. package/src/workspace/template-manifest.ts +221 -0
  393. package/src/workspace/tree.ts +132 -0
  394. package/src/workspace/trust/attestor.ts +93 -0
  395. package/src/workspace/trust/policy.ts +306 -0
  396. package/src/workspace/trust/provenance.ts +181 -0
  397. package/src/workspace/trust/ssh-commit.ts +138 -0
  398. package/src/workspace/trust/test-repo.ts +138 -0
  399. package/src/workspace/trust/trust.test.ts +397 -0
  400. package/src/workspace/trust/verify-cli.ts +56 -0
  401. package/src/workspace/trust/verify.ts +194 -0
  402. package/src/workspace/which-chant.test.ts +117 -0
  403. package/src/workspace/which-chant.ts +151 -0
  404. package/src/workspace/workspace-kinds.schema.json +133 -0
  405. package/dist/components/unbound-gate-approval.d.ts +0 -24
  406. package/dist/components/unbound-gate-approval.d.ts.map +0 -1
  407. package/dist/discovery/convergence.d.ts +0 -97
  408. package/dist/discovery/convergence.d.ts.map +0 -1
  409. package/src/cli/commands/audit-walk-warnings.test.ts +0 -156
  410. package/src/components/unbound-gate-approval.test.ts +0 -82
  411. package/src/components/unbound-gate-approval.ts +0 -51
  412. package/src/discovery/convergence.test.ts +0 -231
  413. package/src/discovery/convergence.ts +0 -399
  414. package/src/op/discover-convergence.test.ts +0 -70
@@ -13,6 +13,7 @@ import { afterAll, describe, expect, test } from "vitest";
13
13
  import { queryRecords, RECORDS_CONTRACT_VERSION, RECORDS_OUTPUT_SCHEMA_ID, type RecordsDocument } from "./records-cli";
14
14
  import { READ_ERROR_CODES, RECORD_REASON_CODES } from "./records";
15
15
  import schema from "./records.schema.json";
16
+ import { PROVENANCE_LEVELS } from "./trust/attestor";
16
17
 
17
18
  const REPO = join(import.meta.dirname, "..", "..", "..", "..");
18
19
  const KIND = "docs/design/decisions/decision.kind.mjs";
@@ -52,6 +53,10 @@ describe("records output schema", () => {
52
53
  expect(schema.$defs.failure.properties.error.properties.code.enum).toEqual([...READ_ERROR_CODES]);
53
54
  });
54
55
 
56
+ test("lists exactly the provenance levels (#2547)", () => {
57
+ expect(schema.$defs.provenance.properties.level.enum).toEqual([...PROVENANCE_LEVELS]);
58
+ });
59
+
55
60
  test("the chant repo's decisions validate, current and not", async () => {
56
61
  for (const current of [false, true]) {
57
62
  const doc = await queryRecords({ kind: KIND, current, cwd: REPO });
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://intentius.io/chant/schemas/workspace/records/v1/records.schema.json",
4
4
  "title": "chant workspace records output",
5
- "description": "What `chant workspace records --json` prints, version 1 of the read contract for records (#2536). This is the first-test slice (#2546): records are unsealed and there is no workspace declaration yet. Readers ignore fields they do not know; a field is only ever added within a version. The reason and error codes are closed lists: a new code is a new contract version.",
5
+ "description": "What `chant workspace records --json` prints, version 1 of the read contract for records (#2536). This is the first-test slice (#2546): records are unsealed and there is no workspace declaration yet. Readers ignore fields they do not know; a field is only ever added within a version. The reason and error codes are closed lists: a new code is a new contract version. Contract version 1 is written by chant 0.81.0 and newer; a reader that needs it refuses output whose `contract` it doesn't know. Every code is in the one closed list of `reason-codes.ts`.",
6
6
  "oneOf": [{ "$ref": "#/$defs/result" }, { "$ref": "#/$defs/failure" }],
7
7
  "$defs": {
8
8
  "result": {
@@ -28,6 +28,7 @@
28
28
  },
29
29
  "error": false,
30
30
  "current": { "type": "boolean", "description": "Whether superseded records were left out (--current)." },
31
+ "trust": { "$ref": "#/$defs/trust" },
31
32
  "records": {
32
33
  "description": "One entry per file the kind locates, sorted by path.",
33
34
  "type": "array",
@@ -61,7 +62,8 @@
61
62
  "data": {
62
63
  "type": ["object", "null"],
63
64
  "description": "The front matter as JSON, following the kind's schema when valid is true. Null when it could not be parsed."
64
- }
65
+ },
66
+ "provenance": { "$ref": "#/$defs/provenance" }
65
67
  },
66
68
  "allOf": [
67
69
  {
@@ -71,6 +73,34 @@
71
73
  }
72
74
  ]
73
75
  },
76
+ "trust": {
77
+ "description": "Added in contract 1 by #2547. Where provenance was judged from: the policy read at the base revision, never the tree read.",
78
+ "type": "object",
79
+ "required": ["base", "baseFrom", "active", "signersPath", "problems"],
80
+ "properties": {
81
+ "base": { "type": ["string", "null"], "pattern": "^[0-9a-f]{40,64}$", "description": "The full commit id the policy was read at, or null when there is no base." },
82
+ "baseFrom": { "enum": ["flag", "origin/HEAD", "main", "master", null], "description": "flag when --base was given; otherwise the target branch that was found." },
83
+ "active": { "type": "boolean", "description": "Whether a signers file exists at base. False means attestation is off and every record is unattested." },
84
+ "signersPath": { "type": "string", "description": "The signers file, relative to the repository root." },
85
+ "problems": { "type": "array", "items": { "type": "string" }, "description": "Why the policy could not be read. Nothing is attested when this is not empty." }
86
+ }
87
+ },
88
+ "provenance": {
89
+ "description": "Added in contract 1 by #2547. How far the record's authorship is established, judged by the signers at base.",
90
+ "type": "object",
91
+ "required": ["level", "commit", "reason"],
92
+ "properties": {
93
+ "level": {
94
+ "enum": ["attested", "attested-unverifiable-here", "adopted", "unattested"],
95
+ "description": "attested: the last commit to change the file is signed by a signer the policy at base lists. attested-unverifiable-here: it carries a signature nothing here can check. adopted: it lies in a commit range the policy adopts. unattested: none of these, including uncommitted content."
96
+ },
97
+ "commit": { "type": ["string", "null"], "pattern": "^[0-9a-f]{40,64}$", "description": "The commit the record's content comes from, or null for uncommitted content." },
98
+ "attestor": { "type": "string", "description": "The attestor that answered, such as ssh-commit." },
99
+ "principal": { "type": "string", "description": "The signer, when attested." },
100
+ "key": { "type": "string", "description": "The signing key's fingerprint, when known." },
101
+ "reason": { "type": "string" }
102
+ }
103
+ },
74
104
  "reason": {
75
105
  "type": "object",
76
106
  "required": ["code", "message"],
@@ -21,6 +21,7 @@ import { dirname, posix, relative, resolve, sep } from "node:path";
21
21
  import yaml from "js-yaml";
22
22
  import { z } from "zod";
23
23
  import { importLexiconModule, registerLexiconDeclarations } from "../lexicon-module";
24
+ import type { ReasonCode } from "./reason-codes";
24
25
  import type { RecordSource } from "./record-source";
25
26
 
26
27
  // ── Reason codes ─────────────────────────────────────────────────────────────
@@ -40,7 +41,7 @@ export const RECORD_REASON_CODES = [
40
41
  "record-supersedes-unknown",
41
42
  /** A second closed record supersedes a record another one already superseded. */
42
43
  "record-supersedes-conflict",
43
- ] as const;
44
+ ] as const satisfies readonly ReasonCode[];
44
45
  export type RecordReasonCode = (typeof RECORD_REASON_CODES)[number];
45
46
 
46
47
  /**
@@ -64,7 +65,7 @@ export const READ_ERROR_CODES = [
64
65
  "not-a-git-repository",
65
66
  /** `--at` names no commit. */
66
67
  "revision-unknown",
67
- ] as const;
68
+ ] as const satisfies readonly ReasonCode[];
68
69
  export type ReadErrorCode = (typeof READ_ERROR_CODES)[number];
69
70
 
70
71
  export class RecordReadError extends Error {
@@ -0,0 +1,97 @@
1
+ /**
2
+ * `chant build` and `chant lint` at the root of a declared workspace (#2537,
3
+ * #2524 D0 and D12, ws-023).
4
+ *
5
+ * Once `chant.workspace.json` declares members, their directories leave the
6
+ * root project. A top-level `chant build` there would quietly build less than
7
+ * it did before the file existed, so both commands refuse with `WSP000` and
8
+ * point to `chant workspace build` and `chant workspace lint`. `--root-only`,
9
+ * or `rootOnly: true` in the root's `chant.config`, lets them run on the root
10
+ * project alone, with the member directories and example-group matches left
11
+ * out of discovery.
12
+ *
13
+ * `main.ts` decides with `findWorkspaceRoot` and `findProjectRoot`, which only
14
+ * test whether files exist, and imports this module only when the command's
15
+ * project is a declared root. A project with no declaration never loads it.
16
+ */
17
+
18
+ import { relative } from "node:path";
19
+ import { formatError } from "../cli/format";
20
+ import { ownerOf, readDeclaration, resolveGroups, rootExclusions, WorkspaceReadError } from "./declaration";
21
+ import { workingTree } from "./tree";
22
+
23
+ /** The code a refusal carries. `WSP` ids name workspace findings (#2524 D16); `000` is the refusal itself. */
24
+ export const ROOT_REFUSAL_CODE = "WSP000";
25
+
26
+ export interface RootGuardInput {
27
+ verb: "build" | "lint";
28
+ /** The command's target, absolute. */
29
+ target: string;
30
+ /** The directory holding the declaration, absolute. */
31
+ workspaceDir: string;
32
+ /** `--root-only` on the command line, or `rootOnly: true` in the root's chant.config. */
33
+ rootOnly: boolean;
34
+ }
35
+
36
+ export type RootGuardResult =
37
+ /** The target belongs to a member or an example group, so it is not the root project; nothing changes. */
38
+ | { action: "not-root" }
39
+ /** The command runs on the root project with these directories (relative to the root) left out. */
40
+ | { action: "root-only"; excluded: string[] }
41
+ /** The command stops; `message` says why and what to run instead. */
42
+ | { action: "refuse"; message: string };
43
+
44
+ /** Decide what a root `build` or `lint` does. Pure apart from reading the declaration. */
45
+ export function decideRootCommand(input: RootGuardInput): RootGuardResult {
46
+ const tree = workingTree(input.workspaceDir);
47
+ let declaration;
48
+ let groups;
49
+ try {
50
+ declaration = readDeclaration(tree);
51
+ groups = resolveGroups(declaration, tree);
52
+ } catch (err) {
53
+ if (!(err instanceof WorkspaceReadError)) throw err;
54
+ return {
55
+ action: "refuse",
56
+ message:
57
+ `${ROOT_REFUSAL_CODE}: ${input.workspaceDir} holds a workspace declaration that can't be read, so chant ${input.verb} ` +
58
+ `can't tell which directories belong to the root project. ${err.code}: ${err.describe()}`,
59
+ };
60
+ }
61
+ const rel = relative(input.workspaceDir, input.target).split("\\").join("/") || ".";
62
+ const owner = ownerOf(declaration, groups, rel === "." ? "." : rel);
63
+ if (owner && ("group" in owner || owner.member.dir !== ".")) return { action: "not-root" };
64
+
65
+ const excluded = rootExclusions(declaration, groups).map((e) => e.dir);
66
+ if (input.rootOnly) return { action: "root-only", excluded };
67
+
68
+ const members = declaration.members.filter((m) => m.dir !== ".").length;
69
+ const matches = groups.reduce((n, g) => n + g.matches.length, 0);
70
+ const moved = [
71
+ `${members} member${members === 1 ? "" : "s"}`,
72
+ ...(matches > 0 ? [`${matches} example project${matches === 1 ? "" : "s"}`] : []),
73
+ ].join(" and ");
74
+ return {
75
+ action: "refuse",
76
+ message:
77
+ `${ROOT_REFUSAL_CODE}: this is the root of workspace ${declaration.name} (${declaration.file}), which moves ` +
78
+ `${moved} out of the root project. Run chant workspace ${input.verb} to ${input.verb} each member with its own ` +
79
+ `toolchain, or pass --root-only (or set rootOnly: true in chant.config) to ${input.verb} the root project alone.`,
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Apply {@link decideRootCommand}: print the refusal and return 1, or return
85
+ * undefined so the command goes on. A root-only run needs nothing set up:
86
+ * every discovery walk under a declaration already leaves the member
87
+ * directories and group matches out (#2527, `workspaceMemberDirs` in
88
+ * `../discovery/walk.ts`), and `excluded` names the same directories.
89
+ */
90
+ export function guardRootCommand(input: RootGuardInput): number | undefined {
91
+ const result = decideRootCommand(input);
92
+ if (result.action === "refuse") {
93
+ console.error(formatError({ message: result.message }));
94
+ return 1;
95
+ }
96
+ return undefined;
97
+ }
@@ -0,0 +1,308 @@
1
+ /**
2
+ * The read contract for `chant workspace status --json` (#2544, #2524 D15,
3
+ * D19): the output schema is a valid draft 2020-12 document, its closed code
4
+ * lists match the code, and real output validates against it, for workspaces
5
+ * built here with a `chant/lifecycle` branch holding member ledgers in both
6
+ * layouts, and for the chant repo's own declaration (#2557).
7
+ */
8
+
9
+ import { execFileSync } from "node:child_process";
10
+ import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
11
+ import { tmpdir } from "node:os";
12
+ import { dirname, join } from "node:path";
13
+ import Ajv2020 from "ajv/dist/2020";
14
+ import { afterAll, describe, expect, test, vi } from "vitest";
15
+ import type { CommandContext } from "../cli/registry";
16
+ import { listWorkspace } from "./ls";
17
+ import {
18
+ formatStatus,
19
+ runWorkspaceStatus,
20
+ STATUS_CONTRACT_VERSION,
21
+ STATUS_ERROR_CODES,
22
+ STATUS_OUTPUT_SCHEMA_ID,
23
+ STATUS_REASON_CODES,
24
+ workspaceStatus,
25
+ type StatusDocument,
26
+ } from "./status";
27
+ import schema from "./status.schema.json";
28
+
29
+ const REPO = join(import.meta.dirname, "..", "..", "..", "..");
30
+
31
+ const ajv = new Ajv2020({ strict: true, allErrors: true });
32
+ const validate = ajv.compile(schema);
33
+
34
+ function expectValid(doc: StatusDocument): void {
35
+ const ok = validate(doc);
36
+ expect(ok, JSON.stringify(validate.errors, null, 2)).toBe(true);
37
+ }
38
+
39
+ function result(doc: StatusDocument): Extract<StatusDocument, { members: unknown }> {
40
+ if ("error" in doc) throw new Error(`${doc.error.code}: ${doc.error.message}`);
41
+ return doc;
42
+ }
43
+
44
+ const scratch: string[] = [];
45
+ afterAll(() => {
46
+ for (const d of scratch) rmSync(d, { recursive: true, force: true });
47
+ });
48
+
49
+ function git(cwd: string, args: string[], env?: NodeJS.ProcessEnv, input?: string): string {
50
+ return execFileSync("git", ["-c", "user.name=t", "-c", "user.email=t@example.com", "-c", "commit.gpgsign=false", ...args], {
51
+ cwd,
52
+ encoding: "utf-8",
53
+ input,
54
+ env: { ...process.env, ...env },
55
+ stdio: [input === undefined ? "ignore" : "pipe", "pipe", "pipe"],
56
+ }).trim();
57
+ }
58
+
59
+ /** A git repository holding `files`, committed. */
60
+ function repo(files: Record<string, string>): string {
61
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "chant-status-")));
62
+ scratch.push(root);
63
+ for (const [path, text] of Object.entries(files)) {
64
+ mkdirSync(dirname(join(root, path)), { recursive: true });
65
+ writeFileSync(join(root, path), text);
66
+ }
67
+ git(root, ["init", "-q", "-b", "main"]);
68
+ git(root, ["add", "-A"]);
69
+ git(root, ["commit", "-q", "--allow-empty", "-m", "init"]);
70
+ return root;
71
+ }
72
+
73
+ /**
74
+ * Write `files` as the whole tree of a `chant/lifecycle` commit, the way the
75
+ * lifecycle code stores ledgers, without touching the working tree or index.
76
+ */
77
+ function lifecycle(root: string, files: Record<string, string>): string {
78
+ const index = join(root, ".git", "status-test-index");
79
+ const env = { GIT_INDEX_FILE: index };
80
+ rmSync(index, { force: true });
81
+ for (const [path, text] of Object.entries(files)) {
82
+ const blob = git(root, ["hash-object", "-w", "--stdin"], env, text);
83
+ git(root, ["update-index", "--add", "--cacheinfo", `100644,${blob},${path}`], env);
84
+ }
85
+ const tree = git(root, ["write-tree"], env);
86
+ const commit = git(root, ["commit-tree", tree, "-m", "ledger"], env);
87
+ git(root, ["update-ref", "refs/heads/chant/lifecycle", commit]);
88
+ rmSync(index, { force: true });
89
+ return commit;
90
+ }
91
+
92
+ let n = 0;
93
+ /** One ledger line. */
94
+ function release(component: string, env: string, digest: string, gitSha: string, extra: Record<string, string> = {}): string {
95
+ n++;
96
+ return JSON.stringify({
97
+ version: 1,
98
+ component,
99
+ env,
100
+ digest,
101
+ gitSha,
102
+ runId: `run-${n}`,
103
+ timestamp: `2026-09-${String(10 + n).padStart(2, "0")}T00:00:00.000Z`,
104
+ actor: "ci",
105
+ ...extra,
106
+ });
107
+ }
108
+ const jsonl = (...lines: string[]) => lines.join("\n") + "\n";
109
+
110
+ const D = (c: string) => `sha256:${c.repeat(64)}`;
111
+
112
+ const declaration = (members: unknown[]) => JSON.stringify({ name: "acme", schema: 1, members }, null, 2);
113
+
114
+ /** web has moved to _members/ (#2538); api and the root member still read the flat ledger. */
115
+ function twoLayouts(): string {
116
+ const root = repo({
117
+ "chant.workspace.json": declaration([
118
+ { name: "site", dir: ".", kind: "other", because: "the root project" },
119
+ { name: "web", dir: "apps/web", kind: "chant" },
120
+ { name: "api", dir: "apps/api", kind: "chant" },
121
+ { name: "examples", kind: "examples", glob: "examples/*" },
122
+ ]),
123
+ "apps/web/chant.config.ts": "",
124
+ "apps/api/chant.config.ts": "",
125
+ });
126
+ lifecycle(root, {
127
+ "_members/web/staging/releases.jsonl": jsonl(release("web", "staging", D("0"), "0".repeat(40)), release("web", "staging", D("a"), "a".repeat(40))),
128
+ "_members/web/prod/releases.jsonl": jsonl(release("web", "prod", D("a"), "a".repeat(40))),
129
+ "staging/releases.jsonl": jsonl(
130
+ release("api", "staging", D("b"), "b".repeat(40)),
131
+ release("worker", "staging", D("d"), "d".repeat(40)),
132
+ ),
133
+ "prod/releases.jsonl": jsonl(release("api", "prod", D("c"), "c".repeat(40))),
134
+ });
135
+ return root;
136
+ }
137
+
138
+ describe("status output schema", () => {
139
+ test("is a valid draft 2020-12 document with the published $id", () => {
140
+ expect(schema.$schema).toBe("https://json-schema.org/draft/2020-12/schema");
141
+ expect(ajv.validateSchema(schema)).toBe(true);
142
+ expect(schema.$id).toBe(STATUS_OUTPUT_SCHEMA_ID);
143
+ expect(STATUS_CONTRACT_VERSION).toBe(1);
144
+ });
145
+
146
+ test("lists exactly the reason and error codes the code can return", () => {
147
+ expect(schema.$defs.environment.properties.reason.oneOf[1].properties!.code.enum).toEqual([...STATUS_REASON_CODES]);
148
+ expect(schema.$defs.failure.properties.error.properties.code.enum).toEqual([...STATUS_ERROR_CODES]);
149
+ });
150
+ });
151
+
152
+ describe("chant workspace status on built workspaces", () => {
153
+ test("lists each member's latest release per environment, from _members/ and from the flat layout", async () => {
154
+ const root = twoLayouts();
155
+ const doc = result(await workspaceStatus({ cwd: join(root, "apps", "web"), env: "staging" }));
156
+ expectValid(doc);
157
+ expect(doc.lifecycle.commit).toBe(git(root, ["rev-parse", "chant/lifecycle"]));
158
+ expect(doc.members.map((m) => m.name)).toEqual(["site", "web", "api"]);
159
+ const [site, web, api] = doc.members;
160
+ expect(web.environments).toHaveLength(1);
161
+ expect(web.environments[0].ledger).toEqual({ layout: "members", path: "_members/web/staging/releases.jsonl", shared: false });
162
+ // The later of the two records wins.
163
+ expect(web.environments[0].releases.map((r) => [r.component, r.digest, r.gitSha])).toEqual([["web", D("a"), "a".repeat(40)]]);
164
+ // api has no _members/api/ yet, so it falls back to the flat ledger, which the root member reads too.
165
+ expect(api.environments[0].ledger).toEqual({ layout: "flat", path: "staging/releases.jsonl", shared: true });
166
+ expect(site.environments[0].ledger).toEqual({ layout: "flat", path: "staging/releases.jsonl", shared: true });
167
+ expect(api.environments[0].releases.map((r) => r.component)).toEqual(["api", "worker"]);
168
+ expect(web.compare).toBeNull();
169
+ expect(doc.summary).toEqual({ members: 3, released: 3, unreadable: 0, differing: null });
170
+ });
171
+
172
+ test("--compare-to shows both environments and marks the members whose digests differ", async () => {
173
+ const root = twoLayouts();
174
+ const doc = result(await workspaceStatus({ cwd: root, env: "staging", compareTo: "prod" }));
175
+ expectValid(doc);
176
+ const web = doc.members.find((m) => m.name === "web")!;
177
+ const api = doc.members.find((m) => m.name === "api")!;
178
+ expect(web.environments.map((e) => [e.env, e.ledger.path])).toEqual([
179
+ ["staging", "_members/web/staging/releases.jsonl"],
180
+ ["prod", "_members/web/prod/releases.jsonl"],
181
+ ]);
182
+ expect(web.compare).toEqual({ differs: false, components: [{ component: "web", state: "same", digest: D("a"), compareDigest: D("a") }] });
183
+ expect(api.compare).toEqual({
184
+ differs: true,
185
+ components: [
186
+ { component: "api", state: "differs", digest: D("b"), compareDigest: D("c") },
187
+ { component: "worker", state: "only-env", digest: D("d"), compareDigest: null },
188
+ ],
189
+ });
190
+ expect(doc.summary).toEqual({ members: 3, released: 3, unreadable: 0, differing: 2 });
191
+
192
+ const text = formatStatus(doc);
193
+ expect(text).toContain("acme staging compared to prod (chant/lifecycle at ");
194
+ expect(text).toMatch(/web\s+web\s+sha256:aaaaaaaaaaaa aaaaaaaa\s+sha256:aaaaaaaaaaaa aaaaaaaa\n/);
195
+ expect(text).toMatch(/api\s+api\s+sha256:bbbbbbbbbbbb bbbbbbbb\s+sha256:cccccccccccc cccccccc\s+differs/);
196
+ expect(text).toMatch(/api\s+worker\s+sha256:dddddddddddd dddddddd\s+-\s+only-env/);
197
+ expect(text).toContain("3 members, 3 with a release in staging, 2 differ from prod, 0 unreadable");
198
+ });
199
+
200
+ test("compares on the input digest when a release has one", async () => {
201
+ const root = repo({ "chant.workspace.json": declaration([{ name: "chart", dir: "chart", kind: "other", because: "helm" }]) });
202
+ lifecycle(root, {
203
+ "staging/releases.jsonl": jsonl(release("chart", "staging", D("1"), "1".repeat(40), { inputDigest: D("9") })),
204
+ "prod/releases.jsonl": jsonl(release("chart", "prod", D("2"), "1".repeat(40), { inputDigest: D("9") })),
205
+ });
206
+ const doc = result(await workspaceStatus({ cwd: root, env: "staging", compareTo: "prod" }));
207
+ expectValid(doc);
208
+ expect(doc.members[0].compare?.components[0]).toMatchObject({ state: "same", digest: D("1"), compareDigest: D("2") });
209
+ expect(doc.members[0].environments[0].ledger.shared).toBe(false);
210
+ });
211
+
212
+ test("a member whose ledger can't be read is listed with a reason code, and the read still succeeds", async () => {
213
+ const root = twoLayouts();
214
+ lifecycle(root, {
215
+ "_members/web/staging/releases.jsonl": jsonl(release("web", "staging", D("a"), "a".repeat(40)), "{not json", JSON.stringify({ component: "web" })),
216
+ "staging/releases.jsonl": jsonl(release("api", "staging", D("b"), "b".repeat(40))),
217
+ });
218
+ const doc = result(
219
+ await workspaceStatus({
220
+ cwd: root,
221
+ env: "staging",
222
+ compareTo: "prod",
223
+ readLedger: async (path, cwd) => {
224
+ if (path === "prod/releases.jsonl") throw new Error("fatal: bad object\nmore");
225
+ const { readReleaseLedger } = await import("../lifecycle/release-ledger");
226
+ return readReleaseLedger(path.slice(0, -"/releases.jsonl".length), { cwd });
227
+ },
228
+ }),
229
+ );
230
+ expectValid(doc);
231
+ const web = doc.members.find((m) => m.name === "web")!;
232
+ const api = doc.members.find((m) => m.name === "api")!;
233
+ expect(web.readable).toBe(false);
234
+ expect(web.environments[0].reason).toEqual({
235
+ code: "ledger-malformed",
236
+ message: "_members/web/staging/releases.jsonl: 2 lines are not a release record and were skipped",
237
+ });
238
+ expect(web.environments[0].releases.map((r) => r.digest)).toEqual([D("a")]);
239
+ expect(api.environments[1].reason).toEqual({ code: "ledger-unreadable", message: "prod/releases.jsonl: fatal: bad object" });
240
+ expect(api.environments[1].releases).toEqual([]);
241
+ expect(doc.summary.unreadable).toBe(3);
242
+ expect(formatStatus(doc)).toContain(" ledger-unreadable: prod/releases.jsonl: fatal: bad object");
243
+ });
244
+
245
+ test("a checkout with no chant/lifecycle branch lists every member with no release", async () => {
246
+ const root = repo({ "chant.workspace.json": declaration([{ name: "web", dir: "web", kind: "chant" }]) });
247
+ const doc = result(await workspaceStatus({ cwd: root, env: "prod" }));
248
+ expectValid(doc);
249
+ expect(doc.lifecycle.commit).toBeNull();
250
+ expect(doc.members[0].environments[0]).toMatchObject({ releases: [], reason: null, ledger: { layout: "flat", shared: false } });
251
+ expect(formatStatus(doc)).toContain("web - no release");
252
+ });
253
+
254
+ test("every failure validates with its code", async () => {
255
+ const empty = repo({});
256
+ const outside = realpathSync(mkdtempSync(join(tmpdir(), "chant-status-nogit-")));
257
+ scratch.push(outside);
258
+ writeFileSync(join(outside, "chant.workspace.json"), declaration([]));
259
+ const broken = repo({ "chant.workspace.json": '{ "name": "acme", "schema": 1 "members": [] }' });
260
+ const ok = repo({ "chant.workspace.json": declaration([]) });
261
+ const docs = [
262
+ await workspaceStatus({ cwd: empty, env: "prod" }),
263
+ await workspaceStatus({ cwd: outside, env: "prod" }),
264
+ await workspaceStatus({ cwd: broken, env: "prod" }),
265
+ await workspaceStatus({ cwd: ok, env: "_members" }),
266
+ await workspaceStatus({ cwd: ok, env: "prod", compareTo: "../x" }),
267
+ ];
268
+ for (const doc of docs) expectValid(doc);
269
+ expect(docs.map((d) => ("error" in d ? d.error.code : "ok"))).toEqual([
270
+ "declaration-missing",
271
+ "not-a-git-repository",
272
+ "declaration-unparseable",
273
+ "environment-invalid",
274
+ "environment-invalid",
275
+ ]);
276
+ });
277
+
278
+ test("the command needs an environment, refuses --live, and exits 0 with unreadable members", async () => {
279
+ const root = twoLayouts();
280
+ const err = vi.spyOn(console, "error").mockImplementation(() => {});
281
+ const log = vi.spyOn(console, "log").mockImplementation(() => {});
282
+ try {
283
+ const run = (args: Record<string, unknown>) => runWorkspaceStatus({ args: { extraPositional2: root, ...args } } as unknown as CommandContext);
284
+ expect(await run({})).toBe(1);
285
+ expect(String(err.mock.calls.at(-1)?.[0])).toContain("needs an environment");
286
+ expect(await run({ extraPositional: "prod", compareTo: "--live" })).toBe(1);
287
+ expect(String(err.mock.calls.at(-1)?.[0])).toContain("doesn't read live state yet");
288
+ expect(await run({ extraPositional: "prod", compareTo: "staging", json: true })).toBe(0);
289
+ const printed = JSON.parse(String(log.mock.calls.at(-1)?.[0])) as StatusDocument;
290
+ expectValid(printed);
291
+ expect(await run({ extraPositional: "../prod", json: true })).toBe(1);
292
+ } finally {
293
+ err.mockRestore();
294
+ log.mockRestore();
295
+ }
296
+ });
297
+ });
298
+
299
+ describe("chant workspace status on the chant repo (#2557)", () => {
300
+ test("validates and lists every declared member", async () => {
301
+ const doc = result(await workspaceStatus({ cwd: join(REPO, "packages", "core"), env: "prod", compareTo: "staging" }));
302
+ expectValid(doc);
303
+ expect(doc.workspace).toMatchObject({ name: "chant", root: ".", file: "chant.workspace.json" });
304
+ const ls = listWorkspace({ cwd: REPO });
305
+ if ("error" in ls) throw new Error(ls.error.message);
306
+ expect(doc.members.map((m) => m.name)).toEqual(ls.members.map((m) => m.name));
307
+ });
308
+ });