@intentius/chant 0.102.0 → 0.104.1

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 (471) 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/vendor/js-yaml-load.d.ts +56 -0
  114. package/dist/vendor/js-yaml-load.d.ts.map +1 -0
  115. package/dist/workspace/box-cli.d.ts +12 -0
  116. package/dist/workspace/box-cli.d.ts.map +1 -0
  117. package/dist/workspace/box-factory.d.ts +27 -3
  118. package/dist/workspace/box-factory.d.ts.map +1 -1
  119. package/dist/workspace/box-listing.d.ts +85 -0
  120. package/dist/workspace/box-listing.d.ts.map +1 -0
  121. package/dist/workspace/box-publish.d.ts +147 -0
  122. package/dist/workspace/box-publish.d.ts.map +1 -0
  123. package/dist/workspace/broker-protocol.d.ts +190 -0
  124. package/dist/workspace/broker-protocol.d.ts.map +1 -0
  125. package/dist/workspace/checks.d.ts.map +1 -1
  126. package/dist/workspace/composites.d.ts +2 -2
  127. package/dist/workspace/composites.d.ts.map +1 -1
  128. package/dist/workspace/conformance/__fixtures__/reference-broker.d.ts +17 -0
  129. package/dist/workspace/conformance/__fixtures__/reference-broker.d.ts.map +1 -0
  130. package/dist/workspace/conformance/broker.d.ts +202 -0
  131. package/dist/workspace/conformance/broker.d.ts.map +1 -0
  132. package/dist/workspace/conformance/index.d.ts +2 -1
  133. package/dist/workspace/conformance/index.d.ts.map +1 -1
  134. package/dist/workspace/conformance/vitest.d.ts +9 -2
  135. package/dist/workspace/conformance/vitest.d.ts.map +1 -1
  136. package/dist/workspace/conformance/writer.d.ts +108 -14
  137. package/dist/workspace/conformance/writer.d.ts.map +1 -1
  138. package/dist/workspace/decide.d.ts +126 -12
  139. package/dist/workspace/decide.d.ts.map +1 -1
  140. package/dist/workspace/decision-points.schema.json +11 -4
  141. package/dist/workspace/declaration.d.ts +65 -0
  142. package/dist/workspace/declaration.d.ts.map +1 -1
  143. package/dist/workspace/declaration.schema.json +153 -4
  144. package/dist/workspace/environments.d.ts +36 -10
  145. package/dist/workspace/environments.d.ts.map +1 -1
  146. package/dist/workspace/factory-rules.d.ts +180 -0
  147. package/dist/workspace/factory-rules.d.ts.map +1 -0
  148. package/dist/workspace/init.d.ts +21 -0
  149. package/dist/workspace/init.d.ts.map +1 -1
  150. package/dist/workspace/intent-cli.d.ts +1 -1
  151. package/dist/workspace/intent-cli.d.ts.map +1 -1
  152. package/dist/workspace/intent-record.d.ts +28 -1
  153. package/dist/workspace/intent-record.d.ts.map +1 -1
  154. package/dist/workspace/intent.d.ts +102 -5
  155. package/dist/workspace/intent.d.ts.map +1 -1
  156. package/dist/workspace/json-edit.d.ts +24 -0
  157. package/dist/workspace/json-edit.d.ts.map +1 -0
  158. package/dist/workspace/jsonc.d.ts +11 -0
  159. package/dist/workspace/jsonc.d.ts.map +1 -1
  160. package/dist/workspace/kinds.d.ts +48 -0
  161. package/dist/workspace/kinds.d.ts.map +1 -1
  162. package/dist/workspace/lexicon-symbols.d.ts +29 -0
  163. package/dist/workspace/lexicon-symbols.d.ts.map +1 -0
  164. package/dist/workspace/lineage-upgrade.d.ts +31 -2
  165. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  166. package/dist/workspace/point-answer.schema.json +42 -2
  167. package/dist/workspace/points-cli.d.ts +10 -3
  168. package/dist/workspace/points-cli.d.ts.map +1 -1
  169. package/dist/workspace/points.d.ts +66 -5
  170. package/dist/workspace/points.d.ts.map +1 -1
  171. package/dist/workspace/reason-codes.d.ts +33 -4
  172. package/dist/workspace/reason-codes.d.ts.map +1 -1
  173. package/dist/workspace/record-sessions.d.ts +5 -7
  174. package/dist/workspace/record-sessions.d.ts.map +1 -1
  175. package/dist/workspace/records-cli.d.ts +7 -0
  176. package/dist/workspace/records-cli.d.ts.map +1 -1
  177. package/dist/workspace/records-close.d.ts +7 -3
  178. package/dist/workspace/records-close.d.ts.map +1 -1
  179. package/dist/workspace/records-write.d.ts +66 -6
  180. package/dist/workspace/records-write.d.ts.map +1 -1
  181. package/dist/workspace/records.d.ts +28 -9
  182. package/dist/workspace/records.d.ts.map +1 -1
  183. package/dist/workspace/runs-cli.d.ts +138 -6
  184. package/dist/workspace/runs-cli.d.ts.map +1 -1
  185. package/dist/workspace/runs.d.ts +132 -11
  186. package/dist/workspace/runs.d.ts.map +1 -1
  187. package/dist/workspace/session-kinds.d.ts +1 -1
  188. package/dist/workspace/session-kinds.d.ts.map +1 -1
  189. package/dist/workspace/squash.d.ts +80 -0
  190. package/dist/workspace/squash.d.ts.map +1 -0
  191. package/dist/workspace/status-gates.d.ts +2 -0
  192. package/dist/workspace/status-gates.d.ts.map +1 -1
  193. package/dist/workspace/status.d.ts +54 -0
  194. package/dist/workspace/status.d.ts.map +1 -1
  195. package/dist/workspace/symbols.d.ts +16 -5
  196. package/dist/workspace/symbols.d.ts.map +1 -1
  197. package/dist/workspace/trust/run-commits.d.ts +46 -0
  198. package/dist/workspace/trust/run-commits.d.ts.map +1 -0
  199. package/dist/workspace/trust/run-statement.d.ts +128 -0
  200. package/dist/workspace/trust/run-statement.d.ts.map +1 -0
  201. package/dist/workspace/trust/seal.d.ts +6 -1
  202. package/dist/workspace/trust/seal.d.ts.map +1 -1
  203. package/dist/workspace/trust/verify-cli.d.ts +1 -1
  204. package/dist/workspace/trust/verify.d.ts +16 -2
  205. package/dist/workspace/trust/verify.d.ts.map +1 -1
  206. package/dist/workspace/wip-cli.d.ts +21 -0
  207. package/dist/workspace/wip-cli.d.ts.map +1 -0
  208. package/dist/workspace/wip.d.ts +268 -0
  209. package/dist/workspace/wip.d.ts.map +1 -0
  210. package/dist/workspace/work-cli.d.ts.map +1 -1
  211. package/dist/workspace/work-evidence.d.ts +1 -1
  212. package/dist/workspace/work-evidence.d.ts.map +1 -1
  213. package/dist/workspace/work-size.d.ts +55 -0
  214. package/dist/workspace/work-size.d.ts.map +1 -0
  215. package/dist/workspace/workspace-kinds.schema.json +122 -0
  216. package/dist/workspace/write-lock-cli.d.ts +60 -0
  217. package/dist/workspace/write-lock-cli.d.ts.map +1 -0
  218. package/dist/workspace/write-lock.d.ts +152 -0
  219. package/dist/workspace/write-lock.d.ts.map +1 -0
  220. package/dist/workspace/write-scope.d.ts +6 -2
  221. package/dist/workspace/write-scope.d.ts.map +1 -1
  222. package/dist/yaml.d.ts +1 -1
  223. package/dist/yaml.d.ts.map +1 -1
  224. package/package.json +6 -1
  225. package/src/__fixtures__/change-set/lifecycle-plan.floci.json +59 -0
  226. package/src/__fixtures__/change-set/lifecycle-v1.ts.txt +4 -0
  227. package/src/__fixtures__/change-set/lifecycle-v2.ts.txt +4 -0
  228. package/src/__fixtures__/change-set/lifecycle.change-set.golden.json +95 -0
  229. package/src/__fixtures__/change-set/warden.change-set.golden.json +106 -0
  230. package/src/__fixtures__/change-set/warden.plan.json +44 -0
  231. package/src/__fixtures__/plan-summary/normalization-vectors.json +1593 -0
  232. package/src/__fixtures__/pr-loop/pr-apply-applied.note.golden.md +31 -0
  233. package/src/__fixtures__/pr-loop/pr-apply-refused.note.golden.md +31 -0
  234. package/src/__fixtures__/pr-loop/pr-plan.golden.json +127 -0
  235. package/src/__fixtures__/pr-loop/pr-plan.note.golden.md +37 -0
  236. package/src/audit/core.test.ts +15 -0
  237. package/src/audit/core.ts +8 -2
  238. package/src/audit/discover.test.ts +11 -0
  239. package/src/audit/discover.ts +7 -2
  240. package/src/change-set-bundle.test.ts +57 -0
  241. package/src/change-set.test.ts +215 -0
  242. package/src/change-set.ts +445 -0
  243. package/src/cli/commands/audit-opt-in.test.ts +54 -0
  244. package/src/cli/commands/audit.ts +29 -2
  245. package/src/cli/commands/build.test.ts +19 -0
  246. package/src/cli/commands/build.ts +3 -2
  247. package/src/cli/handlers/build.ts +13 -1
  248. package/src/cli/handlers/change-set.test.ts +56 -0
  249. package/src/cli/handlers/change-set.ts +44 -0
  250. package/src/cli/handlers/fan-out.test.ts +221 -2
  251. package/src/cli/handlers/fan-out.ts +165 -75
  252. package/src/cli/handlers/graph.ts +3 -0
  253. package/src/cli/handlers/operator.test.ts +21 -0
  254. package/src/cli/handlers/operator.ts +34 -3
  255. package/src/cli/handlers/pr.ts +373 -0
  256. package/src/cli/handlers/run.ts +1 -0
  257. package/src/cli/main.test.ts +20 -0
  258. package/src/cli/main.ts +185 -23
  259. package/src/cli/mcp/never-over-mcp.test.ts +101 -0
  260. package/src/cli/mcp/op-tools.ts +24 -2
  261. package/src/cli/mcp/workspace-tools.test.ts +1 -1
  262. package/src/cli/mcp/workspace-tools.ts +34 -6
  263. package/src/cli/registry.ts +63 -1
  264. package/src/components/auto-release.ts +1 -1
  265. package/src/components/capability.ts +55 -0
  266. package/src/components/changed-files.ts +66 -0
  267. package/src/components/cli-support.ts +28 -5
  268. package/src/components/component.schema.json +17 -0
  269. package/src/components/component.ts +25 -0
  270. package/src/components/deploy-units.ts +3 -0
  271. package/src/components/driver.test.ts +52 -0
  272. package/src/components/driver.ts +31 -13
  273. package/src/components/fan-out-output.ts +42 -1
  274. package/src/components/fan-out-record.ts +69 -0
  275. package/src/components/fan-out-run.test.ts +17 -0
  276. package/src/components/fan-out-run.ts +304 -1
  277. package/src/components/fan-out-support.ts +3 -0
  278. package/src/components/fan-out-waves.test.ts +375 -0
  279. package/src/components/fan-out.ts +18 -17
  280. package/src/components/gated-wave-pipeline.ts +82 -0
  281. package/src/components/index.ts +1 -0
  282. package/src/components/layers.ts +30 -0
  283. package/src/components/pr-pipeline.ts +91 -0
  284. package/src/components/pr-run.test.ts +205 -0
  285. package/src/components/pr-run.ts +280 -0
  286. package/src/components/promote.ts +5 -0
  287. package/src/components/verbs/shell.test.ts +11 -0
  288. package/src/components/verbs/shell.ts +7 -3
  289. package/src/components/wave-plan.ts +124 -0
  290. package/src/discovery/sandbox/bundle.ts +1 -1
  291. package/src/gated-waves-bundle.test.ts +62 -0
  292. package/src/gated-waves.test.ts +118 -0
  293. package/src/gated-waves.ts +240 -0
  294. package/src/index.ts +1 -0
  295. package/src/lexicon.ts +51 -0
  296. package/src/lifecycle/gate-ledger.ts +7 -0
  297. package/src/lifecycle/git.ts +102 -16
  298. package/src/lint/post-synth.test.ts +19 -0
  299. package/src/lint/post-synth.ts +19 -1
  300. package/src/meta/declared-imports.test.ts +1 -1
  301. package/src/op/activities/activity-contracts.ts +84 -0
  302. package/src/op/activities/change-set.ts +114 -0
  303. package/src/op/activities/decide.ts +3 -0
  304. package/src/op/activities/factory.ts +675 -0
  305. package/src/op/activities/index.ts +6 -0
  306. package/src/op/activity-contract.test.ts +21 -1
  307. package/src/op/activity-contract.ts +19 -9
  308. package/src/op/builders.ts +23 -0
  309. package/src/op/decide-backend.ts +5 -1
  310. package/src/op/decide-read-inputs.ts +33 -5
  311. package/src/op/factory.e2e.test.ts +232 -0
  312. package/src/op/factory.ts +118 -0
  313. package/src/op/index.ts +9 -1
  314. package/src/op/op-ir.test.ts +9 -0
  315. package/src/op/op-ir.ts +4 -0
  316. package/src/op/op-verb-class.ts +3 -0
  317. package/src/op/types.ts +8 -0
  318. package/src/op/work-lease-run.ts +3 -0
  319. package/src/plan-summary.test.ts +263 -0
  320. package/src/plan-summary.ts +845 -0
  321. package/src/pr-forge.test.ts +237 -0
  322. package/src/pr-forge.ts +332 -0
  323. package/src/pr-loop-bundle.test.ts +46 -0
  324. package/src/pr-loop.test.ts +171 -0
  325. package/src/pr-loop.ts +324 -0
  326. package/src/telemetry-attribution.test.ts +23 -1
  327. package/src/telemetry-attribution.ts +51 -0
  328. package/src/terraform/hcl2json.test.ts +21 -0
  329. package/src/terraform/hcl2json.ts +81 -0
  330. package/src/terraform/parse.ts +11 -74
  331. package/src/vendor/js-yaml-load.test.ts +115 -0
  332. package/src/vendor/js-yaml-load.ts +1939 -0
  333. package/src/workspace/__fixtures__/studio-template-84c8b21.json +1 -1
  334. package/src/workspace/app-kind.test.ts +196 -0
  335. package/src/workspace/box-cli.ts +113 -0
  336. package/src/workspace/box-factory.test.ts +4 -1
  337. package/src/workspace/box-factory.ts +37 -4
  338. package/src/workspace/box-listing-write.schema.json +204 -0
  339. package/src/workspace/box-listing.test.ts +234 -0
  340. package/src/workspace/box-listing.ts +324 -0
  341. package/src/workspace/box-publish.schema.json +498 -0
  342. package/src/workspace/box-publish.test.ts +204 -0
  343. package/src/workspace/box-publish.ts +420 -0
  344. package/src/workspace/box-ship.test.ts +96 -0
  345. package/src/workspace/broker-protocol.schema.json +186 -0
  346. package/src/workspace/broker-protocol.test.ts +122 -0
  347. package/src/workspace/broker-protocol.ts +317 -0
  348. package/src/workspace/builder-tiers.test.ts +146 -0
  349. package/src/workspace/change-set.schema.json +460 -0
  350. package/src/workspace/chant-migrations.e2e.test.ts +17 -5
  351. package/src/workspace/checks/boxes.test.ts +3 -0
  352. package/src/workspace/checks.ts +10 -3
  353. package/src/workspace/component-environments.test.ts +105 -0
  354. package/src/workspace/composites.schema.json +23 -4
  355. package/src/workspace/composites.test.ts +8 -8
  356. package/src/workspace/composites.ts +13 -2
  357. package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +2 -2
  358. package/src/workspace/conformance/__fixtures__/reference-broker.ts +207 -0
  359. package/src/workspace/conformance/__writer_fixture__/answers/answer.kind.mjs +7 -2
  360. package/src/workspace/conformance/__writer_fixture__/answers/answer.schema.json +42 -2
  361. package/src/workspace/conformance/__writer_fixture__/contracts/contract.kind.mjs +37 -0
  362. package/src/workspace/conformance/__writer_fixture__/contracts/contract.schema.json +213 -0
  363. package/src/workspace/conformance/__writer_fixture__/decisions/points.json +96 -0
  364. package/src/workspace/conformance/__writer_fixture__/drivers/driver.kind.mjs +15 -0
  365. package/src/workspace/conformance/__writer_fixture__/drivers/driver.schema.json +38 -0
  366. package/src/workspace/conformance/__writer_fixture__/sessions/session.kind.mjs +14 -8
  367. package/src/workspace/conformance/__writer_fixture__/sessions/session.schema.json +187 -6
  368. package/src/workspace/conformance/__writer_fixture__/work/work.schema.json +25 -0
  369. package/src/workspace/conformance/broker.test.ts +122 -0
  370. package/src/workspace/conformance/broker.ts +792 -0
  371. package/src/workspace/conformance/conformance.e2e.test.ts +2 -1
  372. package/src/workspace/conformance/index.mjs +22 -1
  373. package/src/workspace/conformance/index.ts +5 -1
  374. package/src/workspace/conformance/vitest.ts +33 -1
  375. package/src/workspace/conformance/writer-conformance.e2e.test.ts +49 -3
  376. package/src/workspace/conformance/writer.test.ts +32 -2
  377. package/src/workspace/conformance/writer.ts +339 -28
  378. package/src/workspace/decide.test.ts +208 -4
  379. package/src/workspace/decide.ts +427 -73
  380. package/src/workspace/decision-points.schema.json +11 -4
  381. package/src/workspace/declaration.schema.json +153 -4
  382. package/src/workspace/declaration.ts +141 -0
  383. package/src/workspace/environments.ts +66 -14
  384. package/src/workspace/factory-rules.test.ts +62 -0
  385. package/src/workspace/factory-rules.ts +218 -0
  386. package/src/workspace/graph-cli.ts +2 -2
  387. package/src/workspace/graph.schema.json +67 -0
  388. package/src/workspace/identity.test.ts +23 -0
  389. package/src/workspace/init.ts +82 -2
  390. package/src/workspace/intent-cli.ts +26 -9
  391. package/src/workspace/intent-patch-id.test.ts +162 -0
  392. package/src/workspace/intent-record.schema.json +130 -3
  393. package/src/workspace/intent-record.ts +74 -28
  394. package/src/workspace/intent-reviews.test.ts +76 -0
  395. package/src/workspace/intent-squash.test.ts +213 -0
  396. package/src/workspace/intent-why.test.ts +5 -5
  397. package/src/workspace/intent.schema.json +234 -4
  398. package/src/workspace/intent.ts +260 -66
  399. package/src/workspace/json-edit.test.ts +121 -0
  400. package/src/workspace/json-edit.ts +142 -0
  401. package/src/workspace/jsonc.ts +20 -0
  402. package/src/workspace/kinds.ts +157 -2
  403. package/src/workspace/lexicon-symbols.test.ts +148 -0
  404. package/src/workspace/lexicon-symbols.ts +125 -0
  405. package/src/workspace/lineage-upgrade.test.ts +63 -0
  406. package/src/workspace/lineage-upgrade.ts +146 -5
  407. package/src/workspace/plan-summary.schema.json +155 -0
  408. package/src/workspace/point-answer.schema.json +42 -2
  409. package/src/workspace/points-cli.ts +42 -16
  410. package/src/workspace/points-write.schema.json +162 -4
  411. package/src/workspace/points.schema.json +157 -2
  412. package/src/workspace/points.test.ts +13 -1
  413. package/src/workspace/points.ts +118 -12
  414. package/src/workspace/pr-report.schema.json +95 -0
  415. package/src/workspace/profiles.test.ts +159 -0
  416. package/src/workspace/read-contract.test.ts +2 -2
  417. package/src/workspace/reason-codes.test.ts +13 -1
  418. package/src/workspace/reason-codes.ts +38 -4
  419. package/src/workspace/record-sessions.ts +9 -8
  420. package/src/workspace/records-amend.schema.json +78 -0
  421. package/src/workspace/records-cli.ts +20 -6
  422. package/src/workspace/records-close.schema.json +78 -0
  423. package/src/workspace/records-close.ts +31 -7
  424. package/src/workspace/records-concurrent.e2e.test.ts +135 -0
  425. package/src/workspace/records-concurrent.test.ts +145 -0
  426. package/src/workspace/records-formats.test.ts +46 -10
  427. package/src/workspace/records-new.schema.json +7 -0
  428. package/src/workspace/records-review.schema.json +78 -0
  429. package/src/workspace/records-sessions-write.test.ts +69 -0
  430. package/src/workspace/records-sessions.test.ts +22 -0
  431. package/src/workspace/records-write.ts +192 -25
  432. package/src/workspace/records.schema.json +16 -0
  433. package/src/workspace/records.ts +50 -9
  434. package/src/workspace/reference-kinds/app/package.json +10 -0
  435. package/src/workspace/reference-kinds/app/workspace-kinds.json +45 -0
  436. package/src/workspace/run-statement.schema.json +622 -0
  437. package/src/workspace/runs-cli.ts +343 -14
  438. package/src/workspace/runs-statement.test.ts +239 -0
  439. package/src/workspace/runs-write.schema.json +416 -5
  440. package/src/workspace/runs.schema.json +451 -4
  441. package/src/workspace/runs.test.ts +39 -0
  442. package/src/workspace/runs.ts +296 -35
  443. package/src/workspace/session-kinds.ts +3 -3
  444. package/src/workspace/squash.ts +198 -0
  445. package/src/workspace/status-contract.test.ts +5 -2
  446. package/src/workspace/status-gates.ts +3 -1
  447. package/src/workspace/status.schema.json +126 -3
  448. package/src/workspace/status.ts +90 -1
  449. package/src/workspace/symbols.ts +20 -9
  450. package/src/workspace/trust/run-commits.ts +115 -0
  451. package/src/workspace/trust/run-statement.ts +226 -0
  452. package/src/workspace/trust/seal.ts +8 -2
  453. package/src/workspace/trust/verify-cli.ts +6 -5
  454. package/src/workspace/trust/verify.ts +39 -6
  455. package/src/workspace/wip-cli.ts +97 -0
  456. package/src/workspace/wip-write.schema.json +504 -0
  457. package/src/workspace/wip.schema.json +334 -0
  458. package/src/workspace/wip.test.ts +294 -0
  459. package/src/workspace/wip.ts +753 -0
  460. package/src/workspace/work-cli.ts +3 -0
  461. package/src/workspace/work-evidence.schema.json +3 -0
  462. package/src/workspace/work-size.test.ts +77 -0
  463. package/src/workspace/work-size.ts +93 -0
  464. package/src/workspace/workspace-kinds.schema.json +122 -0
  465. package/src/workspace/write-lock-cli.ts +131 -0
  466. package/src/workspace/write-lock.schema.json +172 -0
  467. package/src/workspace/write-lock.test.ts +189 -0
  468. package/src/workspace/write-lock.ts +474 -0
  469. package/src/workspace/write-scope.test.ts +16 -1
  470. package/src/workspace/write-scope.ts +33 -11
  471. package/src/yaml.ts +31 -21
@@ -0,0 +1,445 @@
1
+ /**
2
+ * One change-set document across planners (#3181).
3
+ *
4
+ * Several things plan changes, each with its own output: `chant lifecycle
5
+ * plan` for the cloud lexicons, terraform, tofu and choudoufu plans, and a
6
+ * warden's reconcile plan. This module is the one typed document they all
7
+ * become, so a reader can ask one question of every plan in a run: what will
8
+ * change, where, and how risky it is. Its schema is
9
+ * `./workspace/change-set.schema.json`, part of the workspace read contract.
10
+ *
11
+ * Each planner reaches it through an adapter that returns a
12
+ * {@link ChangeSetPart}: one member and its entries. The lifecycle and
13
+ * reconcile adapters are here; the terraform-family ones live in the
14
+ * terraform lexicon's `change-set` subpath. {@link composeChangeSet} joins
15
+ * the parts.
16
+ *
17
+ * ## The digest
18
+ *
19
+ * Every member carries the plan digest a gate on that member alone binds
20
+ * (#2300): `terraformPlanDigest` for a terraform-family root, and here
21
+ * `computePlanDigest("lifecycle-plan", …)` and `computePlanDigest(
22
+ * "reconcile-plan", …)`. The document's digest is {@link changeSetDigest}
23
+ * over the sorted `{ member, planDigest }` pairs, the set digest #3049
24
+ * describes, so a wave's digest is the same function over the wave's
25
+ * members. The entries and the summary are derived from the plans those
26
+ * digests cover and are not hashed again: a change to how chant projects a
27
+ * plan into entries does not expire an approval.
28
+ *
29
+ * ## The import graph
30
+ *
31
+ * terragucci runs this in customer CI as one bundled file with no TypeScript
32
+ * toolchain (#3421). Everything here imports `./lifecycle/plan-digest` and
33
+ * types only, and `change-set-bundle.test.ts` holds that.
34
+ */
35
+
36
+ import { computePlanDigest } from "./lifecycle/plan-digest";
37
+ import type { ChangeSet as LifecycleChangeSet, ChangeSetEntry as LifecycleEntry } from "./lifecycle/change-set";
38
+ import type { ChangeSet as ReconcileChangeSet } from "./reconcile";
39
+
40
+ /** The schema a change-set document names in `$schema`. */
41
+ export const CHANGE_SET_SCHEMA_ID = "https://intentius.io/chant/schemas/workspace/change-set/v1/change-set.schema.json";
42
+
43
+ /** The read-contract version the document follows. */
44
+ export const CHANGE_SET_CONTRACT = 1;
45
+
46
+ /** What a planner proposes for one resource. `no-op`, `read` and `forget` appear only when the planner reports them. */
47
+ export type ChangeSetAction = "create" | "update" | "replace" | "delete" | "read" | "no-op" | "forget";
48
+
49
+ /** Every action, in the order the summary lists them. */
50
+ export const CHANGE_SET_ACTIONS: readonly ChangeSetAction[] = ["create", "update", "replace", "delete", "read", "no-op", "forget"];
51
+
52
+ /** How much applying one change hurts, in the vocabulary of `./lifecycle/disruption.ts`. */
53
+ export type ChangeSetDisruption = "in-place" | "rolling" | "replace" | "destroy" | "unknown";
54
+
55
+ /** Which tool produced a member's plan. */
56
+ export type ChangeSetPlanner = "terraform" | "tofu" | "choudoufu" | "chant" | "warden";
57
+
58
+ /** One attribute a change writes, at top-level granularity. */
59
+ export interface ChangeSetAttribute {
60
+ path: string;
61
+ /** The value before. Absent on a create, and when `sensitive`. */
62
+ before?: unknown;
63
+ /** The value after. Absent on a delete, when `unknown`, and when `sensitive`. */
64
+ after?: unknown;
65
+ /** The value is known only after apply. */
66
+ unknown?: true;
67
+ /** The planner marked the value sensitive, so neither side is carried. */
68
+ sensitive?: true;
69
+ /** Changing this attribute is what forces the replacement. */
70
+ forcesReplacement?: true;
71
+ }
72
+
73
+ export interface ChangeSetEntry {
74
+ member: string;
75
+ lexicon: string;
76
+ planner: ChangeSetPlanner;
77
+ /** The planner's own address for the resource, unique within the member with `deposed`. */
78
+ address: string;
79
+ type: string;
80
+ /** The resource's name, without module path or index, when the planner's address has one. */
81
+ name?: string;
82
+ /** A `count` or `for_each` index. */
83
+ index?: string | number;
84
+ /** The module path the resource sits in. */
85
+ module?: string;
86
+ /** A deposed object's key: a second entry at the same address. */
87
+ deposed?: string;
88
+ /** The provider's own id, when the planner reports one. */
89
+ id?: string;
90
+ action: ChangeSetAction;
91
+ /** The entry also imports the object into state (an `import` block). Its action is whatever else the plan does to it, `no-op` included. */
92
+ importing?: true;
93
+ /** Set on update, replace and delete. `unknown` means nobody could say. */
94
+ disruption?: ChangeSetDisruption;
95
+ region?: string;
96
+ /** The estate, environment or org the change lands in. */
97
+ scope?: string;
98
+ attributes: ChangeSetAttribute[];
99
+ }
100
+
101
+ /** Something a planner could not read, so the plan says nothing about it. */
102
+ export interface ChangeSetHole {
103
+ address: string;
104
+ type?: string;
105
+ reason: string;
106
+ }
107
+
108
+ export interface ChangeSetMember {
109
+ member: string;
110
+ lexicon: string;
111
+ planner: ChangeSetPlanner;
112
+ scope?: string;
113
+ status: "planned" | "failed";
114
+ error?: string;
115
+ /** What a gate on this member alone binds. `null` when the member failed to plan. */
116
+ planDigest: string | null;
117
+ /**
118
+ * Planned before what it reads applied, so it may hold stand-in values (a
119
+ * Terragrunt dependent planned on `mock_outputs` at PR time, #3416). A
120
+ * preview only: {@link changeSetDigest} leaves it out, so no approval of the
121
+ * document covers it, a wave's set digest refuses it, and the grouped
122
+ * summary never folds it into a group of real plans.
123
+ */
124
+ provisional?: true;
125
+ /** The planner's own digest of the same plan, when it prints one (choudoufu's per-root digest). */
126
+ nativeDigest?: string;
127
+ holes: ChangeSetHole[];
128
+ }
129
+
130
+ /** A provider-defined side effect an apply runs: a Terraform `action` block's invocation. */
131
+ export interface ChangeSetSideEffect {
132
+ member: string;
133
+ /** The invocation's address, or its action type when the planner gives none. */
134
+ address: string;
135
+ /** The action's type, such as `aws_lambda_invoke`. */
136
+ type: string;
137
+ /** The address of the resource whose lifecycle triggers it, when one does. */
138
+ trigger?: string;
139
+ /** The lifecycle event, such as `after_update`. */
140
+ event?: string;
141
+ }
142
+
143
+ /** What an adapter returns: one member, the entries it plans and the side effects it will run. */
144
+ export interface ChangeSetPart {
145
+ member: ChangeSetMember;
146
+ entries: ChangeSetEntry[];
147
+ sideEffects?: Array<Omit<ChangeSetSideEffect, "member">>;
148
+ }
149
+
150
+ export type ActionCounts = Partial<Record<ChangeSetAction, number>>;
151
+
152
+ export interface ChangeSetNamed {
153
+ member: string;
154
+ address: string;
155
+ type: string;
156
+ deposed?: string;
157
+ disruption?: ChangeSetDisruption;
158
+ }
159
+
160
+ export interface ChangeSetSummary {
161
+ members: number;
162
+ entries: number;
163
+ /** Every action, zero included. */
164
+ actions: Record<ChangeSetAction, number>;
165
+ /** Per type, the actions it has. */
166
+ types: Record<string, ActionCounts>;
167
+ /** Per member, the actions it has. */
168
+ byMember: Record<string, ActionCounts>;
169
+ deletes: ChangeSetNamed[];
170
+ replacements: ChangeSetNamed[];
171
+ /** Members that failed to plan. */
172
+ failed: string[];
173
+ holes: number;
174
+ }
175
+
176
+ export interface ChangeSetDocument {
177
+ $schema: typeof CHANGE_SET_SCHEMA_ID;
178
+ contract: typeof CHANGE_SET_CONTRACT;
179
+ /** The chant that composed it, when the caller knows. */
180
+ chant?: string;
181
+ digest: string;
182
+ members: ChangeSetMember[];
183
+ entries: ChangeSetEntry[];
184
+ /** Triggered actions, sorted by member and address. Absent when no member runs one. */
185
+ sideEffects?: ChangeSetSideEffect[];
186
+ summary: ChangeSetSummary;
187
+ }
188
+
189
+ const byString = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0);
190
+
191
+ function entryKey(e: ChangeSetEntry): string {
192
+ return `${e.member}\u0000${e.address}\u0000${e.deposed ?? ""}\u0000${e.action}`;
193
+ }
194
+
195
+ /**
196
+ * The set digest over members' plan digests: `computePlanDigest("change-set",
197
+ * …)` over `{ member, planDigest }` sorted by member. Order-independent,
198
+ * and different whenever one member's plan digest is. Refuses a set that
199
+ * names a member twice, which has no single plan for that member.
200
+ *
201
+ * Provisional members are left out: their plans may stand on values that do
202
+ * not exist yet, so an approval of this digest never covers them.
203
+ */
204
+ export function changeSetDigest(members: ReadonlyArray<Pick<ChangeSetMember, "member" | "planDigest" | "provisional">>): string {
205
+ const pairs = members
206
+ .filter((m) => m.provisional !== true)
207
+ .map((m) => ({ member: m.member, planDigest: m.planDigest }))
208
+ .sort((a, b) => byString(a.member, b.member));
209
+ for (let i = 1; i < pairs.length; i++) {
210
+ if (pairs[i - 1].member === pairs[i].member) throw new Error(`the change set names member ${pairs[i].member} twice`);
211
+ }
212
+ return computePlanDigest("change-set", pairs);
213
+ }
214
+
215
+ /** The summary of a set of members and entries. */
216
+ export function summarizeChangeSet(members: ChangeSetMember[], entries: ChangeSetEntry[]): ChangeSetSummary {
217
+ const actions = Object.fromEntries(CHANGE_SET_ACTIONS.map((a) => [a, 0])) as Record<ChangeSetAction, number>;
218
+ const types: Record<string, ActionCounts> = {};
219
+ const byMember: Record<string, ActionCounts> = {};
220
+ const deletes: ChangeSetNamed[] = [];
221
+ const replacements: ChangeSetNamed[] = [];
222
+ const bump = (into: Record<string, ActionCounts>, key: string, action: ChangeSetAction) => {
223
+ const counts = (into[key] ??= {});
224
+ counts[action] = (counts[action] ?? 0) + 1;
225
+ };
226
+ for (const e of entries) {
227
+ actions[e.action]++;
228
+ bump(types, e.type, e.action);
229
+ bump(byMember, e.member, e.action);
230
+ const named: ChangeSetNamed = {
231
+ member: e.member, address: e.address, type: e.type,
232
+ ...(e.deposed !== undefined ? { deposed: e.deposed } : {}),
233
+ ...(e.disruption !== undefined ? { disruption: e.disruption } : {}),
234
+ };
235
+ if (e.action === "delete") deletes.push(named);
236
+ if (e.action === "replace") replacements.push(named);
237
+ }
238
+ return {
239
+ members: members.length,
240
+ entries: entries.length,
241
+ actions,
242
+ types: sortKeys(types),
243
+ byMember: sortKeys(byMember),
244
+ deletes,
245
+ replacements,
246
+ failed: members.filter((m) => m.status === "failed").map((m) => m.member),
247
+ holes: members.reduce((n, m) => n + m.holes.length, 0),
248
+ };
249
+ }
250
+
251
+ function sortKeys<T>(o: Record<string, T>): Record<string, T> {
252
+ return Object.fromEntries(Object.entries(o).sort(([a], [b]) => byString(a, b)));
253
+ }
254
+
255
+ export interface ComposeChangeSetOptions {
256
+ /** The chant version to record in `chant`. */
257
+ chant?: string;
258
+ }
259
+
260
+ /**
261
+ * Join adapters' parts into one document: members sorted by name, entries
262
+ * by member, address, deposed key and action, the summary, and the digest.
263
+ * Throws when two parts name the same member, or an entry names a member
264
+ * other than its part's.
265
+ */
266
+ export function composeChangeSet(parts: ReadonlyArray<ChangeSetPart>, options: ComposeChangeSetOptions = {}): ChangeSetDocument {
267
+ const members = parts.map((p) => p.member).sort((a, b) => byString(a.member, b.member));
268
+ const entries: ChangeSetEntry[] = [];
269
+ for (const part of parts) {
270
+ for (const e of part.entries) {
271
+ if (e.member !== part.member.member) throw new Error(`an entry at ${e.address} names member ${e.member}, inside member ${part.member.member}`);
272
+ entries.push(e);
273
+ }
274
+ }
275
+ entries.sort((a, b) => byString(entryKey(a), entryKey(b)));
276
+ const sideEffects: ChangeSetSideEffect[] = parts
277
+ .flatMap((p) => (p.sideEffects ?? []).map((s) => ({ member: p.member.member, ...s })))
278
+ .sort((a, b) => byString(a.member, b.member) || byString(a.address, b.address) || byString(a.trigger ?? "", b.trigger ?? ""));
279
+ const digest = changeSetDigest(members);
280
+ return {
281
+ $schema: CHANGE_SET_SCHEMA_ID,
282
+ contract: CHANGE_SET_CONTRACT,
283
+ ...(options.chant !== undefined ? { chant: options.chant } : {}),
284
+ digest,
285
+ members,
286
+ entries,
287
+ ...(sideEffects.length > 0 ? { sideEffects } : {}),
288
+ summary: summarizeChangeSet(members, entries),
289
+ };
290
+ }
291
+
292
+ /** Whether a document's digest is the one its members' plan digests give. */
293
+ export function verifyChangeSetDigest(doc: Pick<ChangeSetDocument, "digest" | "members">): boolean {
294
+ return changeSetDigest(doc.members) === doc.digest;
295
+ }
296
+
297
+ // ── chant lifecycle plan ───────────────────────────────────────────────────────
298
+
299
+ export interface LifecyclePlanPartInput {
300
+ member: string;
301
+ /** `chant lifecycle plan <env> --json`, parsed. */
302
+ plan: LifecycleChangeSet;
303
+ /** The lexicon an entry names when it carries none. Default `"chant"`. */
304
+ lexicon?: string;
305
+ }
306
+
307
+ function lifecycleAttributes(entry: LifecycleEntry): ChangeSetAttribute[] {
308
+ const because = new Set(entry.disruptionBecause ?? []);
309
+ return (entry.deltas ?? []).map((d) => ({
310
+ path: d.path,
311
+ ...(d.oldValue !== undefined ? { before: d.oldValue } : {}),
312
+ ...(d.newValue !== undefined ? { after: d.newValue } : {}),
313
+ ...(because.has(d.path) ? { forcesReplacement: true as const } : {}),
314
+ }));
315
+ }
316
+
317
+ /**
318
+ * A chant member's part, from `chant lifecycle plan <env> --json`.
319
+ *
320
+ * `create`, `update`, `delete` and `noop` map across. An `update` whose
321
+ * lexicon says it replaces or destroys becomes a `replace`. An `effect`
322
+ * entry is the receipt its effect step writes: a `create` when the receipt
323
+ * is absent, else an `update`. `unobserved` entries are holes. `adopt` and
324
+ * `runtime` propose nothing and are left out.
325
+ *
326
+ * The member's plan digest is `computePlanDigest("lifecycle-plan", plan)`
327
+ * over the whole document, which carries no timestamp or run id.
328
+ */
329
+ export function lifecyclePlanPart(input: LifecyclePlanPartInput): ChangeSetPart {
330
+ const { plan } = input;
331
+ const fallback = input.lexicon ?? "chant";
332
+ const entries: ChangeSetEntry[] = [];
333
+ const holes: ChangeSetHole[] = [];
334
+ for (const e of plan.entries ?? []) {
335
+ const lexicon = e.lexicon ?? fallback;
336
+ if (e.action === "unobserved") {
337
+ holes.push({ address: e.name, ...(e.type ? { type: e.type } : {}), reason: e.unobservedReason ?? "unobserved" });
338
+ continue;
339
+ }
340
+ let action: ChangeSetAction;
341
+ let disruption: ChangeSetDisruption | undefined;
342
+ switch (e.action) {
343
+ case "create": action = "create"; break;
344
+ case "noop": action = "no-op"; break;
345
+ case "delete": action = "delete"; disruption = "destroy"; break;
346
+ case "effect": action = e.effectReason === "receipt-absent" ? "create" : "update"; break;
347
+ case "update":
348
+ disruption = e.disruption ?? "unknown";
349
+ action = disruption === "replace" || disruption === "destroy" ? "replace" : "update";
350
+ break;
351
+ default: continue; // adopt, runtime: not a proposal
352
+ }
353
+ entries.push({
354
+ member: input.member,
355
+ lexicon,
356
+ planner: "chant",
357
+ address: e.name,
358
+ type: e.type ?? "unknown",
359
+ ...(e.physicalId ? { id: e.physicalId } : {}),
360
+ action,
361
+ ...(disruption ? { disruption } : {}),
362
+ scope: plan.env,
363
+ attributes: lifecycleAttributes(e),
364
+ });
365
+ }
366
+ const lexicons = [...new Set(entries.map((e) => e.lexicon))].sort();
367
+ return {
368
+ member: {
369
+ member: input.member,
370
+ lexicon: lexicons.length === 1 ? lexicons[0] : fallback,
371
+ planner: "chant",
372
+ scope: plan.env,
373
+ status: "planned",
374
+ planDigest: computePlanDigest("lifecycle-plan", plan),
375
+ holes,
376
+ },
377
+ entries,
378
+ };
379
+ }
380
+
381
+ // ── a warden's reconcile plan ──────────────────────────────────────────────────
382
+
383
+ export interface ReconcilePlanPartInput {
384
+ member: string;
385
+ /** The change sets a reconcile run planned, one per cycle and org. */
386
+ plan: ReconcileChangeSet | ReconcileChangeSet[];
387
+ /** The lexicon the reconciled provider belongs to. Default `"github"`. */
388
+ lexicon?: string;
389
+ }
390
+
391
+ function topLevelKeys(value: unknown): string[] {
392
+ return value && typeof value === "object" && !Array.isArray(value) ? Object.keys(value).sort() : [];
393
+ }
394
+
395
+ /**
396
+ * A warden member's part, from the reconcile `ChangeSet`s it planned
397
+ * (`./reconcile.ts`). The address is `<resourceType>.<key>`, unique within
398
+ * one org, and the scope is the org. An update's disruption is `unknown`:
399
+ * the reconcile model says nothing about it.
400
+ *
401
+ * The plan digest covers each change set's org and entries, and leaves out
402
+ * the managed counts, which are guardrail denominators rather than changes.
403
+ */
404
+ export function reconcilePlanPart(input: ReconcilePlanPartInput): ChangeSetPart {
405
+ const sets = (Array.isArray(input.plan) ? input.plan : [input.plan])
406
+ .map((cs) => ({ org: cs.org, entries: [...cs.entries].sort((a, b) => byString(`${a.resourceType}\u0000${a.key}`, `${b.resourceType}\u0000${b.key}`)) }))
407
+ .sort((a, b) => byString(a.org, b.org));
408
+ const lexicon = input.lexicon ?? "github";
409
+ const entries: ChangeSetEntry[] = [];
410
+ for (const cs of sets) {
411
+ for (const e of cs.entries) {
412
+ const attributes: ChangeSetAttribute[] =
413
+ e.kind === "update"
414
+ ? (e.fields ?? []).map((f) => ({ path: f.field, ...(f.before !== undefined ? { before: f.before } : {}), ...(f.after !== undefined ? { after: f.after } : {}) }))
415
+ : e.kind === "create"
416
+ ? topLevelKeys(e.after).map((k) => ({ path: k, after: (e.after as Record<string, unknown>)[k] }))
417
+ : [];
418
+ entries.push({
419
+ member: input.member,
420
+ lexicon,
421
+ planner: "warden",
422
+ address: `${e.resourceType}.${e.key}`,
423
+ type: e.resourceType,
424
+ name: e.key,
425
+ action: e.kind,
426
+ ...(e.kind === "delete" ? { disruption: "destroy" as const } : e.kind === "update" ? { disruption: "unknown" as const } : {}),
427
+ scope: cs.org,
428
+ attributes,
429
+ });
430
+ }
431
+ }
432
+ const orgs = [...new Set(sets.map((s) => s.org))];
433
+ return {
434
+ member: {
435
+ member: input.member,
436
+ lexicon,
437
+ planner: "warden",
438
+ ...(orgs.length === 1 ? { scope: orgs[0] } : {}),
439
+ status: "planned",
440
+ planDigest: computePlanDigest("reconcile-plan", sets),
441
+ holes: [],
442
+ },
443
+ entries,
444
+ };
445
+ }
@@ -0,0 +1,54 @@
1
+ import { afterEach, beforeAll, describe, expect, test } from "vitest";
2
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import { auditCommand, readEnabledRules } from "./audit";
6
+ import { loadAuditPlugins } from "../../audit/discover";
7
+
8
+ /**
9
+ * #3190: TF040 is an `auditOptIn` check. `chant audit` leaves it out unless the
10
+ * project's `lint.rules` names it, so level-0 audit output does not change.
11
+ */
12
+ const dirs: string[] = [];
13
+ afterEach(() => {
14
+ for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true });
15
+ });
16
+
17
+ let plugins: Awaited<ReturnType<typeof loadAuditPlugins>>;
18
+ beforeAll(async () => {
19
+ plugins = await loadAuditPlugins();
20
+ }, 120_000);
21
+
22
+ function project(config?: object): string {
23
+ const dir = mkdtempSync(join(tmpdir(), "chant-3190-"));
24
+ dirs.push(dir);
25
+ writeFileSync(join(dir, "main.tf"), 'resource "aws_s3_bucket" "b" {\n bucket = "x"\n}\n');
26
+ if (config) writeFileSync(join(dir, "chant.config.json"), JSON.stringify(config));
27
+ return dir;
28
+ }
29
+
30
+ async function ids(dir: string): Promise<string[]> {
31
+ const r = await auditCommand({ path: dir, format: "json", now: "2026-09-23T00:00:00.000Z", toolVersion: "0.0.0", plugins });
32
+ return r.findings.map((f) => f.checkId);
33
+ }
34
+
35
+ describe("chant audit and an auditOptIn check (TF040)", () => {
36
+ test("is not reported by default", async () => {
37
+ expect(await ids(project())).not.toContain("TF040");
38
+ });
39
+
40
+ test("is reported when lint.rules enables it", async () => {
41
+ expect(await ids(project({ lint: { rules: { TF040: "warning" } } }))).toContain("TF040");
42
+ });
43
+
44
+ test("is not reported when lint.rules sets it off", async () => {
45
+ expect(await ids(project({ lint: { rules: { TF040: "off" } } }))).not.toContain("TF040");
46
+ });
47
+ });
48
+
49
+ describe("readEnabledRules", () => {
50
+ test("lists ids not set to off, and tolerates no config", () => {
51
+ expect([...readEnabledRules(project({ lint: { rules: { A: "error", B: "off", C: ["warning", {}] } } }))].sort()).toEqual(["A", "C"]);
52
+ expect(readEnabledRules(project()).size).toBe(0);
53
+ });
54
+ });
@@ -24,7 +24,8 @@ import type { ProveOptions } from "../../audit/proof";
24
24
  import type { Severity } from "../../lint/rule";
25
25
  import { formatWarning } from "../format";
26
26
  import { lexiconPackagesToInstall, lexiconSourceLabel, pathLexiconMap, recordedLexiconModules } from "../../lexicon-module";
27
- import { readLexiconDeclarationsStatically, unknownPathLexiconsNotice } from "../../config-static";
27
+ import { readConfigFieldsStatically, readLexiconDeclarationsStatically, unknownPathLexiconsNotice } from "../../config-static";
28
+ import { findProjectConfigPastFragments } from "../../config";
28
29
 
29
30
  export type AuditFormat = "stylish" | "json" | "sarif" | "markdown" | "html";
30
31
  export type AuditTier = "merge-worthy" | "all";
@@ -140,6 +141,28 @@ function missingLexiconRemedy(names: readonly string[], paths: ReadonlyMap<strin
140
141
  return parts.join("; ");
141
142
  }
142
143
 
144
+ /**
145
+ * The rule ids the audited project enables with `lint.rules` (anything but
146
+ * `"off"`), read from its config without running it, the way the audit reads
147
+ * its lexicon paths. Only a check marked `auditOptIn` consults this (#3190).
148
+ * A config that cannot be read statically enables nothing.
149
+ */
150
+ export function readEnabledRules(path: string): Set<string> {
151
+ const enabled = new Set<string>();
152
+ try {
153
+ const read = readConfigFieldsStatically(findProjectConfigPastFragments(path).dir, ["lint"]);
154
+ const rules = read.status === "read" ? (read.fields.lint as { rules?: unknown } | undefined)?.rules : undefined;
155
+ if (typeof rules !== "object" || rules === null) return enabled;
156
+ for (const [id, value] of Object.entries(rules)) {
157
+ const severity = Array.isArray(value) ? value[0] : value;
158
+ if (severity !== "off") enabled.add(id);
159
+ }
160
+ } catch {
161
+ // an unreadable config enables nothing
162
+ }
163
+ return enabled;
164
+ }
165
+
143
166
  /**
144
167
  * chant#2589 — the lexicons a local target's chant.config declares by path,
145
168
  * with any recorded earlier in this process. The config is read statically
@@ -560,7 +583,11 @@ export async function auditCommand(options: AuditCommandOptions): Promise<AuditC
560
583
  const suppressionStats: SuppressionStats = { count: 0 };
561
584
  if (inputs.length > 0) {
562
585
  try {
563
- findings = await auditFiles(inputs, { checksProvider: options.checksProvider, suppressionStats });
586
+ findings = await auditFiles(inputs, {
587
+ checksProvider: options.checksProvider,
588
+ suppressionStats,
589
+ ...(isUrl ? {} : { enabledRules: readEnabledRules(options.path) }),
590
+ });
564
591
  } catch (err) {
565
592
  const msg = err instanceof Error ? err.message : String(err);
566
593
  return { success: false, output: "", findings: [], scanned, exitCode: 1, error: msg };
@@ -1105,6 +1105,25 @@ export const x = { [Symbol.for("chant.declarable")]: true, entityType: "X", lexi
1105
1105
  };
1106
1106
  }
1107
1107
 
1108
+ test("a lexicon's post-synth checks see the build's telemetry attribution (#3375)", async () => {
1109
+ await writeTrivialEntity();
1110
+ const plugin: LexiconPlugin = {
1111
+ ...fakePostSynthPlugin("FAKE009", "warning"),
1112
+ postSynthChecks: () => [
1113
+ {
1114
+ id: "FAKE009",
1115
+ description: "reports ctx.telemetry",
1116
+ check: (ctx) => [{ checkId: "FAKE009", severity: "warning", message: `telemetry=${JSON.stringify(ctx.telemetry ?? null)}` }],
1117
+ },
1118
+ ],
1119
+ };
1120
+ const run = () => buildCommand({ path: testDir, format: "json", serializers: [mockSerializer], plugins: [plugin] });
1121
+
1122
+ expect((await run()).warnings.join("\n")).toContain("telemetry=null");
1123
+ await writeFile(join(testDir, "chant.config.ts"), `export default { telemetry: { attribution: true } };\n`);
1124
+ expect((await run()).warnings.join("\n")).toContain("telemetry={}");
1125
+ });
1126
+
1108
1127
  test("lint.rules off suppresses a lexicon-shipped post-synth finding and reports a suppressed count", async () => {
1109
1128
  await writeTrivialEntity();
1110
1129
  await writeFile(
@@ -310,6 +310,7 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
310
310
  // Run the build. #2002 — every option comes from the shared assembler
311
311
  // (../build-options.ts), which `evaluateProjectPolicies` calls too, so the
312
312
  // `policyGate` step decides on the same project this command builds.
313
+ const telemetry = await resolveTelemetryAttribution(configDir, config as unknown as Record<string, unknown>, env);
313
314
  const result = await build(
314
315
  infraPath,
315
316
  options.serializers,
@@ -321,7 +322,7 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
321
322
  modes,
322
323
  ownership,
323
324
  buildParams: paramsResolution.provenance,
324
- telemetry: await resolveTelemetryAttribution(configDir, config as unknown as Record<string, unknown>, env),
325
+ telemetry,
325
326
  }),
326
327
  );
327
328
 
@@ -546,7 +547,7 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
546
547
  }
547
548
 
548
549
  const scopedResult = { ...result, outputs: scopedOutputs };
549
- const postDiags = runPostSynthChecks(checks, scopedResult, env, { activityContracts });
550
+ const postDiags = runPostSynthChecks(checks, scopedResult, env, { activityContracts, telemetry });
550
551
  // `lint.presets` (chant #2113) filters WHICH check ids are reported at
551
552
  // all, before `lint.rules`/inline suppression act on the reported
552
553
  // ones. It's a no-op for a lexicon that ships no `lintPresets()` (the
@@ -79,7 +79,19 @@ async function runGenerateComponents(ctx: CommandContext): Promise<number> {
79
79
  const result = await generateComponentsPipeline(
80
80
  args.path,
81
81
  lexicon,
82
- { env: args.env, ...(args.promoteTo ? { promoteTo: args.promoteTo } : {}), ...(plan ? { member: plan.member } : {}) },
82
+ {
83
+ env: args.env,
84
+ ...(args.promoteTo ? { promoteTo: args.promoteTo } : {}),
85
+ ...(plan ? { member: plan.member } : {}),
86
+ // #3049: one job per wave of a gated-wave fan-out instead of one per component.
87
+ ...(args.waveGate
88
+ ? { gatedWaves: { gate: args.waveGate, ...(args.canary?.length ? { canary: args.canary } : {}), ...(args.base ? { base: args.base } : {}) } }
89
+ : {}),
90
+ // #3183: a plan job per pull request and an apply job per merge.
91
+ ...(args.prLoop
92
+ ? { prLoop: { ...(args.gate ? { gate: args.gate } : {}), ...(args.branch ? { branch: args.branch } : {}) } }
93
+ : {}),
94
+ },
83
95
  args.sandbox,
84
96
  paramsResolution.provenance,
85
97
  );
@@ -0,0 +1,56 @@
1
+ import { mkdtempSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, describe, expect, test, vi } from "vitest";
5
+ import { composeChangeSet } from "../../change-set";
6
+ import { commandRegistry, parseArgs } from "../main";
7
+ import { resolveCommand } from "../registry";
8
+ import { runChangeSetSummary } from "./change-set";
9
+
10
+ const dir = mkdtempSync(join(tmpdir(), "chant-change-set-summary-"));
11
+ const file = join(dir, "change-set.json");
12
+ writeFileSync(
13
+ file,
14
+ JSON.stringify(
15
+ composeChangeSet(
16
+ ["a", "b"].map((m, i) => ({
17
+ member: { member: m, lexicon: "terraform", planner: "terraform" as const, status: "planned" as const, planDigest: `jcs1-sha256:${String(i).padStart(64, "0")}`, holes: [] },
18
+ entries: [{ member: m, lexicon: "terraform", planner: "terraform" as const, address: "aws_db_instance.main", type: "aws_db_instance", action: "delete" as const, attributes: [] }],
19
+ })),
20
+ ),
21
+ ),
22
+ );
23
+
24
+ async function run(argv: string[]): Promise<{ code: number; out: string; err: string }> {
25
+ let out = "";
26
+ let err = "";
27
+ vi.spyOn(process.stdout, "write").mockImplementation((s) => ((out += String(s)), true));
28
+ vi.spyOn(console, "log").mockImplementation((s) => void (out += String(s) + "\n"));
29
+ vi.spyOn(console, "error").mockImplementation((s) => void (err += String(s) + "\n"));
30
+ const args = parseArgs(argv);
31
+ expect(resolveCommand(args, commandRegistry)?.def.name).toBe("change-set summary");
32
+ const code = await runChangeSetSummary({ args, plugins: [], serializers: [] });
33
+ return { code, out, err };
34
+ }
35
+
36
+ afterEach(() => vi.restoreAllMocks());
37
+
38
+ describe("chant change-set summary", () => {
39
+ test("prints text, json and markdown, naming each destroy", async () => {
40
+ const text = await run(["change-set", "summary", file]);
41
+ expect(text.code).toBe(0);
42
+ expect(text.out).toContain("2 members: 1 group, 2 destroys or replacements.");
43
+ expect(text.out).toContain("a: aws_db_instance.main (delete)");
44
+ const json = await run(["change-set", "summary", file, "--format", "json"]);
45
+ expect(JSON.parse(json.out).destroys).toHaveLength(2);
46
+ const md = await run(["change-set", "summary", file, "--format", "markdown", "--limit", "500"]);
47
+ expect(md.out).toContain("- `b: aws_db_instance.main (delete)`");
48
+ });
49
+
50
+ test("refuses a file that is not a change-set document, and an unknown format", async () => {
51
+ const other = join(dir, "other.json");
52
+ writeFileSync(other, "{}");
53
+ expect((await run(["change-set", "summary", other])).err).toContain("is not a change-set document");
54
+ expect((await run(["change-set", "summary", file, "--format", "html"])).code).toBe(1);
55
+ });
56
+ });