@intentius/chant 0.79.0 → 0.81.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (462) hide show
  1. package/dist/audit/core.d.ts.map +1 -1
  2. package/dist/audit/discover.d.ts +30 -0
  3. package/dist/audit/discover.d.ts.map +1 -1
  4. package/dist/audit/report-html.d.ts +5 -0
  5. package/dist/audit/report-html.d.ts.map +1 -1
  6. package/dist/audit/report-model.d.ts +13 -0
  7. package/dist/audit/report-model.d.ts.map +1 -1
  8. package/dist/audit/terraform-state.d.ts +13 -0
  9. package/dist/audit/terraform-state.d.ts.map +1 -1
  10. package/dist/cli/command-group.d.ts +6 -0
  11. package/dist/cli/command-group.d.ts.map +1 -1
  12. package/dist/cli/commands/audit.d.ts +44 -3
  13. package/dist/cli/commands/audit.d.ts.map +1 -1
  14. package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
  15. package/dist/cli/commands/check-lexicon.d.ts +8 -0
  16. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  17. package/dist/cli/commands/doctor.d.ts.map +1 -1
  18. package/dist/cli/commands/import-agents.d.ts.map +1 -1
  19. package/dist/cli/commands/init.d.ts +6 -0
  20. package/dist/cli/commands/init.d.ts.map +1 -1
  21. package/dist/cli/commands/lint.d.ts.map +1 -1
  22. package/dist/cli/commands/onboard.d.ts.map +1 -1
  23. package/dist/cli/commands/update.d.ts +6 -0
  24. package/dist/cli/commands/update.d.ts.map +1 -1
  25. package/dist/cli/commands/vendor.d.ts +5 -0
  26. package/dist/cli/commands/vendor.d.ts.map +1 -1
  27. package/dist/cli/conflict-check.d.ts +6 -0
  28. package/dist/cli/conflict-check.d.ts.map +1 -1
  29. package/dist/cli/handlers/build.d.ts.map +1 -1
  30. package/dist/cli/handlers/components.d.ts.map +1 -1
  31. package/dist/cli/handlers/dev.d.ts.map +1 -1
  32. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  33. package/dist/cli/handlers/init.d.ts.map +1 -1
  34. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  35. package/dist/cli/handlers/misc.d.ts.map +1 -1
  36. package/dist/cli/handlers/operator.d.ts +7 -0
  37. package/dist/cli/handlers/operator.d.ts.map +1 -1
  38. package/dist/cli/handlers/promote.d.ts +20 -0
  39. package/dist/cli/handlers/promote.d.ts.map +1 -0
  40. package/dist/cli/handlers/run-generate.d.ts +39 -0
  41. package/dist/cli/handlers/run-generate.d.ts.map +1 -0
  42. package/dist/cli/handlers/run.d.ts +4 -1
  43. package/dist/cli/handlers/run.d.ts.map +1 -1
  44. package/dist/cli/handlers/vendor.d.ts +8 -3
  45. package/dist/cli/handlers/vendor.d.ts.map +1 -1
  46. package/dist/cli/main.d.ts +12 -0
  47. package/dist/cli/main.d.ts.map +1 -1
  48. package/dist/cli/mcp/op-tools.d.ts.map +1 -1
  49. package/dist/cli/plugins.d.ts +14 -0
  50. package/dist/cli/plugins.d.ts.map +1 -1
  51. package/dist/cli/registry.d.ts +46 -1
  52. package/dist/cli/registry.d.ts.map +1 -1
  53. package/dist/codegen/fetch.d.ts +10 -0
  54. package/dist/codegen/fetch.d.ts.map +1 -1
  55. package/dist/components/capability-plugin-loader.d.ts.map +1 -1
  56. package/dist/components/cli-support.d.ts +3 -1
  57. package/dist/components/cli-support.d.ts.map +1 -1
  58. package/dist/components/discover.d.ts.map +1 -1
  59. package/dist/components/driver.d.ts +16 -2
  60. package/dist/components/driver.d.ts.map +1 -1
  61. package/dist/components/fan-out-support.d.ts.map +1 -1
  62. package/dist/components/gate-plan.d.ts +40 -0
  63. package/dist/components/gate-plan.d.ts.map +1 -0
  64. package/dist/components/pilots/alb-ecs.pilot.d.ts +7 -1
  65. package/dist/components/pilots/alb-ecs.pilot.d.ts.map +1 -1
  66. package/dist/components/presets/ecs-fargate.d.ts +10 -4
  67. package/dist/components/presets/ecs-fargate.d.ts.map +1 -1
  68. package/dist/components/promote.d.ts +264 -0
  69. package/dist/components/promote.d.ts.map +1 -0
  70. package/dist/components/sandbox/driver.d.ts.map +1 -1
  71. package/dist/components/sandbox/run.d.ts.map +1 -1
  72. package/dist/components/verbs/build-archive.d.ts +22 -11
  73. package/dist/components/verbs/build-archive.d.ts.map +1 -1
  74. package/dist/components/verbs/run-agent.d.ts.map +1 -1
  75. package/dist/config-static.d.ts +59 -0
  76. package/dist/config-static.d.ts.map +1 -0
  77. package/dist/config.d.ts +104 -5
  78. package/dist/config.d.ts.map +1 -1
  79. package/dist/content-digest.d.ts +17 -0
  80. package/dist/content-digest.d.ts.map +1 -0
  81. package/dist/discovery/files.d.ts +48 -4
  82. package/dist/discovery/files.d.ts.map +1 -1
  83. package/dist/discovery/fold-import.d.ts +32 -1
  84. package/dist/discovery/fold-import.d.ts.map +1 -1
  85. package/dist/discovery/index.d.ts.map +1 -1
  86. package/dist/discovery/sandbox/driver.d.ts +20 -0
  87. package/dist/discovery/sandbox/driver.d.ts.map +1 -1
  88. package/dist/discovery/sandbox/fork.d.ts +20 -0
  89. package/dist/discovery/sandbox/fork.d.ts.map +1 -1
  90. package/dist/discovery/walk.d.ts +119 -0
  91. package/dist/discovery/walk.d.ts.map +1 -0
  92. package/dist/graph-ir.d.ts +17 -1
  93. package/dist/graph-ir.d.ts.map +1 -1
  94. package/dist/join-key.d.ts +31 -0
  95. package/dist/join-key.d.ts.map +1 -0
  96. package/dist/lexicon-module.d.ts +110 -0
  97. package/dist/lexicon-module.d.ts.map +1 -0
  98. package/dist/lexicon.d.ts +82 -1
  99. package/dist/lexicon.d.ts.map +1 -1
  100. package/dist/lifecycle/build-ledger-store.d.ts.map +1 -1
  101. package/dist/lifecycle/digest.d.ts +5 -1
  102. package/dist/lifecycle/digest.d.ts.map +1 -1
  103. package/dist/lifecycle/gate-ledger.d.ts +20 -5
  104. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  105. package/dist/lifecycle/git.d.ts +64 -0
  106. package/dist/lifecycle/git.d.ts.map +1 -1
  107. package/dist/lifecycle/lease.d.ts +8 -1
  108. package/dist/lifecycle/lease.d.ts.map +1 -1
  109. package/dist/lifecycle/legacy-digest.d.ts +59 -0
  110. package/dist/lifecycle/legacy-digest.d.ts.map +1 -0
  111. package/dist/lifecycle/member-ledger.d.ts +67 -0
  112. package/dist/lifecycle/member-ledger.d.ts.map +1 -0
  113. package/dist/lifecycle/plan-digest.d.ts +4 -3
  114. package/dist/lifecycle/plan-digest.d.ts.map +1 -1
  115. package/dist/lifecycle/release-ledger.d.ts +42 -1
  116. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  117. package/dist/lifecycle/status.d.ts +90 -5
  118. package/dist/lifecycle/status.d.ts.map +1 -1
  119. package/dist/lint/rules/comp/comp003-mutating-no-rollback.d.ts +2 -3
  120. package/dist/lint/rules/comp/comp003-mutating-no-rollback.d.ts.map +1 -1
  121. package/dist/op/activities/activity-contracts.d.ts +32 -0
  122. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  123. package/dist/op/activities/index.d.ts +2 -0
  124. package/dist/op/activities/index.d.ts.map +1 -1
  125. package/dist/op/activities/propose-upgrade.d.ts +75 -0
  126. package/dist/op/activities/propose-upgrade.d.ts.map +1 -0
  127. package/dist/op/activity-contract-registry.d.ts.map +1 -1
  128. package/dist/op/activity-registry.d.ts.map +1 -1
  129. package/dist/op/discover.d.ts.map +1 -1
  130. package/dist/op/gate-approval.d.ts.map +1 -1
  131. package/dist/op/gate-name.d.ts +7 -0
  132. package/dist/op/gate-name.d.ts.map +1 -1
  133. package/dist/op/gate-summary.d.ts +16 -0
  134. package/dist/op/gate-summary.d.ts.map +1 -1
  135. package/dist/op/gate.d.ts +16 -2
  136. package/dist/op/gate.d.ts.map +1 -1
  137. package/dist/op/generate-pipeline.d.ts.map +1 -1
  138. package/dist/op/runtimes/local.d.ts.map +1 -1
  139. package/dist/project-root.d.ts +26 -0
  140. package/dist/project-root.d.ts.map +1 -1
  141. package/dist/workspace/__fixtures__/contract-repo.d.ts +37 -0
  142. package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -0
  143. package/dist/workspace/__fixtures__/two-member-workspace.d.ts +11 -0
  144. package/dist/workspace/__fixtures__/two-member-workspace.d.ts.map +1 -0
  145. package/dist/workspace/checks/generated.d.ts +156 -0
  146. package/dist/workspace/checks/generated.d.ts.map +1 -0
  147. package/dist/workspace/checks/ledgers.d.ts +78 -0
  148. package/dist/workspace/checks/ledgers.d.ts.map +1 -0
  149. package/dist/workspace/checks/links.d.ts +25 -0
  150. package/dist/workspace/checks/links.d.ts.map +1 -0
  151. package/dist/workspace/checks/pipelines.d.ts +76 -0
  152. package/dist/workspace/checks/pipelines.d.ts.map +1 -0
  153. package/dist/workspace/checks.d.ts +161 -0
  154. package/dist/workspace/checks.d.ts.map +1 -0
  155. package/dist/workspace/compose-graph.d.ts +115 -0
  156. package/dist/workspace/compose-graph.d.ts.map +1 -0
  157. package/dist/workspace/compose-reports.d.ts +68 -0
  158. package/dist/workspace/compose-reports.d.ts.map +1 -0
  159. package/dist/workspace/declaration.d.ts +220 -0
  160. package/dist/workspace/declaration.d.ts.map +1 -0
  161. package/dist/workspace/declaration.schema.json +434 -0
  162. package/dist/workspace/generated-files.d.ts +65 -0
  163. package/dist/workspace/generated-files.d.ts.map +1 -0
  164. package/dist/workspace/glob.d.ts +13 -0
  165. package/dist/workspace/glob.d.ts.map +1 -0
  166. package/dist/workspace/graph-cli.d.ts +75 -0
  167. package/dist/workspace/graph-cli.d.ts.map +1 -0
  168. package/dist/workspace/init.d.ts +96 -0
  169. package/dist/workspace/init.d.ts.map +1 -0
  170. package/dist/workspace/jsonc.d.ts +38 -0
  171. package/dist/workspace/jsonc.d.ts.map +1 -0
  172. package/dist/workspace/kinds.d.ts +192 -0
  173. package/dist/workspace/kinds.d.ts.map +1 -0
  174. package/dist/workspace/lineage-check.d.ts +84 -0
  175. package/dist/workspace/lineage-check.d.ts.map +1 -0
  176. package/dist/workspace/lineage-cli.d.ts +31 -0
  177. package/dist/workspace/lineage-cli.d.ts.map +1 -0
  178. package/dist/workspace/lineage-init.d.ts +101 -0
  179. package/dist/workspace/lineage-init.d.ts.map +1 -0
  180. package/dist/workspace/lineage-lock.d.ts +258 -0
  181. package/dist/workspace/lineage-lock.d.ts.map +1 -0
  182. package/dist/workspace/lineage-merge.d.ts +14 -0
  183. package/dist/workspace/lineage-merge.d.ts.map +1 -0
  184. package/dist/workspace/lineage-migrations.d.ts +178 -0
  185. package/dist/workspace/lineage-migrations.d.ts.map +1 -0
  186. package/dist/workspace/lineage-update.d.ts +59 -0
  187. package/dist/workspace/lineage-update.d.ts.map +1 -0
  188. package/dist/workspace/lineage-upgrade-cli.d.ts +40 -0
  189. package/dist/workspace/lineage-upgrade-cli.d.ts.map +1 -0
  190. package/dist/workspace/lineage-upgrade.d.ts +146 -0
  191. package/dist/workspace/lineage-upgrade.d.ts.map +1 -0
  192. package/dist/workspace/lineage-vendor.d.ts +63 -0
  193. package/dist/workspace/lineage-vendor.d.ts.map +1 -0
  194. package/dist/workspace/lineage-version.d.ts +32 -0
  195. package/dist/workspace/lineage-version.d.ts.map +1 -0
  196. package/dist/workspace/links.d.ts +150 -0
  197. package/dist/workspace/links.d.ts.map +1 -0
  198. package/dist/workspace/ls.d.ts +99 -0
  199. package/dist/workspace/ls.d.ts.map +1 -0
  200. package/dist/workspace/member-commands.d.ts +139 -0
  201. package/dist/workspace/member-commands.d.ts.map +1 -0
  202. package/dist/workspace/member-handles.d.ts +54 -0
  203. package/dist/workspace/member-handles.d.ts.map +1 -0
  204. package/dist/workspace/member-pipeline.d.ts +143 -0
  205. package/dist/workspace/member-pipeline.d.ts.map +1 -0
  206. package/dist/workspace/member-run.d.ts +67 -0
  207. package/dist/workspace/member-run.d.ts.map +1 -0
  208. package/dist/workspace/reason-codes.d.ts +65 -0
  209. package/dist/workspace/reason-codes.d.ts.map +1 -0
  210. package/dist/workspace/record-source.d.ts +22 -0
  211. package/dist/workspace/record-source.d.ts.map +1 -0
  212. package/dist/workspace/records-cli.d.ts +81 -0
  213. package/dist/workspace/records-cli.d.ts.map +1 -0
  214. package/dist/workspace/records.d.ts +118 -0
  215. package/dist/workspace/records.d.ts.map +1 -0
  216. package/dist/workspace/root-refusal.d.ts +53 -0
  217. package/dist/workspace/root-refusal.d.ts.map +1 -0
  218. package/dist/workspace/status.d.ts +150 -0
  219. package/dist/workspace/status.d.ts.map +1 -0
  220. package/dist/workspace/template-manifest.d.ts +90 -0
  221. package/dist/workspace/template-manifest.d.ts.map +1 -0
  222. package/dist/workspace/tree.d.ts +36 -0
  223. package/dist/workspace/tree.d.ts.map +1 -0
  224. package/dist/workspace/trust/attestor.d.ts +67 -0
  225. package/dist/workspace/trust/attestor.d.ts.map +1 -0
  226. package/dist/workspace/trust/policy.d.ts +111 -0
  227. package/dist/workspace/trust/policy.d.ts.map +1 -0
  228. package/dist/workspace/trust/provenance.d.ts +67 -0
  229. package/dist/workspace/trust/provenance.d.ts.map +1 -0
  230. package/dist/workspace/trust/ssh-commit.d.ts +43 -0
  231. package/dist/workspace/trust/ssh-commit.d.ts.map +1 -0
  232. package/dist/workspace/trust/test-repo.d.ts +42 -0
  233. package/dist/workspace/trust/test-repo.d.ts.map +1 -0
  234. package/dist/workspace/trust/verify-cli.d.ts +9 -0
  235. package/dist/workspace/trust/verify-cli.d.ts.map +1 -0
  236. package/dist/workspace/trust/verify.d.ts +66 -0
  237. package/dist/workspace/trust/verify.d.ts.map +1 -0
  238. package/dist/workspace/which-chant.d.ts +58 -0
  239. package/dist/workspace/which-chant.d.ts.map +1 -0
  240. package/dist/workspace/workspace-kinds.schema.json +133 -0
  241. package/package.json +4 -3
  242. package/src/audit/core.ts +13 -2
  243. package/src/audit/discover.ts +102 -36
  244. package/src/audit/report-html.ts +3 -1
  245. package/src/audit/report-model.ts +14 -1
  246. package/src/audit/terraform-state.test.ts +81 -1
  247. package/src/audit/terraform-state.ts +25 -0
  248. package/src/cli/command-group.ts +7 -0
  249. package/src/cli/commands/audit-truncation.test.ts +167 -0
  250. package/src/cli/commands/audit.ts +132 -29
  251. package/src/cli/commands/check-lexicon-examples.ts +2 -1
  252. package/src/cli/commands/check-lexicon.ts +24 -0
  253. package/src/cli/commands/doctor.test.ts +30 -0
  254. package/src/cli/commands/doctor.ts +21 -0
  255. package/src/cli/commands/import-agents.ts +15 -4
  256. package/src/cli/commands/init.ts +76 -25
  257. package/src/cli/commands/lint.test.ts +52 -1
  258. package/src/cli/commands/lint.ts +41 -71
  259. package/src/cli/commands/onboard.ts +15 -0
  260. package/src/cli/commands/update.ts +18 -5
  261. package/src/cli/commands/vendor.ts +11 -6
  262. package/src/cli/conflict-check.test.ts +18 -1
  263. package/src/cli/conflict-check.ts +13 -0
  264. package/src/cli/discovery-skip.test.ts +168 -0
  265. package/src/cli/handlers/__snapshots__/build-member.test.ts.snap +38 -0
  266. package/src/cli/handlers/build-member.test.ts +127 -0
  267. package/src/cli/handlers/build.test.ts +11 -0
  268. package/src/cli/handlers/build.ts +35 -1
  269. package/src/cli/handlers/components.test.ts +127 -0
  270. package/src/cli/handlers/components.ts +70 -8
  271. package/src/cli/handlers/dev.ts +6 -1
  272. package/src/cli/handlers/fan-out.ts +9 -2
  273. package/src/cli/handlers/graph.test.ts +20 -0
  274. package/src/cli/handlers/graph.ts +4 -2
  275. package/src/cli/handlers/init.ts +54 -1
  276. package/src/cli/handlers/lifecycle.ts +2 -1
  277. package/src/cli/handlers/misc.ts +16 -0
  278. package/src/cli/handlers/operator.test.ts +68 -0
  279. package/src/cli/handlers/operator.ts +39 -10
  280. package/src/cli/handlers/promote.test.ts +377 -0
  281. package/src/cli/handlers/promote.ts +391 -0
  282. package/src/cli/handlers/run-generate-member.test.ts +83 -0
  283. package/src/cli/handlers/run-generate.test.ts +158 -0
  284. package/src/cli/handlers/run-generate.ts +168 -0
  285. package/src/cli/handlers/run.test.ts +47 -0
  286. package/src/cli/handlers/run.ts +41 -6
  287. package/src/cli/handlers/vendor.ts +147 -43
  288. package/src/cli/main.test.ts +137 -1
  289. package/src/cli/main.ts +290 -24
  290. package/src/cli/mcp/op-tools.ts +2 -1
  291. package/src/cli/path-lexicon-messages.test.ts +152 -0
  292. package/src/cli/plugins.ts +43 -5
  293. package/src/cli/registry.ts +46 -1
  294. package/src/cli/static-config-read.test.ts +208 -0
  295. package/src/codegen/fetch.test.ts +63 -1
  296. package/src/codegen/fetch.ts +30 -1
  297. package/src/components/__fixtures__/alb-ecs-service.json +0 -8
  298. package/src/components/capability-plugin-loader.ts +3 -1
  299. package/src/components/cli-support.ts +9 -3
  300. package/src/components/discover.test.ts +27 -57
  301. package/src/components/discover.ts +20 -51
  302. package/src/components/driver.test.ts +90 -12
  303. package/src/components/driver.ts +43 -5
  304. package/src/components/fan-out-support.ts +2 -1
  305. package/src/components/gate-plan.ts +53 -0
  306. package/src/components/pilots/README.md +1 -1
  307. package/src/components/pilots/alb-ecs.pilot.ts +7 -7
  308. package/src/components/presets/ecs-fargate.ts +10 -5
  309. package/src/components/promote.test.ts +645 -0
  310. package/src/components/promote.ts +641 -0
  311. package/src/components/sandbox/driver.ts +8 -8
  312. package/src/components/sandbox/run.ts +4 -1
  313. package/src/components/verbs/build-archive.ts +27 -21
  314. package/src/components/verbs/run-agent.ts +3 -5
  315. package/src/config-static.test.ts +136 -0
  316. package/src/config-static.ts +232 -0
  317. package/src/config.test.ts +40 -0
  318. package/src/config.ts +161 -11
  319. package/src/content-digest.test.ts +120 -0
  320. package/src/content-digest.ts +21 -0
  321. package/src/discovery/files.test.ts +127 -1
  322. package/src/discovery/files.ts +91 -59
  323. package/src/discovery/fold-import.ts +127 -9
  324. package/src/discovery/index.ts +1 -0
  325. package/src/discovery/sandbox/driver.ts +73 -35
  326. package/src/discovery/sandbox/fork-diagnostic.test.ts +136 -13
  327. package/src/discovery/sandbox/fork.ts +83 -43
  328. package/src/discovery/sandbox/path-lexicon-multifile.test.ts +285 -0
  329. package/src/discovery/sandbox/path-lexicon-parity.test.ts +162 -0
  330. package/src/discovery/walk.test.ts +263 -0
  331. package/src/discovery/walk.ts +334 -0
  332. package/src/effect-receipt.ts +2 -2
  333. package/src/graph-ir.ts +18 -1
  334. package/src/join-key.test.ts +25 -0
  335. package/src/join-key.ts +39 -0
  336. package/src/lexicon-module.test.ts +203 -0
  337. package/src/lexicon-module.ts +215 -0
  338. package/src/lexicon.ts +105 -1
  339. package/src/lifecycle/build-ledger-store.ts +13 -2
  340. package/src/lifecycle/digest.ts +9 -5
  341. package/src/lifecycle/gate-ledger.test.ts +20 -0
  342. package/src/lifecycle/gate-ledger.ts +28 -6
  343. package/src/lifecycle/git.test.ts +13 -1
  344. package/src/lifecycle/git.ts +221 -176
  345. package/src/lifecycle/lease.ts +25 -10
  346. package/src/lifecycle/legacy-digest.test.ts +206 -0
  347. package/src/lifecycle/legacy-digest.ts +103 -0
  348. package/src/lifecycle/member-ledger.test.ts +319 -0
  349. package/src/lifecycle/member-ledger.ts +145 -0
  350. package/src/lifecycle/plan-digest.ts +7 -7
  351. package/src/lifecycle/release-ledger.test.ts +20 -0
  352. package/src/lifecycle/release-ledger.ts +53 -3
  353. package/src/lifecycle/status.test.ts +151 -1
  354. package/src/lifecycle/status.ts +228 -7
  355. package/src/lint/rules/comp/comp003-mutating-no-rollback.ts +2 -3
  356. package/src/op/activities/activity-contracts.ts +37 -0
  357. package/src/op/activities/index.ts +10 -0
  358. package/src/op/activities/propose-upgrade.ts +206 -0
  359. package/src/op/activity-contract-registry.ts +18 -4
  360. package/src/op/activity-registry.ts +26 -0
  361. package/src/op/discover.test.ts +14 -4
  362. package/src/op/discover.ts +19 -23
  363. package/src/op/gate-approval.ts +3 -1
  364. package/src/op/gate-name.ts +8 -0
  365. package/src/op/gate-summary.test.ts +14 -0
  366. package/src/op/gate-summary.ts +25 -2
  367. package/src/op/gate.ts +36 -7
  368. package/src/op/generate-pipeline.test.ts +14 -5
  369. package/src/op/generate-pipeline.ts +2 -1
  370. package/src/op/runtimes/local.ts +2 -1
  371. package/src/project-root.test.ts +66 -1
  372. package/src/project-root.ts +48 -0
  373. package/src/workspace/__fixtures__/contract-repo.ts +100 -0
  374. package/src/workspace/__fixtures__/two-member-workspace.ts +35 -0
  375. package/src/workspace/behold-kinds.test.ts +162 -0
  376. package/src/workspace/check-contract.test.ts +127 -0
  377. package/src/workspace/check.schema.json +347 -0
  378. package/src/workspace/checks/generated.test.ts +287 -0
  379. package/src/workspace/checks/generated.ts +475 -0
  380. package/src/workspace/checks/ledgers.test.ts +139 -0
  381. package/src/workspace/checks/ledgers.ts +246 -0
  382. package/src/workspace/checks/links.ts +183 -0
  383. package/src/workspace/checks/pipelines.test.ts +133 -0
  384. package/src/workspace/checks/pipelines.ts +225 -0
  385. package/src/workspace/checks-repo.test.ts +71 -0
  386. package/src/workspace/checks.test.ts +450 -0
  387. package/src/workspace/checks.ts +461 -0
  388. package/src/workspace/compose-graph.test.ts +146 -0
  389. package/src/workspace/compose-graph.ts +228 -0
  390. package/src/workspace/compose-reports.test.ts +109 -0
  391. package/src/workspace/compose-reports.ts +186 -0
  392. package/src/workspace/declaration.schema.json +434 -0
  393. package/src/workspace/declaration.test.ts +347 -0
  394. package/src/workspace/declaration.ts +623 -0
  395. package/src/workspace/generated-files.test.ts +114 -0
  396. package/src/workspace/generated-files.ts +94 -0
  397. package/src/workspace/glob.ts +49 -0
  398. package/src/workspace/graph-cli.ts +220 -0
  399. package/src/workspace/graph-contract.test.ts +204 -0
  400. package/src/workspace/graph.schema.json +662 -0
  401. package/src/workspace/init.test.ts +223 -0
  402. package/src/workspace/init.ts +585 -0
  403. package/src/workspace/jsonc.test.ts +65 -0
  404. package/src/workspace/jsonc.ts +231 -0
  405. package/src/workspace/kinds.test.ts +288 -0
  406. package/src/workspace/kinds.ts +506 -0
  407. package/src/workspace/lineage-check.ts +264 -0
  408. package/src/workspace/lineage-cli.ts +114 -0
  409. package/src/workspace/lineage-init.test.ts +231 -0
  410. package/src/workspace/lineage-init.ts +365 -0
  411. package/src/workspace/lineage-lock.test.ts +224 -0
  412. package/src/workspace/lineage-lock.ts +357 -0
  413. package/src/workspace/lineage-merge.ts +44 -0
  414. package/src/workspace/lineage-migrations.test.ts +178 -0
  415. package/src/workspace/lineage-migrations.ts +343 -0
  416. package/src/workspace/lineage-update.ts +160 -0
  417. package/src/workspace/lineage-upgrade-cli.ts +151 -0
  418. package/src/workspace/lineage-upgrade.test.ts +474 -0
  419. package/src/workspace/lineage-upgrade.ts +624 -0
  420. package/src/workspace/lineage-vendor.test.ts +126 -0
  421. package/src/workspace/lineage-vendor.ts +215 -0
  422. package/src/workspace/lineage-version.ts +127 -0
  423. package/src/workspace/links.test.ts +344 -0
  424. package/src/workspace/links.ts +331 -0
  425. package/src/workspace/ls-contract.test.ts +200 -0
  426. package/src/workspace/ls.schema.json +190 -0
  427. package/src/workspace/ls.ts +254 -0
  428. package/src/workspace/member-commands.e2e.test.ts +244 -0
  429. package/src/workspace/member-commands.test.ts +217 -0
  430. package/src/workspace/member-commands.ts +540 -0
  431. package/src/workspace/member-handles.test.ts +76 -0
  432. package/src/workspace/member-handles.ts +228 -0
  433. package/src/workspace/member-pipeline.test.ts +135 -0
  434. package/src/workspace/member-pipeline.ts +317 -0
  435. package/src/workspace/member-run.ts +146 -0
  436. package/src/workspace/read-contract.test.ts +129 -0
  437. package/src/workspace/reason-codes.test.ts +113 -0
  438. package/src/workspace/reason-codes.ts +81 -0
  439. package/src/workspace/record-source.ts +117 -0
  440. package/src/workspace/records-cli.ts +198 -0
  441. package/src/workspace/records-contract.test.ts +93 -0
  442. package/src/workspace/records.schema.json +150 -0
  443. package/src/workspace/records.test.ts +243 -0
  444. package/src/workspace/records.ts +384 -0
  445. package/src/workspace/root-refusal.ts +97 -0
  446. package/src/workspace/status-contract.test.ts +308 -0
  447. package/src/workspace/status.schema.json +210 -0
  448. package/src/workspace/status.ts +412 -0
  449. package/src/workspace/template-manifest.test.ts +79 -0
  450. package/src/workspace/template-manifest.ts +221 -0
  451. package/src/workspace/tree.ts +132 -0
  452. package/src/workspace/trust/attestor.ts +93 -0
  453. package/src/workspace/trust/policy.ts +306 -0
  454. package/src/workspace/trust/provenance.ts +181 -0
  455. package/src/workspace/trust/ssh-commit.ts +138 -0
  456. package/src/workspace/trust/test-repo.ts +138 -0
  457. package/src/workspace/trust/trust.test.ts +397 -0
  458. package/src/workspace/trust/verify-cli.ts +56 -0
  459. package/src/workspace/trust/verify.ts +194 -0
  460. package/src/workspace/which-chant.test.ts +117 -0
  461. package/src/workspace/which-chant.ts +151 -0
  462. package/src/workspace/workspace-kinds.schema.json +133 -0
@@ -0,0 +1,206 @@
1
+ /**
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.
6
+ */
7
+ import { describe, test, expect, beforeEach, afterEach, vi } from "vitest";
8
+ import { withTestDir } from "@intentius/chant-test-utils";
9
+ import { spawnSync } from "node:child_process";
10
+ import { createHash } from "node:crypto";
11
+ import { writeFileSync } from "node:fs";
12
+ import { join } from "node:path";
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";
30
+ import { addArchiveEntry, contentDigest, createBuildArchiveManifest } from "../components/verbs/build-archive";
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
+
42
+ function git(args: string[], cwd: string): void {
43
+ spawnSync("git", args, { cwd, encoding: "utf-8" });
44
+ }
45
+
46
+ function initRepo(dir: string): void {
47
+ git(["init", "-q", "-b", "main"], dir);
48
+ git(["config", "user.email", "test@chant.dev"], dir);
49
+ git(["config", "user.name", "Test"], dir);
50
+ writeFileSync(join(dir, "README.md"), "fixture\n");
51
+ git(["add", "README.md"], dir);
52
+ git(["commit", "-q", "-m", "init"], dir);
53
+ }
54
+
55
+ function release(digest: string, runId: string): ReleaseRecordInput {
56
+ return {
57
+ component: "web",
58
+ env: "prod",
59
+ digest,
60
+ gitSha: "deadbeef",
61
+ runId,
62
+ timestamp: "2026-01-01T00:00:00.000Z",
63
+ actor: "ci-bot",
64
+ };
65
+ }
66
+
67
+ const realSha256 = `sha256:${createHash("sha256").update("x").digest("hex")}`;
68
+
69
+ describe("legacy content digests (#2514)", () => {
70
+ let warn: { mock: { calls: unknown[][] }; mockRestore(): void };
71
+
72
+ beforeEach(() => {
73
+ resetLegacyDigestWarnings();
74
+ warn = vi.spyOn(console, "warn").mockImplementation(() => {});
75
+ });
76
+
77
+ afterEach(() => {
78
+ warn.mockRestore();
79
+ resetLegacyDigestWarnings();
80
+ });
81
+
82
+ const legacyWarnings = () => warn.mock.calls.filter((c: unknown[]) => String(c[0]).includes("issues/2514"));
83
+
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);
88
+ });
89
+
90
+ test("a real SHA-256 value, a short value and a non-string are not", () => {
91
+ expect(isLegacyContentDigest(realSha256)).toBe(false);
92
+ expect(isLegacyContentDigest("sha256:abc123")).toBe(false);
93
+ expect(isLegacyContentDigest(undefined)).toBe(false);
94
+ });
95
+
96
+ test("warns once per project, and not at all without an old value", () => {
97
+ expect(warnOnLegacyDigests([realSha256], "release ledger", "/p/a")).toBe(false);
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);
101
+ expect(legacyWarnings()).toHaveLength(2);
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}`);
104
+ });
105
+
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 () => {
116
+ await withTestDir(async (dir) => {
117
+ initRepo(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 });
121
+
122
+ const { records } = await readReleaseLedger("prod", { cwd: dir });
123
+ await readReleaseLedger("prod", { cwd: dir });
124
+ expect(legacyWarnings()).toHaveLength(1);
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");
140
+ });
141
+ });
142
+
143
+ test("a release ledger of real SHA-256 values stays quiet", async () => {
144
+ await withTestDir(async (dir) => {
145
+ initRepo(dir);
146
+ await appendReleaseRecord(release(realSha256, "run-1"), { cwd: dir });
147
+ await readReleaseLedger("prod", { cwd: dir });
148
+ expect(legacyWarnings()).toHaveLength(0);
149
+ });
150
+ });
151
+
152
+ test("an old persisted build manifest reads back under its old key, flagged, and is found by its old artifact digest", async () => {
153
+ await withTestDir(async (dir) => {
154
+ initRepo(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 });
171
+ await readAllBuildManifests({ cwd: dir });
172
+ expect(legacyWarnings()).toHaveLength(1);
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");
186
+ });
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
+ });
206
+ });
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Digests recorded before real SHA-256 (chant #2514).
3
+ *
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.
18
+ *
19
+ * An old value is recognisable by its shape alone. A real SHA-256 hex digest
20
+ * whose 64 characters are one 8-character block repeated eight times has
21
+ * probability 2^-224, so the check has no false positives worth considering.
22
+ */
23
+
24
+ import { resolve } from "node:path";
25
+
26
+ const LEGACY_DIGEST = /^sha256:([0-9a-f]{8})\1{7}$/;
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
+
39
+ /** Whether `value` is a `sha256:` string made by the old 32-bit `contentDigest`. */
40
+ export function isLegacyContentDigest(value: unknown): boolean {
41
+ return typeof value === "string" && LEGACY_DIGEST.test(value);
42
+ }
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
+
74
+ /** Project roots already warned about in this process. */
75
+ const warnedProjects = new Set<string>();
76
+
77
+ /** Forget which projects were warned about. For tests. */
78
+ export function resetLegacyDigestWarnings(): void {
79
+ warnedProjects.clear();
80
+ }
81
+
82
+ /**
83
+ * Warn once for the project at `cwd` (default: the working directory) when
84
+ * any of `values` is an old-form digest. `where` names the record that held
85
+ * it, for the message. Returns whether a warning was printed.
86
+ */
87
+ export function warnOnLegacyDigests(
88
+ values: Iterable<unknown>,
89
+ where: "release ledger" | "build manifests",
90
+ cwd?: string,
91
+ ): boolean {
92
+ const project = resolve(cwd ?? process.cwd());
93
+ if (warnedProjects.has(project)) return false;
94
+ if (!hasLegacyContentDigest(values)) return false;
95
+ warnedProjects.add(project);
96
+ console.warn(
97
+ `[chant] warning: this project's ${where} ${where === "release ledger" ? "holds" : "hold"} sha256: values made by chant's old 32-bit content hash, ` +
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`,
101
+ );
102
+ return true;
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
+ });