@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,412 @@
1
+ /**
2
+ * Effect receipts (chant #1831, epic #1703).
3
+ *
4
+ * A RECEIPT is the declared witness that an out-of-band effect (a migration,
5
+ * a seed job, a one-shot bootstrap) has run — declared, diffed, and observed
6
+ * like any resource, but observe-only to the generic apply path: the
7
+ * `effect()` step (#1834) is the sole writer, on success, last. Anything
8
+ * else silently converts at-least-once into never (epic decision log, item
9
+ * 3). That write-exclusion is #1832's enforcement; what THIS module provides
10
+ * is the recognition marker that makes it possible: core code (lint, plan,
11
+ * apply) identifies receipts via {@link EFFECT_RECEIPT_MARKER} without any
12
+ * lexicon knowledge, while per-lexicon rows (#1835's `AWS::SSM::Parameter`)
13
+ * materialize them as real resources. Unlike a secret provenance declaration
14
+ * (./secret-provenance.ts), a receipt DOES serialize — the marker identifies,
15
+ * it does not exclude.
16
+ *
17
+ * The resolution split (epic decision log, item 5):
18
+ *
19
+ * - STATIC inputs hash at synthesis, in the serializer — a fully static
20
+ * receipt's expectation is already known when the template is written
21
+ * ({@link receiptExpectation}).
22
+ * - REFERENCE inputs (attr-refs and other intrinsics — deploy-time values)
23
+ * are recorded in placeholder form at synthesis and resolve in the plan
24
+ * engine (#1832) and again in the effect step (#1834), via
25
+ * {@link resolveReceiptExpectation}. Synthesis resolves NOTHING:
26
+ * {@link receiptExpectation} refuses a hash-flavor receipt that still
27
+ * carries references rather than hashing a placeholder.
28
+ * - Build-time `params.*` are not references here: they fold to literals
29
+ * before discovery ever runs (../params.ts), so by the time this factory
30
+ * sees them they are static.
31
+ *
32
+ * Nothing in this module reads live state. Every function is pure over the
33
+ * declaration and, for resolution, the caller-supplied resolver.
34
+ *
35
+ * Hashing is JCS-style canonical JSON (RFC 8785 shape: sorted keys, standard
36
+ * ECMAScript number/string encoding — implemented minimally here, no
37
+ * dependency) digested with sha256 ({@link canonicalJson},
38
+ * `sha256:<hex>` like the build-ledger digests in ./lifecycle/build-ledger.ts).
39
+ */
40
+
41
+ import { createHash } from "node:crypto";
42
+ import { DECLARABLE_MARKER, type Declarable } from "./declarable";
43
+ import { isIntrinsic, type Intrinsic } from "./intrinsic";
44
+
45
+ /** The closed union of receipt flavors. */
46
+ export type EffectReceiptFlavor = "existence" | "hash";
47
+
48
+ /** Every receipt flavor, for exhaustiveness checks. */
49
+ export const EFFECT_RECEIPT_FLAVORS: readonly EffectReceiptFlavor[] = ["existence", "hash"];
50
+
51
+ /** Marker symbol identifying an effect receipt. `Symbol.for` so it survives
52
+ * the entity-wire codec (./discovery/entity-wire-codec.ts) and so a lexicon
53
+ * row can stamp the SAME symbol on its materialized resource — core's lint
54
+ * (#1833), plan (#1832), and apply write-exclusion recognize receipts through
55
+ * this marker alone, lexicon-independently. */
56
+ export const EFFECT_RECEIPT_MARKER = Symbol.for("chant.effect-receipt");
57
+
58
+ /** The `entityType` the core factory stamps. Lexicon materialization rows
59
+ * (#1835) use their own entityType and carry the marker instead. */
60
+ export const EFFECT_RECEIPT_ENTITY_TYPE = "Chant::EffectReceipt";
61
+
62
+ /**
63
+ * The expected stored value of an `existence`-flavor receipt: a fixed marker
64
+ * constant, the same for every existence receipt. Present-and-equal means the
65
+ * effect has run; anything else renders an effect-will-fire row (#1832).
66
+ */
67
+ export const EXISTENCE_EXPECTATION = "chant.effect-receipt:exists";
68
+
69
+ /**
70
+ * An effect receipt declaration — a Declarable, so discovery collects it,
71
+ * `chant list` shows it, and (unlike a secret declaration) serialization
72
+ * keeps it: a lexicon row turns it into a real, observable resource.
73
+ */
74
+ export interface EffectReceiptDeclaration extends Declarable {
75
+ readonly [EFFECT_RECEIPT_MARKER]: true;
76
+ /** `"chant"` for the plain factory; a lexicon row (#1835) declares its own. */
77
+ readonly lexicon: string;
78
+ /** The core factory stamps EFFECT_RECEIPT_ENTITY_TYPE; a lexicon row uses its materialized kind. */
79
+ readonly entityType: string;
80
+ /** The receipt's own name (the export-level identity of the witness). */
81
+ readonly name: string;
82
+ /** The effect this receipt witnesses — the identity the `effect()` step
83
+ * (#1834) and the receipt path (#1835) key on. */
84
+ readonly effect: string;
85
+ /** How the receipt is compared: mere presence, or a digest of the inputs. */
86
+ readonly flavor: EffectReceiptFlavor;
87
+ /**
88
+ * The effect's inputs as recorded at synthesis: static values verbatim,
89
+ * references (intrinsics) kept in placeholder form — never resolved here.
90
+ */
91
+ readonly inputs: Readonly<Record<string, unknown>>;
92
+ }
93
+
94
+ /** Factory options for {@link EffectReceipt}. */
95
+ export interface EffectReceiptOptions {
96
+ /** The effect this receipt witnesses. Non-empty. */
97
+ readonly effect: string;
98
+ /** `existence` — presence is the witness; `hash` — a digest of the inputs
99
+ * is, so changed inputs re-propose the fire. */
100
+ readonly flavor: EffectReceiptFlavor;
101
+ /** The effect's inputs. Static values hash at synthesis; intrinsic values
102
+ * (attr-refs, deploy-time references) resolve at plan and at run. */
103
+ readonly inputs?: Record<string, unknown>;
104
+ }
105
+
106
+ /**
107
+ * Declare an effect receipt. The returned object is a locked Declarable:
108
+ * declared fields are immutable, plain-data input structures are frozen
109
+ * (intrinsics are left live — discovery still stamps logical names onto
110
+ * attr-refs), and the top-level object stays extensible for discovery's own
111
+ * symbol-keyed metadata.
112
+ */
113
+ export function EffectReceipt(name: string, options: EffectReceiptOptions): EffectReceiptDeclaration {
114
+ if (typeof name !== "string" || name.length === 0) {
115
+ throw new Error("EffectReceipt: `name` must be a non-empty string");
116
+ }
117
+ if (typeof options?.effect !== "string" || options.effect.length === 0) {
118
+ throw new Error(`EffectReceipt("${name}"): \`effect\` must be a non-empty string`);
119
+ }
120
+ if (!EFFECT_RECEIPT_FLAVORS.includes(options.flavor)) {
121
+ throw new Error(
122
+ `EffectReceipt("${name}"): unknown flavor "${String(options.flavor)}" — ` +
123
+ `expected one of ${EFFECT_RECEIPT_FLAVORS.join(", ")}`,
124
+ );
125
+ }
126
+ if (options.inputs !== undefined && (typeof options.inputs !== "object" || options.inputs === null || Array.isArray(options.inputs))) {
127
+ throw new Error(`EffectReceipt("${name}"): \`inputs\` must be a plain object when present`);
128
+ }
129
+
130
+ const inputs = deepFreezeStatic({ ...(options.inputs ?? {}) }) as Readonly<Record<string, unknown>>;
131
+
132
+ const decl: EffectReceiptDeclaration = {
133
+ [DECLARABLE_MARKER]: true,
134
+ [EFFECT_RECEIPT_MARKER]: true,
135
+ lexicon: "chant",
136
+ entityType: EFFECT_RECEIPT_ENTITY_TYPE,
137
+ name,
138
+ effect: options.effect,
139
+ flavor: options.flavor,
140
+ inputs,
141
+ };
142
+ // Declared fields immutable, object extensible — same shape secret
143
+ // declarations use (./secret-provenance.ts's lockDeclaredFields).
144
+ for (const key of Object.keys(decl)) {
145
+ Object.defineProperty(decl, key, { writable: false, configurable: false });
146
+ }
147
+ return decl;
148
+ }
149
+
150
+ /** Freeze plain objects and arrays in place, leaving intrinsics (and any
151
+ * other class instance) untouched — discovery mutates AttrRefs when it
152
+ * assigns logical names. */
153
+ function deepFreezeStatic(value: unknown, seen: Set<object> = new Set()): unknown {
154
+ if (typeof value !== "object" || value === null || isIntrinsic(value)) return value;
155
+ if (seen.has(value)) return value;
156
+ seen.add(value);
157
+ if (Array.isArray(value)) {
158
+ for (const el of value) deepFreezeStatic(el, seen);
159
+ return Object.freeze(value);
160
+ }
161
+ if (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null) {
162
+ for (const el of Object.values(value)) deepFreezeStatic(el, seen);
163
+ return Object.freeze(value);
164
+ }
165
+ return value;
166
+ }
167
+
168
+ /** Type guard for an effect receipt — the recognition read core's guards use.
169
+ * True for the core declaration AND for any lexicon-materialized resource
170
+ * that carries the marker. */
171
+ export function isEffectReceipt(value: unknown): value is EffectReceiptDeclaration {
172
+ return (
173
+ typeof value === "object" &&
174
+ value !== null &&
175
+ EFFECT_RECEIPT_MARKER in value &&
176
+ (value as Record<symbol, unknown>)[EFFECT_RECEIPT_MARKER] === true
177
+ );
178
+ }
179
+
180
+ /**
181
+ * Extract the effect receipts from a discovered entity map — the read surface
182
+ * for lint (#1833), the plan engine (#1832), and the apply write-exclusion.
183
+ * Keyed by entity name (export name), the same key `DiscoveryResult.entities`
184
+ * uses.
185
+ */
186
+ export function collectEffectReceipts(
187
+ entities: ReadonlyMap<string, Declarable>,
188
+ ): Map<string, EffectReceiptDeclaration> {
189
+ const out = new Map<string, EffectReceiptDeclaration>();
190
+ for (const [name, entity] of entities) {
191
+ if (isEffectReceipt(entity)) out.set(name, entity);
192
+ }
193
+ return out;
194
+ }
195
+
196
+ /**
197
+ * Split an entity map into the apply-bound set and the receipts (#1832).
198
+ *
199
+ * The write-exclusion seam: receipts are observe-only to the generic apply
200
+ * path — the `effect()` step is the sole writer (epic #1703, decision 3), and
201
+ * a receipt the generic apply stamped would silently convert at-least-once
202
+ * into never. The build calls this at serializer-input assembly, the one core
203
+ * choke point every applier's input flows through (appliers consume serialized
204
+ * build outputs), so no lexicon's serialized apply document ever contains a
205
+ * receipt and no applier's desired or prune set can. Receipts still reach the
206
+ * serializer — for visibility rendering outside the apply-bound document
207
+ * (#1835) — via `SerializeContext.receipts`, never in the entity map.
208
+ */
209
+ export function splitReceiptEntities(entities: ReadonlyMap<string, Declarable>): {
210
+ applyBound: Map<string, Declarable>;
211
+ receipts: Map<string, EffectReceiptDeclaration>;
212
+ } {
213
+ const applyBound = new Map<string, Declarable>();
214
+ const receipts = new Map<string, EffectReceiptDeclaration>();
215
+ for (const [name, entity] of entities) {
216
+ if (isEffectReceipt(entity)) receipts.set(name, entity);
217
+ else applyBound.set(name, entity);
218
+ }
219
+ return { applyBound, receipts };
220
+ }
221
+
222
+ // ─────────────────────────────────────────────────────────────────────────
223
+ // Canonical hashing — JCS-style canonical JSON + sha256.
224
+ // ─────────────────────────────────────────────────────────────────────────
225
+
226
+ /**
227
+ * JCS-style canonical JSON (the RFC 8785 shape, implemented minimally):
228
+ * object keys sorted by UTF-16 code units, numbers and strings in standard
229
+ * ECMAScript `JSON.stringify` encoding, no insignificant whitespace.
230
+ * `toJSON()` is honored the way `JSON.stringify` honors it — which is what
231
+ * puts an intrinsic's PLACEHOLDER envelope (e.g. an attr-ref's
232
+ * `{"__attrRef":{...}}`) into the canonical form rather than a resolved
233
+ * value. Non-representable values (undefined outside an object property,
234
+ * functions, symbols, bigints, non-finite numbers, cycles) throw — a hash
235
+ * input silently coerced is a wrong expectation.
236
+ */
237
+ export function canonicalJson(value: unknown): string {
238
+ const out = encodeCanonical(value, "$", new Set());
239
+ if (out === undefined) {
240
+ throw new Error(`canonicalJson: value at $ is not representable in JSON`);
241
+ }
242
+ return out;
243
+ }
244
+
245
+ /** Returns undefined only for values JSON.stringify would drop as an object
246
+ * property (undefined); throws for everything else non-representable. */
247
+ function encodeCanonical(value: unknown, path: string, seen: Set<object>): string | undefined {
248
+ // toJSON first, like JSON.stringify — once per node.
249
+ if (value !== null && (typeof value === "object" || typeof value === "function")) {
250
+ const toJSON = (value as { toJSON?: unknown }).toJSON;
251
+ if (typeof toJSON === "function") {
252
+ value = (toJSON as () => unknown).call(value);
253
+ }
254
+ }
255
+ if (value === undefined) return undefined;
256
+ if (value === null) return "null";
257
+ switch (typeof value) {
258
+ case "boolean":
259
+ return value ? "true" : "false";
260
+ case "number":
261
+ if (!Number.isFinite(value)) {
262
+ throw new Error(`canonicalJson: non-finite number at ${path}`);
263
+ }
264
+ return String(value); // ECMAScript ToString — what JCS specifies.
265
+ case "string":
266
+ return JSON.stringify(value);
267
+ case "bigint":
268
+ throw new Error(`canonicalJson: bigint at ${path} is not representable in JSON`);
269
+ case "function":
270
+ case "symbol":
271
+ throw new Error(`canonicalJson: ${typeof value} at ${path} is not representable in JSON`);
272
+ }
273
+ const obj = value as object;
274
+ if (seen.has(obj)) {
275
+ throw new Error(`canonicalJson: circular structure at ${path}`);
276
+ }
277
+ seen.add(obj);
278
+ try {
279
+ if (Array.isArray(obj)) {
280
+ const parts = obj.map((el, i) => encodeCanonical(el, `${path}[${i}]`, seen) ?? "null");
281
+ return `[${parts.join(",")}]`;
282
+ }
283
+ const keys = Object.keys(obj).sort();
284
+ const parts: string[] = [];
285
+ for (const key of keys) {
286
+ const encoded = encodeCanonical((obj as Record<string, unknown>)[key], `${path}.${key}`, seen);
287
+ if (encoded !== undefined) parts.push(`${JSON.stringify(key)}:${encoded}`);
288
+ }
289
+ return `{${parts.join(",")}}`;
290
+ } finally {
291
+ seen.delete(obj);
292
+ }
293
+ }
294
+
295
+ function sha256Digest(canonical: string): string {
296
+ return `sha256:${createHash("sha256").update(canonical, "utf8").digest("hex")}`;
297
+ }
298
+
299
+ // ─────────────────────────────────────────────────────────────────────────
300
+ // The resolution split.
301
+ // ─────────────────────────────────────────────────────────────────────────
302
+
303
+ /** The paths (dot/bracket, rooted at `inputs`) of every reference (intrinsic)
304
+ * input still unresolved on the receipt. Empty means the receipt is fully
305
+ * static and {@link receiptExpectation} can stamp its digest at synthesis. */
306
+ export function referenceInputPaths(receipt: EffectReceiptDeclaration): string[] {
307
+ const paths: string[] = [];
308
+ collectReferencePaths(receipt.inputs, "inputs", paths, new Set());
309
+ return paths;
310
+ }
311
+
312
+ function collectReferencePaths(value: unknown, path: string, out: string[], seen: Set<object>): void {
313
+ if (typeof value !== "object" || value === null) return;
314
+ if (isIntrinsic(value)) {
315
+ out.push(path);
316
+ return;
317
+ }
318
+ if (seen.has(value)) return;
319
+ seen.add(value);
320
+ if (Array.isArray(value)) {
321
+ value.forEach((el, i) => collectReferencePaths(el, `${path}[${i}]`, out, seen));
322
+ return;
323
+ }
324
+ for (const [key, el] of Object.entries(value)) {
325
+ collectReferencePaths(el, `${path}.${key}`, out, seen);
326
+ }
327
+ }
328
+
329
+ /**
330
+ * The receipt's expected stored value, computable at synthesis:
331
+ *
332
+ * - `existence` → {@link EXISTENCE_EXPECTATION}, always.
333
+ * - `hash` → `sha256:<hex>` over the canonical JSON of
334
+ * `{ effect, inputs }` — the effect name is bound into the digest so the
335
+ * same inputs under a different effect never collide.
336
+ *
337
+ * A hash-flavor receipt that still carries reference inputs THROWS instead
338
+ * of hashing placeholders: synthesis resolves nothing (epic decision log,
339
+ * item 5). The plan engine and the effect step get the digest through
340
+ * {@link resolveReceiptExpectation}.
341
+ */
342
+ export function receiptExpectation(receipt: EffectReceiptDeclaration): string {
343
+ if (receipt.flavor === "existence") return EXISTENCE_EXPECTATION;
344
+ const refs = referenceInputPaths(receipt);
345
+ if (refs.length > 0) {
346
+ throw new Error(
347
+ `receiptExpectation("${receipt.name}"): unresolved reference inputs at ${refs.join(", ")} — ` +
348
+ `references resolve at plan and at run, never at synthesis; ` +
349
+ `use resolveReceiptExpectation with a resolver`,
350
+ );
351
+ }
352
+ return sha256Digest(canonicalJson({ effect: receipt.effect, inputs: receipt.inputs }));
353
+ }
354
+
355
+ /**
356
+ * Resolves one reference input to its live value. `path` is the reference's
357
+ * location (as {@link referenceInputPaths} renders it). Must return a
358
+ * JSON-representable value — returning `undefined` or another intrinsic is
359
+ * an error, reported with the path.
360
+ */
361
+ export type ReceiptInputResolver = (ref: Intrinsic, path: string) => unknown;
362
+
363
+ /**
364
+ * The receipt's expected stored value with references resolved — the form
365
+ * the plan engine (#1832) compares against the live receipt and the effect
366
+ * step (#1834) writes on success. Deterministic over the receipt and the
367
+ * resolver's answers; resolves nothing itself and reads no live state (the
368
+ * resolver is the caller's seam to deploy-time values).
369
+ */
370
+ export function resolveReceiptExpectation(
371
+ receipt: EffectReceiptDeclaration,
372
+ resolver: ReceiptInputResolver,
373
+ ): string {
374
+ if (receipt.flavor === "existence") return EXISTENCE_EXPECTATION;
375
+ const resolved = resolveValue(receipt.inputs, "inputs", resolver, new Set());
376
+ return sha256Digest(canonicalJson({ effect: receipt.effect, inputs: resolved }));
377
+ }
378
+
379
+ function resolveValue(
380
+ value: unknown,
381
+ path: string,
382
+ resolver: ReceiptInputResolver,
383
+ seen: Set<object>,
384
+ ): unknown {
385
+ if (typeof value !== "object" || value === null) return value;
386
+ if (isIntrinsic(value)) {
387
+ const resolved = resolver(value, path);
388
+ if (resolved === undefined) {
389
+ throw new Error(`resolveReceiptExpectation: resolver returned undefined for reference at ${path}`);
390
+ }
391
+ if (isIntrinsic(resolved)) {
392
+ throw new Error(`resolveReceiptExpectation: resolver returned another reference for ${path}`);
393
+ }
394
+ return resolved;
395
+ }
396
+ if (seen.has(value)) {
397
+ throw new Error(`resolveReceiptExpectation: circular structure at ${path}`);
398
+ }
399
+ seen.add(value);
400
+ try {
401
+ if (Array.isArray(value)) {
402
+ return value.map((el, i) => resolveValue(el, `${path}[${i}]`, resolver, seen));
403
+ }
404
+ const out: Record<string, unknown> = {};
405
+ for (const [key, el] of Object.entries(value)) {
406
+ out[key] = resolveValue(el, `${path}.${key}`, resolver, seen);
407
+ }
408
+ return out;
409
+ } finally {
410
+ seen.delete(value);
411
+ }
412
+ }
@@ -435,7 +435,11 @@ describe("fold — registered call-form intrinsics (#1044)", () => {
435
435
  expect(() => fold(expr, consts, [REF])).toThrow(FoldError);
436
436
  });
437
437
 
438
- test("an array method taking a closure is rejected — .map is arbitrary JS, not a registered intrinsic", () => {
438
+ test("an array method taking a closure is rejected — the closure argument, not .map itself, is what fails (chant #1966)", () => {
439
+ // chant #1966 — `.map` is now attempted (a real method on a real folded
440
+ // array), so the rejection moves from "no case for this call" to the
441
+ // closure argument itself: a function used as a value is never foldable,
442
+ // with or without a registered intrinsic inside it.
439
443
  const consts = parseConsts(`const cidrs = ["10.0.0.0/24"]; const x = cidrs.map((c) => Ref(c));`);
440
444
  const expr = consts.get("x");
441
445
  if (!expr) throw new Error("fixture error");
@@ -446,7 +450,7 @@ describe("fold — registered call-form intrinsics (#1044)", () => {
446
450
  error = e;
447
451
  }
448
452
  expect(error).toBeInstanceOf(FoldError);
449
- expect((error as FoldError).message).toContain("cidrs.map(...)");
453
+ expect((error as FoldError).message).toContain("a function used as a value is not foldable");
450
454
  });
451
455
 
452
456
  test("a user-defined function is rejected however it's named", () => {
package/src/fold/fold.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as ts from "typescript";
2
2
  import { relative } from "node:path";
3
- import { intrinsicCallFolds, intrinsicTagFolds, type IntrinsicDef } from "../lexicon";
3
+ import { intrinsicCallFolds, intrinsicCallFoldsEagerly, intrinsicTagFolds, type IntrinsicDef } from "../lexicon";
4
4
  import {
5
5
  SUPPORTED_BINARY_OPERATORS,
6
6
  SUPPORTED_UNARY_OPERATORS,
@@ -67,7 +67,27 @@ import { isFoldableHelperName } from "./foldable-helpers";
67
67
  * folds its body against the defining module's scope with the folded
68
68
  * arguments bound. Still nothing is imported or run.
69
69
  *
70
- * Everything else — a package's function, a method call, an array `.map`, a
70
+ * A bare composite factory call (`Checkout({...})` on its own) is still out
71
+ * of scope (epic Phase 5, #1023 covers only interpreting a factory's OWN
72
+ * body, not consuming its result as a value elsewhere). chant #1174 adds one
73
+ * narrow exception at the PROPERTY-ACCESS level rather than here: the
74
+ * `<Identifier>(...).step` idiom — see {@link FoldedCompositeStepCall} on the
75
+ * property-access branch below. A call with no `.step` narrowing, or any
76
+ * other member, still throws from this branch exactly as before.
77
+ *
78
+ * chant #1966 adds a fourth call shape, and a method call on top of any of
79
+ * the four: a lexicon-package function its lexicon registered with
80
+ * {@link intrinsicCallFoldsEagerly} (../lexicon.ts) evaluates eagerly —
81
+ * unlike the other three, which envelope for later revival — because its
82
+ * usual use (`` `${matrix("os")}` ``) coerces the result via `String()` at
83
+ * fold time, before any revival would run. A `CallExpression` whose callee is
84
+ * a property access — `github.actor.toString()`, `[...].join(",")`,
85
+ * `matrix("os").toString()` — folds its receiver and calls the named method
86
+ * on it directly, PROVIDED the receiver is a real value and not one of
87
+ * fold's own symbolic envelopes (see {@link isFoldSymbolicEnvelope}); nothing
88
+ * about the method name is otherwise restricted.
89
+ *
90
+ * Everything else — an ordinary package function, an array `.map`, a
71
91
  * registered name shadowed by a local binding — still throws.
72
92
  *
73
93
  * Cross-file identifier resolution (chant #1020): `consts` alone is always
@@ -105,7 +125,8 @@ export type FoldedValue =
105
125
  | FoldedIntrinsic
106
126
  | FoldedHelperCall
107
127
  | SymbolicValue
108
- | FoldedResource;
128
+ | FoldedResource
129
+ | FoldedCompositeStepCall;
109
130
 
110
131
  /**
111
132
  * Symbolic reference produced when a property/element access resolves to an
@@ -225,6 +246,40 @@ export interface FoldedResource {
225
246
  args?: FoldedValue[];
226
247
  }
227
248
 
249
+ /**
250
+ * The result of folding the `<Identifier>(...).step` composite-consumer
251
+ * idiom (chant #1174) — `Checkout({...}).step`, `SetupNode({...}).step`,
252
+ * every lexicon's single-action `Composite()` wrapper embedded inline in a
253
+ * `Job`'s `steps` array, exactly as composites.mdx documents it. This is the
254
+ * SAME shape chant #1544 already carved out of EVL001 as a documented,
255
+ * correct fallback rather than a lint error
256
+ * ({@link "./subset"}'s `allowCompositeStepAccess`) — `fold()` never set
257
+ * that flag, so before this it fell back to run every time, as designed.
258
+ * This is the fold-side counterpart that actually reduces it instead.
259
+ *
260
+ * Symbolic, exactly like {@link FoldedIntrinsic}/{@link FoldedHelperCall}:
261
+ * `fold()` executes nothing here. It records which composite factory the
262
+ * source named, its folded arguments (in source order), and that the result
263
+ * was immediately narrowed to `.step`. ../discovery/fold-import.ts's bridge
264
+ * resolves the callee through the folding file's own imports — a
265
+ * project-file registered `Composite` is interpreted (chant #1023's existing
266
+ * machinery), anything else (every lexicon-package composite, which is what
267
+ * `Checkout`/`SetupNode` are) is imported and invoked for real, exactly as a
268
+ * top-level `export const x = Checkout({...})` already does via
269
+ * `resolveCallExpression` — and then reads `.step` off the REAL result.
270
+ *
271
+ * Deliberately narrower than "any call, any member access": the member name
272
+ * is fixed to `"step"`, matching the one idiom ../fold/subset.ts's EVL
273
+ * carve-out already permits. `fold()` may never accept a shape EVL doesn't
274
+ * (see that module's doc, point 2c, for the direction it is not allowed to
275
+ * be wrong in) — widening past `.step` here without widening the shared
276
+ * predicate in lockstep would open exactly that gap.
277
+ */
278
+ export interface FoldedCompositeStepCall {
279
+ __compositeStep: string;
280
+ args: FoldedValue[];
281
+ }
282
+
228
283
  /**
229
284
  * One entry per exported `const` resource declaration in {@link foldModule}'s
230
285
  * result. The `ok: false` case surfaces the same located, rule-id-tagged
@@ -648,11 +703,57 @@ function resolvesToResource(consts: Map<string, ts.Expression>, ident: ts.Identi
648
703
  return init !== undefined && ts.isNewExpression(init);
649
704
  }
650
705
 
706
+ /**
707
+ * True when `node` is a call through a bare identifier that isn't ALREADY
708
+ * one of `fold()`'s other three call shapes — a registered authoring helper,
709
+ * a registered call-form intrinsic, or a project-local {@link FoldableFunction}
710
+ * — i.e. exactly the callee the `CallExpression` branch below would
711
+ * otherwise throw {@link callExpressionMessage} for. Used only by the
712
+ * `.step` narrowing in the property-access branch (chant #1174,
713
+ * {@link FoldedCompositeStepCall}): checked from the OUTSIDE, at the
714
+ * property-access node, so `Checkout({...})` alone (no `.step`) still falls
715
+ * through to the ordinary `CallExpression` throw, unchanged.
716
+ */
717
+ function isUnclaimedBareCall(
718
+ node: ts.Expression,
719
+ consts: Map<string, ts.Expression>,
720
+ intrinsics: readonly IntrinsicDef[],
721
+ externals?: ReadonlyMap<string, unknown>,
722
+ ): node is ts.CallExpression {
723
+ if (!ts.isCallExpression(node) || !ts.isIdentifier(node.expression)) return false;
724
+ const name = node.expression.text;
725
+ if (consts.has(name)) return false;
726
+ if (isFoldableHelperName(name)) return false;
727
+ if (intrinsics.some((i) => i.name === name && intrinsicCallFolds(i))) return false;
728
+ if (isFoldableFunction(externals?.get(name))) return false;
729
+ return true;
730
+ }
731
+
651
732
  /** True when a folded value is a {@link FoldedResource} envelope (a `new Type(...)` that nothing constructed yet). */
652
733
  function isFoldedResource(value: FoldedValue): value is FoldedResource {
653
734
  return typeof value === "object" && value !== null && !Array.isArray(value) && "__resource" in value;
654
735
  }
655
736
 
737
+ /**
738
+ * True when `value` is one of `fold()`'s own symbolic envelope shapes — a
739
+ * stand-in for a value nothing has constructed or revived yet, not the value
740
+ * itself. A method call (see the `CallExpression` branch's property-access
741
+ * case below) must refuse one rather than silently falling through to
742
+ * `Object.prototype`'s own inherited methods — `toString` chief among them —
743
+ * which would answer with the placeholder's shape instead of what the real,
744
+ * eventually-revived value would produce.
745
+ */
746
+ function isFoldSymbolicEnvelope(value: FoldedValue): boolean {
747
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
748
+ return (
749
+ "__attrRef" in value ||
750
+ "__intrinsic" in value ||
751
+ "__helper" in value ||
752
+ "__resource" in value ||
753
+ "__compositeStep" in value
754
+ );
755
+ }
756
+
656
757
  /**
657
758
  * chant #1535 — an attribute read whose object folded to a resource ENVELOPE
658
759
  * rather than resolving through {@link resolvesToResource}. That happens when
@@ -892,6 +993,15 @@ export function fold(
892
993
  if (isFoldableFunction(external)) {
893
994
  throw foldError(node, `function "${node.text}" used as a value is not foldable`);
894
995
  }
996
+ // chant #1966 — a registered eager-fold lexicon helper (see the
997
+ // CallExpression branch below) is callable, never a bare value:
998
+ // nothing downstream can serialize a function.
999
+ if (
1000
+ typeof external === "function" &&
1001
+ intrinsics.some((i) => i.name === node.text && intrinsicCallFoldsEagerly(i))
1002
+ ) {
1003
+ throw foldError(node, `function "${node.text}" used as a value is not foldable — call it instead`);
1004
+ }
895
1005
  return external as FoldedValue;
896
1006
  }
897
1007
  // chant #1064 — a bare `process` reference is ALWAYS an ambient
@@ -948,6 +1058,21 @@ export function fold(
948
1058
  if (ts.isIdentifier(node.expression) && resolvesToResource(consts, node.expression)) {
949
1059
  return { __attrRef: { entity: node.expression.text, attribute: node.name.text } };
950
1060
  }
1061
+ // chant #1174 — `<Identifier>(...).step`, the composite-consumer idiom
1062
+ // (`Checkout({...}).step`) — see FoldedCompositeStepCall's doc. Checked
1063
+ // here, at the property-access node, rather than inside the
1064
+ // CallExpression branch: a bare `Checkout({...})` with no `.step` still
1065
+ // has no case there and throws exactly as before.
1066
+ if (node.name.text === "step" && isUnclaimedBareCall(node.expression, consts, intrinsics, externals)) {
1067
+ const call = node.expression;
1068
+ const calleeName = (call.expression as ts.Identifier).text;
1069
+ const inside = insideFunctionBody(node, `composite call \`${calleeName}(...).step\``);
1070
+ if (inside) throw inside;
1071
+ return {
1072
+ __compositeStep: calleeName,
1073
+ args: call.arguments.map((arg) => fold(arg, consts, intrinsics, externals)),
1074
+ };
1075
+ }
951
1076
  const obj = fold(node.expression, consts, intrinsics, externals);
952
1077
  if (obj === null || obj === undefined) return undefined;
953
1078
  if (isFoldedResource(obj)) return attrRefOnFoldedResource(node, node.name.text);
@@ -1146,6 +1271,62 @@ export function fold(
1146
1271
  }
1147
1272
  }
1148
1273
 
1274
+ // chant #1966 — the fourth call shape: a lexicon-package function its
1275
+ // lexicon registered with {@link intrinsicCallFoldsEagerly} (../lexicon.ts),
1276
+ // resolved into `externals` by ../discovery/fold-import.ts's
1277
+ // `resolveActiveLexiconExport` exactly like a plain data export
1278
+ // (`Azure.ResourceGroupLocation`, chant #1063), except callable. Unlike
1279
+ // the intrinsic-call shape above, this one is EVALUATED right here rather
1280
+ // than enveloped: a lexicon's own string-building helper (`matrix("os")`,
1281
+ // github lexicon) is typically embedded directly in a template literal
1282
+ // (`` `${matrix("os")}` ``), which coerces its result via native
1283
+ // `String()` at fold time — an envelope deferred to later revival would
1284
+ // stringify as "[object Object]" there. Evaluating eagerly, with the
1285
+ // folded arguments, produces the real, already-live return value instead
1286
+ // — the same guarantee a live external's own getter execution already
1287
+ // gives {@link fold}'s property-access branch.
1288
+ if (
1289
+ ts.isIdentifier(node.expression) &&
1290
+ !consts.has(node.expression.text) &&
1291
+ intrinsics.some((i) => i.name === (node.expression as ts.Identifier).text && intrinsicCallFoldsEagerly(i))
1292
+ ) {
1293
+ const callee = externals?.get(node.expression.text);
1294
+ if (typeof callee !== "function") {
1295
+ throw foldError(node, `"${node.expression.text}" did not resolve to a function — falls back to run`);
1296
+ }
1297
+ const args = node.arguments.map((arg) => fold(arg, consts, intrinsics, externals));
1298
+ return (callee as (...callArgs: unknown[]) => unknown)(...args) as FoldedValue;
1299
+ }
1300
+
1301
+ // chant #1966 — a method call whose RECEIVER is itself foldable: property
1302
+ // access on a live external (`github.actor.toString()`), a call folded by
1303
+ // one of the shapes above (`matrix("os").toString()`), or fold's own
1304
+ // array/object literal (`[...].join(",")`). The method is never checked
1305
+ // by name — only that the receiver is a REAL value (not one of fold's own
1306
+ // symbolic envelopes, see {@link isFoldSymbolicEnvelope}) and that the
1307
+ // named property on it is actually a function. Calling it with the folded
1308
+ // arguments is then no different from what running the file would do:
1309
+ // the receiver is the same real object either way.
1310
+ if (ts.isPropertyAccessExpression(node.expression)) {
1311
+ const methodName = node.expression.name.text;
1312
+ const receiver = fold(node.expression.expression, consts, intrinsics, externals);
1313
+ if (receiver === null || receiver === undefined) {
1314
+ throw foldError(node, `cannot call ".${methodName}(...)" on ${String(receiver)}`);
1315
+ }
1316
+ if (isFoldSymbolicEnvelope(receiver)) {
1317
+ throw foldError(
1318
+ node,
1319
+ `method call \`.${methodName}(...)\` on an unresolved value is not foldable — falls back to run`,
1320
+ );
1321
+ }
1322
+ const method = (receiver as Record<string, unknown>)[methodName];
1323
+ if (typeof method !== "function") {
1324
+ throw foldError(node, `"${methodName}" is not a callable method on the folded value — falls back to run`);
1325
+ }
1326
+ const args = node.arguments.map((arg) => fold(arg, consts, intrinsics, externals));
1327
+ return (method as (...methodArgs: unknown[]) => unknown).apply(receiver, args) as FoldedValue;
1328
+ }
1329
+
1149
1330
  throw foldError(node, callExpressionMessage(node));
1150
1331
  }
1151
1332