@intentius/chant 0.124.51 → 0.124.54

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 (593) hide show
  1. package/dist/audit/discover.d.ts.map +1 -1
  2. package/dist/audit/discover.js +3 -1
  3. package/dist/behaviour.d.ts +1 -1
  4. package/dist/behaviour.d.ts.map +1 -1
  5. package/dist/capabilities/templates.d.ts +65 -0
  6. package/dist/capabilities/templates.d.ts.map +1 -0
  7. package/dist/capabilities/templates.js +193 -0
  8. package/dist/capabilities/templates.json +145 -0
  9. package/dist/change-set-graph.d.ts +35 -3
  10. package/dist/change-set-graph.d.ts.map +1 -1
  11. package/dist/change-set-graph.js +60 -9
  12. package/dist/change-set.d.ts +65 -7
  13. package/dist/change-set.d.ts.map +1 -1
  14. package/dist/change-set.js +23 -12
  15. package/dist/ci-identity.d.ts +7 -2
  16. package/dist/ci-identity.d.ts.map +1 -1
  17. package/dist/ci-identity.js +4 -1
  18. package/dist/cli/commands/import-index.d.ts +52 -0
  19. package/dist/cli/commands/import-index.d.ts.map +1 -0
  20. package/dist/cli/commands/import-index.js +226 -0
  21. package/dist/cli/commands/lint.d.ts.map +1 -1
  22. package/dist/cli/commands/lint.js +4 -2
  23. package/dist/cli/handlers/components.d.ts.map +1 -1
  24. package/dist/cli/handlers/components.js +11 -2
  25. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  26. package/dist/cli/handlers/fan-out.js +16 -3
  27. package/dist/cli/handlers/graph.d.ts.map +1 -1
  28. package/dist/cli/handlers/graph.js +22 -4
  29. package/dist/cli/handlers/index-cloud.d.ts +14 -0
  30. package/dist/cli/handlers/index-cloud.d.ts.map +1 -0
  31. package/dist/cli/handlers/index-cloud.js +22 -0
  32. package/dist/cli/handlers/index-diff.d.ts +18 -0
  33. package/dist/cli/handlers/index-diff.d.ts.map +1 -0
  34. package/dist/cli/handlers/index-diff.js +142 -0
  35. package/dist/cli/handlers/lifecycle-drift.d.ts +25 -0
  36. package/dist/cli/handlers/lifecycle-drift.d.ts.map +1 -0
  37. package/dist/cli/handlers/lifecycle-drift.js +64 -0
  38. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  39. package/dist/cli/handlers/lifecycle.js +188 -28
  40. package/dist/cli/handlers/misc.d.ts +9 -0
  41. package/dist/cli/handlers/misc.d.ts.map +1 -1
  42. package/dist/cli/handlers/misc.js +77 -0
  43. package/dist/cli/handlers/pr.d.ts.map +1 -1
  44. package/dist/cli/handlers/pr.js +17 -3
  45. package/dist/cli/handlers/promote.d.ts.map +1 -1
  46. package/dist/cli/handlers/promote.js +89 -15
  47. package/dist/cli/handlers/query.d.ts.map +1 -1
  48. package/dist/cli/handlers/query.js +28 -4
  49. package/dist/cli/handlers/scenario.d.ts.map +1 -1
  50. package/dist/cli/handlers/scenario.js +8 -7
  51. package/dist/cli/handlers/search.d.ts +34 -4
  52. package/dist/cli/handlers/search.d.ts.map +1 -1
  53. package/dist/cli/handlers/search.js +216 -36
  54. package/dist/cli/main.d.ts.map +1 -1
  55. package/dist/cli/main.js +55 -1
  56. package/dist/cli/mcp/lifecycle-tools.d.ts.map +1 -1
  57. package/dist/cli/mcp/lifecycle-tools.js +3 -2
  58. package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
  59. package/dist/cli/mcp/resource-handlers.js +4 -3
  60. package/dist/cli/mcp/workspace-tools.js +1 -1
  61. package/dist/cli/parse-args.d.ts.map +1 -1
  62. package/dist/cli/parse-args.js +55 -2
  63. package/dist/cli/registry.d.ts +19 -1
  64. package/dist/cli/registry.d.ts.map +1 -1
  65. package/dist/cloud-lister/aws.d.ts +35 -0
  66. package/dist/cloud-lister/aws.d.ts.map +1 -0
  67. package/dist/cloud-lister/aws.js +203 -0
  68. package/dist/cloud-lister/azure.d.ts +34 -0
  69. package/dist/cloud-lister/azure.d.ts.map +1 -0
  70. package/dist/cloud-lister/azure.js +129 -0
  71. package/dist/cloud-lister/common.d.ts +40 -0
  72. package/dist/cloud-lister/common.d.ts.map +1 -0
  73. package/dist/cloud-lister/common.js +83 -0
  74. package/dist/cloud-lister/credentials.d.ts +50 -0
  75. package/dist/cloud-lister/credentials.d.ts.map +1 -0
  76. package/dist/cloud-lister/credentials.js +86 -0
  77. package/dist/cloud-lister/fakes.d.ts +27 -0
  78. package/dist/cloud-lister/fakes.d.ts.map +1 -0
  79. package/dist/cloud-lister/fakes.js +50 -0
  80. package/dist/cloud-lister/gcp.d.ts +42 -0
  81. package/dist/cloud-lister/gcp.d.ts.map +1 -0
  82. package/dist/cloud-lister/gcp.js +143 -0
  83. package/dist/cloud-lister/guard.d.ts +49 -0
  84. package/dist/cloud-lister/guard.d.ts.map +1 -0
  85. package/dist/cloud-lister/guard.js +103 -0
  86. package/dist/cloud-lister/ids.d.ts +23 -0
  87. package/dist/cloud-lister/ids.d.ts.map +1 -0
  88. package/dist/cloud-lister/ids.js +42 -0
  89. package/dist/cloud-lister/index.d.ts +67 -0
  90. package/dist/cloud-lister/index.d.ts.map +1 -0
  91. package/dist/cloud-lister/index.js +102 -0
  92. package/dist/cloud-lister/kubernetes.d.ts +64 -0
  93. package/dist/cloud-lister/kubernetes.d.ts.map +1 -0
  94. package/dist/cloud-lister/kubernetes.js +276 -0
  95. package/dist/cloud-lister/markers.d.ts +31 -0
  96. package/dist/cloud-lister/markers.d.ts.map +1 -0
  97. package/dist/cloud-lister/markers.js +73 -0
  98. package/dist/cloud-lister/mask.d.ts +31 -0
  99. package/dist/cloud-lister/mask.d.ts.map +1 -0
  100. package/dist/cloud-lister/mask.js +74 -0
  101. package/dist/cloud-lister/sigv4.d.ts +29 -0
  102. package/dist/cloud-lister/sigv4.d.ts.map +1 -0
  103. package/dist/cloud-lister/sigv4.js +54 -0
  104. package/dist/cloud-lister/transport.d.ts +55 -0
  105. package/dist/cloud-lister/transport.d.ts.map +1 -0
  106. package/dist/cloud-lister/transport.js +86 -0
  107. package/dist/cloud-lister/types.d.ts +121 -0
  108. package/dist/cloud-lister/types.d.ts.map +1 -0
  109. package/dist/cloud-lister/types.js +24 -0
  110. package/dist/components/capability-plugin-loader.d.ts.map +1 -1
  111. package/dist/components/capability-plugin-loader.js +6 -0
  112. package/dist/components/capability-plugin.d.ts +9 -0
  113. package/dist/components/capability-plugin.d.ts.map +1 -1
  114. package/dist/components/capability.d.ts +224 -0
  115. package/dist/components/capability.d.ts.map +1 -1
  116. package/dist/components/cli-support.d.ts +21 -0
  117. package/dist/components/cli-support.d.ts.map +1 -1
  118. package/dist/components/cli-support.js +58 -14
  119. package/dist/components/covered-round.d.ts +173 -0
  120. package/dist/components/covered-round.d.ts.map +1 -0
  121. package/dist/components/covered-round.js +200 -0
  122. package/dist/components/declared-reads.d.ts +38 -0
  123. package/dist/components/declared-reads.d.ts.map +1 -0
  124. package/dist/components/declared-reads.js +131 -0
  125. package/dist/components/deploy-units.d.ts.map +1 -1
  126. package/dist/components/deploy-units.js +4 -0
  127. package/dist/components/driver.d.ts +42 -1
  128. package/dist/components/driver.d.ts.map +1 -1
  129. package/dist/components/driver.js +74 -3
  130. package/dist/components/fan-out-run.d.ts +23 -1
  131. package/dist/components/fan-out-run.d.ts.map +1 -1
  132. package/dist/components/fan-out-run.js +82 -14
  133. package/dist/components/fan-out-support.d.ts +6 -0
  134. package/dist/components/fan-out-support.d.ts.map +1 -1
  135. package/dist/components/fan-out-support.js +4 -1
  136. package/dist/components/fan-out.d.ts +28 -0
  137. package/dist/components/fan-out.d.ts.map +1 -1
  138. package/dist/components/groups.d.ts +229 -0
  139. package/dist/components/groups.d.ts.map +1 -0
  140. package/dist/components/groups.js +760 -0
  141. package/dist/components/index.d.ts +7 -2
  142. package/dist/components/index.d.ts.map +1 -1
  143. package/dist/components/index.js +5 -0
  144. package/dist/components/plan-shape-conformance.d.ts +48 -0
  145. package/dist/components/plan-shape-conformance.d.ts.map +1 -0
  146. package/dist/components/plan-shape-conformance.js +122 -0
  147. package/dist/components/pr-run.d.ts +62 -7
  148. package/dist/components/pr-run.d.ts.map +1 -1
  149. package/dist/components/pr-run.js +354 -53
  150. package/dist/components/promote-rounds.d.ts +9 -0
  151. package/dist/components/promote-rounds.d.ts.map +1 -1
  152. package/dist/components/promote-rounds.js +23 -2
  153. package/dist/components/promote.d.ts +17 -0
  154. package/dist/components/promote.d.ts.map +1 -1
  155. package/dist/components/promote.js +36 -6
  156. package/dist/components/release-gate.d.ts +23 -1
  157. package/dist/components/release-gate.d.ts.map +1 -1
  158. package/dist/components/release-gate.js +97 -33
  159. package/dist/components/run-groups.d.ts +58 -0
  160. package/dist/components/run-groups.d.ts.map +1 -0
  161. package/dist/components/run-groups.js +78 -0
  162. package/dist/components/starter-plugin.d.ts +1 -0
  163. package/dist/components/starter-plugin.d.ts.map +1 -1
  164. package/dist/components/starter-plugin.js +6 -1
  165. package/dist/components/verbs/index.d.ts +1 -0
  166. package/dist/components/verbs/index.d.ts.map +1 -1
  167. package/dist/components/verbs/index.js +1 -0
  168. package/dist/components/verbs/member-outputs.d.ts +187 -0
  169. package/dist/components/verbs/member-outputs.d.ts.map +1 -0
  170. package/dist/components/verbs/member-outputs.js +218 -0
  171. package/dist/components/wave-plan.d.ts +2 -0
  172. package/dist/components/wave-plan.d.ts.map +1 -1
  173. package/dist/components/wave-plan.js +4 -1
  174. package/dist/config.d.ts +60 -0
  175. package/dist/config.d.ts.map +1 -1
  176. package/dist/config.js +27 -0
  177. package/dist/deep-observation.d.ts +1 -13
  178. package/dist/deep-observation.d.ts.map +1 -1
  179. package/dist/deep-observation.js +5 -22
  180. package/dist/drift-record.d.ts +542 -0
  181. package/dist/drift-record.d.ts.map +1 -0
  182. package/dist/drift-record.js +886 -0
  183. package/dist/engine-facts.d.ts +144 -0
  184. package/dist/engine-facts.d.ts.map +1 -0
  185. package/dist/engine-facts.js +14 -0
  186. package/dist/graph-ir.d.ts +153 -2
  187. package/dist/graph-ir.d.ts.map +1 -1
  188. package/dist/graph-ir.js +224 -6
  189. package/dist/graph-refs.d.ts +145 -0
  190. package/dist/graph-refs.d.ts.map +1 -1
  191. package/dist/graph-refs.js +305 -0
  192. package/dist/graph-schema/fixtures/adopt.json +15 -0
  193. package/dist/graph-schema/fixtures/choudoufu.engine-graph.json +169 -0
  194. package/dist/graph-schema/fixtures/index.json +68 -0
  195. package/dist/graph-schema/fixtures/plunge.engine-graph.json +158 -0
  196. package/dist/graph-schema/fixtures.d.ts +23 -0
  197. package/dist/graph-schema/fixtures.d.ts.map +1 -0
  198. package/dist/graph-schema/fixtures.js +246 -0
  199. package/dist/graph-schema/generate.d.ts +15 -0
  200. package/dist/graph-schema/generate.d.ts.map +1 -0
  201. package/dist/graph-schema/generate.js +267 -0
  202. package/dist/graph-schema/graph.schema.json +1387 -0
  203. package/dist/health.schema.json +2 -1
  204. package/dist/identity.d.ts.map +1 -1
  205. package/dist/identity.js +2 -0
  206. package/dist/index.d.ts +1 -0
  207. package/dist/index.d.ts.map +1 -1
  208. package/dist/index.js +2 -0
  209. package/dist/instance-nodes.d.ts +159 -0
  210. package/dist/instance-nodes.d.ts.map +1 -0
  211. package/dist/instance-nodes.js +231 -0
  212. package/dist/lexicon.d.ts +72 -0
  213. package/dist/lexicon.d.ts.map +1 -1
  214. package/dist/lifecycle/adoption-file.d.ts +56 -0
  215. package/dist/lifecycle/adoption-file.d.ts.map +1 -0
  216. package/dist/lifecycle/adoption-file.js +123 -0
  217. package/dist/lifecycle/cloud-index.d.ts +186 -0
  218. package/dist/lifecycle/cloud-index.d.ts.map +1 -0
  219. package/dist/lifecycle/cloud-index.js +618 -0
  220. package/dist/lifecycle/derive-edges.d.ts +59 -0
  221. package/dist/lifecycle/derive-edges.d.ts.map +1 -0
  222. package/dist/lifecycle/derive-edges.js +126 -0
  223. package/dist/lifecycle/git.d.ts +10 -9
  224. package/dist/lifecycle/git.d.ts.map +1 -1
  225. package/dist/lifecycle/git.js +7 -1
  226. package/dist/lifecycle/identity-match.d.ts +133 -0
  227. package/dist/lifecycle/identity-match.d.ts.map +1 -0
  228. package/dist/lifecycle/identity-match.js +147 -0
  229. package/dist/lifecycle/index-diff.d.ts +224 -0
  230. package/dist/lifecycle/index-diff.d.ts.map +1 -0
  231. package/dist/lifecycle/index-diff.js +327 -0
  232. package/dist/lifecycle/index-document.d.ts +130 -0
  233. package/dist/lifecycle/index-document.d.ts.map +1 -0
  234. package/dist/lifecycle/index-document.js +189 -0
  235. package/dist/lifecycle/index-drift.d.ts +121 -0
  236. package/dist/lifecycle/index-drift.d.ts.map +1 -0
  237. package/dist/lifecycle/index-drift.js +178 -0
  238. package/dist/lifecycle/index-import.d.ts +168 -0
  239. package/dist/lifecycle/index-import.d.ts.map +1 -0
  240. package/dist/lifecycle/index-import.js +515 -0
  241. package/dist/lifecycle/index-join.d.ts +129 -0
  242. package/dist/lifecycle/index-join.d.ts.map +1 -0
  243. package/dist/lifecycle/index-join.js +248 -0
  244. package/dist/lifecycle/index.d.ts +4 -0
  245. package/dist/lifecycle/index.d.ts.map +1 -1
  246. package/dist/lifecycle/index.js +4 -0
  247. package/dist/lifecycle/observation-baseline.d.ts +61 -21
  248. package/dist/lifecycle/observation-baseline.d.ts.map +1 -1
  249. package/dist/lifecycle/observation-baseline.js +144 -19
  250. package/dist/lifecycle/observe.d.ts.map +1 -1
  251. package/dist/lifecycle/observe.js +6 -1
  252. package/dist/lifecycle/query.d.ts +45 -4
  253. package/dist/lifecycle/query.d.ts.map +1 -1
  254. package/dist/lifecycle/query.js +89 -7
  255. package/dist/lifecycle/replay.d.ts +10 -3
  256. package/dist/lifecycle/replay.d.ts.map +1 -1
  257. package/dist/lifecycle/replay.js +10 -6
  258. package/dist/lifecycle/scenario.d.ts +4 -3
  259. package/dist/lifecycle/scenario.d.ts.map +1 -1
  260. package/dist/lifecycle/snapshot-store-fake.d.ts +9 -0
  261. package/dist/lifecycle/snapshot-store-fake.d.ts.map +1 -0
  262. package/dist/lifecycle/snapshot-store-fake.js +21 -0
  263. package/dist/lifecycle/snapshot-store-s3.d.ts +102 -0
  264. package/dist/lifecycle/snapshot-store-s3.d.ts.map +1 -0
  265. package/dist/lifecycle/snapshot-store-s3.js +254 -0
  266. package/dist/lifecycle/snapshot-store.d.ts +170 -0
  267. package/dist/lifecycle/snapshot-store.d.ts.map +1 -0
  268. package/dist/lifecycle/snapshot-store.js +832 -0
  269. package/dist/lifecycle/snapshot.d.ts +11 -0
  270. package/dist/lifecycle/snapshot.d.ts.map +1 -1
  271. package/dist/lifecycle/snapshot.js +18 -5
  272. package/dist/lifecycle/type-listing.d.ts +148 -0
  273. package/dist/lifecycle/type-listing.d.ts.map +1 -0
  274. package/dist/lifecycle/type-listing.js +166 -0
  275. package/dist/lint/component-checks.d.ts +15 -1
  276. package/dist/lint/component-checks.d.ts.map +1 -1
  277. package/dist/lint/component-checks.js +1 -0
  278. package/dist/lint/presets/relaxed.json +2 -1
  279. package/dist/lint/presets/strict.json +2 -1
  280. package/dist/lint/rules/__fixtures__/comp/comp008/fail/dns.component.d.ts +4 -0
  281. package/dist/lint/rules/__fixtures__/comp/comp008/fail/dns.component.d.ts.map +1 -0
  282. package/dist/lint/rules/__fixtures__/comp/comp008/fail/dns.component.js +13 -0
  283. package/dist/lint/rules/__fixtures__/comp/comp008/pass/dns.component.d.ts +4 -0
  284. package/dist/lint/rules/__fixtures__/comp/comp008/pass/dns.component.d.ts.map +1 -0
  285. package/dist/lint/rules/__fixtures__/comp/comp008/pass/dns.component.js +8 -0
  286. package/dist/lint/rules/comp/comp008-applies-signal-only-root.d.ts +33 -0
  287. package/dist/lint/rules/comp/comp008-applies-signal-only-root.d.ts.map +1 -0
  288. package/dist/lint/rules/comp/comp008-applies-signal-only-root.js +123 -0
  289. package/dist/lint/rules/comp/index.d.ts +2 -1
  290. package/dist/lint/rules/comp/index.d.ts.map +1 -1
  291. package/dist/lint/rules/comp/index.js +4 -1
  292. package/dist/observation.d.ts +19 -2
  293. package/dist/observation.d.ts.map +1 -1
  294. package/dist/observation.js +18 -1
  295. package/dist/op/activities/activity-contracts.d.ts +1 -1
  296. package/dist/op/activities/activity-contracts.js +1 -1
  297. package/dist/op/activities/change-set.d.ts +6 -4
  298. package/dist/op/activities/change-set.d.ts.map +1 -1
  299. package/dist/op/activities/change-set.js +4 -2
  300. package/dist/op/activity-failure.d.ts +27 -0
  301. package/dist/op/activity-failure.d.ts.map +1 -1
  302. package/dist/op/activity-failure.js +31 -0
  303. package/dist/op/index.d.ts +2 -1
  304. package/dist/op/index.d.ts.map +1 -1
  305. package/dist/op/index.js +1 -1
  306. package/dist/op/local-executor.d.ts +5 -1
  307. package/dist/op/local-executor.d.ts.map +1 -1
  308. package/dist/op/local-executor.js +22 -8
  309. package/dist/pr-loop.d.ts +92 -3
  310. package/dist/pr-loop.d.ts.map +1 -1
  311. package/dist/pr-loop.js +62 -3
  312. package/dist/pulumi-urn.d.ts +101 -0
  313. package/dist/pulumi-urn.d.ts.map +1 -0
  314. package/dist/pulumi-urn.js +131 -0
  315. package/dist/release-floors.d.ts +23 -0
  316. package/dist/release-floors.d.ts.map +1 -0
  317. package/dist/release-floors.js +22 -0
  318. package/dist/sensitive-key.d.ts +23 -0
  319. package/dist/sensitive-key.d.ts.map +1 -0
  320. package/dist/sensitive-key.js +31 -0
  321. package/dist/wave-runtime.mjs +234 -234
  322. package/dist/workspace/adoption-report.d.ts +53 -0
  323. package/dist/workspace/adoption-report.d.ts.map +1 -0
  324. package/dist/workspace/adoption-report.js +76 -0
  325. package/dist/workspace/capabilities-contract.d.ts +7 -0
  326. package/dist/workspace/capabilities-contract.d.ts.map +1 -1
  327. package/dist/workspace/capabilities-contract.js +2 -1
  328. package/dist/workspace/capabilities.schema.json +98 -0
  329. package/dist/workspace/change-set.schema.json +42 -3
  330. package/dist/workspace/ci-cloud-identity.d.ts +13 -1
  331. package/dist/workspace/ci-cloud-identity.d.ts.map +1 -1
  332. package/dist/workspace/ci-cloud-identity.js +30 -1
  333. package/dist/workspace/compose-graph.d.ts +10 -1
  334. package/dist/workspace/compose-graph.d.ts.map +1 -1
  335. package/dist/workspace/compose-graph.js +6 -1
  336. package/dist/workspace/conformance/index.d.ts +1 -1
  337. package/dist/workspace/conformance/index.d.ts.map +1 -1
  338. package/dist/workspace/conformance/index.js +6 -1
  339. package/dist/workspace/drift-cli.d.ts +81 -0
  340. package/dist/workspace/drift-cli.d.ts.map +1 -0
  341. package/dist/workspace/drift-cli.js +317 -0
  342. package/dist/workspace/drift.schema.json +672 -0
  343. package/dist/workspace/graph-cli.d.ts +2 -0
  344. package/dist/workspace/graph-cli.d.ts.map +1 -1
  345. package/dist/workspace/graph-cli.js +3 -0
  346. package/dist/workspace/graph.schema.json +72 -0
  347. package/dist/workspace/init.d.ts +5 -1
  348. package/dist/workspace/init.d.ts.map +1 -1
  349. package/dist/workspace/init.js +55 -9
  350. package/dist/workspace/ledger.schema.json +1 -1
  351. package/dist/workspace/nested-graph.d.ts.map +1 -1
  352. package/dist/workspace/nested-graph.js +2 -0
  353. package/dist/workspace/pr-report.schema.json +76 -3
  354. package/dist/workspace/promote-waves.d.ts +112 -0
  355. package/dist/workspace/promote-waves.d.ts.map +1 -0
  356. package/dist/workspace/promote-waves.js +290 -0
  357. package/dist/workspace/promote.d.ts +84 -2
  358. package/dist/workspace/promote.d.ts.map +1 -1
  359. package/dist/workspace/promote.js +347 -61
  360. package/dist/workspace/reason-codes.d.ts +12 -0
  361. package/dist/workspace/reason-codes.d.ts.map +1 -1
  362. package/dist/workspace/reason-codes.js +14 -0
  363. package/dist/workspace/search-contract.d.ts +3 -3
  364. package/dist/workspace/search-contract.d.ts.map +1 -1
  365. package/dist/workspace/search-contract.js +2 -0
  366. package/dist/workspace/search.schema.json +103 -5
  367. package/package.json +14 -1
  368. package/src/__fixtures__/drift-record/README.md +58 -0
  369. package/src/__fixtures__/drift-record/choudoufu-cone.ndjson +4 -0
  370. package/src/__fixtures__/drift-record/choudoufu-resource.ndjson +3 -0
  371. package/src/__fixtures__/drift-record/component-status-ledger.json +65 -0
  372. package/src/__fixtures__/drift-record/component-status.json +91 -0
  373. package/src/__fixtures__/drift-record/deep-diff.json +259 -0
  374. package/src/__fixtures__/drift-record/lifecycle-diff-live.json +825 -0
  375. package/src/__fixtures__/drift-record/live-diff.json +125 -0
  376. package/src/audit/discover.e2e.test.ts +14 -0
  377. package/src/audit/discover.ts +3 -1
  378. package/src/behaviour.ts +1 -1
  379. package/src/capabilities/templates.json +145 -0
  380. package/src/capabilities/templates.test.ts +212 -0
  381. package/src/capabilities/templates.ts +204 -0
  382. package/src/change-set-bundle.test.ts +83 -0
  383. package/src/change-set-graph.ts +80 -9
  384. package/src/change-set.test.ts +24 -0
  385. package/src/change-set.ts +77 -14
  386. package/src/ci-identity-targets.test.ts +23 -0
  387. package/src/ci-identity.ts +10 -4
  388. package/src/cli/commands/import-index.test.ts +117 -0
  389. package/src/cli/commands/import-index.ts +265 -0
  390. package/src/cli/commands/lint.ts +4 -2
  391. package/src/cli/handlers/components.test.ts +9 -0
  392. package/src/cli/handlers/components.ts +12 -2
  393. package/src/cli/handlers/fan-out.ts +17 -3
  394. package/src/cli/handlers/graph.test.ts +42 -0
  395. package/src/cli/handlers/graph.ts +26 -4
  396. package/src/cli/handlers/index-cloud.ts +32 -0
  397. package/src/cli/handlers/index-diff.test.ts +68 -0
  398. package/src/cli/handlers/index-diff.ts +146 -0
  399. package/src/cli/handlers/ledger-root-member.test.ts +161 -0
  400. package/src/cli/handlers/lifecycle-drift.ts +68 -0
  401. package/src/cli/handlers/lifecycle.test.ts +33 -11
  402. package/src/cli/handlers/lifecycle.ts +205 -30
  403. package/src/cli/handlers/misc.ts +73 -0
  404. package/src/cli/handlers/pr-waves-blocked.test.ts +37 -2
  405. package/src/cli/handlers/pr.ts +16 -3
  406. package/src/cli/handlers/promote.test.ts +64 -0
  407. package/src/cli/handlers/promote.ts +81 -6
  408. package/src/cli/handlers/query.ts +27 -4
  409. package/src/cli/handlers/run-declared-reads-cli.test.ts +187 -0
  410. package/src/cli/handlers/run-groups-cli.test.ts +229 -0
  411. package/src/cli/handlers/scenario.test.ts +13 -0
  412. package/src/cli/handlers/scenario.ts +8 -7
  413. package/src/cli/handlers/search-index.test.ts +255 -0
  414. package/src/cli/handlers/search-json.test.ts +1 -1
  415. package/src/cli/handlers/search-ownership.test.ts +194 -0
  416. package/src/cli/handlers/search.ts +271 -38
  417. package/src/cli/main.test.ts +7 -0
  418. package/src/cli/main.ts +56 -1
  419. package/src/cli/mcp/lifecycle-tools.ts +3 -2
  420. package/src/cli/mcp/resource-handlers.ts +4 -3
  421. package/src/cli/mcp/workspace-tools.ts +1 -1
  422. package/src/cli/parse-args.ts +40 -2
  423. package/src/cli/registry.ts +19 -1
  424. package/src/cloud-lister/aws.test.ts +202 -0
  425. package/src/cloud-lister/aws.ts +226 -0
  426. package/src/cloud-lister/azure.test.ts +132 -0
  427. package/src/cloud-lister/azure.ts +142 -0
  428. package/src/cloud-lister/common.ts +115 -0
  429. package/src/cloud-lister/credentials.ts +110 -0
  430. package/src/cloud-lister/fakes.ts +66 -0
  431. package/src/cloud-lister/gcp.test.ts +121 -0
  432. package/src/cloud-lister/gcp.ts +159 -0
  433. package/src/cloud-lister/guard.test.ts +198 -0
  434. package/src/cloud-lister/guard.ts +111 -0
  435. package/src/cloud-lister/ids.ts +54 -0
  436. package/src/cloud-lister/index.ts +134 -0
  437. package/src/cloud-lister/kubernetes.test.ts +160 -0
  438. package/src/cloud-lister/kubernetes.ts +284 -0
  439. package/src/cloud-lister/markers.ts +74 -0
  440. package/src/cloud-lister/mask.ts +77 -0
  441. package/src/cloud-lister/shape.test.ts +81 -0
  442. package/src/cloud-lister/sigv4.ts +70 -0
  443. package/src/cloud-lister/transport.ts +118 -0
  444. package/src/cloud-lister/types.ts +135 -0
  445. package/src/components/capability-plugin-loader.ts +4 -0
  446. package/src/components/capability-plugin.ts +10 -0
  447. package/src/components/capability.ts +238 -0
  448. package/src/components/cli-support.ts +70 -14
  449. package/src/components/covered-round.test.ts +166 -0
  450. package/src/components/covered-round.ts +293 -0
  451. package/src/components/declared-reads.test.ts +103 -0
  452. package/src/components/declared-reads.ts +134 -0
  453. package/src/components/deploy-units.ts +4 -0
  454. package/src/components/driver.ts +101 -4
  455. package/src/components/fan-out-run.ts +95 -15
  456. package/src/components/fan-out-support.ts +10 -1
  457. package/src/components/fan-out.ts +30 -0
  458. package/src/components/groups.test.ts +320 -0
  459. package/src/components/groups.ts +880 -0
  460. package/src/components/index.ts +59 -0
  461. package/src/components/plan-shape-conformance.ts +130 -0
  462. package/src/components/pr-env-run-bundle.test.ts +2 -0
  463. package/src/components/pr-run.test.ts +728 -4
  464. package/src/components/pr-run.ts +395 -55
  465. package/src/components/promote-rounds.ts +25 -2
  466. package/src/components/promote.ts +55 -5
  467. package/src/components/registry.test.ts +1 -1
  468. package/src/components/release-gate.ts +115 -30
  469. package/src/components/release-groups.test.ts +362 -0
  470. package/src/components/run-groups.ts +113 -0
  471. package/src/components/starter-plugin.ts +6 -0
  472. package/src/components/verbs/index.ts +1 -0
  473. package/src/components/verbs/member-outputs.test.ts +205 -0
  474. package/src/components/verbs/member-outputs.ts +308 -0
  475. package/src/components/wave-plan.ts +5 -2
  476. package/src/config.ts +44 -0
  477. package/src/deep-observation.ts +5 -23
  478. package/src/drift-record.test.ts +596 -0
  479. package/src/drift-record.ts +1206 -0
  480. package/src/engine-facts.guard.test.ts +111 -0
  481. package/src/engine-facts.test.ts +178 -0
  482. package/src/engine-facts.ts +154 -0
  483. package/src/gated-waves-bundle.test.ts +2 -0
  484. package/src/graph-ir-live.test.ts +92 -0
  485. package/src/graph-ir.ts +317 -7
  486. package/src/graph-refs.ts +416 -0
  487. package/src/graph-schema/fixtures/adopt.json +15 -0
  488. package/src/graph-schema/fixtures/choudoufu.engine-graph.json +169 -0
  489. package/src/graph-schema/fixtures/index.json +68 -0
  490. package/src/graph-schema/fixtures/plunge.engine-graph.json +158 -0
  491. package/src/graph-schema/fixtures.ts +267 -0
  492. package/src/graph-schema/generate.ts +263 -0
  493. package/src/graph-schema/graph-schema.test.ts +153 -0
  494. package/src/graph-schema/graph.schema.json +1387 -0
  495. package/src/health.schema.json +2 -1
  496. package/src/identity.ts +2 -0
  497. package/src/index.ts +2 -0
  498. package/src/instance-nodes.ts +288 -0
  499. package/src/lexicon.ts +76 -0
  500. package/src/lifecycle/__fixtures__/index-diff/20261026T120000Z-aaaaaaaaaaaa.json +149 -0
  501. package/src/lifecycle/__fixtures__/index-diff/20261026T130000Z-bbbbbbbbbbbb.json +127 -0
  502. package/src/lifecycle/adoption-file.test.ts +100 -0
  503. package/src/lifecycle/adoption-file.ts +154 -0
  504. package/src/lifecycle/cloud-index.test.ts +517 -0
  505. package/src/lifecycle/cloud-index.ts +736 -0
  506. package/src/lifecycle/derive-edges.test.ts +193 -0
  507. package/src/lifecycle/derive-edges.ts +182 -0
  508. package/src/lifecycle/git.ts +10 -4
  509. package/src/lifecycle/identity-match.test.ts +308 -0
  510. package/src/lifecycle/identity-match.ts +257 -0
  511. package/src/lifecycle/index-diff.test.ts +198 -0
  512. package/src/lifecycle/index-diff.ts +476 -0
  513. package/src/lifecycle/index-document.test.ts +64 -0
  514. package/src/lifecycle/index-document.ts +299 -0
  515. package/src/lifecycle/index-drift.test.ts +191 -0
  516. package/src/lifecycle/index-drift.ts +304 -0
  517. package/src/lifecycle/index-import.test.ts +255 -0
  518. package/src/lifecycle/index-import.ts +634 -0
  519. package/src/lifecycle/index-join.test.ts +250 -0
  520. package/src/lifecycle/index-join.ts +304 -0
  521. package/src/lifecycle/index-store-load-bearing.test.ts +187 -0
  522. package/src/lifecycle/index.ts +4 -0
  523. package/src/lifecycle/member-ledger.test.ts +25 -7
  524. package/src/lifecycle/observation-baseline.test.ts +72 -1
  525. package/src/lifecycle/observation-baseline.ts +190 -27
  526. package/src/lifecycle/observe.ts +6 -1
  527. package/src/lifecycle/query.test.ts +69 -1
  528. package/src/lifecycle/query.ts +103 -10
  529. package/src/lifecycle/replay.test.ts +9 -5
  530. package/src/lifecycle/replay.ts +9 -5
  531. package/src/lifecycle/scenario.ts +4 -3
  532. package/src/lifecycle/snapshot-store-fake.ts +29 -0
  533. package/src/lifecycle/snapshot-store-s3.ts +316 -0
  534. package/src/lifecycle/snapshot-store.test.ts +385 -0
  535. package/src/lifecycle/snapshot-store.ts +981 -0
  536. package/src/lifecycle/snapshot.test.ts +13 -0
  537. package/src/lifecycle/snapshot.ts +23 -6
  538. package/src/lifecycle/type-listing.test.ts +166 -0
  539. package/src/lifecycle/type-listing.ts +309 -0
  540. package/src/lint/component-checks.ts +17 -1
  541. package/src/lint/config.test.ts +3 -0
  542. package/src/lint/presets/relaxed.json +2 -1
  543. package/src/lint/presets/strict.json +2 -1
  544. package/src/lint/rules/__fixtures__/comp/comp008/fail/dns.component.ts +15 -0
  545. package/src/lint/rules/__fixtures__/comp/comp008/pass/dns.component.ts +10 -0
  546. package/src/lint/rules/comp/comp.test.ts +2 -1
  547. package/src/lint/rules/comp/comp008-applies-signal-only-root.ts +121 -0
  548. package/src/lint/rules/comp/comp008.test.ts +77 -0
  549. package/src/lint/rules/comp/index.ts +4 -1
  550. package/src/observation-edges.test.ts +33 -0
  551. package/src/observation.ts +36 -1
  552. package/src/op/activities/activity-contracts.ts +1 -1
  553. package/src/op/activities/change-set.ts +8 -5
  554. package/src/op/activity-failure.ts +36 -0
  555. package/src/op/index.ts +2 -1
  556. package/src/op/local-executor.test.ts +80 -0
  557. package/src/op/local-executor.ts +27 -9
  558. package/src/pr-loop-bundle.test.ts +5 -2
  559. package/src/pr-loop.test.ts +57 -1
  560. package/src/pr-loop.ts +154 -6
  561. package/src/pulumi-urn.ts +151 -0
  562. package/src/release-floors.ts +22 -0
  563. package/src/sensitive-key.ts +34 -0
  564. package/src/workspace/adoption-report.test.ts +61 -0
  565. package/src/workspace/adoption-report.ts +103 -0
  566. package/src/workspace/capabilities-contract.test.ts +4 -0
  567. package/src/workspace/capabilities-contract.ts +9 -1
  568. package/src/workspace/capabilities.schema.json +98 -0
  569. package/src/workspace/change-set.schema.json +42 -3
  570. package/src/workspace/ci-cloud-identity.ts +31 -1
  571. package/src/workspace/compose-graph.test.ts +12 -0
  572. package/src/workspace/compose-graph.ts +12 -1
  573. package/src/workspace/conformance/conformance.e2e.test.ts +2 -1
  574. package/src/workspace/conformance/index.ts +6 -1
  575. package/src/workspace/drift-cli.test.ts +65 -0
  576. package/src/workspace/drift-cli.ts +365 -0
  577. package/src/workspace/drift.schema.json +672 -0
  578. package/src/workspace/graph-cli.ts +5 -0
  579. package/src/workspace/graph.schema.json +72 -0
  580. package/src/workspace/init.test.ts +38 -1
  581. package/src/workspace/init.ts +50 -8
  582. package/src/workspace/intent-gaps.test.ts +18 -2
  583. package/src/workspace/ledger.schema.json +1 -1
  584. package/src/workspace/nested-graph.ts +1 -0
  585. package/src/workspace/pr-report.schema.json +76 -3
  586. package/src/workspace/promote-waves.test.ts +296 -0
  587. package/src/workspace/promote-waves.ts +340 -0
  588. package/src/workspace/promote.test.ts +158 -4
  589. package/src/workspace/promote.ts +398 -57
  590. package/src/workspace/reason-codes.test.ts +4 -1
  591. package/src/workspace/reason-codes.ts +14 -0
  592. package/src/workspace/search-contract.ts +2 -0
  593. package/src/workspace/search.schema.json +103 -5
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Investigation templates (#4531): the capability catalog's second kind of
3
+ * entry, after the capabilities themselves (behold#1022 contract 3).
4
+ *
5
+ * A template is a fixed query with the resource filled in, so a click in
6
+ * behold, choudoufu's `investigate` (choudoufu#2280) and an agent run the
7
+ * same query. Each says which language it expands to (behold#1022 ruling Q4):
8
+ * a `chant search` path pattern where one can express the question, Cypher
9
+ * otherwise. A path template needs nothing beyond chant search, so it keeps
10
+ * working where behold's optional LadybugDB peer is missing; a Cypher one
11
+ * needs behold's graph pane.
12
+ *
13
+ * The catalog lives here, beside the capability rules (ruling Q5), and is
14
+ * data: `templates.json` beside this module, shipped as
15
+ * `@intentius/chant/capabilities/templates.json` and carried as `templates`
16
+ * on the capabilities document (`chant search --capabilities --json`).
17
+ * behold and choudoufu read it as data. The initial set is the one behold#1027
18
+ * stubbed in behold's src/graph-db/templates.ts.
19
+ *
20
+ * Filling in: a path template's `{{<input>}}` is replaced by the input's
21
+ * value, the resource's id for `{{node}}`. A Cypher template takes its inputs
22
+ * as query parameters, never spliced into the text: the resource's key is
23
+ * `$key` and every other input is `$<name>`.
24
+ */
25
+ import { NEXT_RELEASE } from "../release-floors";
26
+ import data from "./templates.json" with { type: "json" };
27
+
28
+ /** The first chant whose capabilities document carries `templates` (#4531). */
29
+ export const CAPABILITY_TEMPLATES_FLOOR = NEXT_RELEASE;
30
+
31
+ /** The version of templates.json's shape. */
32
+ export const TEMPLATES_DATA_VERSION = 1;
33
+
34
+ export type TemplateQueryKind = "path" | "cypher";
35
+
36
+ /** What answering reads: the declared source, a live read, recorded snapshots, or only the session's loaded graph. */
37
+ export type TemplateReads = "source" | "live" | "snapshots" | "session";
38
+
39
+ /**
40
+ * resource: the node the template is about, given as a name, an IR id, a
41
+ * block address or a cloud ID, and resolved to one node before expanding.
42
+ * scope: an environment or estate the query is read in. term: one search
43
+ * term, such as a kind or a capability call.
44
+ */
45
+ export type TemplateInputKind = "resource" | "scope" | "term";
46
+
47
+ export interface TemplateInput {
48
+ name: string;
49
+ kind: TemplateInputKind;
50
+ description: string;
51
+ }
52
+
53
+ export interface TemplateEntry {
54
+ id: string;
55
+ title: string;
56
+ /** The question it answers, in people's words. */
57
+ question: string;
58
+ /** The sources whose nodes it applies to (behold's source kinds); absent means every source. */
59
+ sources?: string[];
60
+ inputs: TemplateInput[];
61
+ reads: TemplateReads;
62
+ query: { kind: TemplateQueryKind; text: string };
63
+ /** What it does not answer, said where it is offered. */
64
+ note?: string;
65
+ }
66
+
67
+ /** A templates file that does not hold to the entry kind, naming the template. */
68
+ export class TemplateCatalogError extends Error {}
69
+
70
+ const ID = /^[a-z][a-z0-9-]*$/;
71
+ const INPUT_NAME = /^[a-z][A-Za-z0-9_]*$/;
72
+ const KINDS: readonly TemplateQueryKind[] = ["path", "cypher"];
73
+ const READS: readonly TemplateReads[] = ["source", "live", "snapshots", "session"];
74
+ const INPUT_KINDS: readonly TemplateInputKind[] = ["resource", "scope", "term"];
75
+ const PLACEHOLDER = /\{\{([^{}]*)\}\}/g;
76
+ const PARAM = /\$([A-Za-z_][A-Za-z0-9_]*)/g;
77
+
78
+ const str = (v: unknown): v is string => typeof v === "string" && v.trim() !== "";
79
+
80
+ /**
81
+ * Read a templates file, checking each entry against the entry kind: a
82
+ * unique id, exactly one resource input first, a query of a known kind
83
+ * whose placeholders (path) or parameters (Cypher) are its inputs, and every
84
+ * input used.
85
+ */
86
+ export function parseTemplates(raw: unknown): TemplateEntry[] {
87
+ const doc = raw as { version?: unknown; templates?: unknown };
88
+ if (!doc || typeof doc !== "object") throw new TemplateCatalogError("a templates file is an object");
89
+ if (doc.version !== TEMPLATES_DATA_VERSION) throw new TemplateCatalogError(`templates file version ${String(doc.version)} is not ${TEMPLATES_DATA_VERSION}`);
90
+ if (!Array.isArray(doc.templates)) throw new TemplateCatalogError("templates is a list");
91
+ const seen = new Set<string>();
92
+ return doc.templates.map((t, i) => {
93
+ const e = t as Partial<TemplateEntry>;
94
+ const where = str(e?.id) ? `template ${e.id}` : `template ${i}`;
95
+ const fail = (m: string): never => {
96
+ throw new TemplateCatalogError(`${where}: ${m}`);
97
+ };
98
+ if (!e || typeof e !== "object") fail("is not an object");
99
+ if (!str(e.id) || !ID.test(e.id)) fail("needs an id of lowercase letters, digits and -");
100
+ if (seen.has(e.id!)) fail("is defined twice");
101
+ seen.add(e.id!);
102
+ if (!str(e.title)) fail("needs a title");
103
+ if (!str(e.question)) fail("needs the question it answers");
104
+ if (!READS.includes(e.reads as TemplateReads)) fail(`reads is one of ${READS.join(", ")}`);
105
+ if (e.sources !== undefined && (!Array.isArray(e.sources) || !e.sources.every(str))) fail("sources is a list of source kinds");
106
+ if (e.note !== undefined && !str(e.note)) fail("a note is text");
107
+ if (!Array.isArray(e.inputs)) fail("needs its inputs");
108
+ const names = new Set<string>();
109
+ for (const input of e.inputs!) {
110
+ if (!input || !str(input.name) || !INPUT_NAME.test(input.name)) fail("an input needs a name");
111
+ if (names.has(input.name)) fail(`input ${input.name} is listed twice`);
112
+ names.add(input.name);
113
+ if (!INPUT_KINDS.includes(input.kind)) fail(`input ${input.name}'s kind is one of ${INPUT_KINDS.join(", ")}`);
114
+ if (!str(input.description)) fail(`input ${input.name} needs a description`);
115
+ }
116
+ const resources = e.inputs!.filter((x) => x.kind === "resource");
117
+ if (resources.length !== 1 || e.inputs![0] !== resources[0]) fail("takes one resource input, first");
118
+ const q = e.query;
119
+ if (!q || !KINDS.includes(q.kind) || !str(q.text)) fail(`query is { kind: ${KINDS.join(" | ")}, text }`);
120
+ const resource = resources[0].name;
121
+ if (q!.kind === "path") {
122
+ if (/\$[A-Za-z_]/.test(q!.text)) fail("a path pattern takes {{input}} placeholders, not $parameters");
123
+ const used = new Set([...q!.text.matchAll(PLACEHOLDER)].map((m) => m[1]));
124
+ for (const u of used) if (!names.has(u)) fail(`{{${u}}} names no input`);
125
+ // A scope is where the query is read, not part of its text.
126
+ for (const input of e.inputs!) if (input.kind !== "scope" && !used.has(input.name)) fail(`the path pattern never uses ${input.name}`);
127
+ } else {
128
+ if (/\{\{/.test(q!.text)) fail("a Cypher query takes $parameters, never spliced {{input}} text");
129
+ if (!/^\s*(MATCH|OPTIONAL MATCH|WITH|CALL|UNWIND)\b/i.test(q!.text)) fail("a Cypher query starts with a reading clause");
130
+ if (/\b(CREATE|MERGE|DELETE|DETACH|SET|REMOVE|DROP)\b/i.test(q!.text)) fail("a Cypher query only reads");
131
+ if (!balanced(q!.text)) fail("its brackets do not balance");
132
+ const params = new Set([...q!.text.matchAll(PARAM)].map((m) => m[1]));
133
+ if (!params.has("key")) fail("a Cypher query binds the resource as $key");
134
+ for (const p of params) if (p !== "key" && !names.has(p)) fail(`$${p} names no input`);
135
+ for (const input of e.inputs!) if (input.name !== resource && input.kind !== "scope" && !params.has(input.name)) fail(`the query never uses $${input.name}`);
136
+ }
137
+ return e as TemplateEntry;
138
+ });
139
+ }
140
+
141
+ /** Whether (), [] and {} nest, outside quoted strings. */
142
+ function balanced(text: string): boolean {
143
+ const open: string[] = [];
144
+ const pair: Record<string, string> = { ")": "(", "]": "[", "}": "{" };
145
+ let quote: string | undefined;
146
+ for (const c of text) {
147
+ if (quote) {
148
+ if (c === quote) quote = undefined;
149
+ continue;
150
+ }
151
+ if (c === "'" || c === '"') quote = c;
152
+ else if (c === "(" || c === "[" || c === "{") open.push(c);
153
+ else if (c in pair && open.pop() !== pair[c]) return false;
154
+ }
155
+ return open.length === 0 && !quote;
156
+ }
157
+
158
+ let cache: TemplateEntry[] | undefined;
159
+
160
+ /** The templates chant ships, checked. */
161
+ export function investigationTemplates(): TemplateEntry[] {
162
+ cache ??= parseTemplates(data);
163
+ return cache;
164
+ }
165
+
166
+ /** A value spliced into a path pattern is one search term: letters, digits and `_ : . - / * ( ) , [ ]` only. */
167
+ const TERM_VALUE = /^[A-Za-z0-9_:.\-/*(),[\]]+$/;
168
+
169
+ export interface ExpandedTemplate {
170
+ template: string;
171
+ kind: TemplateQueryKind;
172
+ reads: TemplateReads;
173
+ /** path: the chant search query. cypher: the statement, unchanged. */
174
+ text: string;
175
+ /** cypher: the parameters, the resource as `key`. */
176
+ params?: Record<string, string>;
177
+ /** The scope inputs given, which say where the query is read. */
178
+ scope?: Record<string, string>;
179
+ }
180
+
181
+ /**
182
+ * Fill one template. `inputs` holds every input by name, the resource input
183
+ * as the resolved node's id (path) or key (Cypher). A value that would make
184
+ * a second search term is refused.
185
+ */
186
+ export function expandTemplate(t: TemplateEntry, inputs: Record<string, string>): ExpandedTemplate {
187
+ for (const i of t.inputs) if (!inputs[i.name]) throw new TemplateCatalogError(`${t.id} needs ${i.name}: ${i.description}`);
188
+ for (const k of Object.keys(inputs)) if (!t.inputs.some((i) => i.name === k)) throw new TemplateCatalogError(`${t.id} takes no input ${k}`);
189
+ const scopes = t.inputs.filter((i) => i.kind === "scope");
190
+ const scope = scopes.length ? Object.fromEntries(scopes.map((i) => [i.name, inputs[i.name]!])) : undefined;
191
+ const head = { template: t.id, kind: t.query.kind, reads: t.reads, ...(scope ? { scope } : {}) };
192
+ if (t.query.kind === "cypher") {
193
+ const resource = t.inputs[0].name;
194
+ const params: Record<string, string> = { key: inputs[resource]! };
195
+ for (const i of t.inputs) if (i.name !== resource && i.kind !== "scope") params[i.name] = inputs[i.name]!;
196
+ return { ...head, text: t.query.text, params };
197
+ }
198
+ const text = t.query.text.replace(PLACEHOLDER, (_, name: string) => {
199
+ const v = inputs[name]!;
200
+ if (!TERM_VALUE.test(v)) throw new TemplateCatalogError(`${t.id}: ${name} ${JSON.stringify(v)} is not one search term`);
201
+ return v;
202
+ });
203
+ return { ...head, text };
204
+ }
@@ -7,6 +7,12 @@
7
7
  * lexicon's `change-set` subpath must not reach `typescript`, esbuild, zod or
8
8
  * chant's fold, lint and codegen modules. This bundles the three entry points
9
9
  * the way terragucci would and reads esbuild's own list of what went in.
10
+ *
11
+ * The drift record (#4451) ships under the same rule: `@intentius/chant/
12
+ * drift-record` and the terraform lexicon's `drift-record` subpath bundle to
13
+ * the plan hashing code, the observation contract, the change-set graph's
14
+ * node rule, the instance rollup, the sensitive-key list and the plan projection, and nothing
15
+ * heavier.
10
16
  */
11
17
 
12
18
  import { join } from "node:path";
@@ -50,8 +56,85 @@ describe("the change-set bundle", () => {
50
56
  "packages/core/src/intrinsic.ts",
51
57
  "packages/core/src/lifecycle/plan-digest.ts",
52
58
  "packages/core/src/plan-summary.ts",
59
+ // The next-release floor constant (#4480): no imports.
60
+ "packages/core/src/release-floors.ts",
53
61
  ]);
54
62
  const bytes = result.outputFiles.reduce((n, f) => n + f.contents.byteLength, 0);
55
63
  expect(bytes).toBeLessThan(BUDGET_BYTES);
56
64
  });
65
+
66
+ test("the drift-record bundle holds the record, its adapters and the digest, and nothing heavier", async () => {
67
+ const result = await build({
68
+ entryPoints: [
69
+ join(REPO, "packages", "core", "src", "drift-record.ts"),
70
+ join(REPO, "lexicons", "terraform", "src", "drift-record.ts"),
71
+ ],
72
+ bundle: true,
73
+ platform: "node",
74
+ format: "esm",
75
+ write: false,
76
+ metafile: true,
77
+ outdir: "out",
78
+ logLevel: "silent",
79
+ });
80
+ const inputs = Object.keys(result.metafile.inputs);
81
+ for (const pattern of FORBIDDEN) {
82
+ expect(inputs.filter((i) => pattern.test(i)), String(pattern)).toEqual([]);
83
+ }
84
+ expect(inputs.map((i) => i.replace(/^.*?(packages|lexicons)\//, "$1/")).sort()).toEqual([
85
+ "lexicons/terraform/src/drift-record.ts",
86
+ "lexicons/terraform/src/live-plan-reasons.ts",
87
+ "lexicons/terraform/src/plan-observation.ts",
88
+ "packages/core/src/change-set-graph.ts",
89
+ "packages/core/src/content-digest.ts",
90
+ "packages/core/src/declarable.ts",
91
+ "packages/core/src/drift-record.ts",
92
+ "packages/core/src/effect-receipt.ts",
93
+ "packages/core/src/instance-nodes.ts",
94
+ "packages/core/src/intrinsic.ts",
95
+ "packages/core/src/lifecycle/plan-digest.ts",
96
+ "packages/core/src/observation.ts",
97
+ "packages/core/src/pulumi-urn.ts",
98
+ "packages/core/src/sensitive-key.ts",
99
+ ]);
100
+ // About 66 KB today: each entry point carries its own copy of the shared modules.
101
+ const bytes = result.outputFiles.reduce((n, f) => n + f.contents.byteLength, 0);
102
+ expect(bytes).toBeLessThan(96 * 1024);
103
+ });
104
+
105
+ test("the pulumi lexicon's change-set and drift-record adapters (#4453) bundle the same way", async () => {
106
+ const result = await build({
107
+ entryPoints: [
108
+ join(REPO, "lexicons", "pulumi", "src", "change-set.ts"),
109
+ join(REPO, "lexicons", "pulumi", "src", "drift-record.ts"),
110
+ ],
111
+ bundle: true,
112
+ platform: "node",
113
+ format: "esm",
114
+ write: false,
115
+ metafile: true,
116
+ outdir: "out",
117
+ logLevel: "silent",
118
+ });
119
+ const inputs = Object.keys(result.metafile.inputs);
120
+ for (const pattern of FORBIDDEN) {
121
+ expect(inputs.filter((i) => pattern.test(i)), String(pattern)).toEqual([]);
122
+ }
123
+ expect(inputs.map((i) => i.replace(/^.*?(packages|lexicons)\//, "$1/")).sort()).toEqual([
124
+ "lexicons/pulumi/src/change-set.ts",
125
+ "lexicons/pulumi/src/drift-record.ts",
126
+ "lexicons/pulumi/src/values.ts",
127
+ "packages/core/src/change-set-graph.ts",
128
+ "packages/core/src/content-digest.ts",
129
+ "packages/core/src/declarable.ts",
130
+ "packages/core/src/drift-record.ts",
131
+ "packages/core/src/effect-receipt.ts",
132
+ "packages/core/src/instance-nodes.ts",
133
+ "packages/core/src/intrinsic.ts",
134
+ "packages/core/src/lifecycle/plan-digest.ts",
135
+ "packages/core/src/observation.ts",
136
+ "packages/core/src/pulumi-urn.ts",
137
+ "packages/core/src/sensitive-key.ts",
138
+ ]);
139
+ });
57
140
  });
@@ -18,11 +18,25 @@
18
18
  * a delete of a block the source no longer declares, or a warden or sql
19
19
  * change outside the graph.
20
20
  *
21
- * Like `./change-set.ts`, this imports nothing at run time, so it bundles
22
- * into one file with the change-set code.
21
+ * An instance of a `count` or `for_each` block gets its instance node
22
+ * (#4454): the block node's id followed by the instance key as the planner
23
+ * prints it, `<root>/module.app/terraform_data.instance[0]`, with
24
+ * `instanceOf` naming the block node. That holds whether or not the graph
25
+ * lists the instance node, since a graph read of the source lists only the
26
+ * instances it can count (`./instance-nodes.ts`). Under an expanded module
27
+ * call the module key sits on the call's segment (#4486):
28
+ * `module.app[0].aws_eip.pool[1]` is `<root>/module.app[0]/aws_eip.pool[1]`.
29
+ * {@link graphNodeFor} is the same rule for a single address, which a drift
30
+ * row uses.
31
+ *
32
+ * Like `./change-set.ts`, this imports nothing at run time but
33
+ * `./instance-nodes.ts` and `./pulumi-urn.ts`, which import nothing either,
34
+ * so it bundles into one file with the change-set code.
23
35
  */
24
36
 
25
37
  import type { ChangeSetDocument, ChangeSetEntry } from "./change-set";
38
+ import { terraformInstancePath } from "./instance-nodes";
39
+ import { pulumiNodeCandidates } from "./pulumi-urn";
26
40
 
27
41
  /** The graph a document is mapped onto: the node ids of one member's own IR. */
28
42
  export interface ChangeSetGraphNodes {
@@ -104,9 +118,14 @@ export function terraformBlockKey(address: string): string | undefined {
104
118
  * - `terraform`, `tofu`, `choudoufu`: `<root>/<block key>`, the root being
105
119
  * the change-set member (`terraformBlockKey`), then the address.
106
120
  * - `warden`, `sql`: the address.
121
+ * - `pulumi`: the address is a URN, and the id is `<member>/<key>` by the
122
+ * one URN rule (`pulumiNodeCandidates`, `./pulumi-urn.ts`, #4456); none
123
+ * for a provider or the root stack.
107
124
  */
108
125
  export function changeSetNodeCandidates(entry: Pick<ChangeSetEntry, "member" | "planner" | "address">): string[] {
109
126
  switch (entry.planner) {
127
+ case "pulumi":
128
+ return pulumiNodeCandidates(entry);
110
129
  case "terraform":
111
130
  case "tofu":
112
131
  case "choudoufu": {
@@ -118,24 +137,76 @@ export function changeSetNodeCandidates(entry: Pick<ChangeSetEntry, "member" | "
118
137
  }
119
138
  }
120
139
 
140
+ /** Where one address sits on a graph: its node, and the block node when that node is an instance's. */
141
+ export interface GraphNodePlacement {
142
+ /** The node id, with the member in front inside a workspace, or null when the graph holds none for the address. */
143
+ node: string | null;
144
+ /** The block node's id, set when `node` is an instance node (#4454). */
145
+ instanceOf?: string;
146
+ /** The source file of the node, or of its block node for an instance, from the graph's `sources`. */
147
+ source?: string;
148
+ }
149
+
150
+ /**
151
+ * The node an address of a member's planner is on, in `ids` (one member's
152
+ * node ids, without the member) with `prefix` in front: the first candidate
153
+ * ({@link changeSetNodeCandidates}) the graph holds, and for an instance of a
154
+ * terraform-family block that the graph holds, the instance node (#4454).
155
+ */
156
+ function placeOn(
157
+ entry: Pick<ChangeSetEntry, "member" | "planner" | "address">,
158
+ ids: ReadonlySet<string>,
159
+ prefix: string,
160
+ sources: ReadonlyMap<string, string> | undefined,
161
+ ): GraphNodePlacement {
162
+ const id = changeSetNodeCandidates(entry).find((c) => ids.has(c));
163
+ if (id === undefined) return { node: null };
164
+
165
+ const path =
166
+ (entry.planner === "terraform" || entry.planner === "tofu" || entry.planner === "choudoufu") && id !== entry.address
167
+ ? terraformInstancePath(entry.address)
168
+ : undefined;
169
+ // The block node the graph holds is `<root>/<block>`; its instance is
170
+ // `<root>/<instance>`, the module key on its call segment (#4486).
171
+ const root = path !== undefined && id.endsWith(`/${path.block}`) ? id.slice(0, id.length - path.block.length) : undefined;
172
+ if (path === undefined || root === undefined) {
173
+ const source = sources?.get(id);
174
+ return { node: `${prefix}${id}`, ...(source !== undefined ? { source } : {}) };
175
+ }
176
+ const instance = `${root}${path.instance}`;
177
+ const source = sources?.get(instance) ?? sources?.get(id);
178
+ return { node: `${prefix}${instance}`, instanceOf: `${prefix}${id}`, ...(source !== undefined ? { source } : {}) };
179
+ }
180
+
181
+ /**
182
+ * The node one engine address is on in `graph`, by the change set's mapping
183
+ * rules: what a drift row's `node` and `instanceOf` are (#4454), so a drift
184
+ * row and a change-set entry for the same instance name the same node. A null
185
+ * `graph` places nothing.
186
+ */
187
+ export function graphNodeFor(
188
+ entry: Pick<ChangeSetEntry, "member" | "planner" | "address">,
189
+ graph: ChangeSetGraphNodes | null,
190
+ ): GraphNodePlacement {
191
+ if (graph === null) return { node: null };
192
+ return placeOn(entry, new Set(graph.ids), graph.member ? `${graph.member}/` : "", graph.sources);
193
+ }
194
+
121
195
  /**
122
196
  * The document with each entry's `node` set from `graph`, and `graph`
123
197
  * recording which graph it was. A null `graph` (none could be read) gives
124
- * every entry `node: null`. The digest is unchanged: `node` is not hashed.
198
+ * every entry `node: null`. The digest is unchanged: `node`, `instanceOf` and
199
+ * `source` are not hashed.
125
200
  */
126
201
  export function mapChangeSetToGraph(doc: ChangeSetDocument, graph: ChangeSetGraphNodes | null): ChangeSetDocument {
127
202
  if (graph === null) {
128
- return { ...doc, entries: doc.entries.map((e) => ({ ...e, node: null })), graph: null };
203
+ return { ...doc, entries: doc.entries.map(({ instanceOf: _i, ...e }) => ({ ...e, node: null })), graph: null };
129
204
  }
130
205
  const ids = new Set(graph.ids);
131
206
  const prefix = graph.member ? `${graph.member}/` : "";
132
207
  return {
133
208
  ...doc,
134
- entries: doc.entries.map((e) => {
135
- const id = changeSetNodeCandidates(e).find((c) => ids.has(c));
136
- const source = id === undefined ? undefined : graph.sources?.get(id);
137
- return { ...e, node: id === undefined ? null : `${prefix}${id}`, ...(source !== undefined ? { source } : {}) };
138
- }),
209
+ entries: doc.entries.map(({ instanceOf: _i, ...e }) => ({ ...e, ...placeOn(e, ids, prefix, graph.sources) })),
139
210
  graph: { member: graph.member, at: graph.at },
140
211
  };
141
212
  }
@@ -28,6 +28,7 @@ import {
28
28
  changeSetDigest,
29
29
  changeSetDocumentDigest,
30
30
  composeChangeSet,
31
+ isEmptyChangeSet,
31
32
  lifecyclePlanPart,
32
33
  readChangeSetPartFile,
33
34
  reconcilePlanPart,
@@ -291,3 +292,26 @@ describe("readChangeSetPartFile (#3767)", () => {
291
292
  expect(failed.member).toMatchObject({ status: "failed", error: "out of order", planDigest: null });
292
293
  });
293
294
  });
295
+
296
+ describe("a grouped member (ws-107, #4440)", () => {
297
+ const digest = computePlanDigest("capability-group", { kind: "k", members: [], composition: "c" });
298
+ const part = (member: string, group?: { digest: string; reads: string[] }): ChangeSetPart => ({
299
+ member: { member, lexicon: "chant", planner: "chant", status: "planned", planDigest: computePlanDigest("p", member), holes: [], ...(group ? { group } : {}) },
300
+ entries: [],
301
+ });
302
+
303
+ test("a member without a group hashes as before; with one, the group is in the digest and the schema", () => {
304
+ const plain = composeChangeSet([part("a"), part("b")]);
305
+ const grouped = composeChangeSet([part("a", { digest, reads: [] }), part("b", { digest, reads: ["a.id"] })]);
306
+ expect(plain.digest).toBe(changeSetDocumentDigest({ members: plain.members, entries: [] }));
307
+ expect(grouped.digest).not.toBe(plain.digest);
308
+ expect(composeChangeSet([part("a", { digest, reads: [] }), part("b", { digest, reads: [] })]).digest).not.toBe(grouped.digest);
309
+ expect(verifyChangeSetDigest(grouped)).toBe(true);
310
+ expectValid(grouped);
311
+ });
312
+
313
+ test("a change set whose member reads a value its group computes is never empty (ruling 4)", () => {
314
+ expect(isEmptyChangeSet(composeChangeSet([part("a", { digest, reads: [] }), part("b", { digest, reads: [] })]))).toBe(true);
315
+ expect(isEmptyChangeSet(composeChangeSet([part("a", { digest, reads: [] }), part("b", { digest, reads: ["a.id"] })]))).toBe(false);
316
+ });
317
+ });
package/src/change-set.ts CHANGED
@@ -11,8 +11,8 @@
11
11
  * Each planner reaches it through an adapter that returns a
12
12
  * {@link ChangeSetPart}: one member and its entries. The lifecycle and
13
13
  * reconcile adapters are here; the terraform-family ones live in the
14
- * terraform lexicon's `change-set` subpath and the SQL ones in the sql
15
- * lexicon's. A part another process wrote to a file reads back through
14
+ * terraform lexicon's `change-set` subpath, the SQL ones in the sql
15
+ * lexicon's and the Pulumi one in the pulumi lexicon's. A part another process wrote to a file reads back through
16
16
  * {@link readChangeSetPartFile}. {@link composeChangeSet} joins
17
17
  * the parts.
18
18
  *
@@ -45,6 +45,7 @@
45
45
  import { computePlanDigest } from "./lifecycle/plan-digest";
46
46
  import type { ChangeSet as LifecycleChangeSet, ChangeSetEntry as LifecycleEntry } from "./lifecycle/change-set";
47
47
  import type { ChangeSet as ReconcileChangeSet } from "./reconcile";
48
+ import { NEXT_RELEASE } from "./release-floors";
48
49
 
49
50
  /** The schema a change-set document names in `$schema`. */
50
51
  export const CHANGE_SET_SCHEMA_ID = "https://intentius.io/chant/schemas/workspace/change-set/v1/change-set.schema.json";
@@ -52,6 +53,12 @@ export const CHANGE_SET_SCHEMA_ID = "https://intentius.io/chant/schemas/workspac
52
53
  /** The read-contract version the document follows. */
53
54
  export const CHANGE_SET_CONTRACT = 1;
54
55
 
56
+ /** The first chant whose change set can carry a member's `group` (ws-107, #4440). */
57
+ export const CHANGE_SET_GROUP_FLOOR = NEXT_RELEASE;
58
+
59
+ /** The first chant whose change set can carry a member's `placeholders` (#4463). */
60
+ export const CHANGE_SET_PLACEHOLDERS_FLOOR = NEXT_RELEASE;
61
+
55
62
  /** What a planner proposes for one resource. `no-op`, `read` and `forget` appear only when the planner reports them. */
56
63
  export type ChangeSetAction = "create" | "update" | "replace" | "delete" | "read" | "no-op" | "forget";
57
64
 
@@ -61,16 +68,17 @@ export const CHANGE_SET_ACTIONS: readonly ChangeSetAction[] = ["create", "update
61
68
  /** How much applying one change hurts, in the vocabulary of `./lifecycle/disruption.ts`. */
62
69
  export type ChangeSetDisruption = "in-place" | "rolling" | "replace" | "destroy" | "unknown";
63
70
 
64
- /** Which tool produced a member's plan. */
65
71
  /**
66
72
  * Which tool produced a member's plan. `sql` is a SQL schema change: a
67
73
  * versioned plan (a product's migrations) or one of the sql lexicon's
68
74
  * migration Ops' plans, through `@intentius/chant-lexicon-sql/change-set`.
75
+ * `pulumi` is a `pulumi preview --json` of a Pulumi member, through
76
+ * `@intentius/chant-lexicon-pulumi/change-set` (#4453); its addresses are URNs.
69
77
  */
70
- export type ChangeSetPlanner = "terraform" | "tofu" | "choudoufu" | "chant" | "warden" | "sql";
78
+ export type ChangeSetPlanner = "terraform" | "tofu" | "choudoufu" | "chant" | "warden" | "sql" | "pulumi";
71
79
 
72
80
  /** Every planner, in the order the schema lists them. */
73
- export const CHANGE_SET_PLANNERS: readonly ChangeSetPlanner[] = ["terraform", "tofu", "choudoufu", "chant", "warden", "sql"];
81
+ export const CHANGE_SET_PLANNERS: readonly ChangeSetPlanner[] = ["terraform", "tofu", "choudoufu", "chant", "warden", "sql", "pulumi"];
74
82
 
75
83
  /** One attribute a change writes, at top-level granularity. */
76
84
  export interface ChangeSetAttribute {
@@ -109,6 +117,18 @@ export interface ChangeSetEntry {
109
117
  importing?: true;
110
118
  /** Set on update, replace and delete. `unknown` means nobody could say. */
111
119
  disruption?: ChangeSetDisruption;
120
+ /**
121
+ * The planner could not enumerate what this entry stands for (#4453). A
122
+ * Pulumi program can register resources inside an `apply()`, which does not
123
+ * run at preview while its input is unknown, so the preview never lists
124
+ * them. The pulumi adapter keeps them in the set rather than dropping them:
125
+ * one entry per component whose children may appear, at
126
+ * `pulumiMayAppearAddress` of the component, and an entry for each resource
127
+ * the last export holds under such a component that the preview did not
128
+ * list or plans to delete. An entry carrying it is never empty
129
+ * ({@link isEmptyChangeSet}). Part of the digest.
130
+ */
131
+ mayAppear?: true;
112
132
  region?: string;
113
133
  /** The estate, environment or org the change lands in. */
114
134
  scope?: string;
@@ -120,8 +140,19 @@ export interface ChangeSetEntry {
120
140
  * a delete of a block the source no longer declares. Absent until the
121
141
  * document is mapped onto a graph (`./change-set-graph.ts`), and never
122
142
  * part of the digest.
143
+ *
144
+ * An instance of a `count` or `for_each` block (#4454) gets the instance
145
+ * node's id, the block node's id followed by the key as the planner prints
146
+ * it (`<member>/aws_eip.pool[0]`), and {@link instanceOf} names the block
147
+ * node. See `./instance-nodes.ts`.
123
148
  */
124
149
  node?: string | null;
150
+ /**
151
+ * On an entry whose `node` is an instance node (#4454): the id of the block
152
+ * node it is an instance of, the rollup, in the same id space as `node`.
153
+ * Absent on every other entry. Like `node`, never part of the digest.
154
+ */
155
+ instanceOf?: string;
125
156
  /**
126
157
  * The source file that declares the entry's node, from the repository
127
158
  * root (#4025): the node's `sourceLoc.file` in the member's graph IR, with
@@ -158,9 +189,36 @@ export interface ChangeSetMember {
158
189
  * summary never folds it into a group of real plans.
159
190
  */
160
191
  provisional?: true;
192
+ /**
193
+ * The inputs the member was previewed with stand-in values for (#4463),
194
+ * sorted: a Terraform root whose `vars` read an output not known yet is
195
+ * planned with a typed placeholder per such variable (`var.<name>`), and
196
+ * every attribute those drive is marked unknown in its entries. Its plan
197
+ * digest covers them. The apply plans it again with the real values and
198
+ * holds that plan to the preview's shape. Added within contract 1 by chant
199
+ * {@link CHANGE_SET_PLACEHOLDERS_FLOOR}.
200
+ */
201
+ placeholders?: string[];
161
202
  /** The planner's own digest of the same plan, when it prints one (choudoufu's per-root digest). */
162
203
  nativeDigest?: string;
163
204
  holes: ChangeSetHole[];
205
+ /**
206
+ * Set on a member a capability planned with others as one group (ws-107,
207
+ * #4440). Part of the document's digest when present. Added within
208
+ * contract 1 by chant {@link CHANGE_SET_GROUP_FLOOR}.
209
+ */
210
+ group?: ChangeSetMemberGroup;
211
+ }
212
+
213
+ /** A grouped member's group (ws-107): the group digest folded into its `planDigest`, and what it reads inside the group. */
214
+ export interface ChangeSetMemberGroup {
215
+ digest: string;
216
+ /**
217
+ * The `<component>.<output>` values it reads from another member of its
218
+ * group that the plan does not know, sorted. The group's apply computes
219
+ * them. A change set with any is never empty ({@link isEmptyChangeSet}).
220
+ */
221
+ reads: string[];
164
222
  }
165
223
 
166
224
  /** A provider-defined side effect an apply runs: a Terraform `action` block's invocation. */
@@ -288,7 +346,8 @@ export function changeSetDigest(members: ReadonlyArray<Pick<ChangeSetMember, "me
288
346
  *
289
347
  * A release gate's document (#4045) also hashes the `releaseDigest` and
290
348
  * `environment` it was planned for. A document without them hashes as it
291
- * always has, so no earlier digest moves.
349
+ * always has, so no earlier digest moves. A grouped member's `group`
350
+ * (ws-107) is hashed with it the same way: only when present.
292
351
  */
293
352
  export function changeSetDocumentDigest(
294
353
  doc: Pick<ChangeSetDocument, "members" | "entries" | "sideEffects" | "releaseDigest" | "environment">,
@@ -296,15 +355,15 @@ export function changeSetDocumentDigest(
296
355
  const provisional = new Set(doc.members.filter((m) => m.provisional === true).map((m) => m.member));
297
356
  const members = doc.members
298
357
  .filter((m) => !provisional.has(m.member))
299
- .map((m) => ({ member: m.member, status: m.status, holes: m.holes }))
358
+ .map((m) => ({ member: m.member, status: m.status, holes: m.holes, ...(m.group ? { group: { digest: m.group.digest, reads: [...m.group.reads].sort() } } : {}) }))
300
359
  .sort((a, b) => byString(a.member, b.member));
301
- // An entry's graph node (#4022) and its source file (#4025) say where on
302
- // the graph the change is, not what the plan does, so they stay outside
303
- // the digest: mapping a stored
360
+ // An entry's graph node (#4022), its block node (#4454) and its source
361
+ // file (#4025) say where on the graph the change is, not what the plan
362
+ // does, so they stay outside the digest: mapping a stored
304
363
  // document onto a graph never moves the value its gate bound.
305
364
  const entries = doc.entries
306
365
  .filter((e) => !provisional.has(e.member))
307
- .map(({ node: _node, source: _source, ...e }) => e)
366
+ .map(({ node: _node, instanceOf: _instanceOf, source: _source, ...e }) => e)
308
367
  .sort((a, b) => byString(entryKey(a), entryKey(b)));
309
368
  const sideEffects = (doc.sideEffects ?? []).filter((s) => !provisional.has(s.member)).sort(bySideEffect);
310
369
  const planned =
@@ -413,15 +472,19 @@ export function verifyChangeSetDigest(
413
472
  /**
414
473
  * Whether a change set changes nothing (ws-104 question 6, #4045): it has at
415
474
  * least one member, every member planned, none failed or provisional, no
416
- * member has a hole, and every entry is a `no-op` or a `read`. A release
417
- * gate over such a document passes without an approval.
475
+ * member has a hole, and every entry is a `no-op` or a `read` that no
476
+ * planner marked `mayAppear`. A release gate over such a document passes
477
+ * without an approval. A member that reads a value its group computes in its
478
+ * apply (`group.reads`, ws-107 ruling 4) makes it not empty: what the plan
479
+ * shows may not be all the apply does.
418
480
  */
419
481
  export function isEmptyChangeSet(doc: Pick<ChangeSetDocument, "members" | "entries">): boolean {
420
482
  if (doc.members.length === 0) return false;
421
483
  for (const m of doc.members) {
422
484
  if (m.status !== "planned" || m.provisional === true || m.holes.length > 0) return false;
485
+ if (m.group && m.group.reads.length > 0) return false;
423
486
  }
424
- return doc.entries.every((e) => e.action === "no-op" || e.action === "read");
487
+ return doc.entries.every((e) => (e.action === "no-op" || e.action === "read") && e.mayAppear !== true);
425
488
  }
426
489
 
427
490
  // ── chant lifecycle plan ───────────────────────────────────────────────────────