@intentius/chant 0.46.0 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (377) hide show
  1. package/dist/audit/catalog.d.ts +13 -3
  2. package/dist/audit/catalog.d.ts.map +1 -1
  3. package/dist/audit/core.d.ts +30 -3
  4. package/dist/audit/core.d.ts.map +1 -1
  5. package/dist/audit/discover.d.ts +9 -2
  6. package/dist/audit/discover.d.ts.map +1 -1
  7. package/dist/audit/fetch.d.ts.map +1 -1
  8. package/dist/audit/report-html.d.ts.map +1 -1
  9. package/dist/audit/report-model.d.ts +6 -0
  10. package/dist/audit/report-model.d.ts.map +1 -1
  11. package/dist/audit/report.d.ts.map +1 -1
  12. package/dist/audit/rules-doc.d.ts.map +1 -1
  13. package/dist/audit/secrets.d.ts +95 -0
  14. package/dist/audit/secrets.d.ts.map +1 -0
  15. package/dist/audit/wrangler.d.ts +33 -0
  16. package/dist/audit/wrangler.d.ts.map +1 -0
  17. package/dist/build.d.ts +3 -3
  18. package/dist/build.d.ts.map +1 -1
  19. package/dist/cli/commands/audit.d.ts +7 -0
  20. package/dist/cli/commands/audit.d.ts.map +1 -1
  21. package/dist/cli/commands/build.d.ts +23 -0
  22. package/dist/cli/commands/build.d.ts.map +1 -1
  23. package/dist/cli/commands/lint.d.ts.map +1 -1
  24. package/dist/cli/handlers/build.d.ts.map +1 -1
  25. package/dist/cli/handlers/components.d.ts +31 -0
  26. package/dist/cli/handlers/components.d.ts.map +1 -1
  27. package/dist/cli/handlers/lifecycle.d.ts +12 -1
  28. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  29. package/dist/cli/handlers/operator.d.ts +32 -0
  30. package/dist/cli/handlers/operator.d.ts.map +1 -0
  31. package/dist/cli/handlers/scenario.d.ts +39 -0
  32. package/dist/cli/handlers/scenario.d.ts.map +1 -0
  33. package/dist/cli/main.d.ts.map +1 -1
  34. package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
  35. package/dist/cli/mcp/server.d.ts +35 -2
  36. package/dist/cli/mcp/server.d.ts.map +1 -1
  37. package/dist/cli/mcp/tools/explain.d.ts +6 -0
  38. package/dist/cli/mcp/tools/explain.d.ts.map +1 -1
  39. package/dist/cli/mcp/types.d.ts +29 -1
  40. package/dist/cli/mcp/types.d.ts.map +1 -1
  41. package/dist/cli/plugins.d.ts +1 -1
  42. package/dist/cli/plugins.d.ts.map +1 -1
  43. package/dist/cli/registry.d.ts +14 -2
  44. package/dist/cli/registry.d.ts.map +1 -1
  45. package/dist/cli/reporters/stylish.d.ts +15 -1
  46. package/dist/cli/reporters/stylish.d.ts.map +1 -1
  47. package/dist/codegen/docs-rule-scanning.d.ts.map +1 -1
  48. package/dist/components/auto-release.d.ts +4 -0
  49. package/dist/components/auto-release.d.ts.map +1 -1
  50. package/dist/components/capability.d.ts +17 -2
  51. package/dist/components/capability.d.ts.map +1 -1
  52. package/dist/components/cli-support.d.ts +7 -0
  53. package/dist/components/cli-support.d.ts.map +1 -1
  54. package/dist/components/component.d.ts +15 -0
  55. package/dist/components/component.d.ts.map +1 -1
  56. package/dist/components/driver.d.ts.map +1 -1
  57. package/dist/components/starter-plugin.d.ts +2 -0
  58. package/dist/components/starter-plugin.d.ts.map +1 -1
  59. package/dist/components/verbs/ensure-secret.d.ts +50 -0
  60. package/dist/components/verbs/ensure-secret.d.ts.map +1 -0
  61. package/dist/components/verbs/index.d.ts +13 -0
  62. package/dist/components/verbs/index.d.ts.map +1 -1
  63. package/dist/components/verbs/r2-sync.d.ts +76 -0
  64. package/dist/components/verbs/r2-sync.d.ts.map +1 -0
  65. package/dist/components/verbs/run-agent.d.ts +499 -0
  66. package/dist/components/verbs/run-agent.d.ts.map +1 -0
  67. package/dist/components/verbs/sign.d.ts +30 -0
  68. package/dist/components/verbs/sign.d.ts.map +1 -1
  69. package/dist/components/verbs/wrangler.d.ts +108 -0
  70. package/dist/components/verbs/wrangler.d.ts.map +1 -0
  71. package/dist/composite.d.ts +6 -1
  72. package/dist/composite.d.ts.map +1 -1
  73. package/dist/config.d.ts +26 -0
  74. package/dist/config.d.ts.map +1 -1
  75. package/dist/deep-observation.d.ts +14 -0
  76. package/dist/deep-observation.d.ts.map +1 -1
  77. package/dist/discovery/collect.d.ts.map +1 -1
  78. package/dist/discovery/fold-import.d.ts +15 -1
  79. package/dist/discovery/fold-import.d.ts.map +1 -1
  80. package/dist/discovery/fold-rank.d.ts +66 -0
  81. package/dist/discovery/fold-rank.d.ts.map +1 -0
  82. package/dist/discovery/index.d.ts +15 -0
  83. package/dist/discovery/index.d.ts.map +1 -1
  84. package/dist/discovery/param-deps.d.ts +17 -0
  85. package/dist/discovery/param-deps.d.ts.map +1 -0
  86. package/dist/effect-receipt.d.ts +177 -0
  87. package/dist/effect-receipt.d.ts.map +1 -0
  88. package/dist/fold/fold.d.ts +55 -2
  89. package/dist/fold/fold.d.ts.map +1 -1
  90. package/dist/fold/subset.d.ts +20 -0
  91. package/dist/fold/subset.d.ts.map +1 -1
  92. package/dist/index.d.ts +4 -0
  93. package/dist/index.d.ts.map +1 -1
  94. package/dist/lexicon-schema.d.ts +2 -0
  95. package/dist/lexicon-schema.d.ts.map +1 -1
  96. package/dist/lexicon.d.ts +137 -3
  97. package/dist/lexicon.d.ts.map +1 -1
  98. package/dist/lifecycle/change-set.d.ts +33 -5
  99. package/dist/lifecycle/change-set.d.ts.map +1 -1
  100. package/dist/lifecycle/converge-ledger.d.ts +90 -0
  101. package/dist/lifecycle/converge-ledger.d.ts.map +1 -0
  102. package/dist/lifecycle/deep-diff.d.ts +18 -0
  103. package/dist/lifecycle/deep-diff.d.ts.map +1 -1
  104. package/dist/lifecycle/deep-observe.d.ts +9 -1
  105. package/dist/lifecycle/deep-observe.d.ts.map +1 -1
  106. package/dist/lifecycle/gate-ledger.d.ts +33 -0
  107. package/dist/lifecycle/gate-ledger.d.ts.map +1 -0
  108. package/dist/lifecycle/git.d.ts +145 -21
  109. package/dist/lifecycle/git.d.ts.map +1 -1
  110. package/dist/lifecycle/index.d.ts +6 -0
  111. package/dist/lifecycle/index.d.ts.map +1 -1
  112. package/dist/lifecycle/lease.d.ts +113 -0
  113. package/dist/lifecycle/lease.d.ts.map +1 -0
  114. package/dist/lifecycle/observation-baseline.d.ts +21 -3
  115. package/dist/lifecycle/observation-baseline.d.ts.map +1 -1
  116. package/dist/lifecycle/receipt-plan.d.ts +62 -0
  117. package/dist/lifecycle/receipt-plan.d.ts.map +1 -0
  118. package/dist/lifecycle/release-ledger.d.ts +20 -0
  119. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  120. package/dist/lifecycle/scenario-eval.d.ts +42 -0
  121. package/dist/lifecycle/scenario-eval.d.ts.map +1 -0
  122. package/dist/lifecycle/scenario.d.ts +163 -0
  123. package/dist/lifecycle/scenario.d.ts.map +1 -0
  124. package/dist/lifecycle/symptoms.d.ts +63 -0
  125. package/dist/lifecycle/symptoms.d.ts.map +1 -0
  126. package/dist/lifecycle/teardown.d.ts +6 -4
  127. package/dist/lifecycle/teardown.d.ts.map +1 -1
  128. package/dist/lifecycle/unobserved-gate.d.ts +67 -0
  129. package/dist/lifecycle/unobserved-gate.d.ts.map +1 -0
  130. package/dist/lint/knowledge-checks.d.ts +48 -0
  131. package/dist/lint/knowledge-checks.d.ts.map +1 -0
  132. package/dist/lint/output-checks.d.ts +5 -0
  133. package/dist/lint/output-checks.d.ts.map +1 -0
  134. package/dist/lint/output-docs.d.ts +94 -0
  135. package/dist/lint/output-docs.d.ts.map +1 -0
  136. package/dist/lint/pipeline-change-gate.d.ts +101 -0
  137. package/dist/lint/pipeline-change-gate.d.ts.map +1 -0
  138. package/dist/lint/post-synth.d.ts +41 -0
  139. package/dist/lint/post-synth.d.ts.map +1 -1
  140. package/dist/lint/receipt-checks.d.ts +9 -0
  141. package/dist/lint/receipt-checks.d.ts.map +1 -0
  142. package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.d.ts +11 -0
  143. package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.d.ts.map +1 -0
  144. package/dist/lint/rules/cor022-receipt-leaf.d.ts +13 -0
  145. package/dist/lint/rules/cor022-receipt-leaf.d.ts.map +1 -0
  146. package/dist/lint/rules/cor024-receipt-secret-pointer.d.ts +3 -0
  147. package/dist/lint/rules/cor024-receipt-secret-pointer.d.ts.map +1 -0
  148. package/dist/lint/rules/evl001-non-literal-expression.d.ts.map +1 -1
  149. package/dist/lint/rules/index.d.ts +3 -1
  150. package/dist/lint/rules/index.d.ts.map +1 -1
  151. package/dist/lsp/lexicon-providers.d.ts +7 -0
  152. package/dist/lsp/lexicon-providers.d.ts.map +1 -1
  153. package/dist/okf-read.d.ts +78 -0
  154. package/dist/okf-read.d.ts.map +1 -0
  155. package/dist/op/activity-contract.d.ts +139 -0
  156. package/dist/op/activity-contract.d.ts.map +1 -0
  157. package/dist/op/builders.d.ts +140 -3
  158. package/dist/op/builders.d.ts.map +1 -1
  159. package/dist/op/converge-rule.d.ts +161 -0
  160. package/dist/op/converge-rule.d.ts.map +1 -0
  161. package/dist/op/generate-pipeline.d.ts +39 -0
  162. package/dist/op/generate-pipeline.d.ts.map +1 -0
  163. package/dist/op/index.d.ts +18 -2
  164. package/dist/op/index.d.ts.map +1 -1
  165. package/dist/op/local-executor.d.ts +2 -1
  166. package/dist/op/local-executor.d.ts.map +1 -1
  167. package/dist/op/op-verb-class.d.ts +42 -0
  168. package/dist/op/op-verb-class.d.ts.map +1 -0
  169. package/dist/op/operator.d.ts +128 -0
  170. package/dist/op/operator.d.ts.map +1 -0
  171. package/dist/op/receipt-store.d.ts +138 -0
  172. package/dist/op/receipt-store.d.ts.map +1 -0
  173. package/dist/op/step-output-ref.d.ts +187 -0
  174. package/dist/op/step-output-ref.d.ts.map +1 -0
  175. package/dist/op/types.d.ts +49 -2
  176. package/dist/op/types.d.ts.map +1 -1
  177. package/dist/provenance.d.ts +73 -3
  178. package/dist/provenance.d.ts.map +1 -1
  179. package/dist/runtime-adapter.d.ts +7 -1
  180. package/dist/runtime-adapter.d.ts.map +1 -1
  181. package/dist/secret-materialization.d.ts +138 -0
  182. package/dist/secret-materialization.d.ts.map +1 -0
  183. package/dist/secret-provenance.d.ts +218 -0
  184. package/dist/secret-provenance.d.ts.map +1 -0
  185. package/dist/serializer.d.ts +29 -0
  186. package/dist/serializer.d.ts.map +1 -1
  187. package/dist/toml.d.ts +40 -5
  188. package/dist/toml.d.ts.map +1 -1
  189. package/dist/yaml.d.ts.map +1 -1
  190. package/package.json +4 -1
  191. package/src/audit/catalog.test.ts +1 -1
  192. package/src/audit/catalog.ts +75 -3
  193. package/src/audit/core.test.ts +57 -0
  194. package/src/audit/core.ts +0 -0
  195. package/src/audit/detect-bundle.test.ts +1 -1
  196. package/src/audit/discover.test.ts +24 -0
  197. package/src/audit/discover.ts +40 -4
  198. package/src/audit/fetch.test.ts +216 -3
  199. package/src/audit/fetch.ts +270 -59
  200. package/src/audit/report-html.ts +5 -2
  201. package/src/audit/report-model.ts +9 -0
  202. package/src/audit/report.test.ts +22 -0
  203. package/src/audit/report.ts +3 -2
  204. package/src/audit/rules-doc.ts +13 -1
  205. package/src/audit/secrets.test.ts +303 -0
  206. package/src/audit/secrets.ts +406 -0
  207. package/src/audit/wrangler.test.ts +230 -0
  208. package/src/audit/wrangler.ts +290 -0
  209. package/src/build.test.ts +41 -0
  210. package/src/build.ts +39 -6
  211. package/src/cli/command-group.ts +1 -1
  212. package/src/cli/commands/__fixtures__/audit-fountain/agents/fleet.yaml +27 -0
  213. package/src/cli/commands/__fixtures__/audit-fountain/k8s/deploy.yaml +16 -0
  214. package/src/cli/commands/__fixtures__/audit-fountain-clean/fleet.yaml +20 -0
  215. package/src/cli/commands/__fixtures__/schemas/sarif-2.1.0.schema.json +2882 -0
  216. package/src/cli/commands/audit.test.ts +268 -1
  217. package/src/cli/commands/audit.ts +87 -18
  218. package/src/cli/commands/build.test.ts +245 -0
  219. package/src/cli/commands/build.ts +210 -21
  220. package/src/cli/commands/lint.ts +15 -3
  221. package/src/cli/handlers/build.ts +2 -0
  222. package/src/cli/handlers/components.test.ts +199 -1
  223. package/src/cli/handlers/components.ts +160 -3
  224. package/src/cli/handlers/explain.test.ts +70 -1
  225. package/src/cli/handlers/graph.test.ts +20 -0
  226. package/src/cli/handlers/graph.ts +12 -3
  227. package/src/cli/handlers/lifecycle.test.ts +115 -1
  228. package/src/cli/handlers/lifecycle.ts +96 -15
  229. package/src/cli/handlers/operator.test.ts +255 -0
  230. package/src/cli/handlers/operator.ts +240 -0
  231. package/src/cli/handlers/scenario.test.ts +456 -0
  232. package/src/cli/handlers/scenario.ts +330 -0
  233. package/src/cli/main.test.ts +23 -0
  234. package/src/cli/main.ts +72 -1
  235. package/src/cli/mcp/resource-handlers.ts +38 -1
  236. package/src/cli/mcp/server.test.ts +323 -3
  237. package/src/cli/mcp/server.ts +84 -7
  238. package/src/cli/mcp/tools/explain.ts +51 -2
  239. package/src/cli/mcp/types.ts +27 -1
  240. package/src/cli/plugins.ts +4 -2
  241. package/src/cli/registry.ts +14 -2
  242. package/src/cli/reporters/stylish.test.ts +154 -0
  243. package/src/cli/reporters/stylish.ts +154 -33
  244. package/src/codegen/docs-rule-scanning.test.ts +42 -0
  245. package/src/codegen/docs-rule-scanning.ts +25 -2
  246. package/src/components/README.md +7 -0
  247. package/src/components/auto-release.ts +6 -0
  248. package/src/components/capability.ts +17 -2
  249. package/src/components/cli-support.test.ts +17 -0
  250. package/src/components/cli-support.ts +13 -1
  251. package/src/components/component-schema.test.ts +32 -0
  252. package/src/components/component.schema.json +6 -0
  253. package/src/components/component.test.ts +21 -0
  254. package/src/components/component.ts +15 -0
  255. package/src/components/driver.ts +12 -4
  256. package/src/components/registry.test.ts +7 -2
  257. package/src/components/starter-plugin.ts +17 -0
  258. package/src/components/verbs/ensure-secret.test.ts +130 -0
  259. package/src/components/verbs/ensure-secret.ts +79 -0
  260. package/src/components/verbs/index.ts +13 -0
  261. package/src/components/verbs/r2-sync.test.ts +107 -0
  262. package/src/components/verbs/r2-sync.ts +124 -0
  263. package/src/components/verbs/run-agent.test.ts +683 -0
  264. package/src/components/verbs/run-agent.ts +786 -0
  265. package/src/components/verbs/sign.test.ts +19 -0
  266. package/src/components/verbs/sign.ts +34 -2
  267. package/src/components/verbs/wrangler.test.ts +170 -0
  268. package/src/components/verbs/wrangler.ts +241 -0
  269. package/src/composite.ts +31 -2
  270. package/src/config.test.ts +15 -0
  271. package/src/config.ts +30 -0
  272. package/src/deep-observation.test.ts +19 -0
  273. package/src/deep-observation.ts +17 -0
  274. package/src/discovery/collect.ts +11 -2
  275. package/src/discovery/fold-import.test.ts +54 -0
  276. package/src/discovery/fold-import.ts +178 -38
  277. package/src/discovery/fold-rank.test.ts +197 -0
  278. package/src/discovery/fold-rank.ts +346 -0
  279. package/src/discovery/index.ts +16 -1
  280. package/src/discovery/param-deps.test.ts +118 -0
  281. package/src/discovery/param-deps.ts +170 -0
  282. package/src/effect-receipt-exclusion.test.ts +190 -0
  283. package/src/effect-receipt.test.ts +419 -0
  284. package/src/effect-receipt.ts +412 -0
  285. package/src/fold/fold.test.ts +6 -2
  286. package/src/fold/fold.ts +184 -3
  287. package/src/fold/subset.test.ts +95 -6
  288. package/src/fold/subset.ts +66 -2
  289. package/src/index.ts +4 -0
  290. package/src/lexicon-schema.ts +3 -0
  291. package/src/lexicon.ts +151 -5
  292. package/src/lifecycle/change-set.ts +46 -7
  293. package/src/lifecycle/converge-ledger.test.ts +199 -0
  294. package/src/lifecycle/converge-ledger.ts +179 -0
  295. package/src/lifecycle/deep-diff.test.ts +79 -1
  296. package/src/lifecycle/deep-diff.ts +23 -0
  297. package/src/lifecycle/deep-observe.ts +13 -2
  298. package/src/lifecycle/gate-ledger.test.ts +103 -0
  299. package/src/lifecycle/gate-ledger.ts +140 -0
  300. package/src/lifecycle/git.test.ts +430 -0
  301. package/src/lifecycle/git.ts +446 -84
  302. package/src/lifecycle/index.ts +6 -0
  303. package/src/lifecycle/lease.test.ts +343 -0
  304. package/src/lifecycle/lease.ts +270 -0
  305. package/src/lifecycle/observation-baseline.test.ts +46 -0
  306. package/src/lifecycle/observation-baseline.ts +33 -1
  307. package/src/lifecycle/receipt-plan.test.ts +250 -0
  308. package/src/lifecycle/receipt-plan.ts +249 -0
  309. package/src/lifecycle/release-ledger.ts +20 -0
  310. package/src/lifecycle/scenario-eval.test.ts +199 -0
  311. package/src/lifecycle/scenario-eval.ts +158 -0
  312. package/src/lifecycle/scenario.test.ts +195 -0
  313. package/src/lifecycle/scenario.ts +321 -0
  314. package/src/lifecycle/symptoms.test.ts +116 -0
  315. package/src/lifecycle/symptoms.ts +126 -0
  316. package/src/lifecycle/teardown.test.ts +31 -0
  317. package/src/lifecycle/teardown.ts +6 -4
  318. package/src/lifecycle/unobserved-gate.test.ts +109 -0
  319. package/src/lifecycle/unobserved-gate.ts +102 -0
  320. package/src/lint/knowledge-checks.test.ts +80 -0
  321. package/src/lint/knowledge-checks.ts +74 -0
  322. package/src/lint/output-checks.test.ts +85 -0
  323. package/src/lint/output-checks.ts +99 -0
  324. package/src/lint/output-docs.test.ts +220 -0
  325. package/src/lint/output-docs.ts +204 -0
  326. package/src/lint/pipeline-change-gate.test.ts +144 -0
  327. package/src/lint/pipeline-change-gate.ts +153 -0
  328. package/src/lint/post-synth.test.ts +97 -0
  329. package/src/lint/post-synth.ts +60 -0
  330. package/src/lint/receipt-checks.test.ts +101 -0
  331. package/src/lint/receipt-checks.ts +93 -0
  332. package/src/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.ts +26 -0
  333. package/src/lint/rules/comp/comp.test.ts +49 -1
  334. package/src/lint/rules/cor022-receipt-leaf.test.ts +116 -0
  335. package/src/lint/rules/cor022-receipt-leaf.ts +130 -0
  336. package/src/lint/rules/cor024-receipt-secret-pointer.test.ts +121 -0
  337. package/src/lint/rules/cor024-receipt-secret-pointer.ts +218 -0
  338. package/src/lint/rules/evl001-non-literal-expression.test.ts +35 -3
  339. package/src/lint/rules/evl001-non-literal-expression.ts +7 -0
  340. package/src/lint/rules/index.ts +7 -1
  341. package/src/lsp/lexicon-providers.test.ts +44 -0
  342. package/src/lsp/lexicon-providers.ts +11 -1
  343. package/src/okf-read.test.ts +149 -0
  344. package/src/okf-read.ts +197 -0
  345. package/src/op/activity-contract.test.ts +180 -0
  346. package/src/op/activity-contract.ts +278 -0
  347. package/src/op/builders-exports.test.ts +17 -1
  348. package/src/op/builders.ts +198 -6
  349. package/src/op/converge-rule.test.ts +179 -0
  350. package/src/op/converge-rule.ts +311 -0
  351. package/src/op/effect-step.test.ts +311 -0
  352. package/src/op/generate-pipeline.test.ts +53 -0
  353. package/src/op/generate-pipeline.ts +99 -0
  354. package/src/op/index.ts +40 -3
  355. package/src/op/local-executor.test.ts +92 -0
  356. package/src/op/local-executor.ts +212 -29
  357. package/src/op/op-verb-class.test.ts +126 -0
  358. package/src/op/op-verb-class.ts +115 -0
  359. package/src/op/op.test.ts +25 -2
  360. package/src/op/operator.test.ts +346 -0
  361. package/src/op/operator.ts +213 -0
  362. package/src/op/receipt-store.ts +211 -0
  363. package/src/op/step-output-ref.test.ts +334 -0
  364. package/src/op/step-output-ref.ts +453 -0
  365. package/src/op/types.ts +51 -2
  366. package/src/provenance.test.ts +151 -4
  367. package/src/provenance.ts +118 -4
  368. package/src/runtime-adapter.ts +31 -10
  369. package/src/secret-materialization.test.ts +199 -0
  370. package/src/secret-materialization.ts +235 -0
  371. package/src/secret-provenance.test.ts +388 -0
  372. package/src/secret-provenance.ts +475 -0
  373. package/src/serializer.ts +30 -0
  374. package/src/toml.test.ts +157 -384
  375. package/src/toml.ts +371 -5
  376. package/src/yaml.test.ts +88 -0
  377. package/src/yaml.ts +76 -6
@@ -11,8 +11,10 @@
11
11
  * so core never statically depends on `@intentius/chant-lexicon-temporal`.
12
12
  */
13
13
 
14
- import type { OpConfig, PhaseDefinition, ActivityStep, GateStep, StepDefinition } from "./types";
14
+ import type { OpConfig, PhaseDefinition, ActivityStep, GateStep, EffectStep, StepDefinition } from "./types";
15
15
  import { resolveActivity, type ActivityFn, type ActivityProfile } from "./activity-registry";
16
+ import type { ReceiptReadResult } from "./receipt-store";
17
+ import { isStepOutputRef } from "./step-output-ref";
16
18
 
17
19
  // ── Records ─────────────────────────────────────────────────────────────────
18
20
 
@@ -69,6 +71,7 @@ const FALLBACK_TIMEOUT_MS = 5 * 60_000;
69
71
 
70
72
  const isActivity = (s: StepDefinition): s is ActivityStep => s.kind === "activity";
71
73
  const isGate = (s: StepDefinition): s is GateStep => s.kind === "gate";
74
+ const isEffect = (s: StepDefinition): s is EffectStep => s.kind === "effect";
72
75
 
73
76
  /** Parse a Temporal duration string ("5m", "30s", "1h30m", "100ms") to ms. */
74
77
  export function parseDuration(s: string): number {
@@ -92,12 +95,37 @@ function resolvePath(value: unknown, path?: string): unknown {
92
95
  );
93
96
  }
94
97
 
95
- /** Find the first gate step anywhere in the Op (phases + onFailure), if any. */
98
+ /**
99
+ * Deep-walk `value`, replacing every {@link StepOutputRef} with the recorded
100
+ * result of its producer step, resolved through the ref's optional dot-path
101
+ * exactly like the serializer's compiled `__rN?.path?.segments` (chant #1290)
102
+ * — `resolvePath` gives the same "undefined intermediate resolves to
103
+ * undefined, never throws" semantics. A reference to a step whose result
104
+ * isn't in `resultsById` (never ran, or ran out of the scope
105
+ * `validateStepOutputRefs` allows) resolves to `undefined`, the same way a
106
+ * producer whose declared field is absent would.
107
+ */
108
+ function resolveStepOutputRefs(value: unknown, resultsById: ReadonlyMap<string, unknown>): unknown {
109
+ if (isStepOutputRef(value)) return resolvePath(resultsById.get(value.step), value.path);
110
+ if (Array.isArray(value)) return value.map((v) => resolveStepOutputRefs(v, resultsById));
111
+ if (value && typeof value === "object") {
112
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, resolveStepOutputRefs(v, resultsById)]));
113
+ }
114
+ return value;
115
+ }
116
+
117
+ /** Find the first gate step anywhere in the Op (phases + onFailure, including
118
+ * gates nested inside effect steps), if any. */
96
119
  export function findGate(config: OpConfig): GateStep | undefined {
97
120
  const all = [...config.phases, ...(config.onFailure ?? [])];
98
121
  for (const phase of all) {
99
- const gate = phase.steps.find(isGate);
100
- if (gate) return gate;
122
+ for (const step of phase.steps) {
123
+ if (isGate(step)) return step;
124
+ if (isEffect(step)) {
125
+ const nested = step.steps.find(isGate);
126
+ if (nested) return nested;
127
+ }
128
+ }
101
129
  }
102
130
  return undefined;
103
131
  }
@@ -145,15 +173,29 @@ async function callWithTimeout(
145
173
 
146
174
  // ── Step + phase execution ────────────────────────────────────────────────��─
147
175
 
148
- /** Run one activity step with retry + timeout. Never throws — returns a record. */
176
+ /** A finished step: its record plus (on success) the activity's return value —
177
+ * the effect-step path needs `receiptRead`'s result, not just its status. */
178
+ interface RanStep {
179
+ record: StepRecord;
180
+ result?: unknown;
181
+ }
182
+
183
+ /**
184
+ * Run one activity step with retry + timeout. Never throws — returns a
185
+ * record. Any {@link StepOutputRef} in `step.args` is resolved against
186
+ * `resultsById` before the activity is called (#1290) — the local executor
187
+ * is a first-class peer of the Temporal path, so an unresolved placeholder
188
+ * must never reach an activity function; see `resolveStepOutputRefs`.
189
+ */
149
190
  async function runStep(
150
191
  step: ActivityStep,
151
192
  phaseName: string,
152
193
  activities: Map<string, ActivityFn>,
153
194
  profiles: Record<string, ActivityProfile>,
195
+ resultsById: Map<string, unknown>,
154
196
  signal?: AbortSignal,
155
- ): Promise<StepRecord> {
156
- const args = step.args ?? {};
197
+ ): Promise<RanStep> {
198
+ const args = resolveStepOutputRefs(step.args ?? {}, resultsById) as Record<string, unknown>;
157
199
  const base = { phase: phaseName, fn: step.fn, args };
158
200
  const start = Date.now();
159
201
 
@@ -161,7 +203,7 @@ async function runStep(
161
203
  try {
162
204
  fn = resolveActivity(activities, step.fn);
163
205
  } catch (err) {
164
- return { ...base, status: "fail", durationMs: 0, error: errMessage(err) };
206
+ return { record: { ...base, status: "fail", durationMs: 0, error: errMessage(err) } };
165
207
  }
166
208
 
167
209
  const profile = profiles[step.profile ?? DEFAULT_PROFILE] ?? {};
@@ -183,6 +225,7 @@ async function runStep(
183
225
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
184
226
  try {
185
227
  const result = await callWithTimeout(fn, args, timeoutMs, signal);
228
+ if (step.id) resultsById.set(step.id, result);
186
229
  const record: StepRecord = { ...base, status: "ok", durationMs: Date.now() - start };
187
230
  if (step.outcomeAttribute) {
188
231
  record.outcome = {
@@ -190,7 +233,7 @@ async function runStep(
190
233
  value: resolvePath(result, step.outcomeAttribute.from),
191
234
  };
192
235
  }
193
- return record;
236
+ return { record, result };
194
237
  } catch (err) {
195
238
  lastErr = err;
196
239
  // Stop retrying on abort (Ctrl-C / timeout cascade) or a non-retryable error.
@@ -204,7 +247,107 @@ async function runStep(
204
247
  break;
205
248
  }
206
249
  }
207
- return { ...base, status: "fail", durationMs: Date.now() - start, error: errMessage(lastErr) };
250
+ return { record: { ...base, status: "fail", durationMs: Date.now() - start, error: errMessage(lastErr) } };
251
+ }
252
+
253
+ // ── Effect steps (#1834) ──────────────────────────────────────────────────────
254
+
255
+ /** The step data the executor synthesizes to read a receipt through the store
256
+ * activities (`receiptRead`/`receiptWrite` — provided by the receipt row's
257
+ * lexicon, #1835, or a mock store in tests via `receiptActivities`). */
258
+ function receiptReadStep(step: EffectStep): ActivityStep {
259
+ return {
260
+ kind: "activity",
261
+ fn: "receiptRead",
262
+ args: {
263
+ receipt: step.receipt,
264
+ ...(step.expectation !== undefined ? { expectation: step.expectation } : {}),
265
+ },
266
+ profile: "fastIdempotent",
267
+ outcomeAttribute: { name: "EffectApplied", from: "applied" },
268
+ };
269
+ }
270
+
271
+ /** A skipped-record for a step that will not run. */
272
+ function skippedRecord(phaseName: string, fn: string, args?: Record<string, unknown>): StepRecord {
273
+ return { phase: phaseName, fn, args: args ?? {}, status: "skipped", durationMs: 0 };
274
+ }
275
+
276
+ /**
277
+ * Run one effect step: read-compare-run-write. On a match the nested steps are
278
+ * recorded as skipped ("effect already applied") and nothing is written. On a
279
+ * mismatch the nested steps run in authored order; only when every one
280
+ * succeeds is the receipt written — last, once (the sole writer, #1703
281
+ * decision 3). Any failure leaves the receipt untouched (stale), so the next
282
+ * run re-proposes the effect.
283
+ */
284
+ async function runEffectStep(
285
+ step: EffectStep,
286
+ phaseName: string,
287
+ activities: Map<string, ActivityFn>,
288
+ profiles: Record<string, ActivityProfile>,
289
+ resultsById: Map<string, unknown>,
290
+ signal?: AbortSignal,
291
+ ): Promise<{ records: StepRecord[]; failed: boolean }> {
292
+ const records: StepRecord[] = [];
293
+
294
+ const read = await runStep(receiptReadStep(step), phaseName, activities, profiles, resultsById, signal);
295
+ records.push(read.record);
296
+ if (read.record.status === "fail") return { records, failed: true };
297
+
298
+ const result = read.result as Partial<ReceiptReadResult> | undefined;
299
+ if (typeof result?.expectation !== "string") {
300
+ records.push({
301
+ phase: phaseName,
302
+ fn: `effect:${step.receipt.name}`,
303
+ args: {},
304
+ status: "fail",
305
+ durationMs: 0,
306
+ error: "receiptRead returned no expectation — the receipt store activity must return { current, expectation }",
307
+ });
308
+ return { records, failed: true };
309
+ }
310
+ const expectation = result.expectation;
311
+
312
+ if (result.current === expectation) {
313
+ // Effect already applied — skip the nested steps, write nothing.
314
+ for (const nested of step.steps) {
315
+ if (nested.kind === "activity") records.push(skippedRecord(phaseName, nested.fn, nested.args));
316
+ }
317
+ return { records, failed: false };
318
+ }
319
+
320
+ // Gates are pre-flighted by findGate; only activities remain here.
321
+ const nestedActivities = step.steps.filter(isActivity);
322
+ for (let i = 0; i < nestedActivities.length; i++) {
323
+ const ran = await runStep(nestedActivities[i], phaseName, activities, profiles, resultsById, signal);
324
+ records.push(ran.record);
325
+ if (ran.record.status === "fail") {
326
+ // Receipt left untouched (stale) — the next run re-proposes the effect.
327
+ for (const skipped of nestedActivities.slice(i + 1)) {
328
+ records.push(skippedRecord(phaseName, skipped.fn, skipped.args));
329
+ }
330
+ records.push(skippedRecord(phaseName, "receiptWrite"));
331
+ return { records, failed: true };
332
+ }
333
+ }
334
+
335
+ // Sole writer of the receipt: on success of every nested step, last.
336
+ const wrote = await runStep(
337
+ {
338
+ kind: "activity",
339
+ fn: "receiptWrite",
340
+ args: { receipt: step.receipt, expectation },
341
+ profile: "fastIdempotent",
342
+ },
343
+ phaseName,
344
+ activities,
345
+ profiles,
346
+ resultsById,
347
+ signal,
348
+ );
349
+ records.push(wrote.record);
350
+ return { records, failed: wrote.record.status === "fail" };
208
351
  }
209
352
 
210
353
  /** Run a phase. Throws PhaseFailure (with records so far) if any step fails. */
@@ -212,37 +355,60 @@ async function runPhase(
212
355
  phase: PhaseDefinition,
213
356
  activities: Map<string, ActivityFn>,
214
357
  profiles: Record<string, ActivityProfile>,
358
+ resultsById: Map<string, unknown>,
215
359
  signal?: AbortSignal,
216
360
  ): Promise<StepRecord[]> {
217
- // Defensive: gates are pre-flighted, but never execute one if it slips through.
218
- const gate = phase.steps.find(isGate);
361
+ // Defensive: gates are pre-flighted, but never execute one if it slips
362
+ // through — including a gate nested inside an effect step.
363
+ const gate =
364
+ phase.steps.find(isGate) ??
365
+ phase.steps.filter(isEffect).flatMap((e) => e.steps).find(isGate);
219
366
  if (gate) throw new LocalGateUnsupportedError(gate.signalName);
220
367
 
221
- const steps = phase.steps.filter(isActivity);
222
-
223
368
  if (phase.parallel) {
224
- const records = await Promise.all(
225
- steps.map((s) => runStep(s, phase.name, activities, profiles, signal)),
226
- );
369
+ const eff = phase.steps.find(isEffect);
370
+ if (eff) {
371
+ throw new Error(
372
+ `effect step "${eff.receipt.name}" cannot run in a parallel phase — read-compare-run-write is ordered`,
373
+ );
374
+ }
375
+ const steps = phase.steps.filter(isActivity);
376
+ const records = (
377
+ await Promise.all(steps.map((s) => runStep(s, phase.name, activities, profiles, resultsById, signal)))
378
+ ).map((r) => r.record);
227
379
  if (records.some((r) => r.status === "fail")) throw new PhaseFailure(records);
228
380
  return records;
229
381
  }
230
382
 
383
+ const steps = phase.steps.filter((s): s is ActivityStep | EffectStep => !isGate(s));
231
384
  const records: StepRecord[] = [];
385
+
386
+ const skipRemaining = (from: number) => {
387
+ for (const skipped of steps.slice(from)) {
388
+ if (isEffect(skipped)) {
389
+ records.push(skippedRecord(phase.name, `effect:${skipped.receipt.name}`));
390
+ } else {
391
+ records.push(skippedRecord(phase.name, skipped.fn, skipped.args));
392
+ }
393
+ }
394
+ };
395
+
232
396
  for (let i = 0; i < steps.length; i++) {
233
- const record = await runStep(steps[i], phase.name, activities, profiles, signal);
397
+ const step = steps[i];
398
+ if (isEffect(step)) {
399
+ const { records: effRecords, failed } = await runEffectStep(step, phase.name, activities, profiles, resultsById, signal);
400
+ records.push(...effRecords);
401
+ if (failed) {
402
+ skipRemaining(i + 1);
403
+ throw new PhaseFailure(records);
404
+ }
405
+ continue;
406
+ }
407
+ const { record } = await runStep(step, phase.name, activities, profiles, resultsById, signal);
234
408
  records.push(record);
235
409
  if (record.status === "fail") {
236
410
  // Mark the remaining steps in this phase as skipped, then abort.
237
- for (const skipped of steps.slice(i + 1)) {
238
- records.push({
239
- phase: phase.name,
240
- fn: skipped.fn,
241
- args: skipped.args ?? {},
242
- status: "skipped",
243
- durationMs: 0,
244
- });
245
- }
411
+ skipRemaining(i + 1);
246
412
  throw new PhaseFailure(records);
247
413
  }
248
414
  }
@@ -266,13 +432,30 @@ export async function runOpLocally(
266
432
  const gate = findGate(config);
267
433
  if (gate) throw new LocalGateUnsupportedError(gate.signalName);
268
434
 
435
+ // Effect steps are ordered (read-compare-run-write): refuse them in a
436
+ // parallel phase up front, with the phase named, rather than mid-run.
437
+ for (const phase of [...config.phases, ...(config.onFailure ?? [])]) {
438
+ const eff = phase.parallel ? phase.steps.find(isEffect) : undefined;
439
+ if (eff) {
440
+ throw new Error(
441
+ `phase "${phase.name}": effect step "${eff.receipt.name}" cannot run in a ` +
442
+ `parallel phase — read-compare-run-write is ordered`,
443
+ );
444
+ }
445
+ }
446
+
269
447
  const records: StepRecord[] = [];
270
448
  const start = Date.now();
271
449
 
450
+ // Completed step id → its activity result (#1290). Populated as main-phase
451
+ // steps finish, so a later step's step-output references resolve to real
452
+ // values before reaching an activity function — see `runStep`.
453
+ const resultsById = new Map<string, unknown>();
454
+
272
455
  try {
273
456
  for (const phase of config.phases) {
274
457
  if (signal?.aborted) throw new PhaseFailure([]);
275
- records.push(...(await runPhase(phase, activities, profiles, signal)));
458
+ records.push(...(await runPhase(phase, activities, profiles, resultsById, signal)));
276
459
  }
277
460
  } catch (err) {
278
461
  if (err instanceof PhaseFailure) records.push(...err.records);
@@ -282,7 +465,7 @@ export async function runOpLocally(
282
465
  if (!signal?.aborted) {
283
466
  for (const phase of [...(config.onFailure ?? [])].reverse()) {
284
467
  try {
285
- records.push(...(await runPhase(phase, activities, profiles, signal)));
468
+ records.push(...(await runPhase(phase, activities, profiles, resultsById, signal)));
286
469
  } catch (compErr) {
287
470
  if (compErr instanceof PhaseFailure) records.push(...compErr.records);
288
471
  }
@@ -0,0 +1,126 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { classifyOpVerbClass, isGated } from "./op-verb-class";
3
+ import type { OpConfig } from "./types";
4
+
5
+ function cfg(phases: OpConfig["phases"], onFailure?: OpConfig["onFailure"]): Pick<OpConfig, "phases" | "onFailure"> {
6
+ return { phases, ...(onFailure ? { onFailure } : {}) };
7
+ }
8
+
9
+ describe("classifyOpVerbClass", () => {
10
+ test("read-only for a WatchOp-shaped composition (snapshot + diff)", () => {
11
+ const config = cfg([
12
+ { name: "Snapshot", steps: [{ kind: "activity", fn: "lifecycleSnapshot", args: { env: "prod" } }] },
13
+ { name: "Diff", steps: [{ kind: "activity", fn: "lifecycleDiff", args: { env: "prod" } }] },
14
+ ]);
15
+ expect(classifyOpVerbClass(config)).toBe("read-only");
16
+ });
17
+
18
+ test("read-only for an empty phase list", () => {
19
+ expect(classifyOpVerbClass(cfg([]))).toBe("read-only");
20
+ });
21
+
22
+ test("mutating for nativeApply with deleteMode never", () => {
23
+ const config = cfg([
24
+ { name: "Apply", steps: [{ kind: "activity", fn: "nativeApply", args: { target: "kubectl", env: "prod", output: "dist", deleteMode: "never" } }] },
25
+ ]);
26
+ expect(classifyOpVerbClass(config)).toBe("mutating");
27
+ });
28
+
29
+ test("mutating for nativeApply with no deleteMode at all", () => {
30
+ const config = cfg([
31
+ { name: "Apply", steps: [{ kind: "activity", fn: "nativeApply", args: { target: "kubectl", env: "prod", output: "dist" } }] },
32
+ ]);
33
+ expect(classifyOpVerbClass(config)).toBe("mutating");
34
+ });
35
+
36
+ test("destructive for nativeApply with deleteMode owned-only", () => {
37
+ const config = cfg([
38
+ { name: "Apply", steps: [{ kind: "activity", fn: "nativeApply", args: { target: "kubectl", env: "prod", output: "dist", deleteMode: "owned-only" } }] },
39
+ ]);
40
+ expect(classifyOpVerbClass(config)).toBe("destructive");
41
+ });
42
+
43
+ test("destructive for nativeApply with deleteMode gated", () => {
44
+ const config = cfg([
45
+ { name: "Apply", steps: [{ kind: "activity", fn: "nativeApply", args: { target: "kubectl", env: "prod", output: "dist", deleteMode: "gated" } }] },
46
+ ]);
47
+ expect(classifyOpVerbClass(config)).toBe("destructive");
48
+ });
49
+
50
+ test("destructive for envTeardown regardless of args", () => {
51
+ const config = cfg([{ name: "Teardown", steps: [{ kind: "activity", fn: "envTeardown", args: { env: "staging" } }] }]);
52
+ expect(classifyOpVerbClass(config)).toBe("destructive");
53
+ });
54
+
55
+ test("destructive for awsDelete/azDelete/gcpDelete/chantTeardown", () => {
56
+ for (const fn of ["awsDelete", "azDelete", "gcpDelete", "chantTeardown"]) {
57
+ const config = cfg([{ name: "P", steps: [{ kind: "activity", fn, args: {} }] }]);
58
+ expect(classifyOpVerbClass(config)).toBe("destructive");
59
+ }
60
+ });
61
+
62
+ test("fail-closed: an unrecognized activity fn is mutating, not read-only", () => {
63
+ const config = cfg([{ name: "P", steps: [{ kind: "activity", fn: "someBespokeCustomActivity", args: {} }] }]);
64
+ expect(classifyOpVerbClass(config)).toBe("mutating");
65
+ });
66
+
67
+ test("a single destructive step outranks other read-only/mutating steps in the same Op", () => {
68
+ const config = cfg([
69
+ { name: "Diff", steps: [{ kind: "activity", fn: "lifecycleDiff", args: { env: "prod" } }] },
70
+ { name: "Teardown", steps: [{ kind: "activity", fn: "envTeardown", args: { env: "prod" } }] },
71
+ ]);
72
+ expect(classifyOpVerbClass(config)).toBe("destructive");
73
+ });
74
+
75
+ test("checks onFailure phases too", () => {
76
+ const config = cfg(
77
+ [{ name: "Apply", steps: [{ kind: "activity", fn: "lifecycleDiff", args: { env: "prod" } }] }],
78
+ [{ name: "Rollback", steps: [{ kind: "activity", fn: "envTeardown", args: { env: "prod" } }] }],
79
+ );
80
+ expect(classifyOpVerbClass(config)).toBe("destructive");
81
+ });
82
+
83
+ test("looks inside an effect step's nested activity steps", () => {
84
+ const config = cfg([
85
+ {
86
+ name: "Seed",
87
+ steps: [
88
+ {
89
+ kind: "effect",
90
+ receipt: { name: "seeded", lexicon: "aws" } as never,
91
+ steps: [{ kind: "activity", fn: "nativeApply", args: { target: "kubectl", env: "prod", output: "dist", deleteMode: "gated" } }],
92
+ },
93
+ ],
94
+ },
95
+ ]);
96
+ expect(classifyOpVerbClass(config)).toBe("destructive");
97
+ });
98
+ });
99
+
100
+ describe("isGated", () => {
101
+ test("false with no gate step", () => {
102
+ expect(isGated(cfg([{ name: "Apply", steps: [{ kind: "activity", fn: "nativeApply", args: {} }] }]))).toBe(false);
103
+ });
104
+
105
+ test("true with a gate step in the main phases", () => {
106
+ expect(
107
+ isGated(
108
+ cfg([
109
+ { name: "Approve", steps: [{ kind: "gate", signalName: "approve-x" }] },
110
+ { name: "Apply", steps: [{ kind: "activity", fn: "nativeApply", args: {} }] },
111
+ ]),
112
+ ),
113
+ ).toBe(true);
114
+ });
115
+
116
+ test("true with a gate step in onFailure", () => {
117
+ expect(
118
+ isGated(
119
+ cfg(
120
+ [{ name: "Apply", steps: [{ kind: "activity", fn: "nativeApply", args: {} }] }],
121
+ [{ name: "Rollback", steps: [{ kind: "gate", signalName: "approve-rollback" }] }],
122
+ ),
123
+ ),
124
+ ).toBe(true);
125
+ });
126
+ });
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Op verb classification (#1484) — generalizes `delete: "never" | "owned-only"
3
+ * | "gated"` (`../lifecycle` teardown/apply's `DeleteMode`) up one level, to
4
+ * the whole Op: read-only, mutating, or destructive. `ConvergeOp`'s
5
+ * dial × verb-class matrix (the issue's "Autonomy" table) needs to answer
6
+ * "what kind of thing is this dispatched Op, from the outside" for an
7
+ * arbitrary declared Op — including one this project's authors wrote, not
8
+ * just the pre-built composites — so classification walks `OpConfig`'s
9
+ * steps rather than trusting a self-reported tag.
10
+ *
11
+ * There is no existing per-Op verb-class field (`searchAttributes` is a free
12
+ * `Record<string,string>` an author may or may not set — `ApplyOp` tags
13
+ * `{ Apply: "true" }` but nothing requires it, and it carries no verb-class
14
+ * semantics on its own) and no existing "which activity names are
15
+ * destructive" registry (`../lint/pipeline-change-gate.ts`'s classifier is
16
+ * shaped for a `Component` composition's capability-verb steps, not an
17
+ * `OpConfig`'s activity steps — a genuinely different graph). This module is
18
+ * that registry's Op-shaped sibling: a small, explicit set of known
19
+ * mutating/destructive activity function names, plus the one piece of
20
+ * structured signal that already exists (`nativeApply`'s `deleteMode` arg).
21
+ *
22
+ * Fail-closed by construction: an activity `fn` this module doesn't
23
+ * recognize is classified `mutating`, never `read-only` — an unknown step
24
+ * might do anything, and "unknown never remediates" (the issue's own honesty
25
+ * requirement for symptoms) applies here too: this module never silently
26
+ * clears an Op to free-run just because its steps aren't in the known-safe
27
+ * list.
28
+ */
29
+
30
+ import { findGate } from "./local-executor";
31
+ import type { ActivityStep, EffectStep, OpConfig, StepDefinition } from "./types";
32
+
33
+ export type OpVerbClass = "read-only" | "mutating" | "destructive";
34
+
35
+ /** Activity function names known to be read-only — verification, observation, audit. Anything not in this set is treated as at least `mutating` (fail-closed). */
36
+ const READ_ONLY_ACTIVITY_FNS: ReadonlySet<string> = new Set([
37
+ "lifecycleSnapshot",
38
+ "lifecycleDiff",
39
+ "httpCheck",
40
+ "policyGate",
41
+ "workflowSupplyChainAudit",
42
+ "pipelineSupplyChainAudit",
43
+ "receiptStaleness",
44
+ "waitForStack",
45
+ "waitForReady",
46
+ "waitForArgoSync",
47
+ "spriteReadFile",
48
+ "spriteListDir",
49
+ "listCheckpoints",
50
+ "convergeTick",
51
+ ]);
52
+
53
+ /** Activity function names that always delete/destroy, regardless of args. */
54
+ const ALWAYS_DESTRUCTIVE_ACTIVITY_FNS: ReadonlySet<string> = new Set([
55
+ "envTeardown",
56
+ "chantTeardown",
57
+ "azDelete",
58
+ "awsDelete",
59
+ "gcpDelete",
60
+ "spriteDestroy",
61
+ "k3dDown",
62
+ "k3sUninstall",
63
+ ]);
64
+
65
+ function stepsOf(config: Pick<OpConfig, "phases" | "onFailure">): ActivityStep[] {
66
+ const all: StepDefinition[] = [...config.phases, ...(config.onFailure ?? [])].flatMap((p) => p.steps);
67
+ const flat: ActivityStep[] = [];
68
+ for (const step of all) {
69
+ if (step.kind === "activity") flat.push(step);
70
+ else if (step.kind === "effect") flat.push(...(step as EffectStep).steps.filter((s): s is ActivityStep => s.kind === "activity"));
71
+ }
72
+ return flat;
73
+ }
74
+
75
+ /**
76
+ * Classify an Op's own composition. Pure — reads only the passed config, no
77
+ * I/O. `nativeApply`'s `deleteMode` (`"never" | "owned-only" | "gated"`) is
78
+ * read straight off the step's `args` — `"owned-only"`/`"gated"` are
79
+ * destructive (both can delete a live resource), `"never"`/absent is merely
80
+ * mutating (creates/updates only). A step whose `fn` matches neither the
81
+ * read-only nor the destructive set is `mutating` by default (fail-closed).
82
+ */
83
+ export function classifyOpVerbClass(config: Pick<OpConfig, "phases" | "onFailure">): OpVerbClass {
84
+ let sawMutating = false;
85
+
86
+ for (const step of stepsOf(config)) {
87
+ if (ALWAYS_DESTRUCTIVE_ACTIVITY_FNS.has(step.fn)) return "destructive";
88
+
89
+ if (step.fn === "nativeApply") {
90
+ const deleteMode = step.args?.deleteMode;
91
+ if (deleteMode === "owned-only" || deleteMode === "gated") return "destructive";
92
+ sawMutating = true;
93
+ continue;
94
+ }
95
+
96
+ if (step.fn === "compensateApply") {
97
+ // Rollback compensation — mutating (it re-applies a prior state), not
98
+ // itself a fresh destructive act.
99
+ sawMutating = true;
100
+ continue;
101
+ }
102
+
103
+ if (READ_ONLY_ACTIVITY_FNS.has(step.fn)) continue;
104
+
105
+ // Unknown or known-mutating fn: fail-closed to mutating.
106
+ sawMutating = true;
107
+ }
108
+
109
+ return sawMutating ? "mutating" : "read-only";
110
+ }
111
+
112
+ /** Does this Op have its own approval gate anywhere in its phases (main or `onFailure`)? Thin re-export of `findGate` under the name this module's callers reach for. */
113
+ export function isGated(config: Pick<OpConfig, "phases" | "onFailure">): boolean {
114
+ return findGate(config as OpConfig) !== undefined;
115
+ }
package/src/op/op.test.ts CHANGED
@@ -3,8 +3,8 @@
3
3
  */
4
4
 
5
5
  import { describe, expect, it } from "vitest";
6
- import { Op, phase, activity, gate, build, kubectlApply, helmInstall,
7
- waitForStack, gitlabPipeline, lifecycleSnapshot, shell, teardown, policyGate } from "./builders";
6
+ import { Op, phase, activity, gate, build, kubectlApply, helmInstall, helmInstallPinned,
7
+ waitForStack, gitlabPipeline, lifecycleSnapshot, shell, ensureSecret, teardown, policyGate } from "./builders";
8
8
  import { DECLARABLE_MARKER, type Declarable } from "../declarable";
9
9
 
10
10
  // ── Op() ──────────────────────────────────────────────────────────────────────
@@ -163,6 +163,16 @@ describe("pre-built shortcuts", () => {
163
163
  expect(a.profile).toBe("longInfra");
164
164
  });
165
165
 
166
+ it("helmInstallPinned() produces a helmInstall activity keyed by contentDigest, no chart", () => {
167
+ const a = helmInstallPinned("my-release", "sha256:abc", { namespace: "web" });
168
+ expect(a.fn).toBe("helmInstall");
169
+ expect(a.args?.name).toBe("my-release");
170
+ expect(a.args?.contentDigest).toBe("sha256:abc");
171
+ expect(a.args?.namespace).toBe("web");
172
+ expect(a.args?.chart).toBeUndefined();
173
+ expect(a.profile).toBe("longInfra");
174
+ });
175
+
166
176
  it("waitForStack() produces waitForStack activity with k8sWait profile", () => {
167
177
  const a = waitForStack("my-stack");
168
178
  expect(a.fn).toBe("waitForStack");
@@ -190,6 +200,19 @@ describe("pre-built shortcuts", () => {
190
200
  expect(a.args?.cmd).toBe("echo hello");
191
201
  });
192
202
 
203
+ it("ensureSecret() produces ensureSecret activity with name/keys args and no material-shaped field", () => {
204
+ const a = ensureSecret("master-key", ["MASTER_SECRETS_KEY"], {
205
+ metadata: { "chant.dev/provenance": "generated-once" },
206
+ });
207
+ expect(a.fn).toBe("ensureSecret");
208
+ expect(a.args?.name).toBe("master-key");
209
+ expect(a.args?.keys).toEqual(["MASTER_SECRETS_KEY"]);
210
+ expect(a.args?.metadata).toEqual({ "chant.dev/provenance": "generated-once" });
211
+ // The step is contract-only: nothing on it could carry secret material.
212
+ expect(a.args && "value" in a.args).toBe(false);
213
+ expect(a.args && "data" in a.args).toBe(false);
214
+ });
215
+
193
216
  it("teardown() produces chantTeardown activity with longInfra profile", () => {
194
217
  const a = teardown("./project");
195
218
  expect(a.fn).toBe("chantTeardown");