@intentius/chant 0.124.52 → 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 (584) 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 +15 -2
  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 +215 -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/declared-reads.d.ts +38 -0
  120. package/dist/components/declared-reads.d.ts.map +1 -0
  121. package/dist/components/declared-reads.js +131 -0
  122. package/dist/components/deploy-units.d.ts.map +1 -1
  123. package/dist/components/deploy-units.js +4 -0
  124. package/dist/components/driver.d.ts +34 -1
  125. package/dist/components/driver.d.ts.map +1 -1
  126. package/dist/components/driver.js +66 -3
  127. package/dist/components/fan-out-run.d.ts +23 -1
  128. package/dist/components/fan-out-run.d.ts.map +1 -1
  129. package/dist/components/fan-out-run.js +82 -14
  130. package/dist/components/fan-out-support.d.ts +6 -0
  131. package/dist/components/fan-out-support.d.ts.map +1 -1
  132. package/dist/components/fan-out-support.js +4 -1
  133. package/dist/components/fan-out.d.ts +28 -0
  134. package/dist/components/fan-out.d.ts.map +1 -1
  135. package/dist/components/groups.d.ts +229 -0
  136. package/dist/components/groups.d.ts.map +1 -0
  137. package/dist/components/groups.js +760 -0
  138. package/dist/components/index.d.ts +6 -2
  139. package/dist/components/index.d.ts.map +1 -1
  140. package/dist/components/index.js +4 -0
  141. package/dist/components/plan-shape-conformance.d.ts +48 -0
  142. package/dist/components/plan-shape-conformance.d.ts.map +1 -0
  143. package/dist/components/plan-shape-conformance.js +122 -0
  144. package/dist/components/pr-run.d.ts +41 -4
  145. package/dist/components/pr-run.d.ts.map +1 -1
  146. package/dist/components/pr-run.js +209 -39
  147. package/dist/components/promote-rounds.d.ts +9 -0
  148. package/dist/components/promote-rounds.d.ts.map +1 -1
  149. package/dist/components/promote-rounds.js +23 -2
  150. package/dist/components/promote.d.ts +17 -0
  151. package/dist/components/promote.d.ts.map +1 -1
  152. package/dist/components/promote.js +36 -6
  153. package/dist/components/release-gate.d.ts +23 -1
  154. package/dist/components/release-gate.d.ts.map +1 -1
  155. package/dist/components/release-gate.js +97 -33
  156. package/dist/components/run-groups.d.ts +58 -0
  157. package/dist/components/run-groups.d.ts.map +1 -0
  158. package/dist/components/run-groups.js +78 -0
  159. package/dist/components/starter-plugin.d.ts +1 -0
  160. package/dist/components/starter-plugin.d.ts.map +1 -1
  161. package/dist/components/starter-plugin.js +6 -1
  162. package/dist/components/verbs/index.d.ts +1 -0
  163. package/dist/components/verbs/index.d.ts.map +1 -1
  164. package/dist/components/verbs/index.js +1 -0
  165. package/dist/components/verbs/member-outputs.d.ts +187 -0
  166. package/dist/components/verbs/member-outputs.d.ts.map +1 -0
  167. package/dist/components/verbs/member-outputs.js +218 -0
  168. package/dist/config.d.ts +60 -0
  169. package/dist/config.d.ts.map +1 -1
  170. package/dist/config.js +27 -0
  171. package/dist/deep-observation.d.ts +1 -13
  172. package/dist/deep-observation.d.ts.map +1 -1
  173. package/dist/deep-observation.js +5 -22
  174. package/dist/drift-record.d.ts +542 -0
  175. package/dist/drift-record.d.ts.map +1 -0
  176. package/dist/drift-record.js +886 -0
  177. package/dist/engine-facts.d.ts +144 -0
  178. package/dist/engine-facts.d.ts.map +1 -0
  179. package/dist/engine-facts.js +14 -0
  180. package/dist/graph-ir.d.ts +153 -2
  181. package/dist/graph-ir.d.ts.map +1 -1
  182. package/dist/graph-ir.js +224 -6
  183. package/dist/graph-refs.d.ts +145 -0
  184. package/dist/graph-refs.d.ts.map +1 -1
  185. package/dist/graph-refs.js +305 -0
  186. package/dist/graph-schema/fixtures/adopt.json +15 -0
  187. package/dist/graph-schema/fixtures/choudoufu.engine-graph.json +169 -0
  188. package/dist/graph-schema/fixtures/index.json +68 -0
  189. package/dist/graph-schema/fixtures/plunge.engine-graph.json +158 -0
  190. package/dist/graph-schema/fixtures.d.ts +23 -0
  191. package/dist/graph-schema/fixtures.d.ts.map +1 -0
  192. package/dist/graph-schema/fixtures.js +246 -0
  193. package/dist/graph-schema/generate.d.ts +15 -0
  194. package/dist/graph-schema/generate.d.ts.map +1 -0
  195. package/dist/graph-schema/generate.js +267 -0
  196. package/dist/graph-schema/graph.schema.json +1387 -0
  197. package/dist/health.schema.json +2 -1
  198. package/dist/identity.d.ts.map +1 -1
  199. package/dist/identity.js +2 -0
  200. package/dist/index.d.ts +1 -0
  201. package/dist/index.d.ts.map +1 -1
  202. package/dist/index.js +2 -0
  203. package/dist/instance-nodes.d.ts +159 -0
  204. package/dist/instance-nodes.d.ts.map +1 -0
  205. package/dist/instance-nodes.js +231 -0
  206. package/dist/lexicon.d.ts +72 -0
  207. package/dist/lexicon.d.ts.map +1 -1
  208. package/dist/lifecycle/adoption-file.d.ts +56 -0
  209. package/dist/lifecycle/adoption-file.d.ts.map +1 -0
  210. package/dist/lifecycle/adoption-file.js +123 -0
  211. package/dist/lifecycle/cloud-index.d.ts +186 -0
  212. package/dist/lifecycle/cloud-index.d.ts.map +1 -0
  213. package/dist/lifecycle/cloud-index.js +618 -0
  214. package/dist/lifecycle/derive-edges.d.ts +59 -0
  215. package/dist/lifecycle/derive-edges.d.ts.map +1 -0
  216. package/dist/lifecycle/derive-edges.js +126 -0
  217. package/dist/lifecycle/git.d.ts +10 -9
  218. package/dist/lifecycle/git.d.ts.map +1 -1
  219. package/dist/lifecycle/git.js +7 -1
  220. package/dist/lifecycle/identity-match.d.ts +133 -0
  221. package/dist/lifecycle/identity-match.d.ts.map +1 -0
  222. package/dist/lifecycle/identity-match.js +147 -0
  223. package/dist/lifecycle/index-diff.d.ts +224 -0
  224. package/dist/lifecycle/index-diff.d.ts.map +1 -0
  225. package/dist/lifecycle/index-diff.js +327 -0
  226. package/dist/lifecycle/index-document.d.ts +130 -0
  227. package/dist/lifecycle/index-document.d.ts.map +1 -0
  228. package/dist/lifecycle/index-document.js +189 -0
  229. package/dist/lifecycle/index-drift.d.ts +121 -0
  230. package/dist/lifecycle/index-drift.d.ts.map +1 -0
  231. package/dist/lifecycle/index-drift.js +178 -0
  232. package/dist/lifecycle/index-import.d.ts +168 -0
  233. package/dist/lifecycle/index-import.d.ts.map +1 -0
  234. package/dist/lifecycle/index-import.js +515 -0
  235. package/dist/lifecycle/index-join.d.ts +129 -0
  236. package/dist/lifecycle/index-join.d.ts.map +1 -0
  237. package/dist/lifecycle/index-join.js +248 -0
  238. package/dist/lifecycle/index.d.ts +4 -0
  239. package/dist/lifecycle/index.d.ts.map +1 -1
  240. package/dist/lifecycle/index.js +4 -0
  241. package/dist/lifecycle/observation-baseline.d.ts +61 -21
  242. package/dist/lifecycle/observation-baseline.d.ts.map +1 -1
  243. package/dist/lifecycle/observation-baseline.js +144 -19
  244. package/dist/lifecycle/observe.d.ts.map +1 -1
  245. package/dist/lifecycle/observe.js +6 -1
  246. package/dist/lifecycle/query.d.ts +45 -4
  247. package/dist/lifecycle/query.d.ts.map +1 -1
  248. package/dist/lifecycle/query.js +89 -7
  249. package/dist/lifecycle/replay.d.ts +10 -3
  250. package/dist/lifecycle/replay.d.ts.map +1 -1
  251. package/dist/lifecycle/replay.js +10 -6
  252. package/dist/lifecycle/scenario.d.ts +4 -3
  253. package/dist/lifecycle/scenario.d.ts.map +1 -1
  254. package/dist/lifecycle/snapshot-store-fake.d.ts +9 -0
  255. package/dist/lifecycle/snapshot-store-fake.d.ts.map +1 -0
  256. package/dist/lifecycle/snapshot-store-fake.js +21 -0
  257. package/dist/lifecycle/snapshot-store-s3.d.ts +102 -0
  258. package/dist/lifecycle/snapshot-store-s3.d.ts.map +1 -0
  259. package/dist/lifecycle/snapshot-store-s3.js +254 -0
  260. package/dist/lifecycle/snapshot-store.d.ts +170 -0
  261. package/dist/lifecycle/snapshot-store.d.ts.map +1 -0
  262. package/dist/lifecycle/snapshot-store.js +832 -0
  263. package/dist/lifecycle/snapshot.d.ts +11 -0
  264. package/dist/lifecycle/snapshot.d.ts.map +1 -1
  265. package/dist/lifecycle/snapshot.js +18 -5
  266. package/dist/lifecycle/type-listing.d.ts +148 -0
  267. package/dist/lifecycle/type-listing.d.ts.map +1 -0
  268. package/dist/lifecycle/type-listing.js +166 -0
  269. package/dist/lint/component-checks.d.ts +15 -1
  270. package/dist/lint/component-checks.d.ts.map +1 -1
  271. package/dist/lint/component-checks.js +1 -0
  272. package/dist/lint/presets/relaxed.json +2 -1
  273. package/dist/lint/presets/strict.json +2 -1
  274. package/dist/lint/rules/__fixtures__/comp/comp008/fail/dns.component.d.ts +4 -0
  275. package/dist/lint/rules/__fixtures__/comp/comp008/fail/dns.component.d.ts.map +1 -0
  276. package/dist/lint/rules/__fixtures__/comp/comp008/fail/dns.component.js +13 -0
  277. package/dist/lint/rules/__fixtures__/comp/comp008/pass/dns.component.d.ts +4 -0
  278. package/dist/lint/rules/__fixtures__/comp/comp008/pass/dns.component.d.ts.map +1 -0
  279. package/dist/lint/rules/__fixtures__/comp/comp008/pass/dns.component.js +8 -0
  280. package/dist/lint/rules/comp/comp008-applies-signal-only-root.d.ts +33 -0
  281. package/dist/lint/rules/comp/comp008-applies-signal-only-root.d.ts.map +1 -0
  282. package/dist/lint/rules/comp/comp008-applies-signal-only-root.js +123 -0
  283. package/dist/lint/rules/comp/index.d.ts +2 -1
  284. package/dist/lint/rules/comp/index.d.ts.map +1 -1
  285. package/dist/lint/rules/comp/index.js +4 -1
  286. package/dist/observation.d.ts +19 -2
  287. package/dist/observation.d.ts.map +1 -1
  288. package/dist/observation.js +18 -1
  289. package/dist/op/activities/activity-contracts.d.ts +1 -1
  290. package/dist/op/activities/activity-contracts.js +1 -1
  291. package/dist/op/activities/change-set.d.ts +6 -4
  292. package/dist/op/activities/change-set.d.ts.map +1 -1
  293. package/dist/op/activities/change-set.js +4 -2
  294. package/dist/op/activity-failure.d.ts +27 -0
  295. package/dist/op/activity-failure.d.ts.map +1 -1
  296. package/dist/op/activity-failure.js +31 -0
  297. package/dist/op/index.d.ts +2 -1
  298. package/dist/op/index.d.ts.map +1 -1
  299. package/dist/op/index.js +1 -1
  300. package/dist/op/local-executor.d.ts +5 -1
  301. package/dist/op/local-executor.d.ts.map +1 -1
  302. package/dist/op/local-executor.js +22 -8
  303. package/dist/pr-loop.d.ts +56 -1
  304. package/dist/pr-loop.d.ts.map +1 -1
  305. package/dist/pr-loop.js +51 -1
  306. package/dist/pulumi-urn.d.ts +101 -0
  307. package/dist/pulumi-urn.d.ts.map +1 -0
  308. package/dist/pulumi-urn.js +131 -0
  309. package/dist/release-floors.d.ts +23 -0
  310. package/dist/release-floors.d.ts.map +1 -0
  311. package/dist/release-floors.js +22 -0
  312. package/dist/sensitive-key.d.ts +23 -0
  313. package/dist/sensitive-key.d.ts.map +1 -0
  314. package/dist/sensitive-key.js +31 -0
  315. package/dist/wave-runtime.mjs +234 -234
  316. package/dist/workspace/adoption-report.d.ts +53 -0
  317. package/dist/workspace/adoption-report.d.ts.map +1 -0
  318. package/dist/workspace/adoption-report.js +76 -0
  319. package/dist/workspace/capabilities-contract.d.ts +7 -0
  320. package/dist/workspace/capabilities-contract.d.ts.map +1 -1
  321. package/dist/workspace/capabilities-contract.js +2 -1
  322. package/dist/workspace/capabilities.schema.json +98 -0
  323. package/dist/workspace/change-set.schema.json +42 -3
  324. package/dist/workspace/ci-cloud-identity.d.ts +13 -1
  325. package/dist/workspace/ci-cloud-identity.d.ts.map +1 -1
  326. package/dist/workspace/ci-cloud-identity.js +30 -1
  327. package/dist/workspace/compose-graph.d.ts +10 -1
  328. package/dist/workspace/compose-graph.d.ts.map +1 -1
  329. package/dist/workspace/compose-graph.js +6 -1
  330. package/dist/workspace/conformance/index.d.ts +1 -1
  331. package/dist/workspace/conformance/index.d.ts.map +1 -1
  332. package/dist/workspace/conformance/index.js +6 -1
  333. package/dist/workspace/drift-cli.d.ts +81 -0
  334. package/dist/workspace/drift-cli.d.ts.map +1 -0
  335. package/dist/workspace/drift-cli.js +317 -0
  336. package/dist/workspace/drift.schema.json +672 -0
  337. package/dist/workspace/graph-cli.d.ts +2 -0
  338. package/dist/workspace/graph-cli.d.ts.map +1 -1
  339. package/dist/workspace/graph-cli.js +3 -0
  340. package/dist/workspace/graph.schema.json +72 -0
  341. package/dist/workspace/init.d.ts +5 -1
  342. package/dist/workspace/init.d.ts.map +1 -1
  343. package/dist/workspace/init.js +55 -9
  344. package/dist/workspace/ledger.schema.json +1 -1
  345. package/dist/workspace/nested-graph.d.ts.map +1 -1
  346. package/dist/workspace/nested-graph.js +2 -0
  347. package/dist/workspace/pr-report.schema.json +51 -2
  348. package/dist/workspace/promote-waves.d.ts +112 -0
  349. package/dist/workspace/promote-waves.d.ts.map +1 -0
  350. package/dist/workspace/promote-waves.js +290 -0
  351. package/dist/workspace/promote.d.ts +84 -2
  352. package/dist/workspace/promote.d.ts.map +1 -1
  353. package/dist/workspace/promote.js +347 -61
  354. package/dist/workspace/reason-codes.d.ts +12 -0
  355. package/dist/workspace/reason-codes.d.ts.map +1 -1
  356. package/dist/workspace/reason-codes.js +14 -0
  357. package/dist/workspace/search-contract.d.ts +3 -3
  358. package/dist/workspace/search-contract.d.ts.map +1 -1
  359. package/dist/workspace/search-contract.js +2 -0
  360. package/dist/workspace/search.schema.json +103 -5
  361. package/package.json +14 -1
  362. package/src/__fixtures__/drift-record/README.md +58 -0
  363. package/src/__fixtures__/drift-record/choudoufu-cone.ndjson +4 -0
  364. package/src/__fixtures__/drift-record/choudoufu-resource.ndjson +3 -0
  365. package/src/__fixtures__/drift-record/component-status-ledger.json +65 -0
  366. package/src/__fixtures__/drift-record/component-status.json +91 -0
  367. package/src/__fixtures__/drift-record/deep-diff.json +259 -0
  368. package/src/__fixtures__/drift-record/lifecycle-diff-live.json +825 -0
  369. package/src/__fixtures__/drift-record/live-diff.json +125 -0
  370. package/src/audit/discover.e2e.test.ts +14 -0
  371. package/src/audit/discover.ts +3 -1
  372. package/src/behaviour.ts +1 -1
  373. package/src/capabilities/templates.json +145 -0
  374. package/src/capabilities/templates.test.ts +212 -0
  375. package/src/capabilities/templates.ts +204 -0
  376. package/src/change-set-bundle.test.ts +83 -0
  377. package/src/change-set-graph.ts +80 -9
  378. package/src/change-set.test.ts +24 -0
  379. package/src/change-set.ts +77 -14
  380. package/src/ci-identity-targets.test.ts +23 -0
  381. package/src/ci-identity.ts +10 -4
  382. package/src/cli/commands/import-index.test.ts +117 -0
  383. package/src/cli/commands/import-index.ts +265 -0
  384. package/src/cli/commands/lint.ts +4 -2
  385. package/src/cli/handlers/components.test.ts +9 -0
  386. package/src/cli/handlers/components.ts +12 -2
  387. package/src/cli/handlers/fan-out.ts +17 -3
  388. package/src/cli/handlers/graph.test.ts +42 -0
  389. package/src/cli/handlers/graph.ts +26 -4
  390. package/src/cli/handlers/index-cloud.ts +32 -0
  391. package/src/cli/handlers/index-diff.test.ts +68 -0
  392. package/src/cli/handlers/index-diff.ts +146 -0
  393. package/src/cli/handlers/ledger-root-member.test.ts +161 -0
  394. package/src/cli/handlers/lifecycle-drift.ts +68 -0
  395. package/src/cli/handlers/lifecycle.test.ts +33 -11
  396. package/src/cli/handlers/lifecycle.ts +205 -30
  397. package/src/cli/handlers/misc.ts +73 -0
  398. package/src/cli/handlers/pr-waves-blocked.test.ts +37 -2
  399. package/src/cli/handlers/pr.ts +14 -2
  400. package/src/cli/handlers/promote.test.ts +64 -0
  401. package/src/cli/handlers/promote.ts +81 -6
  402. package/src/cli/handlers/query.ts +27 -4
  403. package/src/cli/handlers/run-declared-reads-cli.test.ts +187 -0
  404. package/src/cli/handlers/run-groups-cli.test.ts +229 -0
  405. package/src/cli/handlers/scenario.test.ts +13 -0
  406. package/src/cli/handlers/scenario.ts +8 -7
  407. package/src/cli/handlers/search-index.test.ts +255 -0
  408. package/src/cli/handlers/search-json.test.ts +1 -1
  409. package/src/cli/handlers/search-ownership.test.ts +194 -0
  410. package/src/cli/handlers/search.ts +271 -38
  411. package/src/cli/main.test.ts +7 -0
  412. package/src/cli/main.ts +56 -1
  413. package/src/cli/mcp/lifecycle-tools.ts +3 -2
  414. package/src/cli/mcp/resource-handlers.ts +4 -3
  415. package/src/cli/mcp/workspace-tools.ts +1 -1
  416. package/src/cli/parse-args.ts +40 -2
  417. package/src/cli/registry.ts +19 -1
  418. package/src/cloud-lister/aws.test.ts +202 -0
  419. package/src/cloud-lister/aws.ts +226 -0
  420. package/src/cloud-lister/azure.test.ts +132 -0
  421. package/src/cloud-lister/azure.ts +142 -0
  422. package/src/cloud-lister/common.ts +115 -0
  423. package/src/cloud-lister/credentials.ts +110 -0
  424. package/src/cloud-lister/fakes.ts +66 -0
  425. package/src/cloud-lister/gcp.test.ts +121 -0
  426. package/src/cloud-lister/gcp.ts +159 -0
  427. package/src/cloud-lister/guard.test.ts +198 -0
  428. package/src/cloud-lister/guard.ts +111 -0
  429. package/src/cloud-lister/ids.ts +54 -0
  430. package/src/cloud-lister/index.ts +134 -0
  431. package/src/cloud-lister/kubernetes.test.ts +160 -0
  432. package/src/cloud-lister/kubernetes.ts +284 -0
  433. package/src/cloud-lister/markers.ts +74 -0
  434. package/src/cloud-lister/mask.ts +77 -0
  435. package/src/cloud-lister/shape.test.ts +81 -0
  436. package/src/cloud-lister/sigv4.ts +70 -0
  437. package/src/cloud-lister/transport.ts +118 -0
  438. package/src/cloud-lister/types.ts +135 -0
  439. package/src/components/capability-plugin-loader.ts +4 -0
  440. package/src/components/capability-plugin.ts +10 -0
  441. package/src/components/capability.ts +229 -0
  442. package/src/components/cli-support.ts +70 -14
  443. package/src/components/declared-reads.test.ts +103 -0
  444. package/src/components/declared-reads.ts +134 -0
  445. package/src/components/deploy-units.ts +4 -0
  446. package/src/components/driver.ts +89 -4
  447. package/src/components/fan-out-run.ts +95 -15
  448. package/src/components/fan-out-support.ts +10 -1
  449. package/src/components/fan-out.ts +30 -0
  450. package/src/components/groups.test.ts +320 -0
  451. package/src/components/groups.ts +880 -0
  452. package/src/components/index.ts +42 -0
  453. package/src/components/plan-shape-conformance.ts +130 -0
  454. package/src/components/pr-env-run-bundle.test.ts +2 -0
  455. package/src/components/pr-run.test.ts +516 -3
  456. package/src/components/pr-run.ts +231 -41
  457. package/src/components/promote-rounds.ts +25 -2
  458. package/src/components/promote.ts +55 -5
  459. package/src/components/registry.test.ts +1 -1
  460. package/src/components/release-gate.ts +115 -30
  461. package/src/components/release-groups.test.ts +362 -0
  462. package/src/components/run-groups.ts +113 -0
  463. package/src/components/starter-plugin.ts +6 -0
  464. package/src/components/verbs/index.ts +1 -0
  465. package/src/components/verbs/member-outputs.test.ts +205 -0
  466. package/src/components/verbs/member-outputs.ts +308 -0
  467. package/src/config.ts +44 -0
  468. package/src/deep-observation.ts +5 -23
  469. package/src/drift-record.test.ts +596 -0
  470. package/src/drift-record.ts +1206 -0
  471. package/src/engine-facts.guard.test.ts +111 -0
  472. package/src/engine-facts.test.ts +178 -0
  473. package/src/engine-facts.ts +154 -0
  474. package/src/gated-waves-bundle.test.ts +2 -0
  475. package/src/graph-ir-live.test.ts +92 -0
  476. package/src/graph-ir.ts +317 -7
  477. package/src/graph-refs.ts +416 -0
  478. package/src/graph-schema/fixtures/adopt.json +15 -0
  479. package/src/graph-schema/fixtures/choudoufu.engine-graph.json +169 -0
  480. package/src/graph-schema/fixtures/index.json +68 -0
  481. package/src/graph-schema/fixtures/plunge.engine-graph.json +158 -0
  482. package/src/graph-schema/fixtures.ts +267 -0
  483. package/src/graph-schema/generate.ts +263 -0
  484. package/src/graph-schema/graph-schema.test.ts +153 -0
  485. package/src/graph-schema/graph.schema.json +1387 -0
  486. package/src/health.schema.json +2 -1
  487. package/src/identity.ts +2 -0
  488. package/src/index.ts +2 -0
  489. package/src/instance-nodes.ts +288 -0
  490. package/src/lexicon.ts +76 -0
  491. package/src/lifecycle/__fixtures__/index-diff/20261026T120000Z-aaaaaaaaaaaa.json +149 -0
  492. package/src/lifecycle/__fixtures__/index-diff/20261026T130000Z-bbbbbbbbbbbb.json +127 -0
  493. package/src/lifecycle/adoption-file.test.ts +100 -0
  494. package/src/lifecycle/adoption-file.ts +154 -0
  495. package/src/lifecycle/cloud-index.test.ts +517 -0
  496. package/src/lifecycle/cloud-index.ts +736 -0
  497. package/src/lifecycle/derive-edges.test.ts +193 -0
  498. package/src/lifecycle/derive-edges.ts +182 -0
  499. package/src/lifecycle/git.ts +10 -4
  500. package/src/lifecycle/identity-match.test.ts +308 -0
  501. package/src/lifecycle/identity-match.ts +257 -0
  502. package/src/lifecycle/index-diff.test.ts +198 -0
  503. package/src/lifecycle/index-diff.ts +476 -0
  504. package/src/lifecycle/index-document.test.ts +64 -0
  505. package/src/lifecycle/index-document.ts +299 -0
  506. package/src/lifecycle/index-drift.test.ts +191 -0
  507. package/src/lifecycle/index-drift.ts +304 -0
  508. package/src/lifecycle/index-import.test.ts +255 -0
  509. package/src/lifecycle/index-import.ts +634 -0
  510. package/src/lifecycle/index-join.test.ts +250 -0
  511. package/src/lifecycle/index-join.ts +304 -0
  512. package/src/lifecycle/index-store-load-bearing.test.ts +187 -0
  513. package/src/lifecycle/index.ts +4 -0
  514. package/src/lifecycle/member-ledger.test.ts +25 -7
  515. package/src/lifecycle/observation-baseline.test.ts +72 -1
  516. package/src/lifecycle/observation-baseline.ts +190 -27
  517. package/src/lifecycle/observe.ts +6 -1
  518. package/src/lifecycle/query.test.ts +69 -1
  519. package/src/lifecycle/query.ts +103 -10
  520. package/src/lifecycle/replay.test.ts +9 -5
  521. package/src/lifecycle/replay.ts +9 -5
  522. package/src/lifecycle/scenario.ts +4 -3
  523. package/src/lifecycle/snapshot-store-fake.ts +29 -0
  524. package/src/lifecycle/snapshot-store-s3.ts +316 -0
  525. package/src/lifecycle/snapshot-store.test.ts +385 -0
  526. package/src/lifecycle/snapshot-store.ts +981 -0
  527. package/src/lifecycle/snapshot.test.ts +13 -0
  528. package/src/lifecycle/snapshot.ts +23 -6
  529. package/src/lifecycle/type-listing.test.ts +166 -0
  530. package/src/lifecycle/type-listing.ts +309 -0
  531. package/src/lint/component-checks.ts +17 -1
  532. package/src/lint/config.test.ts +3 -0
  533. package/src/lint/presets/relaxed.json +2 -1
  534. package/src/lint/presets/strict.json +2 -1
  535. package/src/lint/rules/__fixtures__/comp/comp008/fail/dns.component.ts +15 -0
  536. package/src/lint/rules/__fixtures__/comp/comp008/pass/dns.component.ts +10 -0
  537. package/src/lint/rules/comp/comp.test.ts +2 -1
  538. package/src/lint/rules/comp/comp008-applies-signal-only-root.ts +121 -0
  539. package/src/lint/rules/comp/comp008.test.ts +77 -0
  540. package/src/lint/rules/comp/index.ts +4 -1
  541. package/src/observation-edges.test.ts +33 -0
  542. package/src/observation.ts +36 -1
  543. package/src/op/activities/activity-contracts.ts +1 -1
  544. package/src/op/activities/change-set.ts +8 -5
  545. package/src/op/activity-failure.ts +36 -0
  546. package/src/op/index.ts +2 -1
  547. package/src/op/local-executor.test.ts +80 -0
  548. package/src/op/local-executor.ts +27 -9
  549. package/src/pr-loop-bundle.test.ts +5 -2
  550. package/src/pr-loop.test.ts +43 -0
  551. package/src/pr-loop.ts +111 -2
  552. package/src/pulumi-urn.ts +151 -0
  553. package/src/release-floors.ts +22 -0
  554. package/src/sensitive-key.ts +34 -0
  555. package/src/workspace/adoption-report.test.ts +61 -0
  556. package/src/workspace/adoption-report.ts +103 -0
  557. package/src/workspace/capabilities-contract.test.ts +4 -0
  558. package/src/workspace/capabilities-contract.ts +9 -1
  559. package/src/workspace/capabilities.schema.json +98 -0
  560. package/src/workspace/change-set.schema.json +42 -3
  561. package/src/workspace/ci-cloud-identity.ts +31 -1
  562. package/src/workspace/compose-graph.test.ts +12 -0
  563. package/src/workspace/compose-graph.ts +12 -1
  564. package/src/workspace/conformance/conformance.e2e.test.ts +2 -1
  565. package/src/workspace/conformance/index.ts +6 -1
  566. package/src/workspace/drift-cli.test.ts +65 -0
  567. package/src/workspace/drift-cli.ts +365 -0
  568. package/src/workspace/drift.schema.json +672 -0
  569. package/src/workspace/graph-cli.ts +5 -0
  570. package/src/workspace/graph.schema.json +72 -0
  571. package/src/workspace/init.test.ts +38 -1
  572. package/src/workspace/init.ts +50 -8
  573. package/src/workspace/intent-gaps.test.ts +18 -2
  574. package/src/workspace/ledger.schema.json +1 -1
  575. package/src/workspace/nested-graph.ts +1 -0
  576. package/src/workspace/pr-report.schema.json +51 -2
  577. package/src/workspace/promote-waves.test.ts +296 -0
  578. package/src/workspace/promote-waves.ts +340 -0
  579. package/src/workspace/promote.test.ts +158 -4
  580. package/src/workspace/promote.ts +398 -57
  581. package/src/workspace/reason-codes.test.ts +4 -1
  582. package/src/workspace/reason-codes.ts +14 -0
  583. package/src/workspace/search-contract.ts +2 -0
  584. package/src/workspace/search.schema.json +103 -5
@@ -46,13 +46,14 @@ import {
46
46
  type GateContext,
47
47
  } from "./driver";
48
48
  import type { ReleaseIdentity } from "../telemetry-attribution";
49
- import { downstreamWithin, remainingFanOut, type FanOutPlan, type FanOutProgress, type FanOutSkip } from "./fan-out";
49
+ import { downstreamWithin, remainingFanOut, type FanOutGroup, type FanOutPlan, type FanOutProgress, type FanOutSkip } from "./fan-out";
50
50
  import { evaluateGate, gitGateLedgerPort, type GateCheck, type GateLedgerPort } from "../op/gate";
51
51
  import type { RunProgressEvent } from "./run-progress";
52
52
  import { emitGateStep, joinRunEventSinks, progressSinkOf, type RunEventSink } from "../run-event-stream";
53
- import type { CapabilityRegistry } from "./capability";
53
+ import type { CapabilityRegistry, GroupMemberStatus, GroupRun } from "./capability";
54
54
  import type { PendingGateRecord } from "../lifecycle/gate-ledger";
55
55
  import type { DeployContext } from "./capability";
56
+ import { groupsByComponent, narrowGroups, planAndRunComponentGroup, runComponentGroup } from "./groups";
56
57
  import { planWaveComponent, type WaveComponentPlan } from "./wave-plan";
57
58
  import { computePlanDigest } from "../lifecycle/plan-digest";
58
59
  import { decideWaveGate, runWaveLoop, waveGateName, waveSetDigest, type WaveMember, type WaveRecord } from "../wave-engine";
@@ -247,6 +248,20 @@ export interface FanOutRunOptions {
247
248
  * before it applied.
248
249
  */
249
250
  hold?: (component: string, componentOutputs: Record<string, Record<string, unknown>>) => string | undefined;
251
+ /**
252
+ * The approved plan of each group in `plan.groups` (ws-107), by the
253
+ * group's first component. A group runs through `runGroup` in its wave,
254
+ * in place of its members' own runs, and is held and blocked as one: a
255
+ * member held or blocked holds or blocks the rest. Not with `waveGate`.
256
+ */
257
+ groupRuns?: Record<string, GroupRun>;
258
+ /**
259
+ * A group with no entry in `groupRuns` plans in its wave against the
260
+ * outputs standing, then applies (#4465): `components fan-out` plans
261
+ * nothing before it runs. Without it such a group fails, which is what a
262
+ * pull request's apply wants for a group nobody approved.
263
+ */
264
+ planGroupsAtWave?: boolean;
250
265
  }
251
266
 
252
267
  /** A component {@link FanOutRunOptions.hold} held back, or one downstream of it. */
@@ -276,6 +291,14 @@ export interface FanOutRunResult {
276
291
  waves?: WaveRecord[];
277
292
  /** What {@link FanOutRunOptions.hold} held back, in plan order. Absent when nothing was. */
278
293
  held?: FanOutHeld[];
294
+ /**
295
+ * Members of a group whose apply stopped before their part began (ws-107),
296
+ * sorted. Each has a `fail` result but is not in `failed`; what depends on
297
+ * it outside the group is blocked by it. Absent when none.
298
+ */
299
+ notStarted?: string[];
300
+ /** How each member of a group that ran ended, as its capability said (#4446), by component. Absent when no group ran. */
301
+ groupOutcomes?: Record<string, GroupMemberStatus>;
279
302
  }
280
303
 
281
304
  /**
@@ -299,7 +322,8 @@ export async function runFanOut(
299
322
 
300
323
  // Resume first, so the gate is decided against the work that is actually left
301
324
  // and a re-run does not re-approve. The digest is carried, not recomputed.
302
- const active = options.progress ? remainingFanOut(plan, components, options.progress) : plan;
325
+ // A group keeps what is left of it (#4465).
326
+ const active = options.progress ? narrowGroups(remainingFanOut(plan, components, options.progress), components, plan.groups) : plan;
303
327
 
304
328
  if (options.gate && options.waveGate) {
305
329
  throw new Error("a fan-out takes one gate over the set or one gate per wave, not both");
@@ -307,6 +331,9 @@ export async function runFanOut(
307
331
  if (options.waveGate && options.plans) {
308
332
  throw new Error("a gated-wave fan-out plans each wave itself, so it takes no plans made beforehand");
309
333
  }
334
+ if (options.waveGate && plan.groups?.length) {
335
+ throw new Error("a gated-wave fan-out plans component by component, so it takes no groups");
336
+ }
310
337
  if (options.waveGate) {
311
338
  return runGatedWaves(plan, active, components, registry, options, options.waveGate, gates, componentOutputs);
312
339
  }
@@ -348,44 +375,80 @@ export async function runFanOut(
348
375
  const results: DriverComponentResult[] = [];
349
376
  const completed: string[] = [];
350
377
  const failed: string[] = [];
378
+ const notStarted: string[] = [];
379
+ const groupOutcomes: Record<string, GroupMemberStatus> = {};
351
380
  const blockedBy = new Map<string, string>();
352
381
  const held = new Map<string, FanOutHeld>();
353
382
  let gatedFact: PendingGateRecord | undefined;
354
383
 
355
384
  const inPlan = new Set(active.order);
356
385
  const waveOf = waveNumbers(active.waves);
357
- /** Hold `root` and everything downstream of it that has not settled otherwise. */
358
- const markHeld = (root: string, reason: string): void => {
359
- held.set(root, { component: root, reason });
386
+ // ws-107: a group applies as one unit, so it is held and blocked as one.
387
+ const groupOf = groupsByComponent(active);
388
+ /** Hold `root` and everything downstream of it that has not settled otherwise. With `heldBy`, `root` is held for that component's reason. */
389
+ const markHeld = (root: string, reason: string, heldBy?: string): void => {
390
+ held.set(root, { component: root, reason, ...(heldBy ? { heldBy } : {}) });
360
391
  emit?.({ type: "component-settle", wave: waveOf.get(root)!, component: root, status: "waiting" });
361
392
  for (const [component] of downstreamWithin(components, inPlan, [root])) {
362
393
  if (held.has(component) || blockedBy.has(component) || failed.includes(component)) continue;
363
- held.set(component, { component, reason, heldBy: root });
394
+ held.set(component, { component, reason, heldBy: heldBy ?? root });
364
395
  emit?.({ type: "component-settle", wave: waveOf.get(component)!, component, status: "waiting" });
365
396
  }
366
397
  };
367
- /** Skip everything downstream of `root`, blaming `root` rather than the nearest edge. */
368
- const markBlocked = (root: string): void => {
369
- for (const [component, by] of downstreamWithin(components, inPlan, [root])) {
370
- if (blockedBy.has(component) || failed.includes(component)) continue;
398
+ /** Skip everything downstream of `roots`, blaming a root rather than the nearest edge. */
399
+ const markBlocked = (...roots: string[]): void => {
400
+ for (const [component, by] of downstreamWithin(components, inPlan, roots)) {
401
+ if (blockedBy.has(component) || failed.includes(component) || completed.includes(component) || notStarted.includes(component)) continue;
371
402
  blockedBy.set(component, by);
372
403
  }
373
404
  };
374
405
 
375
406
  for (const [waveIndex, wave] of active.waves.entries()) {
376
407
  const waveNum = waveIndex + 1;
408
+ // A member blocked blocks its whole group, by the same failure.
409
+ for (const name of wave) {
410
+ const by = blockedBy.get(name);
411
+ if (by === undefined) continue;
412
+ for (const mate of groupOf.get(name)?.components ?? []) if (!blockedBy.has(mate) && mate !== by) blockedBy.set(mate, by);
413
+ }
377
414
  // A component whose upstream failed in an earlier wave is not dispatched.
378
415
  const candidates = wave.filter((name) => inPlan.has(name) && !blockedBy.has(name) && !held.has(name));
379
416
  for (const name of candidates) {
417
+ if (held.has(name)) continue;
380
418
  const reason = options.hold?.(name, componentOutputs);
381
- if (reason !== undefined) markHeld(name, reason);
419
+ if (reason === undefined) continue;
420
+ markHeld(name, reason);
421
+ for (const mate of groupOf.get(name)?.components ?? []) if (!held.has(mate)) markHeld(mate, reason, name);
382
422
  }
383
423
  const runnable = candidates.filter((name) => !held.has(name));
384
424
  if (runnable.length === 0) continue;
385
425
 
386
426
  emit?.({ type: "wave-start", wave: waveNum, components: runnable });
427
+ const groupsHere = [...new Set(runnable.map((name) => groupOf.get(name)).filter((g): g is FanOutGroup => g !== undefined))];
428
+ // Groups and the wave's other components run side by side, as a wave's components do.
429
+ const groupRuns = Promise.all(
430
+ groupsHere.map(async (group) => {
431
+ for (const name of group.components) emit?.({ type: "component-start", wave: waveNum, component: name });
432
+ const approved = options.groupRuns?.[group.components[0]!];
433
+ const each = {
434
+ group,
435
+ components: byName,
436
+ registry,
437
+ env: options.env,
438
+ ...(options.vars ? { vars: options.vars } : {}),
439
+ ctxOf: (name: string) => fanOutContext(options, name),
440
+ componentOutputs,
441
+ };
442
+ const ran = approved === undefined && options.planGroupsAtWave ? await planAndRunComponentGroup(each) : await runComponentGroup({ ...each, run: approved });
443
+ for (const result of ran.results) {
444
+ emit?.({ type: "component-settle", wave: waveNum, component: result.component, status: result.status === "fail" ? "failed" : result.status });
445
+ options.onComponentSettled?.(result, componentOutputs[result.component]);
446
+ }
447
+ return ran;
448
+ }),
449
+ );
387
450
  const waveResults = await Promise.all(
388
- runnable.map(async (name) => {
451
+ runnable.filter((name) => !groupOf.has(name)).map(async (name) => {
389
452
  emit?.({ type: "component-start", wave: waveNum, component: name });
390
453
  const planned = options.plans?.[name];
391
454
  const result = await runComponentDeploy(
@@ -407,6 +470,7 @@ export async function runFanOut(
407
470
  return result;
408
471
  }),
409
472
  );
473
+ const groupResults = await groupRuns;
410
474
  results.push(...waveResults);
411
475
 
412
476
  for (const result of waveResults) {
@@ -425,8 +489,22 @@ export async function runFanOut(
425
489
  gatedFact ??= result.gate;
426
490
  markBlocked(result.component);
427
491
  }
492
+ // A group's members settle together; what depends on a member that failed
493
+ // or never started, outside the group, is blocked by that member.
494
+ for (const ran of groupResults) {
495
+ results.push(...ran.results);
496
+ Object.assign(groupOutcomes, ran.outcomes);
497
+ const stopped: string[] = [];
498
+ for (const result of ran.results) {
499
+ if (result.status === "ok") completed.push(result.component);
500
+ else if (ran.notStarted.includes(result.component)) notStarted.push(result.component);
501
+ else failed.push(result.component);
502
+ if (result.status !== "ok") stopped.push(result.component);
503
+ }
504
+ if (stopped.length > 0) markBlocked(...stopped);
505
+ }
428
506
 
429
- const waveFailed = waveResults.some((r) => r.status === "fail");
507
+ const waveFailed = waveResults.some((r) => r.status === "fail") || groupResults.some((ran) => ran.results.some((r) => r.status === "fail"));
430
508
  emit?.({
431
509
  type: "wave-settle",
432
510
  wave: waveNum,
@@ -442,7 +520,7 @@ export async function runFanOut(
442
520
 
443
521
  // A failure outranks a gate: something is actually broken, and reporting the
444
522
  // run as "waiting on a human" would hide it. Same precedence the driver uses.
445
- const status: FanOutRunResult["status"] = failed.length > 0 ? "fail" : gatedFact ? "gated" : "ok";
523
+ const status: FanOutRunResult["status"] = failed.length > 0 || notStarted.length > 0 ? "fail" : gatedFact ? "gated" : "ok";
446
524
  emit?.({ type: "run-settle", status: status === "fail" ? "failed" : status, durationMs: null });
447
525
 
448
526
  return {
@@ -455,6 +533,8 @@ export async function runFanOut(
455
533
  componentOutputs,
456
534
  ...(gatedFact ? { gate: gatedFact } : {}),
457
535
  ...(held.size > 0 ? { held: active.order.filter((name) => held.has(name)).map((name) => held.get(name)!) } : {}),
536
+ ...(notStarted.length > 0 ? { notStarted: [...notStarted].sort() } : {}),
537
+ ...(Object.keys(groupOutcomes).length > 0 ? { groupOutcomes } : {}),
458
538
  };
459
539
  }
460
540
 
@@ -19,6 +19,7 @@ import { applyConfigDefaults } from "./config-defaults";
19
19
  import { buildCapabilityRegistry } from "./capability-plugin-loader";
20
20
  import { componentsForUnits, planFanOut, type ChangedUnits, type ComponentChangeSignal, type FanOutPlan } from "./fan-out";
21
21
  import type { CapabilityRegistry } from "./capability";
22
+ import { withDeclaredReads } from "./declared-reads";
22
23
  import type { DriverComponent } from "./driver";
23
24
  import type { ComponentEnvironmentDeclaration } from "./component";
24
25
  import type { BuildParamProvenance } from "../provenance";
@@ -36,6 +37,12 @@ export interface DeriveFanOutOptions {
36
37
  config?: ChantConfig;
37
38
  /** Components that form wave 1 (#3049). See `FanOutRequest.canary`. */
38
39
  canary?: string[];
40
+ /**
41
+ * The registry whose capabilities' `declaredReads` (#4466) are written into
42
+ * the components before the order is derived. Absent, the project's own
43
+ * (`fanOutRegistry`) is built for it.
44
+ */
45
+ registry?: CapabilityRegistry;
39
46
  }
40
47
 
41
48
  export interface DerivedFanOut {
@@ -78,7 +85,9 @@ export async function deriveFanOut(options: DeriveFanOutOptions): Promise<Derive
78
85
  }
79
86
 
80
87
  const { config } = options.config ? { config: options.config } : await loadChantConfig(options.path);
81
- const components = resolved.targets.map((component) => applyConfigDefaults(component, config));
88
+ const defaulted = resolved.targets.map((component) => applyConfigDefaults(component, config));
89
+ // #4466: the reads a capability finds in its steps' files, such as a template's Fn::ImportValue, order and hold like declared ones.
90
+ const components = await withDeclaredReads(defaulted, options.registry ?? (await fanOutRegistry(options.path, config)), options.path);
82
91
 
83
92
  const signal = componentsForUnits(components, options.units);
84
93
  const plan = planFanOut({
@@ -37,6 +37,7 @@ import { resolveComponentGraph, type DriverComponent } from "./driver";
37
37
  import { deployUnits } from "./deploy-units";
38
38
  import { computePlanDigest } from "../lifecycle/plan-digest";
39
39
  import { layerWaves } from "../gated-waves";
40
+ import type { ReportedGroupRead } from "./capability";
40
41
 
41
42
  /** Thrown when `changed` or `indeterminate` names a component the project does not have. */
42
43
  export class UnknownComponentError extends Error {
@@ -124,6 +125,35 @@ export interface FanOutPlan {
124
125
  indeterminate: string[];
125
126
  /** Identity of this derivation, for binding one approval to the whole fan-out (#2300's pattern). */
126
127
  digest: string;
128
+ /**
129
+ * The groups a pull request plans and applies as one (ws-107), each in
130
+ * one wave, set by `groupFanOut` (./groups.ts). `waves` are then the
131
+ * grouped waves. `digest` stays the derivation's. Absent when none formed.
132
+ */
133
+ groups?: FanOutGroup[];
134
+ /** The components that could have grouped and did not, each with why (ws-107, ruling 1). Absent when none. */
135
+ fallbacks?: FanOutGroupFallback[];
136
+ }
137
+
138
+ /** Components one capability plans and applies in one call (ws-107). */
139
+ export interface FanOutGroup {
140
+ /** The kind of the capability every member's one step names. */
141
+ capability: string;
142
+ /** The members, upstreams first. */
143
+ components: string[];
144
+ /**
145
+ * The reads between members the capability reported through `groupReads`
146
+ * (#4447), which no member's step input shows. Absent when none.
147
+ */
148
+ reads?: ReportedGroupRead[];
149
+ }
150
+
151
+ /** A component that did not join a group (ws-107): its capability's `kind`, and the fallback's `kind` and `reason`. */
152
+ export interface FanOutGroupFallback {
153
+ component: string;
154
+ capability: string;
155
+ kind: string;
156
+ reason: string;
127
157
  }
128
158
 
129
159
  /** Reverse the `dependsOn` edges: who has to re-run when this one moves. */
@@ -0,0 +1,320 @@
1
+ /**
2
+ * Which components form a group and where it sits (ws-107, #4440), against
3
+ * fake capabilities: `fake-group` declares `planGroup` and `runGroup`,
4
+ * `fake-plain` does not. Planning and applying a group in a pull request is
5
+ * in `pr-run.test.ts`.
6
+ */
7
+
8
+ import { readFileSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { describe, expect, test } from "vitest";
11
+ import { CapabilityRegistry, type GroupFallback, type GroupMember, type ReportedGroupRead } from "./capability";
12
+ import { planFanOut } from "./fan-out";
13
+ import { findComponentGroups, groupFanOut, narrowGroups, planComponentGroup } from "./groups";
14
+ import { prWavePlan } from "./pr-run";
15
+ import type { DriverComponent } from "./driver";
16
+
17
+ const so = (stack: string, name: string) => ({ stackOutput: { stack, name } });
18
+
19
+ const component = (kind: string, name: string, dependsOn: string[] = [], vars?: Record<string, unknown>, extra: Array<Record<string, unknown>> = []): DriverComponent => ({
20
+ name,
21
+ dependsOn,
22
+ deploy: [{ phase: "Apply", steps: [{ kind, root: name, ...(vars ? { vars } : {}) }, ...(extra as never[])] }],
23
+ });
24
+ const grouped = (name: string, dependsOn: string[] = [], vars?: Record<string, unknown>) => component("fake-group", name, dependsOn, vars);
25
+ const plain = (name: string, dependsOn: string[] = [], vars?: Record<string, unknown>) => component("fake-plain", name, dependsOn, vars);
26
+
27
+ function registry(fallback?: (members: readonly GroupMember[]) => GroupFallback | undefined) {
28
+ const asked: GroupMember[][] = [];
29
+ const r = new CapabilityRegistry();
30
+ const unused = async (): Promise<never> => {
31
+ throw new Error("not called here");
32
+ };
33
+ r.register({
34
+ kind: "fake-group",
35
+ run: unused,
36
+ plan: unused,
37
+ planGroup: unused,
38
+ runGroup: unused,
39
+ ...(fallback
40
+ ? {
41
+ async groupFallback(_ctx: unknown, members: readonly GroupMember[]) {
42
+ asked.push([...members]);
43
+ return fallback(members);
44
+ },
45
+ }
46
+ : {}),
47
+ } as never);
48
+ r.register({ kind: "fake-plain", run: unused, plan: unused } as never);
49
+ return { registry: r, asked };
50
+ }
51
+
52
+ async function groupsOf(components: DriverComponent[], changed: string[], r = registry().registry) {
53
+ const plan = planFanOut({ components, changed });
54
+ const found = await findComponentGroups({ components, plan, registry: r, env: "prod" });
55
+ return { plan, found, grouped: groupFanOut(plan, components, found) };
56
+ }
57
+
58
+ describe("which components form a group (ws-107)", () => {
59
+ /** a; b reads a; c reads b. */
60
+ const CHAIN = [grouped("a"), grouped("b", ["a"], { upstream: so("a", "id") }), grouped("c", ["b"], { upstream: so("b", "arn") })];
61
+
62
+ test("one-step components of a group capability joined by reads form one group, upstreams first", async () => {
63
+ const { found } = await groupsOf(CHAIN, ["a"]);
64
+ expect(found).toEqual({ groups: [{ capability: "fake-group", components: ["a", "b", "c"] }], fallbacks: [] });
65
+ });
66
+
67
+ test("a component that reads none of the others, or is of another capability, joins nothing and is not listed", async () => {
68
+ const components = [...CHAIN, grouped("lone", ["a"]), plain("p", ["a"], { upstream: so("a", "id") })];
69
+ const { found } = await groupsOf(components, ["a"]);
70
+ expect(found.groups).toEqual([{ capability: "fake-group", components: ["a", "b", "c"] }]);
71
+ expect(found.fallbacks).toEqual([]);
72
+ });
73
+
74
+ test("a component outside the set on a dependency path between two members splits it (ruling 2)", async () => {
75
+ // c reads a too, but m sits between them.
76
+ const components = [grouped("a"), plain("m", ["a"], { upstream: so("a", "id") }), grouped("c", ["m"], { upstream: so("m", "x"), first: so("a", "id") })];
77
+ const { found } = await groupsOf(components, ["a"]);
78
+ expect(found.groups).toEqual([]);
79
+ expect(found.fallbacks.map((f) => [f.component, f.capability, f.kind])).toEqual([
80
+ ["a", "fake-group", "split"],
81
+ ["c", "fake-group", "split"],
82
+ ]);
83
+ expect(found.fallbacks[0]!.reason).toMatch(/^m lies on a dependency path between a/);
84
+ });
85
+
86
+ test("a component whose group step is one of several does not join (ruling 3), and is listed as steps", async () => {
87
+ const components = [grouped("a"), component("fake-group", "s", ["a"], { upstream: so("a", "id") }, [{ kind: "gate", gate: "ok" }])];
88
+ const { found } = await groupsOf(components, ["a"]);
89
+ expect(found.groups).toEqual([]);
90
+ expect(found.fallbacks).toEqual([
91
+ { component: "s", capability: "fake-group", kind: "steps", reason: "s deploys in 2 steps, and only a component whose deploy is one fake-group step joins a group" },
92
+ ]);
93
+ });
94
+
95
+ test("the capability's groupFallback is asked before anything plans, with each member's reads and no input, and dissolves the group", async () => {
96
+ const r = registry(() => ({ kind: "off", reason: "grouping is turned off" }));
97
+ const { found, grouped: plan, plan: derived } = await groupsOf(CHAIN, ["a"], r.registry);
98
+ expect(r.asked).toHaveLength(1);
99
+ expect(r.asked[0]!.map((m) => m.component)).toEqual(["a", "b", "c"]);
100
+ expect(r.asked[0]![1]).toMatchObject({ component: "b", step: { root: "b", vars: { upstream: so("a", "id") } }, reads: [{ at: "/vars/upstream", ref: "a.id", component: "a", output: "id" }] });
101
+ expect(r.asked[0]![1]!.input).toBeUndefined();
102
+ expect(found.groups).toEqual([]);
103
+ expect(found.fallbacks.map((f) => [f.component, f.kind, f.reason])).toEqual([
104
+ ["a", "off", "grouping is turned off"],
105
+ ["b", "off", "grouping is turned off"],
106
+ ["c", "off", "grouping is turned off"],
107
+ ]);
108
+ // No group: the waves are the derivation's.
109
+ expect(plan.waves).toEqual(derived.waves);
110
+ expect(plan.groups).toBeUndefined();
111
+ expect(plan.fallbacks).toEqual(found.fallbacks);
112
+ });
113
+ });
114
+
115
+ describe("a groupFallback that drops members (#4419 ruling 1)", () => {
116
+ /** A fallback that drops `names` while any of them is still in the group, and takes the group once none is. */
117
+ const dropping = (names: string[]) => (members: readonly GroupMember[]): GroupFallback | undefined => {
118
+ const hit = members.filter((m) => names.includes(m.component)).map((m) => m.component);
119
+ return hit.length === 0 ? undefined : { kind: "lock", reason: "holds no lock", drop: hit.map((component) => ({ component, reason: `${component} holds no lock` })) };
120
+ };
121
+
122
+ test("dropping one of three leaves the other two grouped; core asks again with the two, and the report names the one", async () => {
123
+ const components = [grouped("a"), grouped("b", ["a"], { upstream: so("a", "id") }), grouped("c", ["b"], { upstream: so("b", "id") })];
124
+ const r = registry(dropping(["c"]));
125
+ const { found, grouped: plan } = await groupsOf(components, ["a"], r.registry);
126
+ expect(r.asked.map((ms) => ms.map((m) => m.component))).toEqual([
127
+ ["a", "b", "c"],
128
+ ["a", "b"],
129
+ ]);
130
+ expect(found.groups).toEqual([{ capability: "fake-group", components: ["a", "b"] }]);
131
+ expect(found.fallbacks).toEqual([{ component: "c", capability: "fake-group", kind: "lock", reason: "c holds no lock" }]);
132
+ expect(plan.fallbacks).toEqual(found.fallbacks);
133
+ // c keeps its wave after the group; b's emptied wave stays so wave numbers keep their meaning (ws-107).
134
+ expect(plan.waves).toEqual([["a", "b"], [], ["c"]]);
135
+ });
136
+
137
+ test("a dropped member on a dependency path between the rest splits them, each as split", async () => {
138
+ // c reads a and b; b reads a. Without b, a and c still read each other, but b applies between them.
139
+ const components = [grouped("a"), grouped("b", ["a"], { upstream: so("a", "id") }), grouped("c", ["a", "b"], { first: so("a", "id"), second: so("b", "id") })];
140
+ const r = registry(dropping(["b"]));
141
+ const { found } = await groupsOf(components, ["a"], r.registry);
142
+ expect(r.asked).toHaveLength(1);
143
+ expect(found.groups).toEqual([]);
144
+ expect(found.fallbacks.map((f) => [f.component, f.kind])).toEqual([
145
+ ["a", "split"],
146
+ ["b", "lock"],
147
+ ["c", "split"],
148
+ ]);
149
+ expect(found.fallbacks[0]!.reason).toMatch(/^b lies on a dependency path between a/);
150
+ });
151
+
152
+ test("a drop that leaves one member is no group, and that member falls back as alone", async () => {
153
+ const components = [grouped("a"), grouped("b", ["a"], { upstream: so("a", "id") })];
154
+ const r = registry(dropping(["a"]));
155
+ const { found, grouped: plan, plan: derived } = await groupsOf(components, ["a"], r.registry);
156
+ expect(r.asked).toHaveLength(1);
157
+ expect(found.groups).toEqual([]);
158
+ expect(found.fallbacks).toEqual([
159
+ { component: "a", capability: "fake-group", kind: "lock", reason: "a holds no lock" },
160
+ { component: "b", capability: "fake-group", kind: "alone", reason: "b is the only member of its group left once a fell back, and a group of one is no group" },
161
+ ]);
162
+ expect(plan.waves).toEqual(derived.waves);
163
+ });
164
+
165
+ test("a drop that names no member of the group dissolves it with the fallback's kind", async () => {
166
+ const r = registry(() => ({ kind: "lock", reason: "holds no lock", drop: [{ component: "elsewhere" }] }));
167
+ const components = [grouped("a"), grouped("b", ["a"], { upstream: so("a", "id") })];
168
+ const { found } = await groupsOf(components, ["a"], r.registry);
169
+ expect(found.groups).toEqual([]);
170
+ expect(found.fallbacks.map((f) => [f.component, f.kind, f.reason])).toEqual([
171
+ ["a", "lock", "holds no lock"],
172
+ ["b", "lock", "holds no lock"],
173
+ ]);
174
+ });
175
+ });
176
+
177
+ describe("where a group sits (ws-107)", () => {
178
+ test("in the earliest wave any member is in; the waves it left stay, empty, and the digest is the derivation's", async () => {
179
+ const components = [grouped("a"), grouped("b", ["a"], { upstream: so("a", "id") }), grouped("c", ["b"], { upstream: so("b", "id") })];
180
+ const { plan, grouped: g } = await groupsOf(components, ["a"]);
181
+ expect(plan.waves).toEqual([["a"], ["b"], ["c"]]);
182
+ expect(g.waves).toEqual([["a", "b", "c"], [], []]);
183
+ expect(g.order).toEqual(["a", "b", "c"]);
184
+ expect(g.digest).toBe(plan.digest);
185
+ });
186
+
187
+ test("after everything its members depend on outside it; what depends on a member moves after the group", async () => {
188
+ // b reads a and depends on u too; z depends on a.
189
+ const components = [plain("u"), grouped("a"), grouped("b", ["a", "u"], { upstream: so("a", "id") }), plain("z", ["a"])];
190
+ const { plan, grouped: g } = await groupsOf(components, ["a", "u"]);
191
+ expect(plan.waves).toEqual([["a", "u"], ["b", "z"]]);
192
+ expect(g.groups).toEqual([{ capability: "fake-group", components: ["a", "b"] }]);
193
+ expect(g.waves).toEqual([["u"], ["a", "b"], ["z"]]);
194
+ });
195
+
196
+ test("a round's narrowing keeps what is left of a group as one; a wave gate's wave carries the group (ws-107)", async () => {
197
+ const components = [plain("x"), grouped("a", ["x"], { upstream: so("x", "id") }), grouped("b", ["a"], { upstream: so("a", "id") })];
198
+ const { grouped: g } = await groupsOf(components, ["x"]);
199
+ expect(g.waves).toEqual([["x"], ["a", "b"], []]);
200
+ const left = narrowGroups({ ...g, order: ["a", "b"], waves: [["a"], ["b"]], groups: undefined }, components, g.groups);
201
+ expect(left.groups).toEqual(g.groups);
202
+ expect(left.waves).toEqual([["a", "b"], []]);
203
+ const wave = prWavePlan(g, components, 2, ["x"]);
204
+ expect(wave).toMatchObject({ order: ["a", "b"], waves: [["a", "b"]], groups: g.groups, seeds: ["x"], digest: g.digest });
205
+ });
206
+ });
207
+
208
+ describe("reads a capability reports through groupReads (#4447)", () => {
209
+ /** A registry whose `fake-group` reports `reads` and records what it was asked. */
210
+ function reporting(reads: ReportedGroupRead[]) {
211
+ const asked: Array<{ components: string[]; members: string[] }> = [];
212
+ const r = new CapabilityRegistry();
213
+ const unused = async (): Promise<never> => {
214
+ throw new Error("not called here");
215
+ };
216
+ r.register({
217
+ kind: "fake-group",
218
+ run: unused,
219
+ plan: unused,
220
+ runGroup: unused,
221
+ async planGroup(_ctx: unknown, members: readonly GroupMember[]) {
222
+ // a knows nothing at plan; b's outputs are all known.
223
+ const plans = Object.fromEntries(
224
+ members.map((m) => [m.component, { member: m.component, planDigest: `d-${m.component}`, outputs: m.component === "a" ? { known: {}, unknown: ["id"] } : { known: { arn: "x" }, unknown: [] } }]),
225
+ );
226
+ return { members: plans, digest: "g" };
227
+ },
228
+ async groupReads(ctx: { components: string[] }, members: readonly GroupMember[]) {
229
+ asked.push({ components: [...ctx.components], members: members.map((m) => m.component) });
230
+ return reads;
231
+ },
232
+ } as never);
233
+ r.register({ kind: "fake-plain", run: unused, plan: unused } as never);
234
+ return { registry: r, asked };
235
+ }
236
+
237
+ test("a reported read joins two components with no stackOutput read, upstream first, and is kept as the group's reads", async () => {
238
+ // a reads b only in a file core cannot see, and declares no dependsOn.
239
+ const components = [grouped("a"), grouped("b")];
240
+ const r = reporting([
241
+ { reader: "a", upstream: "b", ref: "data.remote.b" },
242
+ { reader: "a", upstream: "a", ref: "self" },
243
+ { reader: "a", upstream: "elsewhere", ref: "not.a.component" },
244
+ ]);
245
+ const { found } = await groupsOf(components, ["a", "b"], r.registry);
246
+ expect(r.asked).toHaveLength(1);
247
+ expect([...r.asked[0]!.members].sort()).toEqual(["a", "b"]);
248
+ expect(found).toEqual({
249
+ groups: [{ capability: "fake-group", components: ["b", "a"], reads: [{ reader: "a", upstream: "b", ref: "data.remote.b" }] }],
250
+ fallbacks: [],
251
+ });
252
+ });
253
+
254
+ test("a reported read the upstream's plan does not know is one of the reader's group reads in its change-set part", async () => {
255
+ const components = [grouped("a"), grouped("b")];
256
+ const r = reporting([{ reader: "b", upstream: "a", ref: "data.remote.a" }]);
257
+ const planned = await planComponentGroup({
258
+ group: { capability: "fake-group", components: ["a", "b"], reads: [{ reader: "b", upstream: "a", ref: "data.remote.a" }] },
259
+ components: new Map(components.map((c) => [c.name, c])),
260
+ registry: r.registry,
261
+ env: "prod",
262
+ ctxOf: (component) => ({ env: "prod", component }),
263
+ outputs: {},
264
+ });
265
+ const reads = Object.fromEntries(planned.plans.map((p) => [p.component, "error" in p ? p.error : p.parts?.[0]?.member.group?.reads]));
266
+ expect(reads).toEqual({ a: [], b: ["data.remote.a"] });
267
+ });
268
+
269
+ test("a reported read on a dependency path through a component of another kind splits the group (ruling 2)", async () => {
270
+ // b reads a in a file; m, of another kind, depends on a and b depends on m.
271
+ const components = [grouped("a"), plain("m", ["a"]), grouped("b", ["m"])];
272
+ const r = reporting([{ reader: "b", upstream: "a", ref: "data.remote.a" }]);
273
+ const { found } = await groupsOf(components, ["a"], r.registry);
274
+ expect(found.groups).toEqual([]);
275
+ expect(found.fallbacks.map((f) => [f.component, f.kind])).toEqual([
276
+ ["a", "split"],
277
+ ["b", "split"],
278
+ ]);
279
+ expect(found.fallbacks[0]!.reason).toMatch(/^m lies on a dependency path between a/);
280
+ });
281
+
282
+ test("a capability whose groupReads throws reports no reads", async () => {
283
+ const r = new CapabilityRegistry();
284
+ const unused = async (): Promise<never> => {
285
+ throw new Error("not called here");
286
+ };
287
+ r.register({ kind: "fake-group", run: unused, plan: unused, planGroup: unused, runGroup: unused, groupReads: unused } as never);
288
+ const { found } = await groupsOf([grouped("a"), grouped("b")], ["a", "b"], r);
289
+ expect(found).toEqual({ groups: [], fallbacks: [] });
290
+ });
291
+
292
+ test("narrowing a group keeps only the reported reads between members left", async () => {
293
+ const components = [grouped("a"), grouped("b"), grouped("c")];
294
+ const r = reporting([
295
+ { reader: "b", upstream: "a", ref: "data.remote.a" },
296
+ { reader: "c", upstream: "b", ref: "data.remote.b" },
297
+ ]);
298
+ const { grouped: g } = await groupsOf(components, ["a", "b", "c"], r.registry);
299
+ expect(g.groups).toEqual([{ capability: "fake-group", components: ["a", "b", "c"], reads: [
300
+ { reader: "b", upstream: "a", ref: "data.remote.a" },
301
+ { reader: "c", upstream: "b", ref: "data.remote.b" },
302
+ ] }]);
303
+ const left = narrowGroups({ ...g, order: ["b", "c"], waves: [["b", "c"]], groups: undefined }, components, g.groups);
304
+ expect(left.groups).toEqual([{ capability: "fake-group", components: ["b", "c"], reads: [{ reader: "c", upstream: "b", ref: "data.remote.b" }] }]);
305
+ });
306
+ });
307
+
308
+ describe("core stays lexicon-free here (ws-107)", () => {
309
+ const read = (file: string) => readFileSync(join(import.meta.dirname, file), "utf-8");
310
+ const imports = (source: string) => [...source.matchAll(/^import[^;]*?from\s+"([^"]+)"/gms)].map((m) => m[1]!);
311
+
312
+ test("the group modules import no lexicon, and groups.ts names none", () => {
313
+ for (const file of ["groups.ts", "capability.ts", "pr-run.ts", "fan-out-run.ts", "fan-out.ts"]) {
314
+ const from = imports(read(file));
315
+ expect(from.length, file).toBeGreaterThan(0);
316
+ expect(from.filter((m) => /lexicon|^@intentius\/chant-lexicon|lexicons\//.test(m)), file).toEqual([]);
317
+ }
318
+ expect(read("groups.ts")).not.toMatch(/terraform|tofu|choudoufu|cloudformation|\bcfn\b|kubernetes|\baws\b|\bazure\b|\bgcp\b/i);
319
+ });
320
+ });