@intentius/chant 0.80.0 → 0.81.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (414) hide show
  1. package/dist/audit/discover.d.ts +8 -16
  2. package/dist/audit/discover.d.ts.map +1 -1
  3. package/dist/audit/report-html.d.ts +5 -0
  4. package/dist/audit/report-html.d.ts.map +1 -1
  5. package/dist/audit/report-model.d.ts +13 -0
  6. package/dist/audit/report-model.d.ts.map +1 -1
  7. package/dist/audit/terraform-state.d.ts +4 -5
  8. package/dist/audit/terraform-state.d.ts.map +1 -1
  9. package/dist/cli/commands/audit.d.ts +25 -9
  10. package/dist/cli/commands/audit.d.ts.map +1 -1
  11. package/dist/cli/commands/check-lexicon.d.ts +8 -0
  12. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  13. package/dist/cli/commands/doctor.d.ts.map +1 -1
  14. package/dist/cli/commands/import-agents.d.ts.map +1 -1
  15. package/dist/cli/commands/init.d.ts.map +1 -1
  16. package/dist/cli/commands/lint.d.ts.map +1 -1
  17. package/dist/cli/commands/update.d.ts +6 -0
  18. package/dist/cli/commands/update.d.ts.map +1 -1
  19. package/dist/cli/commands/vendor.d.ts +5 -0
  20. package/dist/cli/commands/vendor.d.ts.map +1 -1
  21. package/dist/cli/handlers/build.d.ts.map +1 -1
  22. package/dist/cli/handlers/components.d.ts.map +1 -1
  23. package/dist/cli/handlers/dev.d.ts.map +1 -1
  24. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  25. package/dist/cli/handlers/init.d.ts.map +1 -1
  26. package/dist/cli/handlers/operator.d.ts +7 -0
  27. package/dist/cli/handlers/operator.d.ts.map +1 -1
  28. package/dist/cli/handlers/promote.d.ts +7 -4
  29. package/dist/cli/handlers/promote.d.ts.map +1 -1
  30. package/dist/cli/handlers/run-generate.d.ts.map +1 -1
  31. package/dist/cli/handlers/run.d.ts +4 -1
  32. package/dist/cli/handlers/run.d.ts.map +1 -1
  33. package/dist/cli/handlers/vendor.d.ts +8 -3
  34. package/dist/cli/handlers/vendor.d.ts.map +1 -1
  35. package/dist/cli/main.d.ts +12 -0
  36. package/dist/cli/main.d.ts.map +1 -1
  37. package/dist/cli/plugins.d.ts +12 -7
  38. package/dist/cli/plugins.d.ts.map +1 -1
  39. package/dist/cli/registry.d.ts +32 -0
  40. package/dist/cli/registry.d.ts.map +1 -1
  41. package/dist/codegen/fetch.d.ts +10 -0
  42. package/dist/codegen/fetch.d.ts.map +1 -1
  43. package/dist/components/cli-support.d.ts +3 -1
  44. package/dist/components/cli-support.d.ts.map +1 -1
  45. package/dist/components/discover.d.ts.map +1 -1
  46. package/dist/components/driver.d.ts +16 -2
  47. package/dist/components/driver.d.ts.map +1 -1
  48. package/dist/components/gate-plan.d.ts +40 -0
  49. package/dist/components/gate-plan.d.ts.map +1 -0
  50. package/dist/components/promote.d.ts +60 -7
  51. package/dist/components/promote.d.ts.map +1 -1
  52. package/dist/components/sandbox/driver.d.ts.map +1 -1
  53. package/dist/components/sandbox/run.d.ts.map +1 -1
  54. package/dist/components/verbs/build-archive.d.ts +22 -11
  55. package/dist/components/verbs/build-archive.d.ts.map +1 -1
  56. package/dist/components/verbs/run-agent.d.ts.map +1 -1
  57. package/dist/config-static.d.ts +59 -0
  58. package/dist/config-static.d.ts.map +1 -0
  59. package/dist/config.d.ts +32 -10
  60. package/dist/config.d.ts.map +1 -1
  61. package/dist/content-digest.d.ts +17 -0
  62. package/dist/content-digest.d.ts.map +1 -0
  63. package/dist/discovery/files.d.ts +10 -5
  64. package/dist/discovery/files.d.ts.map +1 -1
  65. package/dist/discovery/fold-import.d.ts +20 -4
  66. package/dist/discovery/fold-import.d.ts.map +1 -1
  67. package/dist/discovery/index.d.ts.map +1 -1
  68. package/dist/discovery/sandbox/driver.d.ts +20 -0
  69. package/dist/discovery/sandbox/driver.d.ts.map +1 -1
  70. package/dist/discovery/sandbox/fork.d.ts +20 -0
  71. package/dist/discovery/sandbox/fork.d.ts.map +1 -1
  72. package/dist/discovery/walk.d.ts +119 -0
  73. package/dist/discovery/walk.d.ts.map +1 -0
  74. package/dist/graph-ir.d.ts +4 -1
  75. package/dist/graph-ir.d.ts.map +1 -1
  76. package/dist/join-key.d.ts +31 -0
  77. package/dist/join-key.d.ts.map +1 -0
  78. package/dist/lexicon-module.d.ts +51 -6
  79. package/dist/lexicon-module.d.ts.map +1 -1
  80. package/dist/lexicon.d.ts +58 -1
  81. package/dist/lexicon.d.ts.map +1 -1
  82. package/dist/lifecycle/build-ledger-store.d.ts.map +1 -1
  83. package/dist/lifecycle/digest.d.ts +5 -1
  84. package/dist/lifecycle/digest.d.ts.map +1 -1
  85. package/dist/lifecycle/gate-ledger.d.ts +20 -5
  86. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  87. package/dist/lifecycle/git.d.ts +64 -0
  88. package/dist/lifecycle/git.d.ts.map +1 -1
  89. package/dist/lifecycle/lease.d.ts +8 -1
  90. package/dist/lifecycle/lease.d.ts.map +1 -1
  91. package/dist/lifecycle/legacy-digest.d.ts +41 -8
  92. package/dist/lifecycle/legacy-digest.d.ts.map +1 -1
  93. package/dist/lifecycle/member-ledger.d.ts +67 -0
  94. package/dist/lifecycle/member-ledger.d.ts.map +1 -0
  95. package/dist/lifecycle/plan-digest.d.ts +4 -3
  96. package/dist/lifecycle/plan-digest.d.ts.map +1 -1
  97. package/dist/lifecycle/release-ledger.d.ts +18 -2
  98. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  99. package/dist/lifecycle/status.d.ts +90 -5
  100. package/dist/lifecycle/status.d.ts.map +1 -1
  101. package/dist/op/activities/activity-contracts.d.ts +32 -0
  102. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  103. package/dist/op/activities/index.d.ts +2 -0
  104. package/dist/op/activities/index.d.ts.map +1 -1
  105. package/dist/op/activities/propose-upgrade.d.ts +75 -0
  106. package/dist/op/activities/propose-upgrade.d.ts.map +1 -0
  107. package/dist/op/discover.d.ts.map +1 -1
  108. package/dist/op/gate-name.d.ts +7 -0
  109. package/dist/op/gate-name.d.ts.map +1 -1
  110. package/dist/op/gate-summary.d.ts +16 -0
  111. package/dist/op/gate-summary.d.ts.map +1 -1
  112. package/dist/op/gate.d.ts +16 -2
  113. package/dist/op/gate.d.ts.map +1 -1
  114. package/dist/project-root.d.ts +26 -0
  115. package/dist/project-root.d.ts.map +1 -1
  116. package/dist/workspace/__fixtures__/contract-repo.d.ts +37 -0
  117. package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -0
  118. package/dist/workspace/__fixtures__/two-member-workspace.d.ts +11 -0
  119. package/dist/workspace/__fixtures__/two-member-workspace.d.ts.map +1 -0
  120. package/dist/workspace/checks/generated.d.ts +156 -0
  121. package/dist/workspace/checks/generated.d.ts.map +1 -0
  122. package/dist/workspace/checks/ledgers.d.ts +78 -0
  123. package/dist/workspace/checks/ledgers.d.ts.map +1 -0
  124. package/dist/workspace/checks/links.d.ts +25 -0
  125. package/dist/workspace/checks/links.d.ts.map +1 -0
  126. package/dist/workspace/checks/pipelines.d.ts +76 -0
  127. package/dist/workspace/checks/pipelines.d.ts.map +1 -0
  128. package/dist/workspace/checks.d.ts +161 -0
  129. package/dist/workspace/checks.d.ts.map +1 -0
  130. package/dist/workspace/compose-graph.d.ts +115 -0
  131. package/dist/workspace/compose-graph.d.ts.map +1 -0
  132. package/dist/workspace/compose-reports.d.ts +68 -0
  133. package/dist/workspace/compose-reports.d.ts.map +1 -0
  134. package/dist/workspace/declaration.d.ts +220 -0
  135. package/dist/workspace/declaration.d.ts.map +1 -0
  136. package/dist/workspace/declaration.schema.json +434 -0
  137. package/dist/workspace/generated-files.d.ts +65 -0
  138. package/dist/workspace/generated-files.d.ts.map +1 -0
  139. package/dist/workspace/glob.d.ts +13 -0
  140. package/dist/workspace/glob.d.ts.map +1 -0
  141. package/dist/workspace/graph-cli.d.ts +75 -0
  142. package/dist/workspace/graph-cli.d.ts.map +1 -0
  143. package/dist/workspace/init.d.ts +96 -0
  144. package/dist/workspace/init.d.ts.map +1 -0
  145. package/dist/workspace/jsonc.d.ts +38 -0
  146. package/dist/workspace/jsonc.d.ts.map +1 -0
  147. package/dist/workspace/kinds.d.ts +192 -0
  148. package/dist/workspace/kinds.d.ts.map +1 -0
  149. package/dist/workspace/lineage-check.d.ts +84 -0
  150. package/dist/workspace/lineage-check.d.ts.map +1 -0
  151. package/dist/workspace/lineage-cli.d.ts +31 -0
  152. package/dist/workspace/lineage-cli.d.ts.map +1 -0
  153. package/dist/workspace/lineage-init.d.ts +101 -0
  154. package/dist/workspace/lineage-init.d.ts.map +1 -0
  155. package/dist/workspace/lineage-lock.d.ts +258 -0
  156. package/dist/workspace/lineage-lock.d.ts.map +1 -0
  157. package/dist/workspace/lineage-merge.d.ts +14 -0
  158. package/dist/workspace/lineage-merge.d.ts.map +1 -0
  159. package/dist/workspace/lineage-migrations.d.ts +178 -0
  160. package/dist/workspace/lineage-migrations.d.ts.map +1 -0
  161. package/dist/workspace/lineage-update.d.ts +59 -0
  162. package/dist/workspace/lineage-update.d.ts.map +1 -0
  163. package/dist/workspace/lineage-upgrade-cli.d.ts +40 -0
  164. package/dist/workspace/lineage-upgrade-cli.d.ts.map +1 -0
  165. package/dist/workspace/lineage-upgrade.d.ts +146 -0
  166. package/dist/workspace/lineage-upgrade.d.ts.map +1 -0
  167. package/dist/workspace/lineage-vendor.d.ts +63 -0
  168. package/dist/workspace/lineage-vendor.d.ts.map +1 -0
  169. package/dist/workspace/lineage-version.d.ts +32 -0
  170. package/dist/workspace/lineage-version.d.ts.map +1 -0
  171. package/dist/workspace/links.d.ts +150 -0
  172. package/dist/workspace/links.d.ts.map +1 -0
  173. package/dist/workspace/ls.d.ts +99 -0
  174. package/dist/workspace/ls.d.ts.map +1 -0
  175. package/dist/workspace/member-commands.d.ts +139 -0
  176. package/dist/workspace/member-commands.d.ts.map +1 -0
  177. package/dist/workspace/member-handles.d.ts +54 -0
  178. package/dist/workspace/member-handles.d.ts.map +1 -0
  179. package/dist/workspace/member-pipeline.d.ts +143 -0
  180. package/dist/workspace/member-pipeline.d.ts.map +1 -0
  181. package/dist/workspace/member-run.d.ts +67 -0
  182. package/dist/workspace/member-run.d.ts.map +1 -0
  183. package/dist/workspace/reason-codes.d.ts +65 -0
  184. package/dist/workspace/reason-codes.d.ts.map +1 -0
  185. package/dist/workspace/records-cli.d.ts +28 -4
  186. package/dist/workspace/records-cli.d.ts.map +1 -1
  187. package/dist/workspace/records.d.ts.map +1 -1
  188. package/dist/workspace/root-refusal.d.ts +53 -0
  189. package/dist/workspace/root-refusal.d.ts.map +1 -0
  190. package/dist/workspace/status.d.ts +150 -0
  191. package/dist/workspace/status.d.ts.map +1 -0
  192. package/dist/workspace/template-manifest.d.ts +90 -0
  193. package/dist/workspace/template-manifest.d.ts.map +1 -0
  194. package/dist/workspace/tree.d.ts +36 -0
  195. package/dist/workspace/tree.d.ts.map +1 -0
  196. package/dist/workspace/trust/attestor.d.ts +67 -0
  197. package/dist/workspace/trust/attestor.d.ts.map +1 -0
  198. package/dist/workspace/trust/policy.d.ts +111 -0
  199. package/dist/workspace/trust/policy.d.ts.map +1 -0
  200. package/dist/workspace/trust/provenance.d.ts +67 -0
  201. package/dist/workspace/trust/provenance.d.ts.map +1 -0
  202. package/dist/workspace/trust/ssh-commit.d.ts +43 -0
  203. package/dist/workspace/trust/ssh-commit.d.ts.map +1 -0
  204. package/dist/workspace/trust/test-repo.d.ts +42 -0
  205. package/dist/workspace/trust/test-repo.d.ts.map +1 -0
  206. package/dist/workspace/trust/verify-cli.d.ts +9 -0
  207. package/dist/workspace/trust/verify-cli.d.ts.map +1 -0
  208. package/dist/workspace/trust/verify.d.ts +66 -0
  209. package/dist/workspace/trust/verify.d.ts.map +1 -0
  210. package/dist/workspace/which-chant.d.ts +58 -0
  211. package/dist/workspace/which-chant.d.ts.map +1 -0
  212. package/dist/workspace/workspace-kinds.schema.json +133 -0
  213. package/package.json +2 -1
  214. package/src/audit/discover.ts +66 -78
  215. package/src/audit/report-html.ts +3 -1
  216. package/src/audit/report-model.ts +14 -1
  217. package/src/audit/terraform-state.test.ts +4 -9
  218. package/src/audit/terraform-state.ts +4 -5
  219. package/src/cli/commands/audit-truncation.test.ts +167 -0
  220. package/src/cli/commands/audit.ts +89 -60
  221. package/src/cli/commands/check-lexicon.ts +24 -0
  222. package/src/cli/commands/doctor.test.ts +30 -0
  223. package/src/cli/commands/doctor.ts +18 -0
  224. package/src/cli/commands/import-agents.ts +6 -2
  225. package/src/cli/commands/init.ts +22 -3
  226. package/src/cli/commands/lint.test.ts +17 -23
  227. package/src/cli/commands/lint.ts +24 -84
  228. package/src/cli/commands/update.ts +13 -4
  229. package/src/cli/commands/vendor.ts +11 -6
  230. package/src/cli/handlers/__snapshots__/build-member.test.ts.snap +38 -0
  231. package/src/cli/handlers/build-member.test.ts +127 -0
  232. package/src/cli/handlers/build.ts +35 -1
  233. package/src/cli/handlers/components.test.ts +127 -0
  234. package/src/cli/handlers/components.ts +70 -8
  235. package/src/cli/handlers/dev.ts +5 -3
  236. package/src/cli/handlers/fan-out.ts +9 -2
  237. package/src/cli/handlers/init.ts +54 -1
  238. package/src/cli/handlers/operator.test.ts +68 -0
  239. package/src/cli/handlers/operator.ts +37 -9
  240. package/src/cli/handlers/promote.test.ts +118 -2
  241. package/src/cli/handlers/promote.ts +82 -16
  242. package/src/cli/handlers/run-generate-member.test.ts +83 -0
  243. package/src/cli/handlers/run-generate.test.ts +6 -8
  244. package/src/cli/handlers/run-generate.ts +36 -1
  245. package/src/cli/handlers/run.test.ts +47 -0
  246. package/src/cli/handlers/run.ts +28 -5
  247. package/src/cli/handlers/vendor.ts +147 -43
  248. package/src/cli/main.test.ts +105 -1
  249. package/src/cli/main.ts +230 -17
  250. package/src/cli/plugins.ts +17 -17
  251. package/src/cli/registry.ts +32 -0
  252. package/src/cli/static-config-read.test.ts +208 -0
  253. package/src/codegen/fetch.test.ts +63 -1
  254. package/src/codegen/fetch.ts +30 -1
  255. package/src/components/cli-support.ts +6 -1
  256. package/src/components/discover.test.ts +27 -57
  257. package/src/components/discover.ts +13 -68
  258. package/src/components/driver.test.ts +78 -1
  259. package/src/components/driver.ts +43 -7
  260. package/src/components/gate-plan.ts +53 -0
  261. package/src/components/promote.test.ts +276 -2
  262. package/src/components/promote.ts +119 -15
  263. package/src/components/sandbox/driver.ts +8 -8
  264. package/src/components/sandbox/run.ts +4 -1
  265. package/src/components/verbs/build-archive.ts +27 -21
  266. package/src/components/verbs/run-agent.ts +3 -5
  267. package/src/config-static.test.ts +136 -0
  268. package/src/config-static.ts +232 -0
  269. package/src/config.test.ts +2 -2
  270. package/src/config.ts +48 -15
  271. package/src/content-digest.test.ts +120 -0
  272. package/src/content-digest.ts +21 -0
  273. package/src/discovery/files.test.ts +10 -10
  274. package/src/discovery/files.ts +25 -81
  275. package/src/discovery/fold-import.ts +49 -5
  276. package/src/discovery/index.ts +1 -0
  277. package/src/discovery/sandbox/driver.ts +73 -35
  278. package/src/discovery/sandbox/fork-diagnostic.test.ts +136 -13
  279. package/src/discovery/sandbox/fork.ts +83 -43
  280. package/src/discovery/sandbox/path-lexicon-multifile.test.ts +285 -0
  281. package/src/discovery/walk.test.ts +263 -0
  282. package/src/discovery/walk.ts +334 -0
  283. package/src/effect-receipt.ts +2 -2
  284. package/src/graph-ir.ts +4 -1
  285. package/src/join-key.test.ts +25 -0
  286. package/src/join-key.ts +39 -0
  287. package/src/lexicon-module.ts +108 -10
  288. package/src/lexicon.ts +79 -1
  289. package/src/lifecycle/build-ledger-store.ts +11 -10
  290. package/src/lifecycle/digest.ts +9 -5
  291. package/src/lifecycle/gate-ledger.test.ts +20 -0
  292. package/src/lifecycle/gate-ledger.ts +28 -6
  293. package/src/lifecycle/git.test.ts +13 -1
  294. package/src/lifecycle/git.ts +221 -176
  295. package/src/lifecycle/lease.ts +25 -10
  296. package/src/lifecycle/legacy-digest.test.ts +115 -23
  297. package/src/lifecycle/legacy-digest.ts +60 -19
  298. package/src/lifecycle/member-ledger.test.ts +319 -0
  299. package/src/lifecycle/member-ledger.ts +145 -0
  300. package/src/lifecycle/plan-digest.ts +7 -7
  301. package/src/lifecycle/release-ledger.test.ts +20 -0
  302. package/src/lifecycle/release-ledger.ts +23 -7
  303. package/src/lifecycle/status.test.ts +151 -1
  304. package/src/lifecycle/status.ts +228 -7
  305. package/src/op/activities/activity-contracts.ts +37 -0
  306. package/src/op/activities/index.ts +10 -0
  307. package/src/op/activities/propose-upgrade.ts +206 -0
  308. package/src/op/discover.test.ts +14 -4
  309. package/src/op/discover.ts +19 -27
  310. package/src/op/gate-name.ts +8 -0
  311. package/src/op/gate-summary.test.ts +14 -0
  312. package/src/op/gate-summary.ts +25 -2
  313. package/src/op/gate.ts +36 -7
  314. package/src/op/generate-pipeline.test.ts +14 -5
  315. package/src/project-root.test.ts +66 -1
  316. package/src/project-root.ts +48 -0
  317. package/src/workspace/__fixtures__/contract-repo.ts +100 -0
  318. package/src/workspace/__fixtures__/two-member-workspace.ts +35 -0
  319. package/src/workspace/behold-kinds.test.ts +162 -0
  320. package/src/workspace/check-contract.test.ts +127 -0
  321. package/src/workspace/check.schema.json +347 -0
  322. package/src/workspace/checks/generated.test.ts +287 -0
  323. package/src/workspace/checks/generated.ts +475 -0
  324. package/src/workspace/checks/ledgers.test.ts +139 -0
  325. package/src/workspace/checks/ledgers.ts +246 -0
  326. package/src/workspace/checks/links.ts +183 -0
  327. package/src/workspace/checks/pipelines.test.ts +133 -0
  328. package/src/workspace/checks/pipelines.ts +225 -0
  329. package/src/workspace/checks-repo.test.ts +71 -0
  330. package/src/workspace/checks.test.ts +450 -0
  331. package/src/workspace/checks.ts +461 -0
  332. package/src/workspace/compose-graph.test.ts +146 -0
  333. package/src/workspace/compose-graph.ts +228 -0
  334. package/src/workspace/compose-reports.test.ts +109 -0
  335. package/src/workspace/compose-reports.ts +186 -0
  336. package/src/workspace/declaration.schema.json +434 -0
  337. package/src/workspace/declaration.test.ts +347 -0
  338. package/src/workspace/declaration.ts +623 -0
  339. package/src/workspace/generated-files.test.ts +114 -0
  340. package/src/workspace/generated-files.ts +94 -0
  341. package/src/workspace/glob.ts +49 -0
  342. package/src/workspace/graph-cli.ts +220 -0
  343. package/src/workspace/graph-contract.test.ts +204 -0
  344. package/src/workspace/graph.schema.json +662 -0
  345. package/src/workspace/init.test.ts +223 -0
  346. package/src/workspace/init.ts +585 -0
  347. package/src/workspace/jsonc.test.ts +65 -0
  348. package/src/workspace/jsonc.ts +231 -0
  349. package/src/workspace/kinds.test.ts +288 -0
  350. package/src/workspace/kinds.ts +506 -0
  351. package/src/workspace/lineage-check.ts +264 -0
  352. package/src/workspace/lineage-cli.ts +114 -0
  353. package/src/workspace/lineage-init.test.ts +231 -0
  354. package/src/workspace/lineage-init.ts +365 -0
  355. package/src/workspace/lineage-lock.test.ts +224 -0
  356. package/src/workspace/lineage-lock.ts +357 -0
  357. package/src/workspace/lineage-merge.ts +44 -0
  358. package/src/workspace/lineage-migrations.test.ts +178 -0
  359. package/src/workspace/lineage-migrations.ts +343 -0
  360. package/src/workspace/lineage-update.ts +160 -0
  361. package/src/workspace/lineage-upgrade-cli.ts +151 -0
  362. package/src/workspace/lineage-upgrade.test.ts +474 -0
  363. package/src/workspace/lineage-upgrade.ts +624 -0
  364. package/src/workspace/lineage-vendor.test.ts +126 -0
  365. package/src/workspace/lineage-vendor.ts +215 -0
  366. package/src/workspace/lineage-version.ts +127 -0
  367. package/src/workspace/links.test.ts +344 -0
  368. package/src/workspace/links.ts +331 -0
  369. package/src/workspace/ls-contract.test.ts +200 -0
  370. package/src/workspace/ls.schema.json +190 -0
  371. package/src/workspace/ls.ts +254 -0
  372. package/src/workspace/member-commands.e2e.test.ts +244 -0
  373. package/src/workspace/member-commands.test.ts +217 -0
  374. package/src/workspace/member-commands.ts +540 -0
  375. package/src/workspace/member-handles.test.ts +76 -0
  376. package/src/workspace/member-handles.ts +228 -0
  377. package/src/workspace/member-pipeline.test.ts +135 -0
  378. package/src/workspace/member-pipeline.ts +317 -0
  379. package/src/workspace/member-run.ts +146 -0
  380. package/src/workspace/read-contract.test.ts +129 -0
  381. package/src/workspace/reason-codes.test.ts +113 -0
  382. package/src/workspace/reason-codes.ts +81 -0
  383. package/src/workspace/records-cli.ts +86 -14
  384. package/src/workspace/records-contract.test.ts +5 -0
  385. package/src/workspace/records.schema.json +32 -2
  386. package/src/workspace/records.ts +3 -2
  387. package/src/workspace/root-refusal.ts +97 -0
  388. package/src/workspace/status-contract.test.ts +308 -0
  389. package/src/workspace/status.schema.json +210 -0
  390. package/src/workspace/status.ts +412 -0
  391. package/src/workspace/template-manifest.test.ts +79 -0
  392. package/src/workspace/template-manifest.ts +221 -0
  393. package/src/workspace/tree.ts +132 -0
  394. package/src/workspace/trust/attestor.ts +93 -0
  395. package/src/workspace/trust/policy.ts +306 -0
  396. package/src/workspace/trust/provenance.ts +181 -0
  397. package/src/workspace/trust/ssh-commit.ts +138 -0
  398. package/src/workspace/trust/test-repo.ts +138 -0
  399. package/src/workspace/trust/trust.test.ts +397 -0
  400. package/src/workspace/trust/verify-cli.ts +56 -0
  401. package/src/workspace/trust/verify.ts +194 -0
  402. package/src/workspace/which-chant.test.ts +117 -0
  403. package/src/workspace/which-chant.ts +151 -0
  404. package/src/workspace/workspace-kinds.schema.json +133 -0
  405. package/dist/components/unbound-gate-approval.d.ts +0 -24
  406. package/dist/components/unbound-gate-approval.d.ts.map +0 -1
  407. package/dist/discovery/convergence.d.ts +0 -97
  408. package/dist/discovery/convergence.d.ts.map +0 -1
  409. package/src/cli/commands/audit-walk-warnings.test.ts +0 -156
  410. package/src/components/unbound-gate-approval.test.ts +0 -82
  411. package/src/components/unbound-gate-approval.ts +0 -51
  412. package/src/discovery/convergence.test.ts +0 -231
  413. package/src/discovery/convergence.ts +0 -399
  414. package/src/op/discover-convergence.test.ts +0 -70
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The one join key for names matched across projects (#2524 D6, ws-008).
3
+ *
4
+ * A consumer's parameter and a producer's output are joined by name when
5
+ * nobody wrote the join down. Deploy matches names exactly, and real projects
6
+ * rarely spell both sides the same (`clusterArn` against `ClusterArn`), so an
7
+ * inferred join compares {@link joinKey}s and says how it matched: `exact`
8
+ * when the names are equal, `folded` when only case or punctuation differ.
9
+ * A declared member link is always compared exactly and never uses this.
10
+ *
11
+ * Before this module, core only published the handles (a graph IR's
12
+ * `exports` and `imports`) and each viewer matched them with its own rule.
13
+ * Every reader that infers a join by name uses this function instead, so the
14
+ * same workspace shows the same inferred edges everywhere. It imports nothing,
15
+ * so any reader can load it through `@intentius/chant/join-key`.
16
+ */
17
+
18
+ /** How an inferred join matched its two names. */
19
+ export type JoinLabel = "exact" | "folded";
20
+
21
+ /**
22
+ * The key two names are joined on: Unicode-normalised, lowercased, with every
23
+ * character that is not a letter or a digit removed. `ClusterArn`,
24
+ * `clusterArn`, `cluster_arn` and `cluster-arn` share the key `clusterarn`.
25
+ */
26
+ export function joinKey(name: string): string {
27
+ return name.normalize("NFKC").toLowerCase().replace(/[^\p{L}\p{N}]/gu, "");
28
+ }
29
+
30
+ /**
31
+ * How `a` and `b` join: `exact` when they are the same string, `folded` when
32
+ * only their {@link joinKey}s are equal, and undefined when they don't join.
33
+ * Names whose key is empty never join.
34
+ */
35
+ export function joinLabel(a: string, b: string): JoinLabel | undefined {
36
+ if (a === b) return a === "" ? undefined : "exact";
37
+ const key = joinKey(a);
38
+ return key !== "" && key === joinKey(b) ? "folded" : undefined;
39
+ }
@@ -24,10 +24,14 @@
24
24
  * a project that names lexicons by package takes exactly the path it always did.
25
25
  */
26
26
 
27
- import { isAbsolute, relative, resolve, sep } from "node:path";
27
+ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
28
28
 
29
- /** One `lexicons` entry: a package-backed name, or a name plus the module that implements it. */
30
- export type LexiconDeclaration = string | { name: string; module: string };
29
+ /**
30
+ * One `lexicons` entry: a package-backed name, or a name plus the module that
31
+ * implements it. `root` (chant#2590) names the directory the lexicon's files
32
+ * live in, for fold's trust; see {@link pathLexiconRoot}.
33
+ */
34
+ export type LexiconDeclaration = string | { name: string; module: string; root?: string };
31
35
 
32
36
  /** The lexicon name an entry declares, whichever form it takes. */
33
37
  export function lexiconDeclarationName(entry: LexiconDeclaration): string {
@@ -44,6 +48,52 @@ export function lexiconNames(entries: readonly LexiconDeclaration[] | undefined)
44
48
  /** Name to absolute module path, for the lexicons declared by path in the configs loaded so far. */
45
49
  const modulePaths = new Map<string, string>();
46
50
 
51
+ /** Name to the absolute directory fold trusts for that lexicon (chant#2590). Absent: only the module is trusted. */
52
+ const moduleRoots = new Map<string, string>();
53
+
54
+ /** True when `path` is `dir` itself or lies under it. Both are absolute. */
55
+ function isWithin(path: string, dir: string): boolean {
56
+ const rel = relative(dir, path);
57
+ return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
58
+ }
59
+
60
+ /**
61
+ * chant#2590 — the directory fold trusts for a lexicon declared by path.
62
+ *
63
+ * A package has a root that bounds its subpaths. A module path has none, so
64
+ * the directory is either the one `root` names, resolved like `module`, or the
65
+ * module's own directory. It must lie inside the project (`baseDir`, the
66
+ * directory holding `chant.config.ts`) and must not contain the project's
67
+ * source directory (`sourceDir`, relative to `baseDir`, default `.`). The
68
+ * module must lie inside it.
69
+ *
70
+ * Returns the directory, or `undefined` when only the module file is trusted.
71
+ * A declared `root` that breaks a rule returns a `problem` to report; the
72
+ * module's own directory breaking one (a module at the project root, say)
73
+ * just leaves the lexicon with its module file.
74
+ */
75
+ export function pathLexiconRoot(
76
+ entry: { module: string; root?: string },
77
+ baseDir: string,
78
+ sourceDir = ".",
79
+ ): { root?: string; problem?: string } {
80
+ const modulePath = isAbsolute(entry.module) ? entry.module : resolve(baseDir, entry.module);
81
+ const declared = entry.root !== undefined;
82
+ const candidate = declared
83
+ ? isAbsolute(entry.root!)
84
+ ? resolve(entry.root!)
85
+ : resolve(baseDir, entry.root!)
86
+ : dirname(modulePath);
87
+ const project = resolve(baseDir);
88
+ const source = resolve(baseDir, sourceDir);
89
+ let problem: string | undefined;
90
+ if (!isWithin(candidate, project)) problem = `${candidate} is outside the project (${project})`;
91
+ else if (isWithin(source, candidate)) problem = `${candidate} contains the project's source directory (${source})`;
92
+ else if (!isWithin(modulePath, candidate)) problem = `the module ${modulePath} is not inside ${candidate}`;
93
+ if (problem === undefined) return { root: candidate };
94
+ return declared ? { problem } : {};
95
+ }
96
+
47
97
  /**
48
98
  * Record the path-declared entries of one loaded config. `baseDir` is the
49
99
  * directory the config file sits in. A plain-string entry for a name removes
@@ -57,28 +107,65 @@ const modulePaths = new Map<string, string>();
57
107
  export function registerLexiconDeclarations(
58
108
  entries: readonly LexiconDeclaration[] | undefined,
59
109
  baseDir: string,
110
+ sourceDir?: string,
60
111
  ): Record<string, string> {
61
112
  const recorded: Record<string, string> = {};
62
113
  for (const entry of entries ?? []) {
63
114
  if (typeof entry === "string") {
64
115
  modulePaths.delete(entry);
116
+ moduleRoots.delete(entry);
65
117
  continue;
66
118
  }
67
- const path = isAbsolute(entry.module) ? entry.module : resolve(baseDir, entry.module);
119
+ const path = declaredModulePath(entry, baseDir);
68
120
  modulePaths.set(entry.name, path);
69
121
  recorded[entry.name] = path;
122
+ const { root } = pathLexiconRoot(entry, baseDir, sourceDir);
123
+ if (root === undefined) moduleRoots.delete(entry.name);
124
+ else moduleRoots.set(entry.name, root);
70
125
  }
71
126
  return recorded;
72
127
  }
73
128
 
129
+ function declaredModulePath(entry: { module: string }, baseDir: string): string {
130
+ return isAbsolute(entry.module) ? entry.module : resolve(baseDir, entry.module);
131
+ }
132
+
133
+ /**
134
+ * chant#2589 — the name-to-path map `entries` declare, without recording it.
135
+ * For a caller that must name path lexicons in its messages but must never
136
+ * let a loader import one (`chant audit` runs no project code).
137
+ */
138
+ export function pathLexiconMap(entries: readonly LexiconDeclaration[], baseDir: string): Map<string, string> {
139
+ const map = new Map<string, string>();
140
+ for (const entry of entries) {
141
+ if (typeof entry !== "string") map.set(entry.name, declaredModulePath(entry, baseDir));
142
+ }
143
+ return map;
144
+ }
145
+
146
+ /** A copy of the paths recorded so far in this process. */
147
+ export function recordedLexiconModules(): Map<string, string> {
148
+ return new Map(modulePaths);
149
+ }
150
+
74
151
  /** The absolute module path `name` was declared with, or `undefined` for a package-backed lexicon. */
75
152
  export function lexiconModulePath(name: string): string | undefined {
76
153
  return modulePaths.get(name);
77
154
  }
78
155
 
156
+ /**
157
+ * chant#2590 — the directory fold trusts for a path-declared lexicon, or
158
+ * `undefined` when only its module file is trusted (or `name` is package-backed).
159
+ * See {@link pathLexiconRoot}.
160
+ */
161
+ export function lexiconModuleRoot(name: string): string | undefined {
162
+ return moduleRoots.get(name);
163
+ }
164
+
79
165
  /** Forget every recorded path. For tests. */
80
166
  export function resetLexiconModules(): void {
81
167
  modulePaths.clear();
168
+ moduleRoots.clear();
82
169
  }
83
170
 
84
171
  /**
@@ -102,16 +189,27 @@ export async function importLexiconModule(name: string): Promise<Record<string,
102
189
  /**
103
190
  * chant#2578 — how a message should name a lexicon. A package-backed name is
104
191
  * its npm package; a lexicon declared by path is that path, relative to
105
- * `fromDir` when it sits under it.
192
+ * `fromDir` when it sits under it. `paths` defaults to the paths recorded in
193
+ * this process; see {@link pathLexiconMap} for a map that is not recorded.
106
194
  */
107
- export function lexiconSourceLabel(name: string, fromDir: string = process.cwd()): string {
108
- const path = modulePaths.get(name);
195
+ export function lexiconSourceLabel(
196
+ name: string,
197
+ fromDir: string = process.cwd(),
198
+ paths: ReadonlyMap<string, string> = modulePaths,
199
+ ): string {
200
+ const path = paths.get(name);
109
201
  if (path === undefined) return `@intentius/chant-lexicon-${name}`;
110
202
  const rel = relative(fromDir, path);
111
203
  return rel === "" || rel.startsWith("..") || isAbsolute(rel) ? path : `./${rel.split(sep).join("/")}`;
112
204
  }
113
205
 
114
- /** The npm packages that provide `names`, leaving out every lexicon declared by path: those have nothing to install. */
115
- export function lexiconPackagesToInstall(names: readonly string[]): string[] {
116
- return names.filter((name) => !modulePaths.has(name)).map((name) => `@intentius/chant-lexicon-${name}`);
206
+ /**
207
+ * The npm packages that provide `names`, leaving out every lexicon declared by
208
+ * path: those have nothing to install. `paths` as in {@link lexiconSourceLabel}.
209
+ */
210
+ export function lexiconPackagesToInstall(
211
+ names: readonly string[],
212
+ paths: ReadonlyMap<string, string> = modulePaths,
213
+ ): string[] {
214
+ return names.filter((name) => !paths.has(name)).map((name) => `@intentius/chant-lexicon-${name}`);
117
215
  }
package/src/lexicon.ts CHANGED
@@ -501,6 +501,78 @@ export interface ComponentPipelineOptions {
501
501
  * `["chant", "components", "promote", "--from", <env>, "--to", <promoteTo>]`.
502
502
  */
503
503
  promoteCommand?: string[];
504
+ /**
505
+ * The workspace member this pipeline belongs to (#2542, #2524 D19). Core
506
+ * sets it when the project sits in a member of a `chant.workspace.json`;
507
+ * a caller never needs to. Unset, every generator's output is unchanged.
508
+ * Set, the pipeline keeps the triggers it has without a member, and the
509
+ * ones that can take a path filter (an Op's `push` and `pull_request`, and
510
+ * GitLab's job rules) are limited to the member's files and the pipeline
511
+ * file. Its jobs run in the member's directory, and its names carry the
512
+ * member's name so two members' pipelines never collide.
513
+ */
514
+ member?: PipelineMember;
515
+ }
516
+
517
+ /** Where a workspace member sits, for a generated pipeline (#2542). */
518
+ export interface PipelineMember {
519
+ /** The member's name in the declaration. */
520
+ name: string;
521
+ /**
522
+ * The member's directory relative to the repository root, with `/`
523
+ * separators, or `"."` for a member at the root.
524
+ */
525
+ dir: string;
526
+ /**
527
+ * Directories (repository-relative) that belong to other members or to
528
+ * example groups. Only a member at `"."` uses them: its path filter is the
529
+ * whole repository minus these.
530
+ */
531
+ exclude?: string[];
532
+ /** The generated component pipeline's path, repository-relative. Added to the path filter. */
533
+ file?: string;
534
+ /**
535
+ * The directory (repository-relative) the generated Op pipeline files land
536
+ * in. Each file's own path is added to its path filter.
537
+ */
538
+ fileDir?: string;
539
+ }
540
+
541
+ /** `path` (relative to the member's directory) made relative to the repository root. Absolute paths pass through. */
542
+ export function memberRepoPath(member: PipelineMember, path: string): string {
543
+ if (path.startsWith("/") || member.dir === "." || member.dir === "") return path;
544
+ const rest = path.replace(/^\.\/+/, "").replace(/\/+$/, "");
545
+ return rest === "" || rest === "." ? member.dir : `${member.dir}/${rest}`;
546
+ }
547
+
548
+ /**
549
+ * The path filter for a member's `push` or `pull_request` trigger in GitHub
550
+ * Actions syntax (also Forgejo's): the member's directory and the pipeline
551
+ * file. A member at
552
+ * `"."` gets every path, minus the other members' directories.
553
+ */
554
+ export function memberPathFilter(member: PipelineMember, file: string | undefined = member.file): string[] {
555
+ const paths =
556
+ member.dir === "." || member.dir === ""
557
+ ? ["**", ...(member.exclude ?? []).map((d) => `!${d}/**`)]
558
+ : [`${member.dir}/**`];
559
+ if (file && !paths.includes(file)) paths.push(file);
560
+ return paths;
561
+ }
562
+
563
+ /**
564
+ * The `rules: changes:` list for a member's GitLab jobs, or undefined for a
565
+ * member at `"."`: GitLab's `changes` has no exclusions, so the root member's
566
+ * jobs run on every change.
567
+ */
568
+ export function memberGitlabChanges(member: PipelineMember, file: string | undefined = member.file): string[] | undefined {
569
+ if (member.dir === "." || member.dir === "") return undefined;
570
+ return [`${member.dir}/**/*`, ...(file ? [file] : [])];
571
+ }
572
+
573
+ /** A shell word for `cd` into the member's directory. */
574
+ export function memberShellDir(member: PipelineMember): string {
575
+ return /^[A-Za-z0-9._\/-]+$/.test(member.dir) ? member.dir : `'${member.dir.replace(/'/g, `'\\''`)}'`;
504
576
  }
505
577
 
506
578
  /** The synthesized CI pipeline for a component graph (generate mode). */
@@ -1938,7 +2010,13 @@ export interface ResourceMetadata {
1938
2010
  status: string;
1939
2011
  /** ISO timestamp of last update */
1940
2012
  lastUpdated?: string;
1941
- /** Cloud-assigned output properties */
2013
+ /**
2014
+ * Cloud-assigned output properties. Two keys are read by `components status
2015
+ * --live` (#2513): `digest` (the artifact digest serving now) and `gitSha`
2016
+ * (the commit it was built from). Set them when the live read knows them; a
2017
+ * value that differs from the recorded release reports the component
2018
+ * `drifted`. See `LIVE_IDENTITY_ATTRIBUTES` in lifecycle/status.ts.
2019
+ */
1942
2020
  attributes?: Record<string, unknown>;
1943
2021
  /**
1944
2022
  * Live ownership verdict from the resource's marker (#119/#120). `owned` =
@@ -55,7 +55,7 @@
55
55
  */
56
56
 
57
57
  import { sortedJsonReplacer } from "../utils";
58
- import { warnOnLegacyDigests } from "./legacy-digest";
58
+ import { flagLegacyDigest, warnOnLegacyDigests, withoutReadFlags } from "./legacy-digest";
59
59
  import { writeBlobToPath, readBlobFromPath, listFilesInDir } from "./git";
60
60
  import type { BuildArchiveManifest } from "../components/verbs/build-archive";
61
61
 
@@ -87,7 +87,8 @@ export async function persistBuildManifest(
87
87
  manifest: BuildArchiveManifest,
88
88
  opts?: { cwd?: string },
89
89
  ): Promise<{ commit: string }> {
90
- const json = JSON.stringify(manifest, sortedJsonReplacer);
90
+ // `flags` are read-side only (#2514); a manifest read back and persisted again never keeps them.
91
+ const json = JSON.stringify(withoutReadFlags(manifest), sortedJsonReplacer);
91
92
  const commit = await writeBlobToPath(BUILDS_DIR, manifestFilename(manifest.manifestDigest), json, "Build manifest", opts);
92
93
  return { commit };
93
94
  }
@@ -111,14 +112,14 @@ export async function readBuildManifest(
111
112
  } catch {
112
113
  return null;
113
114
  }
114
- // chant #2514 — a release ahead of real SHA-256 digests, say once that
115
- // the persisted manifests hold values in the old form.
116
- warnOnLegacyDigests(
117
- [manifest.manifestDigest, ...(Array.isArray(manifest.contents) ? manifest.contents.map((e) => e?.digest) : [])],
118
- "build manifests",
119
- opts?.cwd,
120
- );
121
- return manifest;
115
+ // chant #2514 — a manifest whose digests predate real SHA-256 is flagged
116
+ // legacy-digest, never re-keyed, and the project is warned once.
117
+ const digests = [
118
+ manifest.manifestDigest,
119
+ ...(Array.isArray(manifest.contents) ? manifest.contents.map((e) => e?.digest) : []),
120
+ ];
121
+ warnOnLegacyDigests(digests, "build manifests", opts?.cwd);
122
+ return flagLegacyDigest(manifest, digests);
122
123
  }
123
124
 
124
125
  /** List the `manifestDigest` of every build manifest persisted on the orphan branch. */
@@ -7,15 +7,19 @@
7
7
  import type { BuildResult } from "../build";
8
8
  import type { Declarable } from "../declarable";
9
9
  import type { BuildDigest, ResourceDigest, DigestDiff } from "./types";
10
- import { sortedJsonReplacer } from "../utils";
11
- import { getRuntime } from "../runtime-adapter";
10
+ import { canonicalJson } from "../effect-receipt";
11
+ import { contentDigest } from "../content-digest";
12
12
 
13
13
  /**
14
- * Hash an entity's props deterministically.
14
+ * Hash an entity's props deterministically: bare hex SHA-256 over their
15
+ * `canonicalJson` (chant #2514). Props are first passed through
16
+ * `JSON.stringify`, which applies `toJSON` and drops what JSON cannot hold
17
+ * (a function-valued prop, say), so a prop that serialises today still
18
+ * hashes rather than throwing.
15
19
  */
16
20
  export function hashProps(props: unknown): string {
17
- const json = JSON.stringify(props, sortedJsonReplacer);
18
- return getRuntime().hash(json);
21
+ const plain: unknown = JSON.parse(JSON.stringify(props) ?? "null");
22
+ return contentDigest(canonicalJson(plain)).slice("sha256:".length);
19
23
  }
20
24
 
21
25
  /**
@@ -253,6 +253,26 @@ describe("lifecycle/gate-ledger — pending facts (#2119)", () => {
253
253
  expect(resolutions).toEqual([]);
254
254
  });
255
255
  });
256
+
257
+ // #2574: an environment that is present but not a string would read as an
258
+ // approval bound to no environment, which a component gate treats differently.
259
+ test("a line whose environment is not a string is malformed", async () => {
260
+ await withTestDir(async (dir) => {
261
+ await initRepo(dir);
262
+ await writeBlobToPath(
263
+ "_gates", "search-service.jsonl",
264
+ [
265
+ JSON.stringify({ version: 1, op: "search-service", gate: "g", resolvedBy: "alex", timestamp: "2026-01-01T00:00:00.000Z", environment: 7 }),
266
+ JSON.stringify({ version: 1, op: "search-service", gate: "g", resolvedBy: "alex", timestamp: "2026-01-01T00:00:00.000Z", environment: "prod" }),
267
+ ].join("\n"),
268
+ "hand-written",
269
+ { cwd: dir },
270
+ );
271
+ const { resolutions, malformed } = await readGateLedger("search-service", { cwd: dir });
272
+ expect(malformed).toBe(1);
273
+ expect(resolutions.map((r) => r.environment)).toEqual(["prod"]);
274
+ });
275
+ });
256
276
  });
257
277
 
258
278
  /**
@@ -140,6 +140,13 @@ export interface GateResolutionRecord {
140
140
  * such a record proves is that somebody approved *something*.
141
141
  */
142
142
  planDigest?: string;
143
+ /**
144
+ * The environment this approval is for (#2574), copied off the pending fact
145
+ * it answers. Set for a component gate, whose ledger is keyed by component
146
+ * rather than by environment. A component gate counts only a resolution
147
+ * recorded for its own environment and plan.
148
+ */
149
+ environment?: string;
143
150
  /**
144
151
  * The channel this resolution was authored on (chant#2384) — set by the
145
152
  * writer, never by the caller. `chant approve` records `"cli"`, the
@@ -217,10 +224,18 @@ export interface PendingGateRecord {
217
224
  * resolution by default, so approving the standing fact approves the plan
218
225
  * the approver was shown rather than the next run's.
219
226
  *
220
- * Absent when the gate binds no plan (a component gate, an authored `gate`
221
- * step with no `plan`), which is the shape every gate had before #2300.
227
+ * Absent when the gate binds no plan (an authored Op `gate` step with no
228
+ * `plan`), which is the shape every gate had before #2300. A component gate
229
+ * binds one since #2574.
222
230
  */
223
231
  planDigest?: string;
232
+ /**
233
+ * The environment the run reached this gate in (#2574). Set by the
234
+ * component driver, whose gate ledger is keyed by component, so pending
235
+ * facts for staging and prod stand side by side and `chant approve --env`
236
+ * can pick one.
237
+ */
238
+ environment?: string;
224
239
  /**
225
240
  * The channel the run that reached this gate was driven from (chant#2384).
226
241
  *
@@ -260,8 +275,8 @@ function filename(op: string): string {
260
275
  * fact was appended to (#2243) rather than describing it in prose that drifts
261
276
  * from `DIR`.
262
277
  */
263
- export function gateLedgerPath(op: string): string {
264
- return `${DIR}/${filename(op)}`;
278
+ export function gateLedgerPath(op: string, memberPrefix = ""): string {
279
+ return `${memberPrefix}${DIR}/${filename(op)}`;
265
280
  }
266
281
 
267
282
  /** Append one immutable gate-resolution record. Does not push to the remote — call `pushLifecycle` (./git.ts) afterward, same two-step shape every other ledger write here uses. Retries on `RefCASConflictError` the same way `appendConvergeRecord` does (./converge-ledger.ts) — a concurrent writer to a different op's/env's file on the same orphan branch is the ordinary case, not an edge case. The baseline read must be `readPathSha` + `readBlobBySha` rather than `readBlobFromPath`, so the exact sha `existing` came from can be passed as `expectPriorPathSha` — see `writeBlobToPath` (./git.ts) for the race that closes. */
@@ -361,6 +376,13 @@ export async function readGateLedger(
361
376
  malformed++;
362
377
  continue;
363
378
  }
379
+ // #2574: the same reasoning for the environment a component gate
380
+ // approval is bound to.
381
+ const rawEnv: unknown = (parsed as { environment?: unknown }).environment;
382
+ if (rawEnv !== undefined && typeof rawEnv !== "string") {
383
+ malformed++;
384
+ continue;
385
+ }
364
386
  if (parsed.kind === "pending") {
365
387
  if (typeof parsed.expiresAt !== "string") {
366
388
  malformed++;
@@ -442,8 +464,8 @@ export interface PlanBoundResolution {
442
464
  * which a run answers yes to no matter what has changed since, and this asks
443
465
  * "is there an approval of *this*".
444
466
  *
445
- * - `planDigest` `undefined` — the gate binds no plan (a component gate, a
446
- * `gate` step authored with no `plan`). Falls straight through to
467
+ * - `planDigest` `undefined` — the gate binds no plan (an Op `gate` step
468
+ * authored with no `plan`). Falls straight through to
447
469
  * {@link latestResolutionSince}: gates that never claimed to bind a plan
448
470
  * behave exactly as they did before #2300.
449
471
  * - A resolution whose `planDigest` equals `planDigest` answers the gate.
@@ -1,7 +1,7 @@
1
1
  import { describe, test, expect } from "vitest";
2
2
  import { withTestDir } from "@intentius/chant-test-utils";
3
3
  import { spawnSync } from "node:child_process";
4
- import { writeFileSync } from "node:fs";
4
+ import { mkdirSync, writeFileSync } from "node:fs";
5
5
  import { tmpdir } from "node:os";
6
6
  import { join } from "node:path";
7
7
  import {
@@ -130,6 +130,18 @@ describe("lifecycle/git", () => {
130
130
  });
131
131
  });
132
132
 
133
+ test("a write from a subdirectory of the repository keeps the rest of the branch (#2550)", async () => {
134
+ await withTestDir(async (dir) => {
135
+ await initRepo(dir);
136
+ await writeBlobToPath("_builds", "a.json", "{}", "m", { cwd: dir });
137
+ const sub = join(dir, "project");
138
+ mkdirSync(sub);
139
+ await writeBlobToPath("_gates", "op.jsonl", "{}", "m", { cwd: sub });
140
+ expect(await listFilesInDir("_builds", { cwd: sub })).toEqual(["a.json"]);
141
+ expect(await readBlobFromPath("_gates", "op.jsonl", { cwd: dir })).toBe("{}");
142
+ });
143
+ });
144
+
133
145
  test("readBlobFromPath returns null for a missing directory/file", async () => {
134
146
  await withTestDir(async (dir) => {
135
147
  await initRepo(dir);