@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
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Receipt store seam (#1834, epic #1703) — how the `effect()` step and the
3
+ * receipt-reading Ops reach a materialized receipt without knowing which
4
+ * lexicon materializes it.
5
+ *
6
+ * The store is injectable: core defines the {@link ReceiptStore} interface and
7
+ * the activity contracts; the receipt row's lexicon implements the store over
8
+ * its own resource (#1835's aws row over `AWS::SSM::Parameter`) and exports
9
+ * the bound activities (`receiptRead`, `receiptWrite`, `receiptStaleness`)
10
+ * from its `op/activities` module via {@link receiptActivities}. The activity
11
+ * registry then resolves them by name, exactly like `ensureSecret`
12
+ * (#1829/#1830, provided by the k8s lexicon). Tests bind a mock store the
13
+ * same way.
14
+ *
15
+ * Write discipline (epic #1703 decision log, item 3): the `effect()` step is
16
+ * the SOLE writer of a receipt — on success of its nested steps, last. The
17
+ * store interface carries a `write`, but only the effect step's emitted
18
+ * read-compare-run-write (and its local-executor twin) reaches it.
19
+ * `receiptStaleness` — the WatchOp phase (#1834) — is read-only by
20
+ * construction: it never touches `write` and runs nothing.
21
+ *
22
+ * Expectation resolution follows the resolution split (decision 5): a fully
23
+ * static receipt's expectation is stamped at synthesis and rides the step
24
+ * data ({@link receiptCheckInput}); a hash-flavor receipt with reference
25
+ * inputs resolves at run, through the store lexicon's
26
+ * {@link ReceiptActivityOptions.resolveExpectation} hook — never by hashing
27
+ * placeholders.
28
+ */
29
+
30
+ import {
31
+ EXISTENCE_EXPECTATION,
32
+ isEffectReceipt,
33
+ receiptExpectation,
34
+ referenceInputPaths,
35
+ type EffectReceiptDeclaration,
36
+ type EffectReceiptFlavor,
37
+ } from "../effect-receipt";
38
+
39
+ /**
40
+ * The serializable identity + declaration data of an effect receipt — what an
41
+ * `effect()` step and a `receiptStaleness` check carry through codegen. Built
42
+ * from the typed {@link EffectReceiptDeclaration} only ({@link receiptCheckInput});
43
+ * there is no string form. Reference inputs stay in placeholder form.
44
+ */
45
+ export interface EffectReceiptRef {
46
+ /** The receipt's own name (the export-level identity of the witness). */
47
+ name: string;
48
+ /** The effect this receipt witnesses. */
49
+ effect: string;
50
+ /** How the receipt is compared: mere presence, or a digest of the inputs. */
51
+ flavor: EffectReceiptFlavor;
52
+ /** The effect's inputs as recorded at synthesis (references as placeholders). */
53
+ inputs: Record<string, unknown>;
54
+ }
55
+
56
+ /**
57
+ * The injectable seam over the materialized receipt (#1835 implements it for
58
+ * SSM; tests implement it with a map). `read` returns the receipt's current
59
+ * stored value, or `undefined` when the receipt is absent. `write` stores the
60
+ * expectation as the new value — called only by the effect step, on success,
61
+ * last.
62
+ */
63
+ export interface ReceiptStore {
64
+ read(receipt: EffectReceiptRef): Promise<string | undefined>;
65
+ write(receipt: EffectReceiptRef, expectation: string): Promise<void>;
66
+ }
67
+
68
+ /** One receipt to check: its identity plus, when the receipt is fully static,
69
+ * the expectation stamped at synthesis. */
70
+ export interface ReceiptCheckInput {
71
+ receipt: EffectReceiptRef;
72
+ /** Present when the expectation was computable at synthesis; absent when
73
+ * reference inputs resolve at run (decision 5). */
74
+ expectation?: string;
75
+ }
76
+
77
+ /**
78
+ * Snapshot a typed receipt declaration into check-input data: the
79
+ * {@link EffectReceiptRef} plus the synthesis-time expectation when the
80
+ * receipt is fully static. Used by the `effect()` builder and by WatchOp's
81
+ * staleness phase — the single place "static enough to hash now" is decided.
82
+ */
83
+ export function receiptCheckInput(receipt: EffectReceiptDeclaration): ReceiptCheckInput {
84
+ if (!isEffectReceipt(receipt)) {
85
+ throw new Error(
86
+ "receiptCheckInput: expected an EffectReceipt declaration — import the exported const; there is no string form",
87
+ );
88
+ }
89
+ const ref: EffectReceiptRef = {
90
+ name: receipt.name,
91
+ effect: receipt.effect,
92
+ flavor: receipt.flavor,
93
+ inputs: receipt.inputs as Record<string, unknown>,
94
+ };
95
+ // An existence receipt's expectation is a constant, references or not; a
96
+ // hash receipt is static only when no reference inputs remain.
97
+ const isStatic = receipt.flavor === "existence" || referenceInputPaths(receipt).length === 0;
98
+ return isStatic ? { receipt: ref, expectation: receiptExpectation(receipt) } : { receipt: ref };
99
+ }
100
+
101
+ // ── Activity contracts ────────────────────────────────────────────────────────
102
+
103
+ /** Args of the `receiptRead` activity, as the effect step's codegen emits them. */
104
+ export interface ReceiptReadArgs extends ReceiptCheckInput {}
105
+
106
+ /** Result of the `receiptRead` activity. `current` is `null` (not `undefined`)
107
+ * for an absent receipt so the value survives JSON transport. */
108
+ export interface ReceiptReadResult {
109
+ /** The receipt's live stored value, or null when absent. */
110
+ current: string | null;
111
+ /** The resolved expectation the workflow compares and later writes. */
112
+ expectation: string;
113
+ /** Convenience: `current === expectation`. */
114
+ applied: boolean;
115
+ }
116
+
117
+ /** Args of the `receiptWrite` activity — the receipt and the resolved
118
+ * expectation returned by the preceding `receiptRead`. */
119
+ export interface ReceiptWriteArgs {
120
+ receipt: EffectReceiptRef;
121
+ expectation: string;
122
+ }
123
+
124
+ /** Args of the read-only `receiptStaleness` activity (WatchOp). */
125
+ export interface ReceiptStalenessArgs {
126
+ receipts: ReceiptCheckInput[];
127
+ }
128
+
129
+ /** One stale receipt, reported as a finding — never acted on. */
130
+ export interface ReceiptStaleFinding {
131
+ /** The receipt's name. */
132
+ receipt: string;
133
+ /** The effect the receipt witnesses. */
134
+ effect: string;
135
+ /** `absent` — no receipt stored; `differs` — stored value is not the expectation. */
136
+ kind: "absent" | "differs";
137
+ /** The resolved expectation. */
138
+ expected: string;
139
+ /** The live stored value (present only for `differs`). */
140
+ current?: string;
141
+ }
142
+
143
+ /** Result of the `receiptStaleness` activity. */
144
+ export interface ReceiptStalenessResult {
145
+ stale: boolean;
146
+ findings: ReceiptStaleFinding[];
147
+ }
148
+
149
+ /** Options for {@link receiptActivities}. */
150
+ export interface ReceiptActivityOptions {
151
+ /**
152
+ * Resolve a reference-carrying receipt's expectation at run (decision 5) —
153
+ * the store lexicon's hook to deploy-time values, typically wrapping core's
154
+ * `resolveReceiptExpectation`. Without it, a check input that carries no
155
+ * synthesis-time expectation fails loudly rather than hashing placeholders.
156
+ */
157
+ resolveExpectation?: (receipt: EffectReceiptRef) => Promise<string> | string;
158
+ }
159
+
160
+ /** The three receipt activities, bound to one store. */
161
+ export interface ReceiptActivities {
162
+ receiptRead: (args: ReceiptReadArgs, signal?: AbortSignal) => Promise<ReceiptReadResult>;
163
+ receiptWrite: (args: ReceiptWriteArgs, signal?: AbortSignal) => Promise<{ written: true; receipt: string }>;
164
+ receiptStaleness: (args: ReceiptStalenessArgs, signal?: AbortSignal) => Promise<ReceiptStalenessResult>;
165
+ }
166
+
167
+ /**
168
+ * Bind the receipt activities to a store. The store's lexicon calls this once
169
+ * and re-exports the result from its `op/activities` module; the activity
170
+ * registry picks the functions up by name for both executors.
171
+ */
172
+ export function receiptActivities(store: ReceiptStore, opts?: ReceiptActivityOptions): ReceiptActivities {
173
+ const expectationOf = async (input: ReceiptCheckInput): Promise<string> => {
174
+ if (input.expectation !== undefined) return input.expectation;
175
+ if (input.receipt.flavor === "existence") return EXISTENCE_EXPECTATION;
176
+ if (opts?.resolveExpectation) return await opts.resolveExpectation(input.receipt);
177
+ throw new Error(
178
+ `receipt "${input.receipt.name}": no synthesis-time expectation and no resolveExpectation hook — ` +
179
+ `a hash-flavor receipt with reference inputs resolves at run (#1703 decision 5), ` +
180
+ `and hashing placeholders would be a wrong expectation`,
181
+ );
182
+ };
183
+
184
+ return {
185
+ async receiptRead(args: ReceiptReadArgs): Promise<ReceiptReadResult> {
186
+ const expectation = await expectationOf(args);
187
+ const current = await store.read(args.receipt);
188
+ return { current: current ?? null, expectation, applied: current === expectation };
189
+ },
190
+
191
+ async receiptWrite(args: ReceiptWriteArgs): Promise<{ written: true; receipt: string }> {
192
+ await store.write(args.receipt, args.expectation);
193
+ return { written: true, receipt: args.receipt.name };
194
+ },
195
+
196
+ // Read-only: reads and reports, runs nothing, never writes.
197
+ async receiptStaleness(args: ReceiptStalenessArgs): Promise<ReceiptStalenessResult> {
198
+ const findings: ReceiptStaleFinding[] = [];
199
+ for (const input of args.receipts) {
200
+ const expected = await expectationOf(input);
201
+ const current = await store.read(input.receipt);
202
+ if (current === undefined) {
203
+ findings.push({ receipt: input.receipt.name, effect: input.receipt.effect, kind: "absent", expected });
204
+ } else if (current !== expected) {
205
+ findings.push({ receipt: input.receipt.name, effect: input.receipt.effect, kind: "differs", expected, current });
206
+ }
207
+ }
208
+ return { stale: findings.length > 0, findings };
209
+ },
210
+ };
211
+ }
@@ -0,0 +1,334 @@
1
+ /**
2
+ * Step-output reference tests (chant #1290).
3
+ *
4
+ * Mirrors activity-contract.test.ts's shape: a small lifecycleDiff-shaped
5
+ * contract so the test doesn't need a real lexicon.
6
+ */
7
+
8
+ import { describe, expect, it } from "vitest";
9
+ import { z } from "zod";
10
+ import { activity, phase, effect } from "./builders";
11
+ import type { OpConfig } from "./types";
12
+ import { activityContract, type ActivityContract } from "./activity-contract";
13
+ import { stepOutput, isStepOutputRef, collectStepOutputRefs, validateStepOutputRefs } from "./step-output-ref";
14
+ import { EffectReceipt } from "../effect-receipt";
15
+
16
+ const lifecycleDiffContract = activityContract(
17
+ "lifecycleDiff",
18
+ z.strictObject({ env: z.string(), live: z.boolean().optional() }),
19
+ z.object({ output: z.string(), exitCode: z.number(), drifted: z.boolean(), driftedStacks: z.array(z.string()) }),
20
+ );
21
+
22
+ const applyStacksContract = activityContract("applyStacks", z.strictObject({ stacks: z.array(z.string()) }));
23
+
24
+ // A producer with no `returns` schema — every arg is fine, nothing to
25
+ // validate a reference into its output against.
26
+ const noReturnContract = activityContract("shellCmd", z.strictObject({ cmd: z.string() }));
27
+
28
+ function opWith(config: Partial<OpConfig> & Pick<OpConfig, "phases">): Pick<OpConfig, "name" | "phases" | "onFailure"> {
29
+ return { name: "test-op", ...config };
30
+ }
31
+
32
+ // ── stepOutput() / isStepOutputRef() ────────────────────────────────────────
33
+
34
+ describe("stepOutput()", () => {
35
+ it("brands its result so isStepOutputRef recognizes it", () => {
36
+ const ref = stepOutput("diff", "driftedStacks");
37
+ expect(isStepOutputRef(ref)).toBe(true);
38
+ expect(ref.step).toBe("diff");
39
+ expect(ref.path).toBe("driftedStacks");
40
+ });
41
+
42
+ it("omitting path references the whole return value", () => {
43
+ const ref = stepOutput("diff");
44
+ expect(ref.path).toBeUndefined();
45
+ });
46
+
47
+ it("accepts a step object with an id, not just a bare string", () => {
48
+ const diff = activity("lifecycleDiff", { env: "prod" }, { id: "diff" });
49
+ expect(stepOutput(diff, "driftedStacks")).toEqual(stepOutput("diff", "driftedStacks"));
50
+ });
51
+
52
+ it("throws when the step has no id", () => {
53
+ expect(() => stepOutput({}, "x")).toThrow(/no `id`/);
54
+ });
55
+
56
+ it("isStepOutputRef rejects a plain object with the same shape", () => {
57
+ expect(isStepOutputRef({ step: "diff", path: "x" })).toBe(false);
58
+ expect(isStepOutputRef(null)).toBe(false);
59
+ expect(isStepOutputRef(undefined)).toBe(false);
60
+ });
61
+
62
+ it("throws when coerced to a primitive — references only, no expressions", () => {
63
+ const ref = stepOutput("diff", "count");
64
+ expect(() => `${ref}`).toThrow(/coerced to a primitive/);
65
+ expect(() => Number(ref)).toThrow(/coerced to a primitive/);
66
+ expect(() => String(ref)).toThrow(/coerced to a primitive/);
67
+ });
68
+ });
69
+
70
+ // ── activity()'s `.out` proxy ───────────────────────────────────────────────
71
+
72
+ describe("activity().out", () => {
73
+ it("diff.out.field builds a reference to this step's declared output", () => {
74
+ const diff = activity("lifecycleDiff", { env: "prod" }, { id: "diff" });
75
+ const ref = diff.out.driftedStacks;
76
+ expect(isStepOutputRef(ref)).toBe(true);
77
+ expect(ref.step).toBe("diff");
78
+ expect(ref.path).toBe("driftedStacks");
79
+ });
80
+
81
+ it("diff.out itself (unaccessed) references the whole return value", () => {
82
+ const diff = activity("lifecycleDiff", { env: "prod" }, { id: "diff" });
83
+ expect(isStepOutputRef(diff.out)).toBe(true);
84
+ expect(diff.out.path).toBeUndefined();
85
+ expect(diff.out.step).toBe("diff");
86
+ });
87
+
88
+ it("throws when the step has no id", () => {
89
+ const diff = activity("lifecycleDiff", { env: "prod" });
90
+ expect(() => diff.out).toThrow(/has no id/);
91
+ });
92
+
93
+ it("out is non-enumerable — doesn't leak into the step's own JSON shape", () => {
94
+ const diff = activity("lifecycleDiff", { env: "prod" }, { id: "diff" });
95
+ expect(Object.keys(diff)).not.toContain("out");
96
+ expect(JSON.stringify(diff)).not.toContain("out");
97
+ });
98
+
99
+ it("string third arg is still a profile, unchanged (backward compatible)", () => {
100
+ const step = activity("kubectlApply", { manifest: "k8s.yaml" }, "longInfra");
101
+ expect(step.profile).toBe("longInfra");
102
+ expect(step.id).toBeUndefined();
103
+ });
104
+ });
105
+
106
+ // ── collectStepOutputRefs() ─────────────────────────────────────────────────
107
+
108
+ describe("collectStepOutputRefs()", () => {
109
+ it("finds a ref nested inside an object and an array", () => {
110
+ const ref1 = stepOutput("diff", "a");
111
+ const ref2 = stepOutput("diff", "b");
112
+ const found = collectStepOutputRefs({ config: { stacks: [ref1, "literal", ref2] } });
113
+ expect(found).toEqual([ref1, ref2]);
114
+ });
115
+
116
+ it("returns nothing for plain args", () => {
117
+ expect(collectStepOutputRefs({ env: "prod", n: 1, list: [1, 2] })).toEqual([]);
118
+ });
119
+ });
120
+
121
+ // ── validateStepOutputRefs() ─────────────────────────────────────────────────
122
+
123
+ describe("validateStepOutputRefs() — passing Ops", () => {
124
+ it("a same-phase-later-step reference against a valid path passes", () => {
125
+ const diff = activity("lifecycleDiff", { env: "prod" }, { id: "diff" });
126
+ const apply = activity("applyStacks", { stacks: diff.out.driftedStacks });
127
+ const config = opWith({ phases: [phase("Reconcile", [diff, apply])] });
128
+ const contracts = new Map<string, ActivityContract>([
129
+ ["lifecycleDiff", lifecycleDiffContract],
130
+ ["applyStacks", applyStacksContract],
131
+ ]);
132
+ expect(validateStepOutputRefs(config, contracts)).toEqual([]);
133
+ });
134
+
135
+ it("a reference from a later phase passes", () => {
136
+ const diff = activity("lifecycleDiff", { env: "prod" }, { id: "diff" });
137
+ const apply = activity("applyStacks", { stacks: diff.out.driftedStacks });
138
+ const config = opWith({ phases: [phase("Diff", [diff]), phase("Apply", [apply])] });
139
+ const contracts = new Map<string, ActivityContract>([
140
+ ["lifecycleDiff", lifecycleDiffContract],
141
+ ["applyStacks", applyStacksContract],
142
+ ]);
143
+ expect(validateStepOutputRefs(config, contracts)).toEqual([]);
144
+ });
145
+
146
+ it("a whole-value reference (no path) passes even without checking the producer's return shape", () => {
147
+ const diff = activity("lifecycleDiff", { env: "prod" }, { id: "diff" });
148
+ const apply = activity("applyStacks", { stacks: stepOutput("diff") });
149
+ const config = opWith({ phases: [phase("P", [diff, apply])] });
150
+ const contracts = new Map<string, ActivityContract>([
151
+ ["lifecycleDiff", lifecycleDiffContract],
152
+ ["applyStacks", applyStacksContract],
153
+ ]);
154
+ expect(validateStepOutputRefs(config, contracts)).toEqual([]);
155
+ });
156
+
157
+ it("no refs anywhere — no issues", () => {
158
+ const config = opWith({ phases: [phase("P", [activity("lifecycleDiff", { env: "prod" })])] });
159
+ expect(validateStepOutputRefs(config, new Map())).toEqual([]);
160
+ });
161
+ });
162
+
163
+ describe("validateStepOutputRefs() — the four failure classes from #1290", () => {
164
+ it("flags a reference to an unknown producer step id", () => {
165
+ const apply = activity("applyStacks", { stacks: stepOutput("nope", "driftedStacks") });
166
+ const config = opWith({ phases: [phase("P", [apply])] });
167
+ const issues = validateStepOutputRefs(config, new Map([["applyStacks", applyStacksContract]]));
168
+ expect(issues.some((i) => i.message.includes('unknown step id "nope"'))).toBe(true);
169
+ });
170
+
171
+ it("flags a path that doesn't exist on the producer's declared return schema", () => {
172
+ const diff = activity("lifecycleDiff", { env: "prod" }, { id: "diff" });
173
+ const apply = activity("applyStacks", { stacks: stepOutput("diff", "notAField") });
174
+ const config = opWith({ phases: [phase("P", [diff, apply])] });
175
+ const contracts = new Map<string, ActivityContract>([
176
+ ["lifecycleDiff", lifecycleDiffContract],
177
+ ["applyStacks", applyStacksContract],
178
+ ]);
179
+ const issues = validateStepOutputRefs(config, contracts);
180
+ expect(issues.some((i) => i.message.includes('path "notAField"') && i.message.includes("does not exist"))).toBe(true);
181
+ });
182
+
183
+ it("flags a reference to a later step", () => {
184
+ const apply = activity("applyStacks", { stacks: stepOutput("diff", "driftedStacks") });
185
+ const diff = activity("lifecycleDiff", { env: "prod" }, { id: "diff" });
186
+ const config = opWith({ phases: [phase("Apply", [apply]), phase("Diff", [diff])] });
187
+ const contracts = new Map<string, ActivityContract>([
188
+ ["lifecycleDiff", lifecycleDiffContract],
189
+ ["applyStacks", applyStacksContract],
190
+ ]);
191
+ const issues = validateStepOutputRefs(config, contracts);
192
+ expect(issues.some((i) => i.message.includes("later phase"))).toBe(true);
193
+ });
194
+
195
+ it("flags a reference into a contract-less producer", () => {
196
+ const diff = activity("lifecycleDiff", { env: "prod" }, { id: "diff" });
197
+ const apply = activity("applyStacks", { stacks: stepOutput("diff", "driftedStacks") });
198
+ const config = opWith({ phases: [phase("P", [diff, apply])] });
199
+ // No contract registered for lifecycleDiff at all.
200
+ const issues = validateStepOutputRefs(config, new Map([["applyStacks", applyStacksContract]]));
201
+ expect(issues.some((i) => i.message.includes("no registered activity contract"))).toBe(true);
202
+ });
203
+ });
204
+
205
+ describe("validateStepOutputRefs() — structural coverage", () => {
206
+ it("flags a self-reference", () => {
207
+ const step = activity("lifecycleDiff", { env: "prod" }, { id: "diff" });
208
+ step.args = { env: "prod", x: stepOutput("diff", "drifted") };
209
+ const config = opWith({ phases: [phase("P", [step])] });
210
+ const issues = validateStepOutputRefs(config, new Map([["lifecycleDiff", lifecycleDiffContract]]));
211
+ expect(issues.some((i) => i.message.includes("does not run before this step"))).toBe(true);
212
+ });
213
+
214
+ it("flags a reference to a step in the same parallel phase", () => {
215
+ const diff = activity("lifecycleDiff", { env: "prod" }, { id: "diff" });
216
+ const apply = activity("applyStacks", { stacks: stepOutput("diff", "driftedStacks") });
217
+ const config = opWith({ phases: [phase("P", [diff, apply], { parallel: true })] });
218
+ const contracts = new Map<string, ActivityContract>([
219
+ ["lifecycleDiff", lifecycleDiffContract],
220
+ ["applyStacks", applyStacksContract],
221
+ ]);
222
+ const issues = validateStepOutputRefs(config, contracts);
223
+ expect(issues.some((i) => i.message.includes("same parallel phase"))).toBe(true);
224
+ });
225
+
226
+ it("flags a duplicate step id", () => {
227
+ const diff1 = activity("lifecycleDiff", { env: "a" }, { id: "diff" });
228
+ const diff2 = activity("lifecycleDiff", { env: "b" }, { id: "diff" });
229
+ const apply = activity("applyStacks", { stacks: stepOutput("diff", "driftedStacks") });
230
+ const config = opWith({ phases: [phase("P", [diff1, diff2, apply])] });
231
+ const contracts = new Map<string, ActivityContract>([
232
+ ["lifecycleDiff", lifecycleDiffContract],
233
+ ["applyStacks", applyStacksContract],
234
+ ]);
235
+ const issues = validateStepOutputRefs(config, contracts);
236
+ expect(issues.some((i) => i.message.includes("used by more than one step"))).toBe(true);
237
+ });
238
+
239
+ it("flags a producer contract with no returns schema", () => {
240
+ const cmd = activity("shellCmd", { cmd: "true" }, { id: "cmd" });
241
+ const apply = activity("applyStacks", { stacks: stepOutput("cmd", "x") });
242
+ const config = opWith({ phases: [phase("P", [cmd, apply])] });
243
+ const contracts = new Map<string, ActivityContract>([
244
+ ["shellCmd", noReturnContract],
245
+ ["applyStacks", applyStacksContract],
246
+ ]);
247
+ const issues = validateStepOutputRefs(config, contracts);
248
+ expect(issues.some((i) => i.message.includes("declares no return schema"))).toBe(true);
249
+ });
250
+
251
+ it("flags a reference authored inside onFailure", () => {
252
+ const diff = activity("lifecycleDiff", { env: "prod" }, { id: "diff" });
253
+ const rollback = activity("applyStacks", { stacks: stepOutput("diff", "driftedStacks") });
254
+ const config = opWith({ phases: [phase("P", [diff])], onFailure: [phase("Rollback", [rollback])] });
255
+ const contracts = new Map<string, ActivityContract>([
256
+ ["lifecycleDiff", lifecycleDiffContract],
257
+ ["applyStacks", applyStacksContract],
258
+ ]);
259
+ const issues = validateStepOutputRefs(config, contracts);
260
+ expect(issues.some((i) => i.message.includes("onFailure compensation"))).toBe(true);
261
+ });
262
+
263
+ it("flags a reference authored inside an effect step's nested steps", () => {
264
+ const receipt = EffectReceipt("seeded", { effect: "db-seed", flavor: "hash", inputs: { file: "seed.sql", version: 3 } });
265
+ const diff = activity("lifecycleDiff", { env: "prod" }, { id: "diff" });
266
+ const nested = activity("applyStacks", { stacks: stepOutput("diff", "driftedStacks") });
267
+ const effectStep = effect(receipt, [nested]);
268
+ const config = opWith({ phases: [phase("P", [diff, effectStep])] });
269
+ const contracts = new Map<string, ActivityContract>([
270
+ ["lifecycleDiff", lifecycleDiffContract],
271
+ ["applyStacks", applyStacksContract],
272
+ ]);
273
+ const issues = validateStepOutputRefs(config, contracts);
274
+ expect(issues.some((i) => i.message.includes("nested inside an effect step"))).toBe(true);
275
+ });
276
+
277
+ it("reports every issue across multiple refs, not just the first", () => {
278
+ const apply1 = activity("applyStacks", { stacks: stepOutput("nope1", "x") });
279
+ const apply2 = activity("applyStacks", { stacks: stepOutput("nope2", "x") });
280
+ const config = opWith({ phases: [phase("P", [apply1, apply2])] });
281
+ const issues = validateStepOutputRefs(config, new Map([["applyStacks", applyStacksContract]]));
282
+ expect(issues.some((i) => i.message.includes("nope1"))).toBe(true);
283
+ expect(issues.some((i) => i.message.includes("nope2"))).toBe(true);
284
+ });
285
+ });
286
+
287
+ // ── validateStepOutputRefs() — cross-contract primitive-type check (#1950-3) ──
288
+
289
+ describe("validateStepOutputRefs() — cross-contract type compatibility", () => {
290
+ const takeCountContract = activityContract("takeCount", z.strictObject({ count: z.number() }));
291
+
292
+ it("flags a string-returning path fed into a number-typed arg", () => {
293
+ const diff = activity("lifecycleDiff", { env: "prod" }, { id: "diff" });
294
+ const consume = activity("takeCount", { count: stepOutput("diff", "output") });
295
+ const config = opWith({ phases: [phase("P", [diff, consume])] });
296
+ const contracts = new Map<string, ActivityContract>([
297
+ ["lifecycleDiff", lifecycleDiffContract],
298
+ ["takeCount", takeCountContract],
299
+ ]);
300
+ const issues = validateStepOutputRefs(config, contracts);
301
+ expect(issues.some((i) => i.message.includes("type mismatch") && i.message.includes("string") && i.message.includes("number"))).toBe(true);
302
+ });
303
+
304
+ it("a matching type (array of strings into array of strings) passes", () => {
305
+ const diff = activity("lifecycleDiff", { env: "prod" }, { id: "diff" });
306
+ const apply = activity("applyStacks", { stacks: stepOutput("diff", "driftedStacks") });
307
+ const config = opWith({ phases: [phase("P", [diff, apply])] });
308
+ const contracts = new Map<string, ActivityContract>([
309
+ ["lifecycleDiff", lifecycleDiffContract],
310
+ ["applyStacks", applyStacksContract],
311
+ ]);
312
+ expect(validateStepOutputRefs(config, contracts)).toEqual([]);
313
+ });
314
+
315
+ it("bails silently when the consumer has no registered contract", () => {
316
+ const diff = activity("lifecycleDiff", { env: "prod" }, { id: "diff" });
317
+ const consume = activity("takeCount", { count: stepOutput("diff", "output") });
318
+ const config = opWith({ phases: [phase("P", [diff, consume])] });
319
+ // No contract registered for "takeCount" — nothing to compare the consumer's arg type against.
320
+ const issues = validateStepOutputRefs(config, new Map([["lifecycleDiff", lifecycleDiffContract]]));
321
+ expect(issues.some((i) => i.message.includes("type mismatch"))).toBe(false);
322
+ });
323
+
324
+ it("bails silently on a whole-value reference (no path)", () => {
325
+ const diff = activity("lifecycleDiff", { env: "prod" }, { id: "diff" });
326
+ const consume = activity("takeCount", { count: stepOutput("diff") });
327
+ const config = opWith({ phases: [phase("P", [diff, consume])] });
328
+ const contracts = new Map<string, ActivityContract>([
329
+ ["lifecycleDiff", lifecycleDiffContract],
330
+ ["takeCount", takeCountContract],
331
+ ]);
332
+ expect(validateStepOutputRefs(config, contracts).some((i) => i.message.includes("type mismatch"))).toBe(false);
333
+ });
334
+ });