@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
@@ -1,7 +1,8 @@
1
1
  /**
2
- * chant #2514 — the warning a release ahead of real SHA-256 digests: a
3
- * project whose release ledger or build manifests hold values from the old
4
- * 32-bit `contentDigest` is told once that they will change.
2
+ * chant #2514 — digests recorded before real SHA-256. A project whose release
3
+ * ledger or build manifests hold values from the old 32-bit `contentDigest`
4
+ * is told once; those records read back flagged `legacy-digest`, and nothing
5
+ * is re-keyed or rewritten.
5
6
  */
6
7
  import { describe, test, expect, beforeEach, afterEach, vi } from "vitest";
7
8
  import { withTestDir } from "@intentius/chant-test-utils";
@@ -9,11 +10,35 @@ import { spawnSync } from "node:child_process";
9
10
  import { createHash } from "node:crypto";
10
11
  import { writeFileSync } from "node:fs";
11
12
  import { join } from "node:path";
12
- import { isLegacyContentDigest, resetLegacyDigestWarnings, warnOnLegacyDigests } from "./legacy-digest";
13
- import { appendReleaseRecord, readReleaseLedger, type ReleaseRecordInput } from "./release-ledger";
14
- import { persistBuildManifest, readAllBuildManifests } from "./build-ledger-store";
13
+ import {
14
+ LEGACY_DIGEST_ACCEPTED_THROUGH,
15
+ LEGACY_DIGEST_FLAG,
16
+ flagLegacyDigest,
17
+ isLegacyContentDigest,
18
+ resetLegacyDigestWarnings,
19
+ warnOnLegacyDigests,
20
+ } from "./legacy-digest";
21
+ import { appendReleaseRecord, readReleaseLedger, type ReleaseRecord, type ReleaseRecordInput } from "./release-ledger";
22
+ import {
23
+ findBuildManifestByArtifactDigest,
24
+ persistBuildManifest,
25
+ readAllBuildManifests,
26
+ readBuildManifest,
27
+ } from "./build-ledger-store";
28
+ import { readBlobFromPath } from "./git";
29
+ import { compareAcrossEnvironments } from "./status";
15
30
  import { addArchiveEntry, contentDigest, createBuildArchiveManifest } from "../components/verbs/build-archive";
16
31
 
32
+ /** The 32-bit hash chant labelled `sha256:` up to 0.80.0, kept here to make old values. */
33
+ function legacyContentDigest(input: string): string {
34
+ let hash = 0;
35
+ for (let i = 0; i < input.length; i++) {
36
+ hash = (Math.imul(hash, 31) + input.charCodeAt(i)) | 0;
37
+ }
38
+ const hex = Math.abs(hash).toString(16).padStart(8, "0");
39
+ return `sha256:${hex.repeat(8).slice(0, 64)}`;
40
+ }
41
+
17
42
  function git(args: string[], cwd: string): void {
18
43
  spawnSync("git", args, { cwd, encoding: "utf-8" });
19
44
  }
@@ -56,9 +81,10 @@ describe("legacy content digests (#2514)", () => {
56
81
 
57
82
  const legacyWarnings = () => warn.mock.calls.filter((c: unknown[]) => String(c[0]).includes("issues/2514"));
58
83
 
59
- test("today's contentDigest output is recognised as the old form", () => {
60
- expect(isLegacyContentDigest(contentDigest("hello"))).toBe(true);
61
- expect(isLegacyContentDigest(contentDigest(""))).toBe(true);
84
+ test("the old contentDigest output is recognised, and today's is not", () => {
85
+ expect(isLegacyContentDigest(legacyContentDigest("hello"))).toBe(true);
86
+ expect(isLegacyContentDigest(legacyContentDigest(""))).toBe(true);
87
+ expect(isLegacyContentDigest(contentDigest("hello"))).toBe(false);
62
88
  });
63
89
 
64
90
  test("a real SHA-256 value, a short value and a non-string are not", () => {
@@ -69,23 +95,48 @@ describe("legacy content digests (#2514)", () => {
69
95
 
70
96
  test("warns once per project, and not at all without an old value", () => {
71
97
  expect(warnOnLegacyDigests([realSha256], "release ledger", "/p/a")).toBe(false);
72
- expect(warnOnLegacyDigests([contentDigest("x")], "release ledger", "/p/a")).toBe(true);
73
- expect(warnOnLegacyDigests([contentDigest("y")], "build manifests", "/p/a")).toBe(false);
74
- expect(warnOnLegacyDigests([contentDigest("y")], "build manifests", "/p/b")).toBe(true);
98
+ expect(warnOnLegacyDigests([legacyContentDigest("x")], "release ledger", "/p/a")).toBe(true);
99
+ expect(warnOnLegacyDigests([legacyContentDigest("y")], "build manifests", "/p/a")).toBe(false);
100
+ expect(warnOnLegacyDigests([legacyContentDigest("y")], "build manifests", "/p/b")).toBe(true);
75
101
  expect(legacyWarnings()).toHaveLength(2);
76
- expect(String(legacyWarnings()[0][0])).toContain("next chant release computes real SHA-256");
102
+ expect(String(legacyWarnings()[0][0])).toContain("chant now computes real SHA-256");
103
+ expect(String(legacyWarnings()[0][0])).toContain(`accepts them through ${LEGACY_DIGEST_ACCEPTED_THROUGH}`);
77
104
  });
78
105
 
79
- test("reading a release ledger that holds an old value warns once", async () => {
106
+ test("flagLegacyDigest adds the flag once, keeps other flags, and leaves real values alone", () => {
107
+ const old = legacyContentDigest("x");
108
+ const record: { id: number; flags?: string[] } = { id: 1 };
109
+ expect(flagLegacyDigest(record, [realSha256])).toEqual({ id: 1 });
110
+ expect(flagLegacyDigest(record, [undefined, old])).toEqual({ id: 1, flags: [LEGACY_DIGEST_FLAG] });
111
+ expect(flagLegacyDigest({ flags: ["legacy-digest"] }, [old])).toEqual({ flags: ["legacy-digest"] });
112
+ expect(flagLegacyDigest({ flags: ["other"] }, [old])).toEqual({ flags: ["other", "legacy-digest"] });
113
+ });
114
+
115
+ test("a release ledger with old and new values: old entries read back flagged, warned once, never re-keyed", async () => {
80
116
  await withTestDir(async (dir) => {
81
117
  initRepo(dir);
82
- await appendReleaseRecord(release(contentDigest("rendered"), "run-1"), { cwd: dir });
83
- await appendReleaseRecord(release(contentDigest("rendered-2"), "run-2"), { cwd: dir });
118
+ const old = legacyContentDigest("rendered");
119
+ await appendReleaseRecord(release(old, "run-1"), { cwd: dir });
120
+ await appendReleaseRecord(release(contentDigest("rendered"), "run-2"), { cwd: dir });
84
121
 
85
- await readReleaseLedger("prod", { cwd: dir });
122
+ const { records } = await readReleaseLedger("prod", { cwd: dir });
86
123
  await readReleaseLedger("prod", { cwd: dir });
87
124
  expect(legacyWarnings()).toHaveLength(1);
88
125
  expect(String(legacyWarnings()[0][0])).toContain("release ledger holds");
126
+
127
+ expect(records.map((r) => [r.runId, r.digest, r.flags])).toEqual([
128
+ ["run-1", old, [LEGACY_DIGEST_FLAG]],
129
+ ["run-2", contentDigest("rendered"), undefined],
130
+ ]);
131
+
132
+ // Appending a record read back with its flag does not persist the flag,
133
+ // and the old digest is written exactly as it was.
134
+ const flagged = { ...records[0]!, runId: "run-3" } as ReleaseRecord;
135
+ const { record: written } = await appendReleaseRecord(flagged, { cwd: dir });
136
+ expect(written.flags).toBeUndefined();
137
+ expect(written.digest).toBe(old);
138
+ const raw = await readBlobFromPath("prod", "releases.jsonl", { cwd: dir });
139
+ expect(raw ?? "").not.toContain("legacy-digest");
89
140
  });
90
141
  });
91
142
 
@@ -98,17 +149,58 @@ describe("legacy content digests (#2514)", () => {
98
149
  });
99
150
  });
100
151
 
101
- test("reading persisted build manifests that hold old values warns once", async () => {
152
+ test("an old persisted build manifest reads back under its old key, flagged, and is found by its old artifact digest", async () => {
102
153
  await withTestDir(async (dir) => {
103
154
  initRepo(dir);
104
- let manifest = createBuildArchiveManifest("web", { now: () => new Date(0) });
105
- manifest = addArchiveEntry(manifest, { kind: "template", path: "web.json", digest: contentDigest("{}") });
106
- await persistBuildManifest(manifest, { cwd: dir });
107
-
108
- await readAllBuildManifests({ cwd: dir });
155
+ const oldEntry = legacyContentDigest("{}");
156
+ const oldManifestDigest = legacyContentDigest(`template:web.json:${oldEntry}`);
157
+ const oldManifest = {
158
+ version: 1 as const,
159
+ component: "web",
160
+ createdAt: new Date(0).toISOString(),
161
+ contents: [{ kind: "template" as const, path: "web.json", digest: oldEntry }],
162
+ manifestDigest: oldManifestDigest,
163
+ };
164
+ await persistBuildManifest(oldManifest, { cwd: dir });
165
+
166
+ let fresh = createBuildArchiveManifest("web", { now: () => new Date(0) });
167
+ fresh = addArchiveEntry(fresh, { kind: "template", path: "web.json", digest: contentDigest("{}") });
168
+ await persistBuildManifest(fresh, { cwd: dir });
169
+
170
+ const all = await readAllBuildManifests({ cwd: dir });
109
171
  await readAllBuildManifests({ cwd: dir });
110
172
  expect(legacyWarnings()).toHaveLength(1);
111
173
  expect(String(legacyWarnings()[0][0])).toContain("build manifests hold");
174
+ expect(all).toHaveLength(2);
175
+
176
+ const old = await readBuildManifest(oldManifestDigest, { cwd: dir });
177
+ expect(old?.manifestDigest).toBe(oldManifestDigest);
178
+ expect(old?.flags).toEqual([LEGACY_DIGEST_FLAG]);
179
+ expect((await findBuildManifestByArtifactDigest(oldEntry, { cwd: dir }))?.manifestDigest).toBe(oldManifestDigest);
180
+ expect((await readBuildManifest(fresh.manifestDigest, { cwd: dir }))?.flags).toBeUndefined();
181
+
182
+ // Persisting a flagged manifest again writes no flag.
183
+ await persistBuildManifest(old!, { cwd: dir });
184
+ const raw = await readBlobFromPath("_builds", `${oldManifestDigest.replace(/:/g, "_")}.json`, { cwd: dir });
185
+ expect(raw ?? "").not.toContain("legacy-digest");
112
186
  });
113
187
  });
188
+
189
+ test("a cross-environment comparison of an old and a new digest says it cannot tell", () => {
190
+ const rec = (env: string, digest: string): ReleaseRecord => ({ version: 1, ...release(digest, `${env}-1`), env });
191
+ const mixed = compareAcrossEnvironments(
192
+ "web",
193
+ { name: "staging", records: [rec("staging", contentDigest("x"))] },
194
+ { name: "prod", records: [rec("prod", legacyContentDigest("x"))] },
195
+ );
196
+ expect(mixed.same).toBe(false);
197
+ expect(mixed.mixedDigestForms).toBe(true);
198
+
199
+ const differentNew = compareAcrossEnvironments(
200
+ "web",
201
+ { name: "staging", records: [rec("staging", contentDigest("x"))] },
202
+ { name: "prod", records: [rec("prod", contentDigest("y"))] },
203
+ );
204
+ expect(differentNew.mixedDigestForms).toBeUndefined();
205
+ });
114
206
  });
@@ -1,13 +1,20 @@
1
1
  /**
2
- * The warning a release ahead of real SHA-256 digests (chant #2514).
2
+ * Digests recorded before real SHA-256 (chant #2514).
3
3
  *
4
- * `contentDigest` (../components/verbs/build-archive.ts) labels its output
5
- * `sha256:` but computes a 32-bit string hash and repeats its 8 hex digits
6
- * eight times. The next release makes it real SHA-256, so every value it
7
- * produces changes once. Values already recorded in a project's release
8
- * ledger or persisted build manifests keep the old form. This release only
9
- * says so: readers of those records warn once per project, per process, when
10
- * they meet an old value. Nothing is rewritten or flagged yet.
4
+ * Up to chant 0.80.0, `contentDigest` labelled its output `sha256:` but
5
+ * computed a 32-bit string hash and repeated its 8 hex digits eight times.
6
+ * It now computes real SHA-256 (../content-digest.ts), so every value it
7
+ * produces changed once. Values already recorded in a project's release
8
+ * ledger or persisted build manifests keep the old form: they are never
9
+ * re-keyed or rewritten, because a digest is an identity other records point
10
+ * at.
11
+ *
12
+ * Readers accept both forms. A record read back with an old value carries
13
+ * `flags: ["legacy-digest"]`, computed on read and never written, and chant
14
+ * warns once per project, per process, that the ledger holds such values.
15
+ * Both forms stay accepted through {@link LEGACY_DIGEST_ACCEPTED_THROUGH}.
16
+ * A later release may stop accepting old values, and it will warn a release
17
+ * ahead, as #2525 requires of any level-0 change.
11
18
  *
12
19
  * An old value is recognisable by its shape alone. A real SHA-256 hex digest
13
20
  * whose 64 characters are one 8-character block repeated eight times has
@@ -18,11 +25,52 @@ import { resolve } from "node:path";
18
25
 
19
26
  const LEGACY_DIGEST = /^sha256:([0-9a-f]{8})\1{7}$/;
20
27
 
28
+ /** The flag a record read back with an old-form digest carries. */
29
+ export const LEGACY_DIGEST_FLAG = "legacy-digest";
30
+ export type LegacyDigestFlag = typeof LEGACY_DIGEST_FLAG;
31
+
32
+ /**
33
+ * The last release series in which readers are guaranteed to accept old-form
34
+ * digests: every 0.8x release. The first release allowed to refuse them is
35
+ * 0.90.0, and only after a release that warns about it.
36
+ */
37
+ export const LEGACY_DIGEST_ACCEPTED_THROUGH = "0.89.x";
38
+
21
39
  /** Whether `value` is a `sha256:` string made by the old 32-bit `contentDigest`. */
22
40
  export function isLegacyContentDigest(value: unknown): boolean {
23
41
  return typeof value === "string" && LEGACY_DIGEST.test(value);
24
42
  }
25
43
 
44
+ /** Whether any of `values` is an old-form digest. */
45
+ export function hasLegacyContentDigest(values: Iterable<unknown>): boolean {
46
+ for (const value of values) {
47
+ if (isLegacyContentDigest(value)) return true;
48
+ }
49
+ return false;
50
+ }
51
+
52
+ /**
53
+ * `record` with `flags: ["legacy-digest"]` added when any of `values` (the
54
+ * record's own digests) is in the old form, and otherwise unchanged. Other
55
+ * flags a record carries are kept.
56
+ */
57
+ export function flagLegacyDigest<T extends { flags?: string[] }>(record: T, values: Iterable<unknown>): T {
58
+ if (!hasLegacyContentDigest(values)) return record;
59
+ const flags = record.flags ?? [];
60
+ if (flags.includes(LEGACY_DIGEST_FLAG)) return record;
61
+ return { ...record, flags: [...flags, LEGACY_DIGEST_FLAG] };
62
+ }
63
+
64
+ /**
65
+ * `record` without its read-side `flags`, for writers. Flags describe how a
66
+ * record reads now, so they are never persisted.
67
+ */
68
+ export function withoutReadFlags<T extends { flags?: unknown }>(record: T): Omit<T, "flags"> {
69
+ if (!("flags" in record)) return record;
70
+ const { flags: _flags, ...rest } = record;
71
+ return rest;
72
+ }
73
+
26
74
  /** Project roots already warned about in this process. */
27
75
  const warnedProjects = new Set<string>();
28
76
 
@@ -43,20 +91,13 @@ export function warnOnLegacyDigests(
43
91
  ): boolean {
44
92
  const project = resolve(cwd ?? process.cwd());
45
93
  if (warnedProjects.has(project)) return false;
46
- let found = false;
47
- for (const value of values) {
48
- if (isLegacyContentDigest(value)) {
49
- found = true;
50
- break;
51
- }
52
- }
53
- if (!found) return false;
94
+ if (!hasLegacyContentDigest(values)) return false;
54
95
  warnedProjects.add(project);
55
96
  console.warn(
56
97
  `[chant] warning: this project's ${where} ${where === "release ledger" ? "holds" : "hold"} sha256: values made by chant's old 32-bit content hash, ` +
57
- "not real SHA-256. The next chant release computes real SHA-256, so digests recorded from then on " +
58
- "will not match these for the same content. Existing entries stay readable and will be marked " +
59
- "legacy-digest. See https://github.com/INTENTIUS/chant/issues/2514",
98
+ "not real SHA-256. chant now computes real SHA-256, so a digest recorded from now on will not match " +
99
+ "an old one for the same content. Old entries are not rewritten: they read back flagged legacy-digest, " +
100
+ `and chant accepts them through ${LEGACY_DIGEST_ACCEPTED_THROUGH}. See https://github.com/INTENTIUS/chant/issues/2514`,
60
101
  );
61
102
  return true;
62
103
  }
@@ -0,0 +1,319 @@
1
+ /**
2
+ * #2538 — member ledgers under `_members/<member>/` on `chant/lifecycle`.
3
+ *
4
+ * One fixture workspace holds a root member `.`, two members `api` and `web`
5
+ * that use the same environment name, an example group, and a nested
6
+ * workspace `platform` with a member `core`. Each store is written from a
7
+ * member's directory and the exact branch path is asserted, then read back
8
+ * from each member to show neither sees the other's records.
9
+ */
10
+
11
+ import { afterAll, beforeEach, describe, expect, test } from "vitest";
12
+ import { spawnSync } from "node:child_process";
13
+ import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
14
+ import { tmpdir } from "node:os";
15
+ import { dirname, join } from "node:path";
16
+ import { clearMemberLedgerCache, memberLedgerPrefix, MemberLedgerError, resolveMemberLedger } from "./member-ledger";
17
+ import { ledgerDir, listLedgerEnvironments, listSnapshots, readEnvironmentSnapshots, writeLedgerFiles, writeSnapshot } from "./git";
18
+ import { appendReleaseRecord, readReleaseLedger, type ReleaseRecordInput } from "./release-ledger";
19
+ import { appendRunRecord, readRunLedger } from "./run-ledger";
20
+ import type { OpRunRecordInput } from "../op/runtime";
21
+ import { appendConvergeRecord, readConvergeLedger, type ConvergeTickRecordInput } from "./converge-ledger";
22
+ import { appendGateResolution, appendPendingGate, gateLedgerPath, readGateLedger } from "./gate-ledger";
23
+ import { persistBuildManifest, listBuildManifestDigests } from "./build-ledger-store";
24
+ import { emptyBaseline, readObservationBaseline, writeObservationBaseline } from "./observation-baseline";
25
+ import { acquireLease, leaseRef, readLease } from "./lease";
26
+ import { createBuildArchiveManifest } from "../components/verbs/build-archive";
27
+
28
+ const scratch: string[] = [];
29
+ afterAll(() => {
30
+ for (const d of scratch) rmSync(d, { recursive: true, force: true });
31
+ });
32
+ beforeEach(() => clearMemberLedgerCache());
33
+
34
+ function git(args: string[], cwd: string): string {
35
+ const r = spawnSync("git", args, { cwd, encoding: "utf-8" });
36
+ if (r.status !== 0) throw new Error(`git ${args.join(" ")}: ${r.stderr}`);
37
+ return r.stdout;
38
+ }
39
+
40
+ function repo(files: Record<string, string>): string {
41
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "chant-member-ledger-")));
42
+ scratch.push(root);
43
+ for (const [path, text] of Object.entries(files)) {
44
+ mkdirSync(dirname(join(root, path)), { recursive: true });
45
+ writeFileSync(join(root, path), text);
46
+ }
47
+ git(["init", "-q", "-b", "main"], root);
48
+ git(["config", "user.email", "test@chant.dev"], root);
49
+ git(["config", "user.name", "Test"], root);
50
+ git(["add", "-A"], root);
51
+ git(["commit", "-q", "-m", "init"], root);
52
+ return root;
53
+ }
54
+
55
+ const CONFIG = JSON.stringify({ lexicons: ["k8s"] });
56
+
57
+ function workspace(): string {
58
+ return repo({
59
+ "chant.workspace.json": JSON.stringify({
60
+ name: "acme",
61
+ schema: 1,
62
+ members: [
63
+ { name: "root", dir: ".", kind: "chant" },
64
+ { name: "api", dir: "services/api", kind: "chant" },
65
+ { name: "web", dir: "services/web", kind: "chant" },
66
+ { name: "platform", dir: "platform", kind: "workspace" },
67
+ { name: "examples", kind: "examples", glob: "examples/*" },
68
+ ],
69
+ }),
70
+ "chant.config.json": CONFIG,
71
+ "services/api/chant.config.json": CONFIG,
72
+ "services/api/src/main.ts": "",
73
+ "services/web/chant.config.json": CONFIG,
74
+ "examples/demo/chant.config.json": CONFIG,
75
+ "docs/README.md": "",
76
+ "platform/chant.workspace.json": JSON.stringify({
77
+ name: "platform",
78
+ schema: 1,
79
+ members: [{ name: "core", dir: "core", kind: "chant" }],
80
+ }),
81
+ "platform/core/chant.config.json": CONFIG,
82
+ });
83
+ }
84
+
85
+ /** Every file on chant/lifecycle, sorted. */
86
+ function branchFiles(root: string): string[] {
87
+ return git(["ls-tree", "-r", "--name-only", "chant/lifecycle"], root).split("\n").filter(Boolean).sort();
88
+ }
89
+
90
+ function release(overrides: Partial<ReleaseRecordInput> = {}): ReleaseRecordInput {
91
+ return {
92
+ component: "svc",
93
+ env: "prod",
94
+ digest: "sha256:abc",
95
+ gitSha: "deadbeef",
96
+ runId: "run-1",
97
+ timestamp: "2026-01-01T00:00:00.000Z",
98
+ actor: "ci",
99
+ ...overrides,
100
+ };
101
+ }
102
+
103
+ function run(overrides: Partial<OpRunRecordInput> = {}): OpRunRecordInput {
104
+ return {
105
+ op: "deploy",
106
+ env: "prod",
107
+ started: "2026-01-01T00:00:00.000Z",
108
+ ended: "2026-01-01T00:00:01.000Z",
109
+ status: "ok",
110
+ labels: { Env: "prod" },
111
+ outcomes: {},
112
+ phases: [],
113
+ ...overrides,
114
+ };
115
+ }
116
+
117
+ function tick(overrides: Partial<ConvergeTickRecordInput> = {}): ConvergeTickRecordInput {
118
+ return {
119
+ op: "converge",
120
+ env: "prod",
121
+ timestamp: "2026-01-01T00:00:00.000Z",
122
+ firedRuleIds: [],
123
+ outcomes: [],
124
+ summary: { drifted: 0, remediated: 0, reported: 0, skippedBudget: 0, skippedFlap: 0, unobserved: 0, adopted: 0 },
125
+ log: "converge(prod)",
126
+ ...overrides,
127
+ };
128
+ }
129
+
130
+ describe("resolveMemberLedger (#2538)", () => {
131
+ test("a project with no declaration keeps the flat layout", async () => {
132
+ const root = repo({ "chant.config.json": CONFIG });
133
+ expect(await resolveMemberLedger(root)).toEqual({ members: [], prefix: "" });
134
+ });
135
+
136
+ test("the root member, unowned directories and example matches keep the flat layout", async () => {
137
+ const root = workspace();
138
+ for (const dir of [".", "docs", "examples/demo"]) {
139
+ expect((await resolveMemberLedger(join(root, dir))).prefix, dir).toBe("");
140
+ }
141
+ });
142
+
143
+ test("a member, and any directory inside it, writes under _members/<member>/", async () => {
144
+ const root = workspace();
145
+ expect(await resolveMemberLedger(join(root, "services/api"))).toEqual({ members: ["api"], prefix: "_members/api/" });
146
+ expect((await resolveMemberLedger(join(root, "services/api/src"))).prefix).toBe("_members/api/");
147
+ expect((await resolveMemberLedger(join(root, "services/web"))).prefix).toBe("_members/web/");
148
+ });
149
+
150
+ test("a nested workspace's ledgers sit inside the outer member that holds it", async () => {
151
+ const root = workspace();
152
+ expect((await resolveMemberLedger(join(root, "platform"))).prefix).toBe("_members/platform/");
153
+ expect(await resolveMemberLedger(join(root, "platform/core"))).toEqual({
154
+ members: ["platform", "core"],
155
+ prefix: "_members/platform/_members/core/",
156
+ });
157
+ });
158
+
159
+ test("an unreadable declaration refuses to place the ledger rather than guess", async () => {
160
+ const root = repo({ "chant.workspace.json": "{ not json", "api/chant.config.json": CONFIG });
161
+ await expect(resolveMemberLedger(join(root, "api"))).rejects.toBeInstanceOf(MemberLedgerError);
162
+ await expect(ledgerDir("prod", { cwd: join(root, "api") })).rejects.toThrow(/cannot place this project's lifecycle ledger/);
163
+ });
164
+
165
+ test("memberLedgerPrefix joins one _members/<name>/ per level", () => {
166
+ expect(memberLedgerPrefix([])).toBe("");
167
+ expect(memberLedgerPrefix(["a", "b"])).toBe("_members/a/_members/b/");
168
+ });
169
+ });
170
+
171
+ describe("every lifecycle store writes under the member path (#2538)", () => {
172
+ test("releases: two members with one environment name keep separate ledgers", async () => {
173
+ const root = workspace();
174
+ const api = join(root, "services/api");
175
+ const web = join(root, "services/web");
176
+ await appendReleaseRecord(release({ component: "api" }), { cwd: api });
177
+ await appendReleaseRecord(release({ component: "web" }), { cwd: web });
178
+ await appendReleaseRecord(release({ component: "root" }), { cwd: root });
179
+
180
+ expect(branchFiles(root)).toEqual(["_members/api/prod/releases.jsonl", "_members/web/prod/releases.jsonl", "prod/releases.jsonl"]);
181
+ expect((await readReleaseLedger("prod", { cwd: api })).records.map((r) => r.component)).toEqual(["api"]);
182
+ expect((await readReleaseLedger("prod", { cwd: web })).records.map((r) => r.component)).toEqual(["web"]);
183
+ expect((await readReleaseLedger("prod", { cwd: root })).records.map((r) => r.component)).toEqual(["root"]);
184
+ expect(await listLedgerEnvironments({ cwd: api })).toEqual(["prod"]);
185
+ // The root member's listing never reads `_members` as an environment.
186
+ expect(await listLedgerEnvironments({ cwd: root })).toEqual(["prod"]);
187
+ });
188
+
189
+ test("snapshots and observation baselines", async () => {
190
+ const root = workspace();
191
+ const api = join(root, "services/api");
192
+ const web = join(root, "services/web");
193
+ await writeSnapshot("prod", "k8s", '{"from":"api"}', { cwd: api });
194
+ await writeSnapshot("prod", "k8s", '{"from":"web"}', { cwd: web });
195
+ await writeObservationBaseline(emptyBaseline("prod"), { cwd: api });
196
+
197
+ const files = branchFiles(root);
198
+ expect(files).toContain("_members/api/prod/k8s.json");
199
+ expect(files).toContain("_members/web/prod/k8s.json");
200
+ expect(files.filter((f) => f.startsWith("_members/api/prod/") && f !== "_members/api/prod/k8s.json")).toHaveLength(1);
201
+ expect(files.every((f) => f.startsWith("_members/"))).toBe(true);
202
+ expect([...(await readEnvironmentSnapshots("prod", { cwd: web })).values()]).toEqual(['{"from":"web"}']);
203
+ expect(await readObservationBaseline("prod", { cwd: api })).toMatchObject({ environment: "prod" });
204
+ expect(await readObservationBaseline("prod", { cwd: web })).toBeNull();
205
+ // A member's history lists only the commits that touched its ledger.
206
+ expect(await listSnapshots({ cwd: web })).toHaveLength(1);
207
+ expect(await listSnapshots({ cwd: root })).toHaveLength(3);
208
+ });
209
+
210
+ test("runs and converge records", async () => {
211
+ const root = workspace();
212
+ const api = join(root, "services/api");
213
+ const web = join(root, "services/web");
214
+ await appendRunRecord(run(), { cwd: api });
215
+ await appendConvergeRecord(tick(), { cwd: web });
216
+
217
+ expect(branchFiles(root)).toEqual(["_members/api/prod/runs__deploy.jsonl", "_members/web/prod/converge.jsonl"]);
218
+ expect((await readRunLedger("prod", "deploy", { cwd: api })).records).toHaveLength(1);
219
+ expect((await readRunLedger("prod", "deploy", { cwd: web })).records).toHaveLength(0);
220
+ expect((await readConvergeLedger("prod", { cwd: web })).records).toHaveLength(1);
221
+ expect((await readConvergeLedger("prod", { cwd: api })).records).toHaveLength(0);
222
+ });
223
+
224
+ test("gates: _gates moves under the member, and an Op name can repeat across members", async () => {
225
+ const root = workspace();
226
+ const api = join(root, "services/api");
227
+ const web = join(root, "services/web");
228
+ await appendPendingGate({ op: "deploy", gate: "approve", timestamp: "2026-01-01T00:00:00.000Z", expiresAt: "2026-01-02T00:00:00.000Z" }, { cwd: api });
229
+ await appendGateResolution({ op: "deploy", gate: "approve", resolvedBy: "alex", timestamp: "2026-01-01T00:00:01.000Z" }, { cwd: web });
230
+
231
+ expect(branchFiles(root)).toEqual(["_members/api/_gates/deploy.jsonl", "_members/web/_gates/deploy.jsonl"]);
232
+ const apiGates = await readGateLedger("deploy", { cwd: api });
233
+ expect(apiGates.pending).toHaveLength(1);
234
+ expect(apiGates.resolutions).toHaveLength(0);
235
+ const webGates = await readGateLedger("deploy", { cwd: web });
236
+ expect(webGates.pending).toHaveLength(0);
237
+ expect(webGates.resolutions).toHaveLength(1);
238
+ expect(gateLedgerPath("deploy", "_members/api/")).toBe("_members/api/_gates/deploy.jsonl");
239
+ expect(gateLedgerPath("deploy")).toBe("_gates/deploy.jsonl");
240
+ });
241
+
242
+ test("build records: _builds moves under the member", async () => {
243
+ const root = workspace();
244
+ const api = join(root, "services/api");
245
+ const manifest = createBuildArchiveManifest("api", { now: () => new Date("2026-01-01T00:00:00.000Z") });
246
+ await persistBuildManifest(manifest, { cwd: api });
247
+
248
+ const files = branchFiles(root);
249
+ expect(files).toHaveLength(1);
250
+ expect(files[0]).toMatch(/^_members\/api\/_builds\/sha256_[0-9a-f]+\.json$/);
251
+ expect(await listBuildManifestDigests({ cwd: api })).toEqual([manifest.manifestDigest]);
252
+ expect(await listBuildManifestDigests({ cwd: join(root, "services/web") })).toEqual([]);
253
+ });
254
+
255
+ test("leases: refs/chant/lease/_members/<member>/<op>, so two members' Ops of one name hold separate leases", async () => {
256
+ const root = workspace();
257
+ const api = join(root, "services/api");
258
+ const web = join(root, "services/web");
259
+ expect((await acquireLease("converge", "holder-api", { cwd: api })).acquired).toBe(true);
260
+ expect((await acquireLease("converge", "holder-web", { cwd: web })).acquired).toBe(true);
261
+ expect((await acquireLease("converge", "holder-root", { cwd: root })).acquired).toBe(true);
262
+
263
+ const refs = git(["for-each-ref", "--format=%(refname)", "refs/chant/lease/"], root).split("\n").filter(Boolean).sort();
264
+ expect(refs).toEqual([
265
+ "refs/chant/lease/_members/api/converge",
266
+ "refs/chant/lease/_members/web/converge",
267
+ "refs/chant/lease/converge",
268
+ ]);
269
+ expect((await readLease("converge", { cwd: api })).record?.holder).toBe("holder-api");
270
+ expect((await readLease("converge", { cwd: web })).record?.holder).toBe("holder-web");
271
+ expect(leaseRef("converge", "_members/api/")).toBe("refs/chant/lease/_members/api/converge");
272
+ });
273
+
274
+ test("a nested workspace member writes under both levels", async () => {
275
+ const root = workspace();
276
+ await appendReleaseRecord(release({ component: "core" }), { cwd: join(root, "platform/core") });
277
+ expect(branchFiles(root)).toEqual(["_members/platform/_members/core/prod/releases.jsonl"]);
278
+ });
279
+ });
280
+
281
+ describe("one commit can write several members (#2538)", () => {
282
+ test("writeLedgerFiles lands two members' records in a single commit, each read back by its own member", async () => {
283
+ const root = workspace();
284
+ const api = join(root, "services/api");
285
+ const web = join(root, "services/web");
286
+ // An earlier write, so the multi-member commit extends a branch that has other entries.
287
+ await appendReleaseRecord(release({ component: "root" }), { cwd: root });
288
+ const before = git(["rev-parse", "chant/lifecycle"], root).trim();
289
+
290
+ const line = (component: string) => JSON.stringify({ version: 1, ...release({ component }) });
291
+ const commit = await writeLedgerFiles(
292
+ [
293
+ { path: `${await ledgerDir("prod", { cwd: api })}/releases.jsonl`, content: line("api") },
294
+ { path: `${await ledgerDir("prod", { cwd: web })}/releases.jsonl`, content: line("web") },
295
+ ],
296
+ "Release records: api, web",
297
+ { cwd: root },
298
+ );
299
+
300
+ expect(git(["rev-parse", "chant/lifecycle"], root).trim()).toBe(commit);
301
+ expect(git(["rev-parse", `${commit}^`], root).trim()).toBe(before);
302
+ expect(git(["diff-tree", "-r", "--name-only", "--no-commit-id", commit], root).split("\n").filter(Boolean).sort()).toEqual([
303
+ "_members/api/prod/releases.jsonl",
304
+ "_members/web/prod/releases.jsonl",
305
+ ]);
306
+ expect((await readReleaseLedger("prod", { cwd: api })).records.map((r) => r.component)).toEqual(["api"]);
307
+ expect((await readReleaseLedger("prod", { cwd: web })).records.map((r) => r.component)).toEqual(["web"]);
308
+ expect((await readReleaseLedger("prod", { cwd: root })).records.map((r) => r.component)).toEqual(["root"]);
309
+ });
310
+
311
+ test("an expected prior sha that moved is a conflict, not a blind overwrite", async () => {
312
+ const root = workspace();
313
+ const api = join(root, "services/api");
314
+ await appendReleaseRecord(release({ component: "api" }), { cwd: api });
315
+ await expect(
316
+ writeLedgerFiles([{ path: "_members/api/prod/releases.jsonl", content: "{}", expectPriorSha: null }], "stale", { cwd: root }),
317
+ ).rejects.toThrow(/target path changed concurrently/);
318
+ });
319
+ });