@intentius/chant 0.79.0 → 0.81.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (462) hide show
  1. package/dist/audit/core.d.ts.map +1 -1
  2. package/dist/audit/discover.d.ts +30 -0
  3. package/dist/audit/discover.d.ts.map +1 -1
  4. package/dist/audit/report-html.d.ts +5 -0
  5. package/dist/audit/report-html.d.ts.map +1 -1
  6. package/dist/audit/report-model.d.ts +13 -0
  7. package/dist/audit/report-model.d.ts.map +1 -1
  8. package/dist/audit/terraform-state.d.ts +13 -0
  9. package/dist/audit/terraform-state.d.ts.map +1 -1
  10. package/dist/cli/command-group.d.ts +6 -0
  11. package/dist/cli/command-group.d.ts.map +1 -1
  12. package/dist/cli/commands/audit.d.ts +44 -3
  13. package/dist/cli/commands/audit.d.ts.map +1 -1
  14. package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
  15. package/dist/cli/commands/check-lexicon.d.ts +8 -0
  16. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  17. package/dist/cli/commands/doctor.d.ts.map +1 -1
  18. package/dist/cli/commands/import-agents.d.ts.map +1 -1
  19. package/dist/cli/commands/init.d.ts +6 -0
  20. package/dist/cli/commands/init.d.ts.map +1 -1
  21. package/dist/cli/commands/lint.d.ts.map +1 -1
  22. package/dist/cli/commands/onboard.d.ts.map +1 -1
  23. package/dist/cli/commands/update.d.ts +6 -0
  24. package/dist/cli/commands/update.d.ts.map +1 -1
  25. package/dist/cli/commands/vendor.d.ts +5 -0
  26. package/dist/cli/commands/vendor.d.ts.map +1 -1
  27. package/dist/cli/conflict-check.d.ts +6 -0
  28. package/dist/cli/conflict-check.d.ts.map +1 -1
  29. package/dist/cli/handlers/build.d.ts.map +1 -1
  30. package/dist/cli/handlers/components.d.ts.map +1 -1
  31. package/dist/cli/handlers/dev.d.ts.map +1 -1
  32. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  33. package/dist/cli/handlers/init.d.ts.map +1 -1
  34. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  35. package/dist/cli/handlers/misc.d.ts.map +1 -1
  36. package/dist/cli/handlers/operator.d.ts +7 -0
  37. package/dist/cli/handlers/operator.d.ts.map +1 -1
  38. package/dist/cli/handlers/promote.d.ts +20 -0
  39. package/dist/cli/handlers/promote.d.ts.map +1 -0
  40. package/dist/cli/handlers/run-generate.d.ts +39 -0
  41. package/dist/cli/handlers/run-generate.d.ts.map +1 -0
  42. package/dist/cli/handlers/run.d.ts +4 -1
  43. package/dist/cli/handlers/run.d.ts.map +1 -1
  44. package/dist/cli/handlers/vendor.d.ts +8 -3
  45. package/dist/cli/handlers/vendor.d.ts.map +1 -1
  46. package/dist/cli/main.d.ts +12 -0
  47. package/dist/cli/main.d.ts.map +1 -1
  48. package/dist/cli/mcp/op-tools.d.ts.map +1 -1
  49. package/dist/cli/plugins.d.ts +14 -0
  50. package/dist/cli/plugins.d.ts.map +1 -1
  51. package/dist/cli/registry.d.ts +46 -1
  52. package/dist/cli/registry.d.ts.map +1 -1
  53. package/dist/codegen/fetch.d.ts +10 -0
  54. package/dist/codegen/fetch.d.ts.map +1 -1
  55. package/dist/components/capability-plugin-loader.d.ts.map +1 -1
  56. package/dist/components/cli-support.d.ts +3 -1
  57. package/dist/components/cli-support.d.ts.map +1 -1
  58. package/dist/components/discover.d.ts.map +1 -1
  59. package/dist/components/driver.d.ts +16 -2
  60. package/dist/components/driver.d.ts.map +1 -1
  61. package/dist/components/fan-out-support.d.ts.map +1 -1
  62. package/dist/components/gate-plan.d.ts +40 -0
  63. package/dist/components/gate-plan.d.ts.map +1 -0
  64. package/dist/components/pilots/alb-ecs.pilot.d.ts +7 -1
  65. package/dist/components/pilots/alb-ecs.pilot.d.ts.map +1 -1
  66. package/dist/components/presets/ecs-fargate.d.ts +10 -4
  67. package/dist/components/presets/ecs-fargate.d.ts.map +1 -1
  68. package/dist/components/promote.d.ts +264 -0
  69. package/dist/components/promote.d.ts.map +1 -0
  70. package/dist/components/sandbox/driver.d.ts.map +1 -1
  71. package/dist/components/sandbox/run.d.ts.map +1 -1
  72. package/dist/components/verbs/build-archive.d.ts +22 -11
  73. package/dist/components/verbs/build-archive.d.ts.map +1 -1
  74. package/dist/components/verbs/run-agent.d.ts.map +1 -1
  75. package/dist/config-static.d.ts +59 -0
  76. package/dist/config-static.d.ts.map +1 -0
  77. package/dist/config.d.ts +104 -5
  78. package/dist/config.d.ts.map +1 -1
  79. package/dist/content-digest.d.ts +17 -0
  80. package/dist/content-digest.d.ts.map +1 -0
  81. package/dist/discovery/files.d.ts +48 -4
  82. package/dist/discovery/files.d.ts.map +1 -1
  83. package/dist/discovery/fold-import.d.ts +32 -1
  84. package/dist/discovery/fold-import.d.ts.map +1 -1
  85. package/dist/discovery/index.d.ts.map +1 -1
  86. package/dist/discovery/sandbox/driver.d.ts +20 -0
  87. package/dist/discovery/sandbox/driver.d.ts.map +1 -1
  88. package/dist/discovery/sandbox/fork.d.ts +20 -0
  89. package/dist/discovery/sandbox/fork.d.ts.map +1 -1
  90. package/dist/discovery/walk.d.ts +119 -0
  91. package/dist/discovery/walk.d.ts.map +1 -0
  92. package/dist/graph-ir.d.ts +17 -1
  93. package/dist/graph-ir.d.ts.map +1 -1
  94. package/dist/join-key.d.ts +31 -0
  95. package/dist/join-key.d.ts.map +1 -0
  96. package/dist/lexicon-module.d.ts +110 -0
  97. package/dist/lexicon-module.d.ts.map +1 -0
  98. package/dist/lexicon.d.ts +82 -1
  99. package/dist/lexicon.d.ts.map +1 -1
  100. package/dist/lifecycle/build-ledger-store.d.ts.map +1 -1
  101. package/dist/lifecycle/digest.d.ts +5 -1
  102. package/dist/lifecycle/digest.d.ts.map +1 -1
  103. package/dist/lifecycle/gate-ledger.d.ts +20 -5
  104. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  105. package/dist/lifecycle/git.d.ts +64 -0
  106. package/dist/lifecycle/git.d.ts.map +1 -1
  107. package/dist/lifecycle/lease.d.ts +8 -1
  108. package/dist/lifecycle/lease.d.ts.map +1 -1
  109. package/dist/lifecycle/legacy-digest.d.ts +59 -0
  110. package/dist/lifecycle/legacy-digest.d.ts.map +1 -0
  111. package/dist/lifecycle/member-ledger.d.ts +67 -0
  112. package/dist/lifecycle/member-ledger.d.ts.map +1 -0
  113. package/dist/lifecycle/plan-digest.d.ts +4 -3
  114. package/dist/lifecycle/plan-digest.d.ts.map +1 -1
  115. package/dist/lifecycle/release-ledger.d.ts +42 -1
  116. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  117. package/dist/lifecycle/status.d.ts +90 -5
  118. package/dist/lifecycle/status.d.ts.map +1 -1
  119. package/dist/lint/rules/comp/comp003-mutating-no-rollback.d.ts +2 -3
  120. package/dist/lint/rules/comp/comp003-mutating-no-rollback.d.ts.map +1 -1
  121. package/dist/op/activities/activity-contracts.d.ts +32 -0
  122. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  123. package/dist/op/activities/index.d.ts +2 -0
  124. package/dist/op/activities/index.d.ts.map +1 -1
  125. package/dist/op/activities/propose-upgrade.d.ts +75 -0
  126. package/dist/op/activities/propose-upgrade.d.ts.map +1 -0
  127. package/dist/op/activity-contract-registry.d.ts.map +1 -1
  128. package/dist/op/activity-registry.d.ts.map +1 -1
  129. package/dist/op/discover.d.ts.map +1 -1
  130. package/dist/op/gate-approval.d.ts.map +1 -1
  131. package/dist/op/gate-name.d.ts +7 -0
  132. package/dist/op/gate-name.d.ts.map +1 -1
  133. package/dist/op/gate-summary.d.ts +16 -0
  134. package/dist/op/gate-summary.d.ts.map +1 -1
  135. package/dist/op/gate.d.ts +16 -2
  136. package/dist/op/gate.d.ts.map +1 -1
  137. package/dist/op/generate-pipeline.d.ts.map +1 -1
  138. package/dist/op/runtimes/local.d.ts.map +1 -1
  139. package/dist/project-root.d.ts +26 -0
  140. package/dist/project-root.d.ts.map +1 -1
  141. package/dist/workspace/__fixtures__/contract-repo.d.ts +37 -0
  142. package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -0
  143. package/dist/workspace/__fixtures__/two-member-workspace.d.ts +11 -0
  144. package/dist/workspace/__fixtures__/two-member-workspace.d.ts.map +1 -0
  145. package/dist/workspace/checks/generated.d.ts +156 -0
  146. package/dist/workspace/checks/generated.d.ts.map +1 -0
  147. package/dist/workspace/checks/ledgers.d.ts +78 -0
  148. package/dist/workspace/checks/ledgers.d.ts.map +1 -0
  149. package/dist/workspace/checks/links.d.ts +25 -0
  150. package/dist/workspace/checks/links.d.ts.map +1 -0
  151. package/dist/workspace/checks/pipelines.d.ts +76 -0
  152. package/dist/workspace/checks/pipelines.d.ts.map +1 -0
  153. package/dist/workspace/checks.d.ts +161 -0
  154. package/dist/workspace/checks.d.ts.map +1 -0
  155. package/dist/workspace/compose-graph.d.ts +115 -0
  156. package/dist/workspace/compose-graph.d.ts.map +1 -0
  157. package/dist/workspace/compose-reports.d.ts +68 -0
  158. package/dist/workspace/compose-reports.d.ts.map +1 -0
  159. package/dist/workspace/declaration.d.ts +220 -0
  160. package/dist/workspace/declaration.d.ts.map +1 -0
  161. package/dist/workspace/declaration.schema.json +434 -0
  162. package/dist/workspace/generated-files.d.ts +65 -0
  163. package/dist/workspace/generated-files.d.ts.map +1 -0
  164. package/dist/workspace/glob.d.ts +13 -0
  165. package/dist/workspace/glob.d.ts.map +1 -0
  166. package/dist/workspace/graph-cli.d.ts +75 -0
  167. package/dist/workspace/graph-cli.d.ts.map +1 -0
  168. package/dist/workspace/init.d.ts +96 -0
  169. package/dist/workspace/init.d.ts.map +1 -0
  170. package/dist/workspace/jsonc.d.ts +38 -0
  171. package/dist/workspace/jsonc.d.ts.map +1 -0
  172. package/dist/workspace/kinds.d.ts +192 -0
  173. package/dist/workspace/kinds.d.ts.map +1 -0
  174. package/dist/workspace/lineage-check.d.ts +84 -0
  175. package/dist/workspace/lineage-check.d.ts.map +1 -0
  176. package/dist/workspace/lineage-cli.d.ts +31 -0
  177. package/dist/workspace/lineage-cli.d.ts.map +1 -0
  178. package/dist/workspace/lineage-init.d.ts +101 -0
  179. package/dist/workspace/lineage-init.d.ts.map +1 -0
  180. package/dist/workspace/lineage-lock.d.ts +258 -0
  181. package/dist/workspace/lineage-lock.d.ts.map +1 -0
  182. package/dist/workspace/lineage-merge.d.ts +14 -0
  183. package/dist/workspace/lineage-merge.d.ts.map +1 -0
  184. package/dist/workspace/lineage-migrations.d.ts +178 -0
  185. package/dist/workspace/lineage-migrations.d.ts.map +1 -0
  186. package/dist/workspace/lineage-update.d.ts +59 -0
  187. package/dist/workspace/lineage-update.d.ts.map +1 -0
  188. package/dist/workspace/lineage-upgrade-cli.d.ts +40 -0
  189. package/dist/workspace/lineage-upgrade-cli.d.ts.map +1 -0
  190. package/dist/workspace/lineage-upgrade.d.ts +146 -0
  191. package/dist/workspace/lineage-upgrade.d.ts.map +1 -0
  192. package/dist/workspace/lineage-vendor.d.ts +63 -0
  193. package/dist/workspace/lineage-vendor.d.ts.map +1 -0
  194. package/dist/workspace/lineage-version.d.ts +32 -0
  195. package/dist/workspace/lineage-version.d.ts.map +1 -0
  196. package/dist/workspace/links.d.ts +150 -0
  197. package/dist/workspace/links.d.ts.map +1 -0
  198. package/dist/workspace/ls.d.ts +99 -0
  199. package/dist/workspace/ls.d.ts.map +1 -0
  200. package/dist/workspace/member-commands.d.ts +139 -0
  201. package/dist/workspace/member-commands.d.ts.map +1 -0
  202. package/dist/workspace/member-handles.d.ts +54 -0
  203. package/dist/workspace/member-handles.d.ts.map +1 -0
  204. package/dist/workspace/member-pipeline.d.ts +143 -0
  205. package/dist/workspace/member-pipeline.d.ts.map +1 -0
  206. package/dist/workspace/member-run.d.ts +67 -0
  207. package/dist/workspace/member-run.d.ts.map +1 -0
  208. package/dist/workspace/reason-codes.d.ts +65 -0
  209. package/dist/workspace/reason-codes.d.ts.map +1 -0
  210. package/dist/workspace/record-source.d.ts +22 -0
  211. package/dist/workspace/record-source.d.ts.map +1 -0
  212. package/dist/workspace/records-cli.d.ts +81 -0
  213. package/dist/workspace/records-cli.d.ts.map +1 -0
  214. package/dist/workspace/records.d.ts +118 -0
  215. package/dist/workspace/records.d.ts.map +1 -0
  216. package/dist/workspace/root-refusal.d.ts +53 -0
  217. package/dist/workspace/root-refusal.d.ts.map +1 -0
  218. package/dist/workspace/status.d.ts +150 -0
  219. package/dist/workspace/status.d.ts.map +1 -0
  220. package/dist/workspace/template-manifest.d.ts +90 -0
  221. package/dist/workspace/template-manifest.d.ts.map +1 -0
  222. package/dist/workspace/tree.d.ts +36 -0
  223. package/dist/workspace/tree.d.ts.map +1 -0
  224. package/dist/workspace/trust/attestor.d.ts +67 -0
  225. package/dist/workspace/trust/attestor.d.ts.map +1 -0
  226. package/dist/workspace/trust/policy.d.ts +111 -0
  227. package/dist/workspace/trust/policy.d.ts.map +1 -0
  228. package/dist/workspace/trust/provenance.d.ts +67 -0
  229. package/dist/workspace/trust/provenance.d.ts.map +1 -0
  230. package/dist/workspace/trust/ssh-commit.d.ts +43 -0
  231. package/dist/workspace/trust/ssh-commit.d.ts.map +1 -0
  232. package/dist/workspace/trust/test-repo.d.ts +42 -0
  233. package/dist/workspace/trust/test-repo.d.ts.map +1 -0
  234. package/dist/workspace/trust/verify-cli.d.ts +9 -0
  235. package/dist/workspace/trust/verify-cli.d.ts.map +1 -0
  236. package/dist/workspace/trust/verify.d.ts +66 -0
  237. package/dist/workspace/trust/verify.d.ts.map +1 -0
  238. package/dist/workspace/which-chant.d.ts +58 -0
  239. package/dist/workspace/which-chant.d.ts.map +1 -0
  240. package/dist/workspace/workspace-kinds.schema.json +133 -0
  241. package/package.json +4 -3
  242. package/src/audit/core.ts +13 -2
  243. package/src/audit/discover.ts +102 -36
  244. package/src/audit/report-html.ts +3 -1
  245. package/src/audit/report-model.ts +14 -1
  246. package/src/audit/terraform-state.test.ts +81 -1
  247. package/src/audit/terraform-state.ts +25 -0
  248. package/src/cli/command-group.ts +7 -0
  249. package/src/cli/commands/audit-truncation.test.ts +167 -0
  250. package/src/cli/commands/audit.ts +132 -29
  251. package/src/cli/commands/check-lexicon-examples.ts +2 -1
  252. package/src/cli/commands/check-lexicon.ts +24 -0
  253. package/src/cli/commands/doctor.test.ts +30 -0
  254. package/src/cli/commands/doctor.ts +21 -0
  255. package/src/cli/commands/import-agents.ts +15 -4
  256. package/src/cli/commands/init.ts +76 -25
  257. package/src/cli/commands/lint.test.ts +52 -1
  258. package/src/cli/commands/lint.ts +41 -71
  259. package/src/cli/commands/onboard.ts +15 -0
  260. package/src/cli/commands/update.ts +18 -5
  261. package/src/cli/commands/vendor.ts +11 -6
  262. package/src/cli/conflict-check.test.ts +18 -1
  263. package/src/cli/conflict-check.ts +13 -0
  264. package/src/cli/discovery-skip.test.ts +168 -0
  265. package/src/cli/handlers/__snapshots__/build-member.test.ts.snap +38 -0
  266. package/src/cli/handlers/build-member.test.ts +127 -0
  267. package/src/cli/handlers/build.test.ts +11 -0
  268. package/src/cli/handlers/build.ts +35 -1
  269. package/src/cli/handlers/components.test.ts +127 -0
  270. package/src/cli/handlers/components.ts +70 -8
  271. package/src/cli/handlers/dev.ts +6 -1
  272. package/src/cli/handlers/fan-out.ts +9 -2
  273. package/src/cli/handlers/graph.test.ts +20 -0
  274. package/src/cli/handlers/graph.ts +4 -2
  275. package/src/cli/handlers/init.ts +54 -1
  276. package/src/cli/handlers/lifecycle.ts +2 -1
  277. package/src/cli/handlers/misc.ts +16 -0
  278. package/src/cli/handlers/operator.test.ts +68 -0
  279. package/src/cli/handlers/operator.ts +39 -10
  280. package/src/cli/handlers/promote.test.ts +377 -0
  281. package/src/cli/handlers/promote.ts +391 -0
  282. package/src/cli/handlers/run-generate-member.test.ts +83 -0
  283. package/src/cli/handlers/run-generate.test.ts +158 -0
  284. package/src/cli/handlers/run-generate.ts +168 -0
  285. package/src/cli/handlers/run.test.ts +47 -0
  286. package/src/cli/handlers/run.ts +41 -6
  287. package/src/cli/handlers/vendor.ts +147 -43
  288. package/src/cli/main.test.ts +137 -1
  289. package/src/cli/main.ts +290 -24
  290. package/src/cli/mcp/op-tools.ts +2 -1
  291. package/src/cli/path-lexicon-messages.test.ts +152 -0
  292. package/src/cli/plugins.ts +43 -5
  293. package/src/cli/registry.ts +46 -1
  294. package/src/cli/static-config-read.test.ts +208 -0
  295. package/src/codegen/fetch.test.ts +63 -1
  296. package/src/codegen/fetch.ts +30 -1
  297. package/src/components/__fixtures__/alb-ecs-service.json +0 -8
  298. package/src/components/capability-plugin-loader.ts +3 -1
  299. package/src/components/cli-support.ts +9 -3
  300. package/src/components/discover.test.ts +27 -57
  301. package/src/components/discover.ts +20 -51
  302. package/src/components/driver.test.ts +90 -12
  303. package/src/components/driver.ts +43 -5
  304. package/src/components/fan-out-support.ts +2 -1
  305. package/src/components/gate-plan.ts +53 -0
  306. package/src/components/pilots/README.md +1 -1
  307. package/src/components/pilots/alb-ecs.pilot.ts +7 -7
  308. package/src/components/presets/ecs-fargate.ts +10 -5
  309. package/src/components/promote.test.ts +645 -0
  310. package/src/components/promote.ts +641 -0
  311. package/src/components/sandbox/driver.ts +8 -8
  312. package/src/components/sandbox/run.ts +4 -1
  313. package/src/components/verbs/build-archive.ts +27 -21
  314. package/src/components/verbs/run-agent.ts +3 -5
  315. package/src/config-static.test.ts +136 -0
  316. package/src/config-static.ts +232 -0
  317. package/src/config.test.ts +40 -0
  318. package/src/config.ts +161 -11
  319. package/src/content-digest.test.ts +120 -0
  320. package/src/content-digest.ts +21 -0
  321. package/src/discovery/files.test.ts +127 -1
  322. package/src/discovery/files.ts +91 -59
  323. package/src/discovery/fold-import.ts +127 -9
  324. package/src/discovery/index.ts +1 -0
  325. package/src/discovery/sandbox/driver.ts +73 -35
  326. package/src/discovery/sandbox/fork-diagnostic.test.ts +136 -13
  327. package/src/discovery/sandbox/fork.ts +83 -43
  328. package/src/discovery/sandbox/path-lexicon-multifile.test.ts +285 -0
  329. package/src/discovery/sandbox/path-lexicon-parity.test.ts +162 -0
  330. package/src/discovery/walk.test.ts +263 -0
  331. package/src/discovery/walk.ts +334 -0
  332. package/src/effect-receipt.ts +2 -2
  333. package/src/graph-ir.ts +18 -1
  334. package/src/join-key.test.ts +25 -0
  335. package/src/join-key.ts +39 -0
  336. package/src/lexicon-module.test.ts +203 -0
  337. package/src/lexicon-module.ts +215 -0
  338. package/src/lexicon.ts +105 -1
  339. package/src/lifecycle/build-ledger-store.ts +13 -2
  340. package/src/lifecycle/digest.ts +9 -5
  341. package/src/lifecycle/gate-ledger.test.ts +20 -0
  342. package/src/lifecycle/gate-ledger.ts +28 -6
  343. package/src/lifecycle/git.test.ts +13 -1
  344. package/src/lifecycle/git.ts +221 -176
  345. package/src/lifecycle/lease.ts +25 -10
  346. package/src/lifecycle/legacy-digest.test.ts +206 -0
  347. package/src/lifecycle/legacy-digest.ts +103 -0
  348. package/src/lifecycle/member-ledger.test.ts +319 -0
  349. package/src/lifecycle/member-ledger.ts +145 -0
  350. package/src/lifecycle/plan-digest.ts +7 -7
  351. package/src/lifecycle/release-ledger.test.ts +20 -0
  352. package/src/lifecycle/release-ledger.ts +53 -3
  353. package/src/lifecycle/status.test.ts +151 -1
  354. package/src/lifecycle/status.ts +228 -7
  355. package/src/lint/rules/comp/comp003-mutating-no-rollback.ts +2 -3
  356. package/src/op/activities/activity-contracts.ts +37 -0
  357. package/src/op/activities/index.ts +10 -0
  358. package/src/op/activities/propose-upgrade.ts +206 -0
  359. package/src/op/activity-contract-registry.ts +18 -4
  360. package/src/op/activity-registry.ts +26 -0
  361. package/src/op/discover.test.ts +14 -4
  362. package/src/op/discover.ts +19 -23
  363. package/src/op/gate-approval.ts +3 -1
  364. package/src/op/gate-name.ts +8 -0
  365. package/src/op/gate-summary.test.ts +14 -0
  366. package/src/op/gate-summary.ts +25 -2
  367. package/src/op/gate.ts +36 -7
  368. package/src/op/generate-pipeline.test.ts +14 -5
  369. package/src/op/generate-pipeline.ts +2 -1
  370. package/src/op/runtimes/local.ts +2 -1
  371. package/src/project-root.test.ts +66 -1
  372. package/src/project-root.ts +48 -0
  373. package/src/workspace/__fixtures__/contract-repo.ts +100 -0
  374. package/src/workspace/__fixtures__/two-member-workspace.ts +35 -0
  375. package/src/workspace/behold-kinds.test.ts +162 -0
  376. package/src/workspace/check-contract.test.ts +127 -0
  377. package/src/workspace/check.schema.json +347 -0
  378. package/src/workspace/checks/generated.test.ts +287 -0
  379. package/src/workspace/checks/generated.ts +475 -0
  380. package/src/workspace/checks/ledgers.test.ts +139 -0
  381. package/src/workspace/checks/ledgers.ts +246 -0
  382. package/src/workspace/checks/links.ts +183 -0
  383. package/src/workspace/checks/pipelines.test.ts +133 -0
  384. package/src/workspace/checks/pipelines.ts +225 -0
  385. package/src/workspace/checks-repo.test.ts +71 -0
  386. package/src/workspace/checks.test.ts +450 -0
  387. package/src/workspace/checks.ts +461 -0
  388. package/src/workspace/compose-graph.test.ts +146 -0
  389. package/src/workspace/compose-graph.ts +228 -0
  390. package/src/workspace/compose-reports.test.ts +109 -0
  391. package/src/workspace/compose-reports.ts +186 -0
  392. package/src/workspace/declaration.schema.json +434 -0
  393. package/src/workspace/declaration.test.ts +347 -0
  394. package/src/workspace/declaration.ts +623 -0
  395. package/src/workspace/generated-files.test.ts +114 -0
  396. package/src/workspace/generated-files.ts +94 -0
  397. package/src/workspace/glob.ts +49 -0
  398. package/src/workspace/graph-cli.ts +220 -0
  399. package/src/workspace/graph-contract.test.ts +204 -0
  400. package/src/workspace/graph.schema.json +662 -0
  401. package/src/workspace/init.test.ts +223 -0
  402. package/src/workspace/init.ts +585 -0
  403. package/src/workspace/jsonc.test.ts +65 -0
  404. package/src/workspace/jsonc.ts +231 -0
  405. package/src/workspace/kinds.test.ts +288 -0
  406. package/src/workspace/kinds.ts +506 -0
  407. package/src/workspace/lineage-check.ts +264 -0
  408. package/src/workspace/lineage-cli.ts +114 -0
  409. package/src/workspace/lineage-init.test.ts +231 -0
  410. package/src/workspace/lineage-init.ts +365 -0
  411. package/src/workspace/lineage-lock.test.ts +224 -0
  412. package/src/workspace/lineage-lock.ts +357 -0
  413. package/src/workspace/lineage-merge.ts +44 -0
  414. package/src/workspace/lineage-migrations.test.ts +178 -0
  415. package/src/workspace/lineage-migrations.ts +343 -0
  416. package/src/workspace/lineage-update.ts +160 -0
  417. package/src/workspace/lineage-upgrade-cli.ts +151 -0
  418. package/src/workspace/lineage-upgrade.test.ts +474 -0
  419. package/src/workspace/lineage-upgrade.ts +624 -0
  420. package/src/workspace/lineage-vendor.test.ts +126 -0
  421. package/src/workspace/lineage-vendor.ts +215 -0
  422. package/src/workspace/lineage-version.ts +127 -0
  423. package/src/workspace/links.test.ts +344 -0
  424. package/src/workspace/links.ts +331 -0
  425. package/src/workspace/ls-contract.test.ts +200 -0
  426. package/src/workspace/ls.schema.json +190 -0
  427. package/src/workspace/ls.ts +254 -0
  428. package/src/workspace/member-commands.e2e.test.ts +244 -0
  429. package/src/workspace/member-commands.test.ts +217 -0
  430. package/src/workspace/member-commands.ts +540 -0
  431. package/src/workspace/member-handles.test.ts +76 -0
  432. package/src/workspace/member-handles.ts +228 -0
  433. package/src/workspace/member-pipeline.test.ts +135 -0
  434. package/src/workspace/member-pipeline.ts +317 -0
  435. package/src/workspace/member-run.ts +146 -0
  436. package/src/workspace/read-contract.test.ts +129 -0
  437. package/src/workspace/reason-codes.test.ts +113 -0
  438. package/src/workspace/reason-codes.ts +81 -0
  439. package/src/workspace/record-source.ts +117 -0
  440. package/src/workspace/records-cli.ts +198 -0
  441. package/src/workspace/records-contract.test.ts +93 -0
  442. package/src/workspace/records.schema.json +150 -0
  443. package/src/workspace/records.test.ts +243 -0
  444. package/src/workspace/records.ts +384 -0
  445. package/src/workspace/root-refusal.ts +97 -0
  446. package/src/workspace/status-contract.test.ts +308 -0
  447. package/src/workspace/status.schema.json +210 -0
  448. package/src/workspace/status.ts +412 -0
  449. package/src/workspace/template-manifest.test.ts +79 -0
  450. package/src/workspace/template-manifest.ts +221 -0
  451. package/src/workspace/tree.ts +132 -0
  452. package/src/workspace/trust/attestor.ts +93 -0
  453. package/src/workspace/trust/policy.ts +306 -0
  454. package/src/workspace/trust/provenance.ts +181 -0
  455. package/src/workspace/trust/ssh-commit.ts +138 -0
  456. package/src/workspace/trust/test-repo.ts +138 -0
  457. package/src/workspace/trust/trust.test.ts +397 -0
  458. package/src/workspace/trust/verify-cli.ts +56 -0
  459. package/src/workspace/trust/verify.ts +194 -0
  460. package/src/workspace/which-chant.test.ts +117 -0
  461. package/src/workspace/which-chant.ts +151 -0
  462. package/src/workspace/workspace-kinds.schema.json +133 -0
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Lexicons declared by module path (chant #2520).
3
+ *
4
+ * A `lexicons` entry has always been a bare name, and chant resolves a name to
5
+ * the package `@intentius/chant-lexicon-<name>`. A project that writes its own
6
+ * lexicon in the same repo can instead point at the module:
7
+ *
8
+ * ```ts
9
+ * export default {
10
+ * lexicons: ["fly", { name: "site", module: "./lexicon/index.ts" }],
11
+ * } satisfies ChantConfig;
12
+ * ```
13
+ *
14
+ * The path is resolved against the directory holding `chant.config.ts`. That
15
+ * module is the whole lexicon: its `LexiconPlugin` export is the plugin, the
16
+ * plugin's `activities()` and `activityContracts()` members stand in for the
17
+ * `op/activities` and `op/activity-contracts` subpaths, a `CapabilityPlugin`
18
+ * export feeds the capability registry, and an `evaluateGatePolicy` export
19
+ * stands in for the `gate-policy` subpath.
20
+ *
21
+ * Loaders take lexicon names, so the config loader records each path-declared
22
+ * name here and every loader asks {@link lexiconModulePath} before falling back
23
+ * to the package name. A name declared as a plain string is never recorded, so
24
+ * a project that names lexicons by package takes exactly the path it always did.
25
+ */
26
+
27
+ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
28
+
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 };
35
+
36
+ /** The lexicon name an entry declares, whichever form it takes. */
37
+ export function lexiconDeclarationName(entry: LexiconDeclaration): string {
38
+ return typeof entry === "string" ? entry : entry.name;
39
+ }
40
+
41
+ /** Every entry's name, in declaration order. `undefined` in, `undefined` out. */
42
+ export function lexiconNames(entries: readonly LexiconDeclaration[]): string[];
43
+ export function lexiconNames(entries: readonly LexiconDeclaration[] | undefined): string[] | undefined;
44
+ export function lexiconNames(entries: readonly LexiconDeclaration[] | undefined): string[] | undefined {
45
+ return entries?.map(lexiconDeclarationName);
46
+ }
47
+
48
+ /** Name to absolute module path, for the lexicons declared by path in the configs loaded so far. */
49
+ const modulePaths = new Map<string, string>();
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
+
97
+ /**
98
+ * Record the path-declared entries of one loaded config. `baseDir` is the
99
+ * directory the config file sits in. A plain-string entry for a name removes
100
+ * any path recorded for it earlier in this process, so loading a second
101
+ * project (the example harness does) cannot leave the first one's module in
102
+ * place of a package.
103
+ *
104
+ * Returns the recorded name-to-path map for this config, empty when every
105
+ * entry is a plain name.
106
+ */
107
+ export function registerLexiconDeclarations(
108
+ entries: readonly LexiconDeclaration[] | undefined,
109
+ baseDir: string,
110
+ sourceDir?: string,
111
+ ): Record<string, string> {
112
+ const recorded: Record<string, string> = {};
113
+ for (const entry of entries ?? []) {
114
+ if (typeof entry === "string") {
115
+ modulePaths.delete(entry);
116
+ moduleRoots.delete(entry);
117
+ continue;
118
+ }
119
+ const path = declaredModulePath(entry, baseDir);
120
+ modulePaths.set(entry.name, path);
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);
125
+ }
126
+ return recorded;
127
+ }
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
+
151
+ /** The absolute module path `name` was declared with, or `undefined` for a package-backed lexicon. */
152
+ export function lexiconModulePath(name: string): string | undefined {
153
+ return modulePaths.get(name);
154
+ }
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
+
165
+ /** Forget every recorded path. For tests. */
166
+ export function resetLexiconModules(): void {
167
+ modulePaths.clear();
168
+ moduleRoots.clear();
169
+ }
170
+
171
+ /**
172
+ * Import the module a path-declared lexicon names. Returns `undefined` when
173
+ * `name` is package-backed, so a caller falls through to its package import.
174
+ * An import failure is thrown with the declared path in the message.
175
+ */
176
+ export async function importLexiconModule(name: string): Promise<Record<string, unknown> | undefined> {
177
+ const path = modulePaths.get(name);
178
+ if (path === undefined) return undefined;
179
+ try {
180
+ return (await import(path)) as Record<string, unknown>;
181
+ } catch (err) {
182
+ throw new Error(
183
+ `lexicon "${name}" is declared with module ${path}, which could not be loaded: ` +
184
+ (err instanceof Error ? err.message : String(err)),
185
+ );
186
+ }
187
+ }
188
+
189
+ /**
190
+ * chant#2578 — how a message should name a lexicon. A package-backed name is
191
+ * its npm package; a lexicon declared by path is that path, relative to
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.
194
+ */
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);
201
+ if (path === undefined) return `@intentius/chant-lexicon-${name}`;
202
+ const rel = relative(fromDir, path);
203
+ return rel === "" || rel.startsWith("..") || isAbsolute(rel) ? path : `./${rel.split(sep).join("/")}`;
204
+ }
205
+
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}`);
215
+ }
package/src/lexicon.ts CHANGED
@@ -486,6 +486,93 @@ export interface ComponentPipelineOptions {
486
486
  * this too when the file should land under some other name than its stage.
487
487
  */
488
488
  opsFileName?: string;
489
+ /**
490
+ * An environment to promote to after the deploy (#2575). The component
491
+ * generators add one job that needs every component job and runs
492
+ * `chant components promote --from <env> --to <promoteTo>`. Each component
493
+ * job also uploads the files its build steps wrote (see
494
+ * `promoteArchivePaths` in `components/promote.ts`), and the promote job
495
+ * downloads them, since a promote publishes from the archive on disk.
496
+ * Unset, the pipeline has no promote job and is unchanged.
497
+ */
498
+ promoteTo?: string;
499
+ /**
500
+ * The promote job's command as argv, when `promoteTo` is set. Default
501
+ * `["chant", "components", "promote", "--from", <env>, "--to", <promoteTo>]`.
502
+ */
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, `'\\''`)}'`;
489
576
  }
490
577
 
491
578
  /** The synthesized CI pipeline for a component graph (generate mode). */
@@ -939,6 +1026,17 @@ export interface LexiconPlugin {
939
1026
  */
940
1027
  activityContracts?(): ActivityContract[];
941
1028
 
1029
+ /**
1030
+ * Op activity implementations, keyed by the name a step's `fn` calls them
1031
+ * by (chant #2520). Read only for a lexicon declared by module path
1032
+ * (`{ name, module }` in `chant.config.ts`), which has no
1033
+ * `@intentius/chant-lexicon-<name>/op/activities` subpath for
1034
+ * `loadActivities` to import. A package-backed lexicon keeps using the
1035
+ * subpath and leaves this out.
1036
+ */
1037
+ activities?(): Record<string, (args: Record<string, unknown>, signal?: AbortSignal) => Promise<unknown>>
1038
+ | Promise<Record<string, (args: Record<string, unknown>, signal?: AbortSignal) => Promise<unknown>>>;
1039
+
942
1040
  /**
943
1041
  * Audit catalog metadata (title/tier/fix/authority/category) for this
944
1042
  * lexicon's `postSynthChecks`, keyed by check id — the per-provider half of
@@ -1912,7 +2010,13 @@ export interface ResourceMetadata {
1912
2010
  status: string;
1913
2011
  /** ISO timestamp of last update */
1914
2012
  lastUpdated?: string;
1915
- /** 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
+ */
1916
2020
  attributes?: Record<string, unknown>;
1917
2021
  /**
1918
2022
  * Live ownership verdict from the resource's marker (#119/#120). `owned` =
@@ -55,6 +55,7 @@
55
55
  */
56
56
 
57
57
  import { sortedJsonReplacer } from "../utils";
58
+ import { flagLegacyDigest, warnOnLegacyDigests, withoutReadFlags } from "./legacy-digest";
58
59
  import { writeBlobToPath, readBlobFromPath, listFilesInDir } from "./git";
59
60
  import type { BuildArchiveManifest } from "../components/verbs/build-archive";
60
61
 
@@ -86,7 +87,8 @@ export async function persistBuildManifest(
86
87
  manifest: BuildArchiveManifest,
87
88
  opts?: { cwd?: string },
88
89
  ): Promise<{ commit: string }> {
89
- 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);
90
92
  const commit = await writeBlobToPath(BUILDS_DIR, manifestFilename(manifest.manifestDigest), json, "Build manifest", opts);
91
93
  return { commit };
92
94
  }
@@ -104,11 +106,20 @@ export async function readBuildManifest(
104
106
  ): Promise<BuildArchiveManifest | null> {
105
107
  const content = await readBlobFromPath(BUILDS_DIR, manifestFilename(manifestDigest), opts);
106
108
  if (!content) return null;
109
+ let manifest: BuildArchiveManifest;
107
110
  try {
108
- return JSON.parse(content) as BuildArchiveManifest;
111
+ manifest = JSON.parse(content) as BuildArchiveManifest;
109
112
  } catch {
110
113
  return null;
111
114
  }
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);
112
123
  }
113
124
 
114
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);