@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
@@ -5,9 +5,23 @@
5
5
  * no working tree changes.
6
6
  */
7
7
  import { getRuntime } from "../runtime-adapter";
8
+ import { resolveMemberLedger } from "./member-ledger";
8
9
 
9
10
  const STATE_BRANCH = "chant/lifecycle";
10
11
 
12
+ /**
13
+ * The branch path of `dir` for the project at `cwd` (#2538): `dir` itself at
14
+ * level 0 and for the root member `.`, `_members/<member>/<dir>` for any
15
+ * other workspace member. Every store in this module and the ones built on it
16
+ * (releases, snapshots, observation baselines, runs, converge records, gates,
17
+ * build records) goes through here, so none of them names the prefix itself.
18
+ * See ./member-ledger.ts for who counts as a member.
19
+ */
20
+ export async function ledgerDir(dir: string, opts?: { cwd?: string }): Promise<string> {
21
+ const { prefix } = await resolveMemberLedger(opts?.cwd ?? process.cwd());
22
+ return `${prefix}${dir}`;
23
+ }
24
+
11
25
  /**
12
26
  * The identity chant's ledger commits fall back to when the checkout has none
13
27
  * of its own (#2301).
@@ -71,6 +85,16 @@ function ledgerWriteError(stage: string, path: string, stderr: string): Error {
71
85
  );
72
86
  }
73
87
 
88
+ /**
89
+ * Bounded retry budget for {@link writeBlobToPath}'s own internal CAS retry
90
+ * (#1959 finding 1). A conflict caused by some *other* env/file entry
91
+ * changing concurrently (the ordinary case — e.g. two operator ticks
92
+ * targeting different envs) is safe to resolve by simply rebuilding the tree
93
+ * against the new tip and retrying; this bounds how many times it does that
94
+ * before giving up and surfacing the conflict.
95
+ */
96
+ const WRITE_BLOB_RETRY_ATTEMPTS = 5;
97
+
74
98
  /**
75
99
  * Write a blob to an arbitrary `<environment>/<filename>` path on the orphan
76
100
  * branch, preserving every other env/file entry already on the branch.
@@ -91,43 +115,84 @@ function ledgerWriteError(stage: string, path: string, stderr: string): Error {
91
115
  * entry name this function's tree-building logic groups by; callers outside
92
116
  * this module that pass a non-env value (like `_builds`) are relying on that
93
117
  * generic behavior, not on any env-specific semantics.
118
+ *
119
+ * `environment` is relative to the project's ledger (#2538): a workspace
120
+ * member's write lands under `_members/<member>/` ({@link ledgerDir}). The
121
+ * write itself is {@link writeLedgerFiles} with one file.
94
122
  */
95
- /**
96
- * Bounded retry budget for {@link writeBlobToPath}'s own internal CAS retry
97
- * (#1959 finding 1). A conflict caused by some *other* env/file entry
98
- * changing concurrently (the ordinary case — e.g. two operator ticks
99
- * targeting different envs) is safe to resolve by simply rebuilding the tree
100
- * against the new tip and retrying; this bounds how many times it does that
101
- * before giving up and surfacing the conflict.
102
- */
103
- const WRITE_BLOB_RETRY_ATTEMPTS = 5;
104
-
105
123
  export async function writeBlobToPath(
106
124
  environment: string,
107
125
  filename: string,
108
126
  content: string,
109
127
  commitMessage: string,
110
128
  opts?: { cwd?: string; expectPriorPathSha?: string | null },
129
+ ): Promise<string> {
130
+ const dir = await ledgerDir(environment, opts);
131
+ return writeLedgerFiles(
132
+ [{ path: `${dir}/${filename}`, content, ...(opts?.expectPriorPathSha !== undefined ? { expectPriorSha: opts.expectPriorPathSha } : {}) }],
133
+ commitMessage,
134
+ opts,
135
+ );
136
+ }
137
+
138
+ /** One file for {@link writeLedgerFiles}. */
139
+ export interface LedgerFileWrite {
140
+ /** The file's full path on the branch, such as `_members/api/prod/releases.jsonl`. No prefix is added. */
141
+ path: string;
142
+ content: string;
143
+ /**
144
+ * For a read-modify-write caller: the blob sha the caller's content was
145
+ * computed from (`null` for "the file did not exist"). A different sha on
146
+ * the branch is a conflict, never a blind retry.
147
+ */
148
+ expectPriorSha?: string | null;
149
+ }
150
+
151
+ /**
152
+ * Write several files to the branch in one commit (#2538). Paths are full
153
+ * branch paths, so one commit can write the ledgers of several members, for
154
+ * example `_members/api/prod/releases.jsonl` and
155
+ * `_members/web/prod/releases.jsonl` together. {@link writeBlobToPath} is the
156
+ * one-file case with the project's own member prefix applied.
157
+ *
158
+ * Pipeline, per attempt: read the directories each path passes through at
159
+ * the branch tip, replace the entries, rebuild the touched trees bottom-up
160
+ * with `git mktree`, `commit-tree` on that tip, then CAS-update the ref.
161
+ * Every entry the write does not touch is carried over as it is, so the tree
162
+ * of an unchanged directory keeps its sha.
163
+ */
164
+ export async function writeLedgerFiles(
165
+ files: LedgerFileWrite[],
166
+ commitMessage: string,
167
+ opts?: { cwd?: string },
111
168
  ): Promise<string> {
112
169
  const rt = getRuntime();
113
170
  const cwd = opts?.cwd;
171
+ if (files.length === 0) throw new Error("writeLedgerFiles needs at least one file");
172
+ // Failures name the first path; one file is the ordinary case.
173
+ const path = files[0].path;
174
+ for (const f of files) {
175
+ const segs = f.path.split("/");
176
+ if (segs.length < 2 || segs.some((seg) => seg === "")) {
177
+ throw new Error(`cannot write ${f.path} on the ${STATE_BRANCH} branch: a ledger path is <dir>/<file>`);
178
+ }
179
+ }
114
180
 
115
- // 1. Write blob — hash-object reads from stdin. Fed directly via spawn's
116
- // stdin (no shell involved), so content is written byte-for-byte: no `sh`
117
- // `echo` reinterpreting backslash-escape sequences (e.g. `\n` inside JSON,
118
- // as in a serialized kubectl.kubernetes.io/last-applied-configuration
119
- // annotation) and no shell-quoting dance around embedded single quotes.
120
- // Content-addressed and idempotent, so this stays outside the retry loop
121
- // below — nothing about a CAS conflict on the ref ever invalidates it.
122
- //
123
- // `path` is resolved first only so every failure below can name it (#2301).
124
- const path = `${environment}/${filename}`;
125
-
126
- const blobResult = await rt.spawn(["git", "hash-object", "-w", "--stdin"], { cwd, stdin: content });
127
- if (blobResult.exitCode !== 0) {
128
- throw ledgerWriteError("git hash-object", path, blobResult.stderr);
181
+ // 1. Write the blobs — hash-object reads from stdin. Fed directly via
182
+ // spawn's stdin (no shell involved), so content is written byte-for-byte:
183
+ // no `sh` `echo` reinterpreting backslash-escape sequences (e.g. `\n`
184
+ // inside JSON, as in a serialized
185
+ // kubectl.kubernetes.io/last-applied-configuration annotation) and no
186
+ // shell-quoting dance around embedded single quotes. Content-addressed
187
+ // and idempotent, so this stays outside the retry loop below.
188
+ const blobs: { path: string; sha: string }[] = [];
189
+ for (const f of files) {
190
+ const blobResult = await rt.spawn(["git", "hash-object", "-w", "--stdin"], { cwd, stdin: f.content });
191
+ if (blobResult.exitCode !== 0) {
192
+ throw ledgerWriteError("git hash-object", f.path, blobResult.stderr);
193
+ }
194
+ blobs.push({ path: f.path, sha: blobResult.stdout.trim() });
129
195
  }
130
- const blobSha = blobResult.stdout.trim();
131
196
 
132
197
  // Resolved once, outside the retry loop: whether the checkout can name a
133
198
  // committer does not change between attempts (#2301).
@@ -156,99 +221,94 @@ export async function writeBlobToPath(
156
221
  await requireLifecycleLedger({ ...(cwd ? { cwd } : {}) });
157
222
  }
158
223
 
159
- // 2-5. Read tree, build the commit, and CAS-update the branch ref — retried
160
- // on a conflict (#1959 finding 1). `writeBlobToPath` had no retry of its
161
- // own before this: a conflict from step 5's `updateRefCAS` simply threw,
162
- // breaking every pre-existing caller (observation-baseline.ts,
163
- // snapshot.ts, build-ledger-store.ts, and — via `appendReleaseRecordLine`
164
- // below — release-ledger.ts) the moment any concurrent writer touched the
165
- // orphan branch, e.g. a live `chant operator` ticking a different env.
224
+ // 2-5. Read the trees, build the commit, and CAS-update the branch ref —
225
+ // retried on a conflict (#1959 finding 1). Before that retry existed a
226
+ // conflict from step 5's `updateRefCAS` simply threw, breaking every caller
227
+ // (observation-baseline.ts, snapshot.ts, build-ledger-store.ts, and — via
228
+ // `appendReleaseRecordLine` below — release-ledger.ts) the moment any
229
+ // concurrent writer touched the orphan branch, e.g. a live `chant
230
+ // operator` ticking a different env.
166
231
  //
167
- // Safety of a *blind* retry (same `content`/`blobSha`, freshly rebuilt
168
- // tree) hinges on whether *our own* target path (`environment/filename`)
169
- // is what actually caused the conflict:
232
+ // Safety of a *blind* retry (same content and blob, freshly rebuilt tree)
233
+ // hinges on whether *our own* target paths are what actually caused the
234
+ // conflict:
170
235
  // - If some OTHER path changed (the ordinary multi-env/multi-file case),
171
236
  // our data is unaffected — rebuilding the tree from the new tip and
172
237
  // retrying the ref update is always correct, no matter what kind of
173
238
  // write the caller is doing (replace or append).
174
- // - If OUR OWN path changed concurrently, a blind retry using `content`
175
- // computed before that race would silently clobber the other writer's
176
- // change — safe only for a caller whose `content` is a self-contained
177
- // replacement (writeObservationBaseline, writeSnapshot,
239
+ // - If one of OUR paths changed concurrently, a blind retry using
240
+ // content computed before that race would silently clobber the other
241
+ // writer's change — safe only for a caller whose content is a
242
+ // self-contained replacement (writeObservationBaseline, writeSnapshot,
178
243
  // persistBuildManifest), never for a read-modify-write caller whose
179
- // `content` already embeds a stale read (an append). Since this
180
- // function can't tell those apart, it does NOT blind-retry in that
181
- // case — it re-throws immediately so a read-modify-write caller's own
182
- // outer retry (appendConvergeRecord, appendGateResolution,
183
- // appendReleaseRecordLine below) can re-read and recompute `content`
184
- // fresh before trying again, exactly as they already do.
244
+ // content already embeds a stale read (an append). Since this function
245
+ // can't tell those apart, it does NOT blind-retry in that case — it
246
+ // re-throws immediately so a read-modify-write caller's own outer retry
247
+ // (appendConvergeRecord, appendGateResolution, appendReleaseRecordLine
248
+ // below) can re-read and recompute content fresh before trying again.
185
249
  //
186
- // - A read-modify-write caller must pass `expectPriorPathSha`. Without it
250
+ // - A read-modify-write caller must pass `expectPriorSha`. Without it
187
251
  // attempt 1 has no prior sha to compare, so a commit landing between the
188
- // caller's baseline read and this function's first tree read reads as the
189
- // ambient starting state rather than a conflict, and is overwritten from
190
- // the caller's stale content.
252
+ // caller's baseline read and this function's first tree read reads as
253
+ // the ambient starting state rather than a conflict, and is overwritten
254
+ // from the caller's stale content.
255
+ const prior = new Map<string, string | null | undefined>(files.map((f) => [f.path, f.expectPriorSha]));
191
256
  let lastErr: unknown;
192
- let priorPathSha: string | null | undefined = opts?.expectPriorPathSha;
193
257
  for (let attempt = 1; attempt <= WRITE_BLOB_RETRY_ATTEMPTS; attempt++) {
194
- // 2. Read existing tree (if branch exists) to preserve other env/file
195
- // entries. `tip` is the commit sha `entries` was read from, and must stay
258
+ // 2. Read the tip once and every listing from that sha. `tip` must stay
196
259
  // the commit parent and CAS oldValue below. Re-resolving the branch name
197
- // there instead can observe a newer tip than `entries` reflects, which
260
+ // there instead can observe a newer tip than the listings reflect, which
198
261
  // makes the CAS succeed against a tree built from a stale read.
199
- const { tip, entries: existingTree } = await readTree(cwd);
200
- const currentPathSha = existingTree.find((e) => e.env === environment && e.name === filename)?.sha ?? null;
201
-
202
- if (priorPathSha !== undefined && currentPathSha !== priorPathSha) {
203
- // Our own path moved since our previous attempt started — a genuine
204
- // content-level race on the exact file we're writing, not just a CAS
205
- // conflict from a sibling path. Not safe to paper over here.
206
- throw lastErr ?? new RefCASConflictError(`refs/heads/${STATE_BRANCH}`, priorPathSha, "target path changed concurrently");
262
+ const tip = await getStateBranchTip(cwd);
263
+ const trees = new Map<string, Map<string, TreeLine>>();
264
+ const listing = async (dir: string): Promise<Map<string, TreeLine>> => {
265
+ let entries = trees.get(dir);
266
+ if (!entries) {
267
+ entries = tip ? await listTreeAt(tip, dir, cwd) : new Map();
268
+ trees.set(dir, entries);
269
+ }
270
+ return entries;
271
+ };
272
+
273
+ for (const b of blobs) {
274
+ const { dir, name } = splitPath(b.path);
275
+ const current = (await listing(dir)).get(name);
276
+ const currentSha = current?.type === "blob" ? current.sha : null;
277
+ const expected = prior.get(b.path);
278
+ if (expected !== undefined && currentSha !== expected) {
279
+ // Our own path moved since the caller read it or our previous
280
+ // attempt started — a genuine content-level race on the exact file
281
+ // we're writing, not just a CAS conflict from a sibling path. Not
282
+ // safe to paper over here.
283
+ throw lastErr ?? new RefCASConflictError(`refs/heads/${STATE_BRANCH}`, expected, "target path changed concurrently");
284
+ }
285
+ prior.set(b.path, currentSha);
207
286
  }
208
- priorPathSha = currentPathSha;
209
-
210
- // 3. Build new tree entries
211
- const entries = mergeTreeEntry(existingTree, path, blobSha);
212
-
213
- // mktree needs a nested tree structure. Build env subtree first, then root tree.
214
- // Build env subtree
215
- const envEntries = entries
216
- .filter((e) => e.env === environment)
217
- .map((e) => `${e.mode} ${e.type} ${e.sha}\t${e.name}`)
218
- .join("\n");
219
287
 
220
- const envTreeResult = await rt.spawn(["git", "mktree"], { cwd, stdin: `${envEntries}\n` });
221
- if (envTreeResult.exitCode !== 0) {
222
- throw ledgerWriteError("git mktree (env)", path, envTreeResult.stderr);
223
- }
224
- const envTreeSha = envTreeResult.stdout.trim();
225
-
226
- // Build root tree: collect env subtrees
227
- const rootEntries: string[] = [];
228
- const envsSeen = new Set<string>();
229
- for (const e of entries) {
230
- if (!envsSeen.has(e.env)) {
231
- envsSeen.add(e.env);
232
- if (e.env === environment) {
233
- rootEntries.push(`040000 tree ${envTreeSha}\t${environment}`);
234
- } else {
235
- rootEntries.push(`040000 tree ${e.envTreeSha!}\t${e.env}`);
236
- }
237
- }
288
+ // 3. Replace the entries, listing every directory up to the root so the
289
+ // rebuild below has each parent in hand.
290
+ for (const b of blobs) {
291
+ const { dir, name } = splitPath(b.path);
292
+ (await listing(dir)).set(name, { mode: "100644", type: "blob", sha: b.sha });
293
+ for (let d = dir; d !== ""; d = parentDir(d)) await listing(parentDir(d));
238
294
  }
239
295
 
240
- const rootTreeResult = await rt.spawn(["git", "mktree"], {
241
- cwd,
242
- stdin: `${rootEntries.join("\n")}\n`,
243
- });
244
- if (rootTreeResult.exitCode !== 0) {
245
- throw ledgerWriteError("git mktree (root)", path, rootTreeResult.stderr);
296
+ // 4. Rebuild the touched trees, deepest first, then the root.
297
+ const touched = [...trees.keys()].sort((a, b) => depth(b) - depth(a) || (a < b ? -1 : 1));
298
+ let rootTreeSha = "";
299
+ for (const dir of touched) {
300
+ const lines = [...trees.get(dir)!].map(([name, e]) => `${e.mode} ${e.type} ${e.sha}\t${name}`).join("\n");
301
+ const result = await rt.spawn(["git", "mktree"], { cwd, stdin: `${lines}\n` });
302
+ if (result.exitCode !== 0) {
303
+ throw ledgerWriteError(`git mktree (${dir === "" ? "root" : dir})`, path, result.stderr);
304
+ }
305
+ const sha = result.stdout.trim();
306
+ if (dir === "") rootTreeSha = sha;
307
+ else trees.get(parentDir(dir))!.set(baseName(dir), { mode: "040000", type: "tree", sha });
246
308
  }
247
- const rootTreeSha = rootTreeResult.stdout.trim();
248
309
 
249
- // 4. Create commit — parented on `tip`, the exact sha `existingTree` was
250
- // read from (see the comment on step 2), not a fresh re-resolution of
251
- // the branch name.
310
+ // 5. Create the commit — parented on `tip`, the exact sha every listing
311
+ // was read from (see step 2), not a fresh re-resolution of the branch.
252
312
  const parentRef = tip;
253
313
  const parentArgs = parentRef ? ["-p", parentRef] : [];
254
314
  const commitResult = await rt.spawn(
@@ -260,7 +320,7 @@ export async function writeBlobToPath(
260
320
  }
261
321
  const commitSha = commitResult.stdout.trim();
262
322
 
263
- // 5. Update ref — CAS-guarded against `parentRef` (#1485): closes the
323
+ // 6. Update ref — CAS-guarded against `parentRef` (#1485): closes the
264
324
  // local race two concurrent writers used to hit silently (whoever called
265
325
  // update-ref last simply overwrote the other's tree, no error). A
266
326
  // conflict here throws RefCASConflictError instead of clobbering.
@@ -290,8 +350,9 @@ export async function readBlobFromPath(
290
350
  opts?: { cwd?: string },
291
351
  ): Promise<string | null> {
292
352
  const rt = getRuntime();
353
+ const dir = await ledgerDir(environment, opts);
293
354
  const result = await rt.spawn(
294
- ["git", "show", `${STATE_BRANCH}:${environment}/${filename}`],
355
+ ["git", "show", `${STATE_BRANCH}:${dir}/${filename}`],
295
356
  { cwd: opts?.cwd },
296
357
  );
297
358
  if (result.exitCode !== 0) return null;
@@ -311,8 +372,9 @@ export async function readPathSha(
311
372
  opts?: { cwd?: string },
312
373
  ): Promise<string | null> {
313
374
  const rt = getRuntime();
375
+ const dir = await ledgerDir(environment, opts);
314
376
  const result = await rt.spawn(
315
- ["git", "rev-parse", "--verify", `${STATE_BRANCH}:${environment}/${filename}`],
377
+ ["git", "rev-parse", "--verify", `${STATE_BRANCH}:${dir}/${filename}`],
316
378
  { cwd: opts?.cwd },
317
379
  );
318
380
  if (result.exitCode !== 0) return null;
@@ -370,8 +432,9 @@ export async function readSnapshotAt(
370
432
  opts?: { cwd?: string },
371
433
  ): Promise<string | null> {
372
434
  const rt = getRuntime();
435
+ const dir = await ledgerDir(environment, opts);
373
436
  const result = await rt.spawn(
374
- ["git", "show", `${ref}:${environment}/${lexicon}.json`],
437
+ ["git", "show", `${ref}:${dir}/${lexicon}.json`],
375
438
  { cwd: opts?.cwd },
376
439
  );
377
440
  if (result.exitCode !== 0) return null;
@@ -460,7 +523,16 @@ export async function listLedgerEnvironments(opts?: { cwd?: string }): Promise<s
460
523
  const tip = await getStateBranchTip(opts?.cwd);
461
524
  if (!tip) return [];
462
525
  const rt = getRuntime();
463
- const rootResult = await rt.spawn(["git", "ls-tree", STATE_BRANCH], { cwd: opts?.cwd });
526
+ // A member lists the directories under its own `_members/<member>/`; the
527
+ // flat layout lists the root, where `_members` holds no releases.jsonl and
528
+ // so never reads as an environment (#2538).
529
+ const base = await ledgerDir("", opts);
530
+ // `--full-tree`: from a subdirectory, a bare `ls-tree` lists only that
531
+ // subdirectory's part of the tree (#2550).
532
+ const rootResult = await rt.spawn(
533
+ ["git", "ls-tree", "--full-tree", base === "" ? STATE_BRANCH : `${STATE_BRANCH}:${base}`],
534
+ { cwd: opts?.cwd },
535
+ );
464
536
  if (rootResult.exitCode !== 0) return [];
465
537
 
466
538
  const envs: string[] = [];
@@ -488,8 +560,9 @@ export async function listFilesInDir(dir: string, opts?: { cwd?: string }): Prom
488
560
  const tip = await getStateBranchTip(opts?.cwd);
489
561
  if (!tip) return [];
490
562
  const rt = getRuntime();
563
+ const path = await ledgerDir(dir, opts);
491
564
  const lsResult = await rt.spawn(
492
- ["git", "ls-tree", "--name-only", `${STATE_BRANCH}:${dir}/`],
565
+ ["git", "ls-tree", "--full-tree", "--name-only", `${STATE_BRANCH}:${path}/`],
493
566
  { cwd: opts?.cwd },
494
567
  );
495
568
  if (lsResult.exitCode !== 0) return [];
@@ -507,8 +580,9 @@ export async function readEnvironmentSnapshots(
507
580
  const snapshots = new Map<string, string>();
508
581
 
509
582
  // List files in the environment directory
583
+ const dir = await ledgerDir(environment, opts);
510
584
  const lsResult = await rt.spawn(
511
- ["git", "ls-tree", "--name-only", `${STATE_BRANCH}:${environment}/`],
585
+ ["git", "ls-tree", "--full-tree", "--name-only", `${STATE_BRANCH}:${dir}/`],
512
586
  { cwd: opts?.cwd },
513
587
  );
514
588
  if (lsResult.exitCode !== 0) return snapshots;
@@ -532,8 +606,12 @@ export async function listSnapshots(
532
606
  opts?: { cwd?: string; environment?: string },
533
607
  ): Promise<Array<{ commit: string; date: string; message: string }>> {
534
608
  const rt = getRuntime();
609
+ // A member sees the commits that touched its own ledger (#2538); the flat
610
+ // layout keeps today's whole-branch history.
611
+ const base = await ledgerDir("", opts);
535
612
  const result = await rt.spawn(
536
- ["git", "log", "--format=%H %aI %s", STATE_BRANCH],
613
+ // `:(top)`: a pathspec is otherwise relative to the directory git runs in.
614
+ ["git", "log", "--format=%H %aI %s", STATE_BRANCH, ...(base === "" ? [] : ["--", `:(top)${base}`])],
537
615
  { cwd: opts?.cwd },
538
616
  );
539
617
  if (result.exitCode !== 0) return [];
@@ -1106,13 +1184,11 @@ export async function getHeadCommit(opts?: { cwd?: string }): Promise<string> {
1106
1184
 
1107
1185
  // ── Internal helpers ────────────────────────────────────────────
1108
1186
 
1109
- interface TreeEntry {
1187
+ /** One entry of a tree listing, keyed by its name in the listing's map. */
1188
+ interface TreeLine {
1110
1189
  mode: string;
1111
1190
  type: string;
1112
1191
  sha: string;
1113
- name: string;
1114
- env: string;
1115
- envTreeSha?: string;
1116
1192
  }
1117
1193
 
1118
1194
  async function getStateBranchTip(cwd?: string): Promise<string | null> {
@@ -1126,76 +1202,45 @@ async function getStateBranchTip(cwd?: string): Promise<string | null> {
1126
1202
  }
1127
1203
 
1128
1204
  /**
1129
- * Read the orphan branch's tip and every env/file tree entry under it as one
1130
- * consistent snapshot. Every listing must stay pinned to the resolved `tip`
1131
- * sha, never to `STATE_BRANCH`: the read spans several `ls-tree` calls (root
1132
- * plus one per env subtree) and a branch name re-resolves on each, so a
1133
- * concurrent commit splices entries from two commits into one array
1134
- * undetectably. Callers need the returned `tip` as their commit parent.
1205
+ * The entries of directory `dir` (branch-relative, `""` for the root) in
1206
+ * commit `tip`, or an empty map when it does not exist. Always pinned to the
1207
+ * resolved `tip` sha, never to `STATE_BRANCH`: a write reads several
1208
+ * directories, and a branch name re-resolves on each read, so a concurrent
1209
+ * commit would splice entries from two commits into one tree undetectably.
1210
+ *
1211
+ * `--full-tree` (#2550): run from a project in a subdirectory of the
1212
+ * repository, a bare `ls-tree` lists only the part of the tree under that
1213
+ * subdirectory, which on the orphan branch is nothing. The write then
1214
+ * rebuilt the branch from an empty tree, and an append's CAS refused it as a
1215
+ * concurrent change.
1135
1216
  */
1136
- async function readTree(cwd?: string): Promise<{ tip: string | null; entries: TreeEntry[] }> {
1217
+ async function listTreeAt(tip: string, dir: string, cwd?: string): Promise<Map<string, TreeLine>> {
1137
1218
  const rt = getRuntime();
1138
- const tip = await getStateBranchTip(cwd);
1139
- if (!tip) return { tip: null, entries: [] };
1140
-
1141
- // List root tree to get env directories — pinned to `tip`, not `STATE_BRANCH`.
1142
- const rootResult = await rt.spawn(
1143
- ["git", "ls-tree", tip],
1144
- { cwd },
1145
- );
1146
- if (rootResult.exitCode !== 0) return { tip, entries: [] };
1147
-
1148
- const entries: TreeEntry[] = [];
1149
- const lines = rootResult.stdout.trim().split("\n").filter(Boolean);
1150
-
1151
- for (const line of lines) {
1219
+ const entries = new Map<string, TreeLine>();
1220
+ const result = await rt.spawn(["git", "ls-tree", "--full-tree", dir === "" ? tip : `${tip}:${dir}/`], { cwd });
1221
+ if (result.exitCode !== 0) return entries;
1222
+ for (const line of result.stdout.split("\n")) {
1152
1223
  // Format: mode type sha\tname
1153
1224
  const match = line.match(/^(\d+)\s+(\w+)\s+([0-9a-f]+)\t(.+)$/);
1154
- if (!match) continue;
1155
- const [, mode, type, sha, name] = match;
1156
-
1157
- if (type === "tree") {
1158
- // This is an env directory — list its contents, still pinned to `tip`.
1159
- const envResult = await rt.spawn(
1160
- ["git", "ls-tree", `${tip}:${name}/`],
1161
- { cwd },
1162
- );
1163
- if (envResult.exitCode !== 0) continue;
1164
-
1165
- const envLines = envResult.stdout.trim().split("\n").filter(Boolean);
1166
- for (const envLine of envLines) {
1167
- const envMatch = envLine.match(/^(\d+)\s+(\w+)\s+([0-9a-f]+)\t(.+)$/);
1168
- if (!envMatch) continue;
1169
- entries.push({
1170
- mode: envMatch[1],
1171
- type: envMatch[2],
1172
- sha: envMatch[3],
1173
- name: envMatch[4],
1174
- env: name,
1175
- envTreeSha: sha,
1176
- });
1177
- }
1178
- }
1225
+ if (match) entries.set(match[4], { mode: match[1], type: match[2], sha: match[3] });
1179
1226
  }
1227
+ return entries;
1228
+ }
1180
1229
 
1181
- return { tip, entries };
1230
+ function splitPath(path: string): { dir: string; name: string } {
1231
+ const slash = path.lastIndexOf("/");
1232
+ return { dir: path.slice(0, slash), name: path.slice(slash + 1) };
1182
1233
  }
1183
1234
 
1184
- function mergeTreeEntry(
1185
- existing: TreeEntry[],
1186
- path: string,
1187
- blobSha: string,
1188
- ): TreeEntry[] {
1189
- const [env, filename] = path.split("/");
1190
- const entries = existing.filter(
1191
- (e) => !(e.env === env && e.name === filename),
1192
- );
1193
- entries.push({
1194
- mode: "100644",
1195
- type: "blob",
1196
- sha: blobSha,
1197
- name: filename,
1198
- env,
1199
- });
1200
- return entries;
1235
+ function parentDir(dir: string): string {
1236
+ const slash = dir.lastIndexOf("/");
1237
+ return slash < 0 ? "" : dir.slice(0, slash);
1238
+ }
1239
+
1240
+ function baseName(dir: string): string {
1241
+ return dir.slice(dir.lastIndexOf("/") + 1);
1242
+ }
1243
+
1244
+ function depth(dir: string): number {
1245
+ return dir === "" ? 0 : dir.split("/").length;
1201
1246
  }
@@ -34,6 +34,7 @@
34
34
  import { hostname } from "node:os";
35
35
  import { randomUUID } from "node:crypto";
36
36
  import { readRefSha, updateRefCAS, deleteRefCAS, writeBlob, readBlobBySha, pushRef, fetchRefInto, RefCASConflictError } from "./git";
37
+ import { resolveMemberLedger } from "./member-ledger";
37
38
 
38
39
  export const LEASE_REF_PREFIX = "refs/chant/lease/";
39
40
 
@@ -59,12 +60,25 @@ export const LEASE_REMOTE_TRACKING_PREFIX = "refs/chant/lease-remote/";
59
60
  */
60
61
  export const DEFAULT_LEASE_TTL_MS = 5 * 60_000;
61
62
 
62
- export function leaseRef(opName: string): string {
63
- return `${LEASE_REF_PREFIX}${opName}`;
63
+ /**
64
+ * The lease ref for `opName`. `memberPrefix` is the project's ledger prefix
65
+ * from ./member-ledger.ts (#2538): empty at level 0 and for the root member
66
+ * `.`, which keeps `refs/chant/lease/<op>`, and `_members/<member>/` for any
67
+ * other workspace member, which gives `refs/chant/lease/_members/<member>/<op>`.
68
+ * Two members may then run Ops of the same name without sharing a lease.
69
+ */
70
+ export function leaseRef(opName: string, memberPrefix = ""): string {
71
+ return `${LEASE_REF_PREFIX}${memberPrefix}${opName}`;
72
+ }
73
+
74
+ function leaseRemoteTrackingRef(opName: string, memberPrefix = ""): string {
75
+ return `${LEASE_REMOTE_TRACKING_PREFIX}${memberPrefix}${opName}`;
64
76
  }
65
77
 
66
- function leaseRemoteTrackingRef(opName: string): string {
67
- return `${LEASE_REMOTE_TRACKING_PREFIX}${opName}`;
78
+ /** The lease refs of `opName` for the project at `opts.cwd` (or the process's directory). */
79
+ async function projectLeaseRefs(opName: string, opts?: { cwd?: string }): Promise<{ ref: string; trackingRef: string }> {
80
+ const { prefix } = await resolveMemberLedger(opts?.cwd ?? process.cwd());
81
+ return { ref: leaseRef(opName, prefix), trackingRef: leaseRemoteTrackingRef(opName, prefix) };
68
82
  }
69
83
 
70
84
  /**
@@ -147,8 +161,7 @@ export interface ReadLeaseResult {
147
161
  * call against it, regardless of what the comparison decided about `record`.
148
162
  */
149
163
  export async function readLease(opName: string, opts?: { cwd?: string }): Promise<ReadLeaseResult> {
150
- const ref = leaseRef(opName);
151
- const trackingRef = leaseRemoteTrackingRef(opName);
164
+ const { ref, trackingRef } = await projectLeaseRefs(opName, opts);
152
165
  await fetchRefInto(ref, trackingRef, opts).catch(() => undefined);
153
166
 
154
167
  const sha = await readRefSha(ref, opts);
@@ -212,9 +225,10 @@ export async function acquireLease(
212
225
  expiresAt: new Date(now.getTime() + ttlMs).toISOString(),
213
226
  };
214
227
 
228
+ const { ref } = await projectLeaseRefs(opName, opts);
215
229
  const blobSha = await writeBlob(JSON.stringify(record), opts);
216
230
  try {
217
- await updateRefCAS(leaseRef(opName), blobSha, sha, opts);
231
+ await updateRefCAS(ref, blobSha, sha, opts);
218
232
  } catch (err) {
219
233
  if (err instanceof RefCASConflictError) {
220
234
  const retry = await readLease(opName, opts);
@@ -225,7 +239,7 @@ export async function acquireLease(
225
239
  // it into `heldBy`, per this function's doc.
226
240
  throw err;
227
241
  }
228
- await pushRef(leaseRef(opName), opts).catch(() => undefined);
242
+ await pushRef(ref, opts).catch(() => undefined);
229
243
  return { acquired: true, lease: record };
230
244
  }
231
245
 
@@ -244,12 +258,13 @@ export async function releaseLease(
244
258
  ): Promise<boolean> {
245
259
  const { sha, record } = await readLease(opName, opts);
246
260
  if (!sha || !record || record.holder !== holder || record.token !== token) return false;
261
+ const { ref } = await projectLeaseRefs(opName, opts);
247
262
  try {
248
- await deleteRefCAS(leaseRef(opName), sha, opts);
263
+ await deleteRefCAS(ref, sha, opts);
249
264
  } catch {
250
265
  return false;
251
266
  }
252
- await pushRef(leaseRef(opName), opts).catch(() => undefined);
267
+ await pushRef(ref, opts).catch(() => undefined);
253
268
  return true;
254
269
  }
255
270