@intentius/chant 0.102.0 → 0.104.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 (464) hide show
  1. package/dist/audit/core.d.ts +2 -0
  2. package/dist/audit/core.d.ts.map +1 -1
  3. package/dist/audit/discover.d.ts.map +1 -1
  4. package/dist/change-set.d.ts +232 -0
  5. package/dist/change-set.d.ts.map +1 -0
  6. package/dist/cli/commands/audit.d.ts +7 -0
  7. package/dist/cli/commands/audit.d.ts.map +1 -1
  8. package/dist/cli/commands/build.d.ts.map +1 -1
  9. package/dist/cli/handlers/build.d.ts.map +1 -1
  10. package/dist/cli/handlers/change-set.d.ts +11 -0
  11. package/dist/cli/handlers/change-set.d.ts.map +1 -0
  12. package/dist/cli/handlers/fan-out.d.ts +33 -0
  13. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  14. package/dist/cli/handlers/operator.d.ts +7 -0
  15. package/dist/cli/handlers/operator.d.ts.map +1 -1
  16. package/dist/cli/handlers/pr.d.ts +31 -0
  17. package/dist/cli/handlers/pr.d.ts.map +1 -0
  18. package/dist/cli/handlers/run.d.ts.map +1 -1
  19. package/dist/cli/main.d.ts.map +1 -1
  20. package/dist/cli/mcp/op-tools.d.ts.map +1 -1
  21. package/dist/cli/mcp/workspace-tools.d.ts +3 -2
  22. package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
  23. package/dist/cli/registry.d.ts +63 -1
  24. package/dist/cli/registry.d.ts.map +1 -1
  25. package/dist/components/auto-release.d.ts +16 -0
  26. package/dist/components/auto-release.d.ts.map +1 -1
  27. package/dist/components/capability.d.ts +53 -0
  28. package/dist/components/capability.d.ts.map +1 -1
  29. package/dist/components/changed-files.d.ts +31 -0
  30. package/dist/components/changed-files.d.ts.map +1 -0
  31. package/dist/components/cli-support.d.ts +9 -1
  32. package/dist/components/cli-support.d.ts.map +1 -1
  33. package/dist/components/component.d.ts +24 -0
  34. package/dist/components/component.d.ts.map +1 -1
  35. package/dist/components/deploy-units.d.ts.map +1 -1
  36. package/dist/components/driver.d.ts +13 -0
  37. package/dist/components/driver.d.ts.map +1 -1
  38. package/dist/components/fan-out-output.d.ts +6 -0
  39. package/dist/components/fan-out-output.d.ts.map +1 -1
  40. package/dist/components/fan-out-record.d.ts +41 -0
  41. package/dist/components/fan-out-record.d.ts.map +1 -0
  42. package/dist/components/fan-out-run.d.ts +65 -0
  43. package/dist/components/fan-out-run.d.ts.map +1 -1
  44. package/dist/components/fan-out-support.d.ts +2 -0
  45. package/dist/components/fan-out-support.d.ts.map +1 -1
  46. package/dist/components/fan-out.d.ts +6 -0
  47. package/dist/components/fan-out.d.ts.map +1 -1
  48. package/dist/components/gated-wave-pipeline.d.ts +50 -0
  49. package/dist/components/gated-wave-pipeline.d.ts.map +1 -0
  50. package/dist/components/index.d.ts +1 -1
  51. package/dist/components/index.d.ts.map +1 -1
  52. package/dist/components/layers.d.ts +25 -0
  53. package/dist/components/layers.d.ts.map +1 -0
  54. package/dist/components/pr-pipeline.d.ts +61 -0
  55. package/dist/components/pr-pipeline.d.ts.map +1 -0
  56. package/dist/components/pr-run.d.ts +119 -0
  57. package/dist/components/pr-run.d.ts.map +1 -0
  58. package/dist/components/promote.d.ts.map +1 -1
  59. package/dist/components/verbs/shell.d.ts.map +1 -1
  60. package/dist/components/wave-plan.d.ts +49 -0
  61. package/dist/components/wave-plan.d.ts.map +1 -0
  62. package/dist/gated-waves.d.ts +147 -0
  63. package/dist/gated-waves.d.ts.map +1 -0
  64. package/dist/index.d.ts +1 -0
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/lexicon.d.ts +48 -0
  67. package/dist/lexicon.d.ts.map +1 -1
  68. package/dist/lifecycle/gate-ledger.d.ts +7 -0
  69. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  70. package/dist/lifecycle/git.d.ts +46 -1
  71. package/dist/lifecycle/git.d.ts.map +1 -1
  72. package/dist/lint/post-synth.d.ts +18 -0
  73. package/dist/lint/post-synth.d.ts.map +1 -1
  74. package/dist/op/activities/activity-contracts.d.ts +126 -0
  75. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  76. package/dist/op/activities/change-set.d.ts +62 -0
  77. package/dist/op/activities/change-set.d.ts.map +1 -0
  78. package/dist/op/activities/decide.d.ts +2 -0
  79. package/dist/op/activities/decide.d.ts.map +1 -1
  80. package/dist/op/activities/factory.d.ts +137 -0
  81. package/dist/op/activities/factory.d.ts.map +1 -0
  82. package/dist/op/activities/index.d.ts +4 -0
  83. package/dist/op/activities/index.d.ts.map +1 -1
  84. package/dist/op/activity-contract.d.ts.map +1 -1
  85. package/dist/op/builders.d.ts +11 -0
  86. package/dist/op/builders.d.ts.map +1 -1
  87. package/dist/op/decide-backend.d.ts +5 -1
  88. package/dist/op/decide-backend.d.ts.map +1 -1
  89. package/dist/op/decide-read-inputs.d.ts +4 -2
  90. package/dist/op/decide-read-inputs.d.ts.map +1 -1
  91. package/dist/op/factory.d.ts +83 -0
  92. package/dist/op/factory.d.ts.map +1 -0
  93. package/dist/op/index.d.ts +4 -1
  94. package/dist/op/index.d.ts.map +1 -1
  95. package/dist/op/op-ir.d.ts +2 -0
  96. package/dist/op/op-ir.d.ts.map +1 -1
  97. package/dist/op/op-verb-class.d.ts.map +1 -1
  98. package/dist/op/types.d.ts +8 -0
  99. package/dist/op/types.d.ts.map +1 -1
  100. package/dist/op/work-lease-run.d.ts.map +1 -1
  101. package/dist/plan-summary.d.ts +207 -0
  102. package/dist/plan-summary.d.ts.map +1 -0
  103. package/dist/pr-forge.d.ts +105 -0
  104. package/dist/pr-forge.d.ts.map +1 -0
  105. package/dist/pr-loop.d.ts +178 -0
  106. package/dist/pr-loop.d.ts.map +1 -0
  107. package/dist/telemetry-attribution.d.ts +31 -0
  108. package/dist/telemetry-attribution.d.ts.map +1 -1
  109. package/dist/terraform/hcl2json.d.ts +45 -0
  110. package/dist/terraform/hcl2json.d.ts.map +1 -0
  111. package/dist/terraform/parse.d.ts +4 -40
  112. package/dist/terraform/parse.d.ts.map +1 -1
  113. package/dist/workspace/box-cli.d.ts +12 -0
  114. package/dist/workspace/box-cli.d.ts.map +1 -0
  115. package/dist/workspace/box-factory.d.ts +27 -3
  116. package/dist/workspace/box-factory.d.ts.map +1 -1
  117. package/dist/workspace/box-listing.d.ts +85 -0
  118. package/dist/workspace/box-listing.d.ts.map +1 -0
  119. package/dist/workspace/box-publish.d.ts +147 -0
  120. package/dist/workspace/box-publish.d.ts.map +1 -0
  121. package/dist/workspace/broker-protocol.d.ts +190 -0
  122. package/dist/workspace/broker-protocol.d.ts.map +1 -0
  123. package/dist/workspace/checks.d.ts.map +1 -1
  124. package/dist/workspace/composites.d.ts +2 -2
  125. package/dist/workspace/composites.d.ts.map +1 -1
  126. package/dist/workspace/conformance/__fixtures__/reference-broker.d.ts +17 -0
  127. package/dist/workspace/conformance/__fixtures__/reference-broker.d.ts.map +1 -0
  128. package/dist/workspace/conformance/broker.d.ts +202 -0
  129. package/dist/workspace/conformance/broker.d.ts.map +1 -0
  130. package/dist/workspace/conformance/index.d.ts +2 -1
  131. package/dist/workspace/conformance/index.d.ts.map +1 -1
  132. package/dist/workspace/conformance/vitest.d.ts +9 -2
  133. package/dist/workspace/conformance/vitest.d.ts.map +1 -1
  134. package/dist/workspace/conformance/writer.d.ts +108 -14
  135. package/dist/workspace/conformance/writer.d.ts.map +1 -1
  136. package/dist/workspace/decide.d.ts +126 -12
  137. package/dist/workspace/decide.d.ts.map +1 -1
  138. package/dist/workspace/decision-points.schema.json +11 -4
  139. package/dist/workspace/declaration.d.ts +65 -0
  140. package/dist/workspace/declaration.d.ts.map +1 -1
  141. package/dist/workspace/declaration.schema.json +153 -4
  142. package/dist/workspace/environments.d.ts +36 -10
  143. package/dist/workspace/environments.d.ts.map +1 -1
  144. package/dist/workspace/factory-rules.d.ts +180 -0
  145. package/dist/workspace/factory-rules.d.ts.map +1 -0
  146. package/dist/workspace/init.d.ts +21 -0
  147. package/dist/workspace/init.d.ts.map +1 -1
  148. package/dist/workspace/intent-cli.d.ts +1 -1
  149. package/dist/workspace/intent-cli.d.ts.map +1 -1
  150. package/dist/workspace/intent-record.d.ts +28 -1
  151. package/dist/workspace/intent-record.d.ts.map +1 -1
  152. package/dist/workspace/intent.d.ts +102 -5
  153. package/dist/workspace/intent.d.ts.map +1 -1
  154. package/dist/workspace/json-edit.d.ts +24 -0
  155. package/dist/workspace/json-edit.d.ts.map +1 -0
  156. package/dist/workspace/jsonc.d.ts +11 -0
  157. package/dist/workspace/jsonc.d.ts.map +1 -1
  158. package/dist/workspace/kinds.d.ts +48 -0
  159. package/dist/workspace/kinds.d.ts.map +1 -1
  160. package/dist/workspace/lexicon-symbols.d.ts +29 -0
  161. package/dist/workspace/lexicon-symbols.d.ts.map +1 -0
  162. package/dist/workspace/lineage-upgrade.d.ts +31 -2
  163. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  164. package/dist/workspace/point-answer.schema.json +42 -2
  165. package/dist/workspace/points-cli.d.ts +10 -3
  166. package/dist/workspace/points-cli.d.ts.map +1 -1
  167. package/dist/workspace/points.d.ts +66 -5
  168. package/dist/workspace/points.d.ts.map +1 -1
  169. package/dist/workspace/reason-codes.d.ts +33 -4
  170. package/dist/workspace/reason-codes.d.ts.map +1 -1
  171. package/dist/workspace/record-sessions.d.ts +5 -7
  172. package/dist/workspace/record-sessions.d.ts.map +1 -1
  173. package/dist/workspace/records-cli.d.ts +7 -0
  174. package/dist/workspace/records-cli.d.ts.map +1 -1
  175. package/dist/workspace/records-close.d.ts +7 -3
  176. package/dist/workspace/records-close.d.ts.map +1 -1
  177. package/dist/workspace/records-write.d.ts +66 -6
  178. package/dist/workspace/records-write.d.ts.map +1 -1
  179. package/dist/workspace/records.d.ts +28 -9
  180. package/dist/workspace/records.d.ts.map +1 -1
  181. package/dist/workspace/runs-cli.d.ts +138 -6
  182. package/dist/workspace/runs-cli.d.ts.map +1 -1
  183. package/dist/workspace/runs.d.ts +132 -11
  184. package/dist/workspace/runs.d.ts.map +1 -1
  185. package/dist/workspace/session-kinds.d.ts +1 -1
  186. package/dist/workspace/session-kinds.d.ts.map +1 -1
  187. package/dist/workspace/squash.d.ts +80 -0
  188. package/dist/workspace/squash.d.ts.map +1 -0
  189. package/dist/workspace/status-gates.d.ts +2 -0
  190. package/dist/workspace/status-gates.d.ts.map +1 -1
  191. package/dist/workspace/status.d.ts +54 -0
  192. package/dist/workspace/status.d.ts.map +1 -1
  193. package/dist/workspace/symbols.d.ts +16 -5
  194. package/dist/workspace/symbols.d.ts.map +1 -1
  195. package/dist/workspace/trust/run-commits.d.ts +46 -0
  196. package/dist/workspace/trust/run-commits.d.ts.map +1 -0
  197. package/dist/workspace/trust/run-statement.d.ts +128 -0
  198. package/dist/workspace/trust/run-statement.d.ts.map +1 -0
  199. package/dist/workspace/trust/seal.d.ts +6 -1
  200. package/dist/workspace/trust/seal.d.ts.map +1 -1
  201. package/dist/workspace/trust/verify-cli.d.ts +1 -1
  202. package/dist/workspace/trust/verify.d.ts +16 -2
  203. package/dist/workspace/trust/verify.d.ts.map +1 -1
  204. package/dist/workspace/wip-cli.d.ts +21 -0
  205. package/dist/workspace/wip-cli.d.ts.map +1 -0
  206. package/dist/workspace/wip.d.ts +268 -0
  207. package/dist/workspace/wip.d.ts.map +1 -0
  208. package/dist/workspace/work-cli.d.ts.map +1 -1
  209. package/dist/workspace/work-evidence.d.ts +1 -1
  210. package/dist/workspace/work-evidence.d.ts.map +1 -1
  211. package/dist/workspace/work-size.d.ts +55 -0
  212. package/dist/workspace/work-size.d.ts.map +1 -0
  213. package/dist/workspace/workspace-kinds.schema.json +122 -0
  214. package/dist/workspace/write-lock-cli.d.ts +60 -0
  215. package/dist/workspace/write-lock-cli.d.ts.map +1 -0
  216. package/dist/workspace/write-lock.d.ts +152 -0
  217. package/dist/workspace/write-lock.d.ts.map +1 -0
  218. package/dist/workspace/write-scope.d.ts +6 -2
  219. package/dist/workspace/write-scope.d.ts.map +1 -1
  220. package/package.json +6 -1
  221. package/src/__fixtures__/change-set/lifecycle-plan.floci.json +59 -0
  222. package/src/__fixtures__/change-set/lifecycle-v1.ts.txt +4 -0
  223. package/src/__fixtures__/change-set/lifecycle-v2.ts.txt +4 -0
  224. package/src/__fixtures__/change-set/lifecycle.change-set.golden.json +95 -0
  225. package/src/__fixtures__/change-set/warden.change-set.golden.json +106 -0
  226. package/src/__fixtures__/change-set/warden.plan.json +44 -0
  227. package/src/__fixtures__/plan-summary/normalization-vectors.json +1593 -0
  228. package/src/__fixtures__/pr-loop/pr-apply-applied.note.golden.md +31 -0
  229. package/src/__fixtures__/pr-loop/pr-apply-refused.note.golden.md +31 -0
  230. package/src/__fixtures__/pr-loop/pr-plan.golden.json +127 -0
  231. package/src/__fixtures__/pr-loop/pr-plan.note.golden.md +37 -0
  232. package/src/audit/core.test.ts +15 -0
  233. package/src/audit/core.ts +8 -2
  234. package/src/audit/discover.test.ts +11 -0
  235. package/src/audit/discover.ts +7 -2
  236. package/src/change-set-bundle.test.ts +57 -0
  237. package/src/change-set.test.ts +215 -0
  238. package/src/change-set.ts +445 -0
  239. package/src/cli/commands/audit-opt-in.test.ts +54 -0
  240. package/src/cli/commands/audit.ts +29 -2
  241. package/src/cli/commands/build.test.ts +19 -0
  242. package/src/cli/commands/build.ts +3 -2
  243. package/src/cli/handlers/build.ts +13 -1
  244. package/src/cli/handlers/change-set.test.ts +56 -0
  245. package/src/cli/handlers/change-set.ts +44 -0
  246. package/src/cli/handlers/fan-out.test.ts +221 -2
  247. package/src/cli/handlers/fan-out.ts +165 -75
  248. package/src/cli/handlers/graph.ts +3 -0
  249. package/src/cli/handlers/operator.test.ts +21 -0
  250. package/src/cli/handlers/operator.ts +34 -3
  251. package/src/cli/handlers/pr.ts +373 -0
  252. package/src/cli/handlers/run.ts +1 -0
  253. package/src/cli/main.test.ts +20 -0
  254. package/src/cli/main.ts +185 -23
  255. package/src/cli/mcp/never-over-mcp.test.ts +101 -0
  256. package/src/cli/mcp/op-tools.ts +24 -2
  257. package/src/cli/mcp/workspace-tools.test.ts +1 -1
  258. package/src/cli/mcp/workspace-tools.ts +34 -6
  259. package/src/cli/registry.ts +63 -1
  260. package/src/components/auto-release.ts +1 -1
  261. package/src/components/capability.ts +55 -0
  262. package/src/components/changed-files.ts +66 -0
  263. package/src/components/cli-support.ts +28 -5
  264. package/src/components/component.schema.json +17 -0
  265. package/src/components/component.ts +25 -0
  266. package/src/components/deploy-units.ts +3 -0
  267. package/src/components/driver.test.ts +52 -0
  268. package/src/components/driver.ts +31 -13
  269. package/src/components/fan-out-output.ts +42 -1
  270. package/src/components/fan-out-record.ts +69 -0
  271. package/src/components/fan-out-run.test.ts +17 -0
  272. package/src/components/fan-out-run.ts +304 -1
  273. package/src/components/fan-out-support.ts +3 -0
  274. package/src/components/fan-out-waves.test.ts +375 -0
  275. package/src/components/fan-out.ts +18 -17
  276. package/src/components/gated-wave-pipeline.ts +82 -0
  277. package/src/components/index.ts +1 -0
  278. package/src/components/layers.ts +30 -0
  279. package/src/components/pr-pipeline.ts +91 -0
  280. package/src/components/pr-run.test.ts +205 -0
  281. package/src/components/pr-run.ts +280 -0
  282. package/src/components/promote.ts +5 -0
  283. package/src/components/verbs/shell.test.ts +11 -0
  284. package/src/components/verbs/shell.ts +7 -3
  285. package/src/components/wave-plan.ts +124 -0
  286. package/src/discovery/sandbox/bundle.ts +1 -1
  287. package/src/gated-waves-bundle.test.ts +62 -0
  288. package/src/gated-waves.test.ts +118 -0
  289. package/src/gated-waves.ts +240 -0
  290. package/src/index.ts +1 -0
  291. package/src/lexicon.ts +51 -0
  292. package/src/lifecycle/gate-ledger.ts +7 -0
  293. package/src/lifecycle/git.ts +102 -16
  294. package/src/lint/post-synth.test.ts +19 -0
  295. package/src/lint/post-synth.ts +19 -1
  296. package/src/meta/declared-imports.test.ts +1 -1
  297. package/src/op/activities/activity-contracts.ts +84 -0
  298. package/src/op/activities/change-set.ts +114 -0
  299. package/src/op/activities/decide.ts +3 -0
  300. package/src/op/activities/factory.ts +675 -0
  301. package/src/op/activities/index.ts +6 -0
  302. package/src/op/activity-contract.test.ts +21 -1
  303. package/src/op/activity-contract.ts +19 -9
  304. package/src/op/builders.ts +23 -0
  305. package/src/op/decide-backend.ts +5 -1
  306. package/src/op/decide-read-inputs.ts +33 -5
  307. package/src/op/factory.e2e.test.ts +232 -0
  308. package/src/op/factory.ts +118 -0
  309. package/src/op/index.ts +9 -1
  310. package/src/op/op-ir.test.ts +9 -0
  311. package/src/op/op-ir.ts +4 -0
  312. package/src/op/op-verb-class.ts +3 -0
  313. package/src/op/types.ts +8 -0
  314. package/src/op/work-lease-run.ts +3 -0
  315. package/src/plan-summary.test.ts +263 -0
  316. package/src/plan-summary.ts +845 -0
  317. package/src/pr-forge.test.ts +237 -0
  318. package/src/pr-forge.ts +332 -0
  319. package/src/pr-loop-bundle.test.ts +46 -0
  320. package/src/pr-loop.test.ts +171 -0
  321. package/src/pr-loop.ts +324 -0
  322. package/src/telemetry-attribution.test.ts +23 -1
  323. package/src/telemetry-attribution.ts +51 -0
  324. package/src/terraform/hcl2json.test.ts +21 -0
  325. package/src/terraform/hcl2json.ts +81 -0
  326. package/src/terraform/parse.ts +11 -74
  327. package/src/workspace/__fixtures__/studio-template-84c8b21.json +1 -1
  328. package/src/workspace/app-kind.test.ts +196 -0
  329. package/src/workspace/box-cli.ts +113 -0
  330. package/src/workspace/box-factory.test.ts +4 -1
  331. package/src/workspace/box-factory.ts +37 -4
  332. package/src/workspace/box-listing-write.schema.json +204 -0
  333. package/src/workspace/box-listing.test.ts +234 -0
  334. package/src/workspace/box-listing.ts +324 -0
  335. package/src/workspace/box-publish.schema.json +498 -0
  336. package/src/workspace/box-publish.test.ts +204 -0
  337. package/src/workspace/box-publish.ts +420 -0
  338. package/src/workspace/box-ship.test.ts +96 -0
  339. package/src/workspace/broker-protocol.schema.json +186 -0
  340. package/src/workspace/broker-protocol.test.ts +122 -0
  341. package/src/workspace/broker-protocol.ts +317 -0
  342. package/src/workspace/builder-tiers.test.ts +146 -0
  343. package/src/workspace/change-set.schema.json +460 -0
  344. package/src/workspace/chant-migrations.e2e.test.ts +17 -5
  345. package/src/workspace/checks/boxes.test.ts +3 -0
  346. package/src/workspace/checks.ts +10 -3
  347. package/src/workspace/component-environments.test.ts +105 -0
  348. package/src/workspace/composites.schema.json +23 -4
  349. package/src/workspace/composites.test.ts +8 -8
  350. package/src/workspace/composites.ts +13 -2
  351. package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +2 -2
  352. package/src/workspace/conformance/__fixtures__/reference-broker.ts +207 -0
  353. package/src/workspace/conformance/__writer_fixture__/answers/answer.kind.mjs +7 -2
  354. package/src/workspace/conformance/__writer_fixture__/answers/answer.schema.json +42 -2
  355. package/src/workspace/conformance/__writer_fixture__/contracts/contract.kind.mjs +37 -0
  356. package/src/workspace/conformance/__writer_fixture__/contracts/contract.schema.json +213 -0
  357. package/src/workspace/conformance/__writer_fixture__/decisions/points.json +96 -0
  358. package/src/workspace/conformance/__writer_fixture__/drivers/driver.kind.mjs +15 -0
  359. package/src/workspace/conformance/__writer_fixture__/drivers/driver.schema.json +38 -0
  360. package/src/workspace/conformance/__writer_fixture__/sessions/session.kind.mjs +14 -8
  361. package/src/workspace/conformance/__writer_fixture__/sessions/session.schema.json +187 -6
  362. package/src/workspace/conformance/__writer_fixture__/work/work.schema.json +25 -0
  363. package/src/workspace/conformance/broker.test.ts +122 -0
  364. package/src/workspace/conformance/broker.ts +792 -0
  365. package/src/workspace/conformance/conformance.e2e.test.ts +2 -1
  366. package/src/workspace/conformance/index.mjs +22 -1
  367. package/src/workspace/conformance/index.ts +5 -1
  368. package/src/workspace/conformance/vitest.ts +33 -1
  369. package/src/workspace/conformance/writer-conformance.e2e.test.ts +49 -3
  370. package/src/workspace/conformance/writer.test.ts +32 -2
  371. package/src/workspace/conformance/writer.ts +339 -28
  372. package/src/workspace/decide.test.ts +208 -4
  373. package/src/workspace/decide.ts +427 -73
  374. package/src/workspace/decision-points.schema.json +11 -4
  375. package/src/workspace/declaration.schema.json +153 -4
  376. package/src/workspace/declaration.ts +141 -0
  377. package/src/workspace/environments.ts +66 -14
  378. package/src/workspace/factory-rules.test.ts +62 -0
  379. package/src/workspace/factory-rules.ts +218 -0
  380. package/src/workspace/graph-cli.ts +2 -2
  381. package/src/workspace/graph.schema.json +67 -0
  382. package/src/workspace/identity.test.ts +23 -0
  383. package/src/workspace/init.ts +82 -2
  384. package/src/workspace/intent-cli.ts +26 -9
  385. package/src/workspace/intent-patch-id.test.ts +162 -0
  386. package/src/workspace/intent-record.schema.json +130 -3
  387. package/src/workspace/intent-record.ts +74 -28
  388. package/src/workspace/intent-reviews.test.ts +76 -0
  389. package/src/workspace/intent-squash.test.ts +213 -0
  390. package/src/workspace/intent-why.test.ts +5 -5
  391. package/src/workspace/intent.schema.json +234 -4
  392. package/src/workspace/intent.ts +260 -66
  393. package/src/workspace/json-edit.test.ts +121 -0
  394. package/src/workspace/json-edit.ts +142 -0
  395. package/src/workspace/jsonc.ts +20 -0
  396. package/src/workspace/kinds.ts +157 -2
  397. package/src/workspace/lexicon-symbols.test.ts +148 -0
  398. package/src/workspace/lexicon-symbols.ts +125 -0
  399. package/src/workspace/lineage-upgrade.test.ts +63 -0
  400. package/src/workspace/lineage-upgrade.ts +146 -5
  401. package/src/workspace/plan-summary.schema.json +155 -0
  402. package/src/workspace/point-answer.schema.json +42 -2
  403. package/src/workspace/points-cli.ts +42 -16
  404. package/src/workspace/points-write.schema.json +162 -4
  405. package/src/workspace/points.schema.json +157 -2
  406. package/src/workspace/points.test.ts +13 -1
  407. package/src/workspace/points.ts +118 -12
  408. package/src/workspace/pr-report.schema.json +95 -0
  409. package/src/workspace/profiles.test.ts +159 -0
  410. package/src/workspace/read-contract.test.ts +2 -2
  411. package/src/workspace/reason-codes.test.ts +13 -1
  412. package/src/workspace/reason-codes.ts +38 -4
  413. package/src/workspace/record-sessions.ts +9 -8
  414. package/src/workspace/records-amend.schema.json +78 -0
  415. package/src/workspace/records-cli.ts +20 -6
  416. package/src/workspace/records-close.schema.json +78 -0
  417. package/src/workspace/records-close.ts +31 -7
  418. package/src/workspace/records-concurrent.e2e.test.ts +135 -0
  419. package/src/workspace/records-concurrent.test.ts +145 -0
  420. package/src/workspace/records-formats.test.ts +46 -10
  421. package/src/workspace/records-new.schema.json +7 -0
  422. package/src/workspace/records-review.schema.json +78 -0
  423. package/src/workspace/records-sessions-write.test.ts +69 -0
  424. package/src/workspace/records-sessions.test.ts +22 -0
  425. package/src/workspace/records-write.ts +192 -25
  426. package/src/workspace/records.schema.json +16 -0
  427. package/src/workspace/records.ts +50 -9
  428. package/src/workspace/reference-kinds/app/package.json +10 -0
  429. package/src/workspace/reference-kinds/app/workspace-kinds.json +45 -0
  430. package/src/workspace/run-statement.schema.json +622 -0
  431. package/src/workspace/runs-cli.ts +343 -14
  432. package/src/workspace/runs-statement.test.ts +239 -0
  433. package/src/workspace/runs-write.schema.json +416 -5
  434. package/src/workspace/runs.schema.json +451 -4
  435. package/src/workspace/runs.test.ts +39 -0
  436. package/src/workspace/runs.ts +296 -35
  437. package/src/workspace/session-kinds.ts +3 -3
  438. package/src/workspace/squash.ts +198 -0
  439. package/src/workspace/status-contract.test.ts +5 -2
  440. package/src/workspace/status-gates.ts +3 -1
  441. package/src/workspace/status.schema.json +126 -3
  442. package/src/workspace/status.ts +90 -1
  443. package/src/workspace/symbols.ts +20 -9
  444. package/src/workspace/trust/run-commits.ts +115 -0
  445. package/src/workspace/trust/run-statement.ts +226 -0
  446. package/src/workspace/trust/seal.ts +8 -2
  447. package/src/workspace/trust/verify-cli.ts +6 -5
  448. package/src/workspace/trust/verify.ts +39 -6
  449. package/src/workspace/wip-cli.ts +97 -0
  450. package/src/workspace/wip-write.schema.json +504 -0
  451. package/src/workspace/wip.schema.json +334 -0
  452. package/src/workspace/wip.test.ts +294 -0
  453. package/src/workspace/wip.ts +753 -0
  454. package/src/workspace/work-cli.ts +3 -0
  455. package/src/workspace/work-evidence.schema.json +3 -0
  456. package/src/workspace/work-size.test.ts +77 -0
  457. package/src/workspace/work-size.ts +93 -0
  458. package/src/workspace/workspace-kinds.schema.json +122 -0
  459. package/src/workspace/write-lock-cli.ts +131 -0
  460. package/src/workspace/write-lock.schema.json +172 -0
  461. package/src/workspace/write-lock.test.ts +189 -0
  462. package/src/workspace/write-lock.ts +474 -0
  463. package/src/workspace/write-scope.test.ts +16 -1
  464. package/src/workspace/write-scope.ts +33 -11
@@ -0,0 +1,171 @@
1
+ /**
2
+ * The pull-request loop's report and note (#3183): the schema, the note a
3
+ * reviewer reads at each stage, and the status line.
4
+ *
5
+ * The report is what the terragucci plan report (#3349) reads, so the JSON
6
+ * has a golden, and so does the note rendered from it.
7
+ */
8
+
9
+ import { readFileSync, writeFileSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { describe, expect, test } from "vitest";
12
+ import { composeChangeSet, type ChangeSetPart } from "./change-set";
13
+ import {
14
+ describePlanChanged,
15
+ memberCounts,
16
+ PR_REPORT_SCHEMA_ID,
17
+ prApproveCommand,
18
+ prNoteMarker,
19
+ prOp,
20
+ prStatusDescription,
21
+ prStatusState,
22
+ renderPrNote,
23
+ type PrReport,
24
+ } from "./pr-loop";
25
+ import { contract, validSchema } from "./workspace/__fixtures__/contract-repo";
26
+ import schema from "./workspace/pr-report.schema.json";
27
+ import changeSetSchema from "./workspace/change-set.schema.json";
28
+
29
+ const { expectValid } = contract(schema);
30
+ const { expectValid: expectValidChangeSet } = contract(changeSetSchema);
31
+
32
+ const DIR = join(import.meta.dirname, "__fixtures__", "pr-loop");
33
+
34
+ function golden(name: string, text: string): void {
35
+ const path = join(DIR, name);
36
+ if (process.env.UPDATE_GOLDEN) writeFileSync(path, text);
37
+ expect(text).toBe(readFileSync(path, "utf-8"));
38
+ }
39
+
40
+ const hex = (n: number) => n.toString(16).padStart(64, "0");
41
+
42
+ function part(member: string, n: number, entries: Array<{ address: string; action: "create" | "update" | "delete" | "replace" }>): ChangeSetPart {
43
+ return {
44
+ member: { member, lexicon: "terraform", planner: "tofu", status: "planned", planDigest: `jcs1-sha256:${hex(n)}`, holes: [] },
45
+ entries: entries.map((e) => ({
46
+ member,
47
+ lexicon: "terraform",
48
+ planner: "tofu",
49
+ address: e.address,
50
+ type: e.address.split(".")[0],
51
+ action: e.action,
52
+ attributes: e.action === "update" ? [{ path: "input", before: "v1", after: "v2" }] : [],
53
+ })),
54
+ };
55
+ }
56
+
57
+ const doc = composeChangeSet([
58
+ part("a", 1, [{ address: "terraform_data.subnet", action: "update" }]),
59
+ part("app", 2, []),
60
+ ]);
61
+
62
+ const planReport: PrReport = {
63
+ $schema: PR_REPORT_SCHEMA_ID,
64
+ contract: 1,
65
+ stage: "plan",
66
+ pr: 12,
67
+ env: "prod",
68
+ base: "1".repeat(40),
69
+ head: "2".repeat(40),
70
+ op: prOp(12),
71
+ gate: "pr-apply",
72
+ digest: doc.digest,
73
+ status: "planned",
74
+ approval: { status: "pending" },
75
+ selection: { changed: ["a"], dependents: ["app"], unclaimed: [], indeterminate: [], waves: [["a"], ["app"]] },
76
+ members: [
77
+ { member: "a", component: "a", planDigest: `jcs1-sha256:${hex(1)}`, status: "planned", counts: memberCounts(doc, "a") },
78
+ { member: "app", component: "app", planDigest: `jcs1-sha256:${hex(2)}`, status: "planned", counts: memberCounts(doc, "app") },
79
+ ],
80
+ changeSet: doc,
81
+ };
82
+
83
+ const changedDigest = `jcs1-sha256:${hex(99)}`;
84
+ const refusedReport: PrReport = {
85
+ ...planReport,
86
+ stage: "apply",
87
+ status: "refused",
88
+ refusal: "plan-changed",
89
+ approval: { status: "changed", approved: changedDigest },
90
+ message: describePlanChanged({ op: planReport.op, gate: planReport.gate, digest: planReport.digest, approval: { status: "changed", approved: changedDigest } }),
91
+ };
92
+
93
+ const appliedReport: PrReport = {
94
+ ...planReport,
95
+ stage: "apply",
96
+ status: "applied",
97
+ approval: { status: "approved", approvedBy: ["github:alice"] },
98
+ members: [
99
+ { ...planReport.members[0], status: "applied" },
100
+ { ...planReport.members[1], status: "applied", inputsMoved: ["a"] },
101
+ ],
102
+ };
103
+
104
+ describe("the pr-report schema", () => {
105
+ test("is a valid schema and names this module's id", () => {
106
+ expect(validSchema(schema)).toBe(true);
107
+ expect(schema.$id).toBe(PR_REPORT_SCHEMA_ID);
108
+ });
109
+
110
+ test("every stage's report validates, and its change set validates against the change-set schema", () => {
111
+ for (const report of [planReport, refusedReport, appliedReport]) {
112
+ expectValid(report);
113
+ expectValidChangeSet(report.changeSet);
114
+ expect(report.changeSet.digest).toBe(report.digest);
115
+ }
116
+ });
117
+
118
+ test("the report has a golden", () => {
119
+ golden("pr-plan.golden.json", JSON.stringify(planReport, null, 2) + "\n");
120
+ });
121
+ });
122
+
123
+ describe("the note", () => {
124
+ test("the plan stage names every member, the digest and the approve command, then the grouped summary", () => {
125
+ const note = renderPrNote(planReport, { approver: "github:<your-login>" });
126
+ expect(note.startsWith(prNoteMarker("prod"))).toBe(true);
127
+ expect(note).toContain(`chant approve pr-12 pr-apply --plan ${doc.digest} --approver github:<your-login> --sign`);
128
+ expect(note).toContain("### Plan summary");
129
+ golden("pr-plan.note.golden.md", note);
130
+ });
131
+
132
+ test("a refused apply names both digests and says nothing applied", () => {
133
+ const note = renderPrNote(refusedReport);
134
+ expect(note).toContain("The apply refused.");
135
+ expect(note).toContain("nothing was applied");
136
+ expect(note).toContain(`approved: ${changedDigest}; planned now: ${doc.digest}`);
137
+ golden("pr-apply-refused.note.golden.md", note);
138
+ });
139
+
140
+ test("an applied member whose inputs moved says so", () => {
141
+ const note = renderPrNote(appliedReport);
142
+ expect(note).toContain("applied, inputs from a moved");
143
+ expect(note).toContain("Approved by `github:alice` for this digest.");
144
+ golden("pr-apply-applied.note.golden.md", note);
145
+ });
146
+
147
+ test("a small limit leaves the summary out rather than cutting the table", () => {
148
+ const note = renderPrNote(planReport, { limit: 800 });
149
+ expect(note).toContain("| `app` |");
150
+ expect(note).not.toContain("### Plan summary");
151
+ });
152
+ });
153
+
154
+ describe("the status", () => {
155
+ test("describes each stage in at most 140 characters", () => {
156
+ expect(prStatusDescription(planReport)).toMatch(/^2 members: 1 to change, 0 to destroy or replace; plan [0-9a-f]{12}$/);
157
+ expect(prStatusDescription(refusedReport)).toBe("refused: the plan changed after review");
158
+ expect(prStatusDescription(appliedReport)).toMatch(/^applied 2 members/);
159
+ for (const r of [planReport, refusedReport, appliedReport]) expect(prStatusDescription(r).length).toBeLessThanOrEqual(140);
160
+ });
161
+
162
+ test("an unapproved apply is pending, a moved plan fails, an applied one succeeds", () => {
163
+ expect(prStatusState({ ...refusedReport, refusal: "not-approved" })).toBe("pending");
164
+ expect(prStatusState(refusedReport)).toBe("failure");
165
+ expect(prStatusState(appliedReport)).toBe("success");
166
+ });
167
+
168
+ test("the approve command binds the digest and asks for a signature", () => {
169
+ expect(prApproveCommand(planReport, "gitlab:ana")).toBe(`chant approve pr-12 pr-apply --plan ${doc.digest} --approver gitlab:ana --sign`);
170
+ });
171
+ });
package/src/pr-loop.ts ADDED
@@ -0,0 +1,324 @@
1
+ /**
2
+ * The pull-request loop (#3183): plan the members a change reaches, show the
3
+ * plan on the pull request, and apply it on merge only when the plan is the
4
+ * one a reviewer approved.
5
+ *
6
+ * `chant components pr-plan` and `chant components pr-apply` run it, and the
7
+ * generated CI for GitHub, GitLab and Forgejo calls them
8
+ * (`components/pr-pipeline.ts`). This module holds the parts that need no
9
+ * runner: the gate's names, the report both commands write, the pull-request
10
+ * note rendered from it and the status line. terragucci reads the report and
11
+ * renders the note from one bundled file with no TypeScript toolchain
12
+ * (#3421), so this imports `./change-set` and `./plan-summary` and nothing
13
+ * else; `pr-loop-bundle.test.ts` holds that.
14
+ *
15
+ * ## What the gate binds
16
+ *
17
+ * One gate per pull request: op `pr-<number>`, gate `pr-apply` unless the
18
+ * pipeline names another. It binds the digest of the change-set document over
19
+ * every planned member (`changeSetDigest`, #3181), the same digest a gated
20
+ * wave binds over its members (#3049). A reviewer approves it with `chant
21
+ * approve pr-<number> pr-apply --plan <digest>`. The op carries the number so
22
+ * that one pull request's approval never answers another's, and the gate name
23
+ * stays fixed so a workspace can require signed approvals for it under
24
+ * `identity.gates` (#3163).
25
+ *
26
+ * On merge the apply plans the same members again, before anything applies,
27
+ * and decides the gate against the new digest. The same plan applies; a moved
28
+ * plan stops with both digests named and nothing applied.
29
+ */
30
+
31
+ import type { ChangeSetAction, ChangeSetDocument } from "./change-set";
32
+ import { GITHUB_COMMENT_LIMIT, groupChangeSet, renderPlanSummaryMarkdown } from "./plan-summary";
33
+
34
+ /** The schema a pull-request report names in `$schema`. */
35
+ export const PR_REPORT_SCHEMA_ID = "https://intentius.io/chant/schemas/workspace/pr-report/v1/pr-report.schema.json";
36
+
37
+ /** The read-contract version the report follows. */
38
+ export const PR_REPORT_CONTRACT = 1;
39
+
40
+ /** The gate a pull request's apply waits on, unless the pipeline names another. */
41
+ export const PR_APPLY_GATE = "pr-apply";
42
+
43
+ /** The op a pull request's gate is recorded under: `pr-<number>`. */
44
+ export function prOp(pr: number): string {
45
+ return `pr-${pr}`;
46
+ }
47
+
48
+ /** The hidden line that marks the pull request's note, one per environment. */
49
+ export function prNoteMarker(env: string): string {
50
+ return `<!-- chant-pr:${env} -->`;
51
+ }
52
+
53
+ /** The status contexts the two stages set on the commit. */
54
+ export const PR_STATUS_CONTEXTS = { plan: "chant/plan", apply: "chant/apply" } as const;
55
+
56
+ /** Changes a member's plan proposes, by the actions a reviewer reads. */
57
+ export interface PrMemberCounts {
58
+ create: number;
59
+ update: number;
60
+ replace: number;
61
+ delete: number;
62
+ }
63
+
64
+ /** Where one member got to. */
65
+ export type PrMemberStatus =
66
+ /** Planned, nothing applied (the plan stage, or an apply that stopped). */
67
+ | "planned"
68
+ /** The plan failed. */
69
+ | "plan-failed"
70
+ /** Applied the approved plan. */
71
+ | "applied"
72
+ /** The apply failed. */
73
+ | "failed"
74
+ /** Not applied because a member it depends on failed. */
75
+ | "blocked";
76
+
77
+ export interface PrMember {
78
+ /** The member's name in the change set: a root, for a terraform-family step. */
79
+ member: string;
80
+ /** The component whose step planned it. */
81
+ component: string;
82
+ /** What a gate on this member alone binds. `null` when it failed to plan. */
83
+ planDigest: string | null;
84
+ status: PrMemberStatus;
85
+ counts: PrMemberCounts;
86
+ error?: string;
87
+ /**
88
+ * Components this one depends on whose outputs changed when they applied
89
+ * (apply stage). Its applied plan read the values from before, so the next
90
+ * run plans it again.
91
+ */
92
+ inputsMoved?: string[];
93
+ }
94
+
95
+ /** Where the approval stands for the planned digest. */
96
+ export interface PrApproval {
97
+ /**
98
+ * `approved`: an approval stands for this digest. `changed`: one stands for
99
+ * another digest, so the plan moved since someone approved it. `pending`:
100
+ * none stands.
101
+ */
102
+ status: "approved" | "changed" | "pending";
103
+ /** Who approved this digest. */
104
+ approvedBy?: string[];
105
+ /** With `changed`: the digest that was approved. */
106
+ approved?: string;
107
+ }
108
+
109
+ /** How a stage ended. */
110
+ export type PrStatus =
111
+ /** Plan stage: every member planned. */
112
+ | "planned"
113
+ /** Plan stage: a member failed to plan. Apply stage: the same, so nothing applied. */
114
+ | "plan-failed"
115
+ /** Nothing the change touches is deployed by a component. */
116
+ | "nothing"
117
+ /** Apply stage: every member applied. */
118
+ | "applied"
119
+ /** Apply stage: a member failed to apply. Its dependents did not run. */
120
+ | "failed"
121
+ /** Apply stage: nothing applied, see `refusal`. */
122
+ | "refused";
123
+
124
+ /** Why an apply refused. */
125
+ export type PrRefusal =
126
+ /** No approval stands for any plan of this pull request. */
127
+ | "not-approved"
128
+ /** The approval stands for another digest: the plan changed after review. */
129
+ | "plan-changed"
130
+ /** The approval names someone who has not approved the pull request on the forge. */
131
+ | "not-a-reviewer"
132
+ /** No pull request merged this commit, so there is no approval to read. */
133
+ | "no-pull-request";
134
+
135
+ /**
136
+ * The document `pr-plan` and `pr-apply` write (`pr-report.schema.json`).
137
+ * The terragucci plan report (#3349) reads it; fields are only ever added.
138
+ */
139
+ export interface PrReport {
140
+ $schema: typeof PR_REPORT_SCHEMA_ID;
141
+ contract: typeof PR_REPORT_CONTRACT;
142
+ stage: "plan" | "apply";
143
+ /** The pull or merge request number, when known. */
144
+ pr: number | null;
145
+ env: string;
146
+ /** The commit the change is measured from: the merge base of the given base and `head`. */
147
+ base: string;
148
+ head: string;
149
+ /** The gate's ledger entry: `_gates/<op>.jsonl` on `chant/lifecycle`. */
150
+ op: string;
151
+ gate: string;
152
+ /** The change-set digest of what this stage planned. What the gate binds. */
153
+ digest: string;
154
+ status: PrStatus;
155
+ refusal?: PrRefusal;
156
+ /** The refusal or failure, as printed. */
157
+ message?: string;
158
+ approval: PrApproval;
159
+ /** Component names: which changed, which run because they depend on one, and what the change touched that no component deploys. */
160
+ selection: {
161
+ changed: string[];
162
+ dependents: string[];
163
+ unclaimed: string[];
164
+ indeterminate: string[];
165
+ /** The components in dependency order, a wave per entry. */
166
+ waves: string[][];
167
+ };
168
+ members: PrMember[];
169
+ changeSet: ChangeSetDocument;
170
+ }
171
+
172
+ /** The counts a member's entries add up to. */
173
+ export function memberCounts(doc: Pick<ChangeSetDocument, "summary">, member: string): PrMemberCounts {
174
+ const by = doc.summary.byMember[member] ?? {};
175
+ const n = (a: ChangeSetAction) => by[a] ?? 0;
176
+ return { create: n("create"), update: n("update"), replace: n("replace"), delete: n("delete") };
177
+ }
178
+
179
+ /** The approve command a reviewer runs for this report's digest. */
180
+ export function prApproveCommand(report: Pick<PrReport, "op" | "gate" | "digest">, approver = "<you>"): string {
181
+ return `chant approve ${report.op} ${report.gate} --plan ${report.digest} --approver ${approver} --sign`;
182
+ }
183
+
184
+ /**
185
+ * The refusal when the approved digest is not the planned one: both digests,
186
+ * that nothing applied, and how to close it.
187
+ */
188
+ export function describePlanChanged(report: Pick<PrReport, "op" | "gate" | "digest" | "approval">): string {
189
+ return (
190
+ `The plan changed after review, so nothing was applied. ` +
191
+ `approved: ${report.approval.approved ?? "(none)"}; planned now: ${report.digest}. ` +
192
+ `Read the new plan, and if it is right, approve it: ${prApproveCommand(report)}; then run the apply again.`
193
+ );
194
+ }
195
+
196
+ const short = (sha: string): string => sha.slice(0, 12);
197
+ const code = (s: string): string => "`" + s.replaceAll("`", "'") + "`";
198
+
199
+ const STATUS_WORDS: Record<PrMemberStatus, string> = {
200
+ planned: "planned",
201
+ "plan-failed": "plan failed",
202
+ applied: "applied",
203
+ failed: "apply failed",
204
+ blocked: "blocked",
205
+ };
206
+
207
+ function headline(report: PrReport): string {
208
+ const n = report.members.length;
209
+ const members = `${n} member${n === 1 ? "" : "s"}`;
210
+ switch (report.status) {
211
+ case "nothing":
212
+ return "Nothing to plan: no component deploys what this change touches.";
213
+ case "planned":
214
+ return `Planned ${members}.`;
215
+ case "plan-failed":
216
+ return `Planning failed for ${report.members.filter((m) => m.status === "plan-failed").length} of ${members}, so nothing can apply.`;
217
+ case "applied":
218
+ return `Applied ${members}.`;
219
+ case "failed":
220
+ return `The apply failed: ${report.members.filter((m) => m.status === "failed").length} failed, ${report.members.filter((m) => m.status === "applied").length} applied.`;
221
+ case "refused":
222
+ return "The apply refused.";
223
+ }
224
+ }
225
+
226
+ function approvalLines(report: PrReport, approver: string): string[] {
227
+ const a = report.approval;
228
+ if (report.status === "nothing") return [];
229
+ if (a.status === "approved") return [`Approved by ${(a.approvedBy ?? []).map(code).join(", ")} for this digest.`];
230
+ const lines =
231
+ a.status === "changed"
232
+ ? [`An approval stands for ${code(a.approved ?? "(none)")}, not for this digest: the plan changed since it was approved.`]
233
+ : ["Not approved yet."];
234
+ if (report.stage === "plan" && report.status === "planned") {
235
+ lines.push(
236
+ "",
237
+ "A reviewer who approved this pull request approves the plan with:",
238
+ "",
239
+ "```",
240
+ prApproveCommand(report, approver),
241
+ "```",
242
+ "",
243
+ "It applies on merge if the plan is still this one. If it moved, the apply refuses and names both digests.",
244
+ );
245
+ }
246
+ return lines;
247
+ }
248
+
249
+ /**
250
+ * The pull-request note: the stage's headline, a row per member, the digest
251
+ * and the approval, then the grouped plan summary (#3188) in whatever room
252
+ * `limit` leaves. The note starts with {@link prNoteMarker}, so the same
253
+ * comment is updated by every push and by the apply.
254
+ */
255
+ export function renderPrNote(report: PrReport, options: { limit?: number; approver?: string } = {}): string {
256
+ const limit = options.limit ?? GITHUB_COMMENT_LIMIT;
257
+ const out: string[] = [
258
+ prNoteMarker(report.env),
259
+ `### chant ${report.stage === "plan" ? "plan" : "apply"} for ${code(report.env)}`,
260
+ "",
261
+ `${headline(report)} Head ${code(short(report.head))}, measured from ${code(short(report.base))}.`,
262
+ ];
263
+ if (report.message) out.push("", `> ${report.message.split("\n").join("\n> ")}`);
264
+ if (report.members.length > 0) {
265
+ out.push("", "| Member | Component | Status | Create | Update | Replace | Delete |", "|---|---|---|---:|---:|---:|---:|");
266
+ for (const m of report.members) {
267
+ const status = STATUS_WORDS[m.status] + (m.inputsMoved?.length ? `, inputs from ${m.inputsMoved.join(", ")} moved` : "");
268
+ out.push(`| ${code(m.member)} | ${m.component} | ${status} | ${m.counts.create} | ${m.counts.update} | ${m.counts.replace} | ${m.counts.delete} |`);
269
+ }
270
+ if (report.members.some((m) => m.inputsMoved?.length)) {
271
+ out.push("", "A member whose inputs moved applied the plan it was approved with. The next run plans it against the new values.");
272
+ }
273
+ }
274
+ if (report.selection.unclaimed.length > 0) {
275
+ out.push("", `Changed, and deployed by no component: ${report.selection.unclaimed.map(code).join(", ")}.`);
276
+ }
277
+ if (report.status !== "nothing") {
278
+ out.push("", `Plan digest: ${code(report.digest)}`, "", ...approvalLines(report, options.approver ?? "<you>"));
279
+ }
280
+ const head = out.join("\n") + "\n";
281
+ if (report.status === "nothing" || report.changeSet.members.length === 0) return head;
282
+ const room = limit - [...head].length - 1;
283
+ if (room < 400) return head;
284
+ return head + "\n" + renderPlanSummaryMarkdown(groupChangeSet(report.changeSet), { limit: room, command: "chant change-set summary .chant/pr/change-set.json" });
285
+ }
286
+
287
+ /** A commit status's description, at most 140 characters (GitHub's limit). */
288
+ export function prStatusDescription(report: PrReport): string {
289
+ const n = report.members.length;
290
+ const sum = report.members.reduce(
291
+ (acc, m) => ({ change: acc.change + m.counts.create + m.counts.update + m.counts.replace, destroy: acc.destroy + m.counts.delete + m.counts.replace }),
292
+ { change: 0, destroy: 0 },
293
+ );
294
+ const digest = report.digest ? `; plan ${report.digest.replace(/^(?:jcs1-)?sha256:/, "").slice(0, 12)}` : "";
295
+ let text: string;
296
+ switch (report.status) {
297
+ case "nothing":
298
+ text = "nothing to plan";
299
+ break;
300
+ case "planned":
301
+ text = `${n} member${n === 1 ? "" : "s"}: ${sum.change} to change, ${sum.destroy} to destroy or replace${digest}`;
302
+ break;
303
+ case "plan-failed":
304
+ text = `a member failed to plan${digest}`;
305
+ break;
306
+ case "applied":
307
+ text = `applied ${n} member${n === 1 ? "" : "s"}${digest}`;
308
+ break;
309
+ case "failed":
310
+ text = `apply failed for ${report.members.filter((m) => m.status === "failed").map((m) => m.member).join(", ")}`;
311
+ break;
312
+ case "refused":
313
+ text = `refused: ${report.refusal === "plan-changed" ? "the plan changed after review" : report.refusal === "not-a-reviewer" ? "the approver has not approved the pull request" : report.refusal === "no-pull-request" ? "no pull request merged this commit" : "the plan is not approved"}`;
314
+ break;
315
+ }
316
+ return text.length > 140 ? text.slice(0, 137) + "..." : text;
317
+ }
318
+
319
+ /** The commit status state for a report: success, failure, or pending while it waits on approval. */
320
+ export function prStatusState(report: PrReport): "success" | "failure" | "pending" {
321
+ if (report.status === "planned" || report.status === "applied" || report.status === "nothing") return "success";
322
+ if (report.status === "refused" && report.refusal === "not-approved") return "pending";
323
+ return "failure";
324
+ }
@@ -2,7 +2,15 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { afterAll, describe, expect, test } from "vitest";
5
- import { mergeResourceAttributes, resolveTelemetryAttribution, TELEMETRY_ATTRIBUTES, telemetryEnvironment } from "./telemetry-attribution";
5
+ import {
6
+ mergeResourceAttributes,
7
+ RELEASE_ATTRIBUTES_VARIABLE,
8
+ releaseAttributesSuffix,
9
+ releaseEnvironment,
10
+ resolveTelemetryAttribution,
11
+ TELEMETRY_ATTRIBUTES,
12
+ telemetryEnvironment,
13
+ } from "./telemetry-attribution";
6
14
 
7
15
  const scratch = mkdtempSync(join(tmpdir(), "chant-2558-"));
8
16
  afterAll(() => rmSync(scratch, { recursive: true, force: true }));
@@ -89,3 +97,17 @@ describe("mergeResourceAttributes", () => {
89
97
  expect(mergeResourceAttributes("", "a=1")).toBe("a=1");
90
98
  });
91
99
  });
100
+
101
+ describe("release attributes (#3061, ws-081)", () => {
102
+ test("the suffix starts with a comma, percent-encodes, and is empty when the release knows nothing", () => {
103
+ expect(releaseAttributesSuffix({ version: "sha256:abc", revision: "0123abc" })).toBe(",service.version=sha256%3Aabc,vcs.ref.head.revision=0123abc");
104
+ expect(releaseAttributesSuffix({ revision: "0123abc" })).toBe(",vcs.ref.head.revision=0123abc");
105
+ expect(releaseAttributesSuffix({})).toBe("");
106
+ expect(releaseAttributesSuffix(undefined)).toBe("");
107
+ });
108
+
109
+ test("the environment carries the suffix under CHANT_RELEASE_ATTRIBUTES, or nothing", () => {
110
+ expect(releaseEnvironment({ revision: "r" })).toEqual({ [RELEASE_ATTRIBUTES_VARIABLE]: ",vcs.ref.head.revision=r" });
111
+ expect(releaseEnvironment({})).toEqual({});
112
+ });
113
+ });
@@ -143,3 +143,54 @@ export function mergeResourceAttributes(existing: string, extra: string): string
143
143
  const added = extra.split(",").filter((kv) => kv !== "" && !have.has(kv.split("=")[0]!));
144
144
  return [existing, ...added].filter((s) => s !== "").join(",");
145
145
  }
146
+
147
+ // ── Release attributes (#3061, ws-081) ─────────────────────────────
148
+
149
+ /**
150
+ * The environment variable a deploy sets to the release attributes, as a
151
+ * suffix for `OTEL_RESOURCE_ATTRIBUTES`. A stamped workload's generated
152
+ * output appends it where the platform expands variables (Compose
153
+ * interpolation, a Kubernetes `$(VAR)` reference), so the committed files are
154
+ * the same bytes whatever is released.
155
+ */
156
+ export const RELEASE_ATTRIBUTES_VARIABLE = "CHANT_RELEASE_ATTRIBUTES";
157
+
158
+ /**
159
+ * The pod-template annotation a Kubernetes release writes the suffix to. A
160
+ * stamped container reads it into {@link RELEASE_ATTRIBUTES_VARIABLE} through
161
+ * the downward API; an absent annotation reads as empty.
162
+ */
163
+ export const RELEASE_ATTRIBUTES_ANNOTATION = "chant.intentius.io/release-attributes";
164
+
165
+ /** What the step that deploys a release knows: the artifact it promoted and the commit it came from. */
166
+ export interface ReleaseIdentity {
167
+ /** `service.version`: the digest of the artifact the run promoted, the release record's `digest`. */
168
+ version?: string;
169
+ /** `vcs.ref.head.revision`: the commit, the release record's `gitSha`. */
170
+ revision?: string;
171
+ }
172
+
173
+ /**
174
+ * The release attributes as a suffix for `OTEL_RESOURCE_ATTRIBUTES`: each
175
+ * pair preceded by a comma and percent-encoded the way
176
+ * {@link telemetryEnvironment} encodes, or `""` when the release knows
177
+ * neither. The build-time list always has `chant.decl`, so the suffix never
178
+ * starts the list.
179
+ */
180
+ export function releaseAttributesSuffix(release: ReleaseIdentity | undefined): string {
181
+ if (!release) return "";
182
+ const pairs: [string, string | undefined][] = [
183
+ ["service.version", release.version],
184
+ ["vcs.ref.head.revision", release.revision],
185
+ ];
186
+ return pairs
187
+ .filter((p): p is [string, string] => p[1] !== undefined && p[1] !== "")
188
+ .map(([k, v]) => `,${k}=${encodeURIComponent(v)}`)
189
+ .join("");
190
+ }
191
+
192
+ /** The variables a deploy step exports for a release: {@link RELEASE_ATTRIBUTES_VARIABLE}, or nothing. */
193
+ export function releaseEnvironment(release: ReleaseIdentity | undefined): Record<string, string> {
194
+ const suffix = releaseAttributesSuffix(release);
195
+ return suffix === "" ? {} : { [RELEASE_ATTRIBUTES_VARIABLE]: suffix };
196
+ }
@@ -0,0 +1,21 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { readFileSync } from "fs";
3
+ import { join } from "path";
4
+
5
+ describe("hcl2json loader", () => {
6
+ // A bundle that only reads HCL must not load the graph, the state reader or
7
+ // the carve tables behind them.
8
+ test("imports nothing from the graph, state or carve modules", () => {
9
+ const src = readFileSync(join(__dirname, "hcl2json.ts"), "utf-8");
10
+ const local = [...src.matchAll(/from "(\.\/[^"]+)"/g)].map((m) => m[1]);
11
+ expect(local.every((p) => p === "./types")).toBe(true);
12
+ });
13
+
14
+ test("parse.ts still re-exports the loader API", async () => {
15
+ const parse = await import("./parse");
16
+ const loader = await import("./hcl2json");
17
+ expect(parse.loadHcl2json).toBe(loader.loadHcl2json);
18
+ expect(parse.Hcl2JsonNotInstalled).toBe(loader.Hcl2JsonNotInstalled);
19
+ expect(parse.HCL2JSON_RECORD_ENV).toBe(loader.HCL2JSON_RECORD_ENV);
20
+ });
21
+ });