@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,116 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import * as ts from "typescript";
3
+ import { cor022ReceiptLeafRule, collectReceiptVariables } from "./cor022-receipt-leaf";
4
+ import type { LintContext } from "../rule";
5
+
6
+ function createContext(code: string, filePath = "test.ts"): LintContext {
7
+ const sourceFile = ts.createSourceFile(filePath, code, ts.ScriptTarget.Latest, true);
8
+ return { sourceFile, entities: [], filePath, lexicon: undefined };
9
+ }
10
+
11
+ describe("COR022: effect receipt is a leaf (#1833)", () => {
12
+ test("rule metadata", () => {
13
+ expect(cor022ReceiptLeafRule.id).toBe("COR022");
14
+ expect(cor022ReceiptLeafRule.severity).toBe("error");
15
+ expect(cor022ReceiptLeafRule.category).toBe("correctness");
16
+ });
17
+
18
+ test("flags a property access on a receipt inside another declarable", () => {
19
+ const ctx = createContext(`
20
+ export const seeded = EffectReceipt("seeded", { effect: "db-seed", flavor: "existence" });
21
+ export const app = new Service({ marker: seeded.effect });
22
+ `);
23
+ const diags = cor022ReceiptLeafRule.check(ctx);
24
+ expect(diags).toHaveLength(1);
25
+ expect(diags[0].ruleId).toBe("COR022");
26
+ expect(diags[0].severity).toBe("error");
27
+ expect(diags[0].message).toContain('"seeded" is an effect receipt');
28
+ expect(diags[0].message).toContain('"effect"');
29
+ expect(diags[0].message).toContain("sole writer");
30
+ });
31
+
32
+ test("const indirection fires: const r = EffectReceipt(...); other.prop = r.something", () => {
33
+ const ctx = createContext(`
34
+ const r = EffectReceipt("migrated", { effect: "schema-migrate", flavor: "hash" });
35
+ export const other = new Store({});
36
+ other.prop = r.something;
37
+ `);
38
+ const diags = cor022ReceiptLeafRule.check(ctx);
39
+ expect(diags).toHaveLength(1);
40
+ expect(diags[0].message).toContain('"r" is an effect receipt');
41
+ expect(diags[0].message).toContain('"something"');
42
+ });
43
+
44
+ test("an alias of a receipt is still the receipt", () => {
45
+ const ctx = createContext(`
46
+ const r = EffectReceipt("migrated", { effect: "schema-migrate", flavor: "existence" });
47
+ const alias = r;
48
+ export const svc = new Service({ tag: alias.name });
49
+ `);
50
+ const diags = cor022ReceiptLeafRule.check(ctx);
51
+ expect(diags).toHaveLength(1);
52
+ expect(diags[0].message).toContain('"alias"');
53
+ });
54
+
55
+ test("element access fires like property access", () => {
56
+ const ctx = createContext(`
57
+ const r = EffectReceipt("seeded", { effect: "db-seed", flavor: "existence" });
58
+ export const svc = new Service({ tag: r["effect"] });
59
+ `);
60
+ const diags = cor022ReceiptLeafRule.check(ctx);
61
+ expect(diags).toHaveLength(1);
62
+ expect(diags[0].message).toContain('"effect"');
63
+ });
64
+
65
+ test("namespaced factory call is recognized", () => {
66
+ const ctx = createContext(`
67
+ const r = chant.EffectReceipt("seeded", { effect: "db-seed", flavor: "existence" });
68
+ export const svc = new Service({ tag: r.effect });
69
+ `);
70
+ expect(cor022ReceiptLeafRule.check(ctx)).toHaveLength(1);
71
+ });
72
+
73
+ test("passes: a receipt that nothing dereferences", () => {
74
+ const ctx = createContext(`
75
+ import { EffectReceipt } from "@intentius/chant";
76
+ export const seeded = EffectReceipt("seeded", {
77
+ effect: "db-seed",
78
+ flavor: "hash",
79
+ inputs: { schema: "v3" },
80
+ });
81
+ export const app = new Service({ image: "app:1" });
82
+ `);
83
+ expect(cor022ReceiptLeafRule.check(ctx)).toHaveLength(0);
84
+ });
85
+
86
+ test("passes: referencing the receipt value itself (no attribute access)", () => {
87
+ const ctx = createContext(`
88
+ const r = EffectReceipt("seeded", { effect: "db-seed", flavor: "existence" });
89
+ registerReceipt(r);
90
+ `);
91
+ expect(cor022ReceiptLeafRule.check(ctx)).toHaveLength(0);
92
+ });
93
+
94
+ test("passes: property access on a non-receipt declarable", () => {
95
+ const ctx = createContext(`
96
+ export const db = new Database({ name: "main" });
97
+ export const app = new Service({ endpoint: db.endpoint });
98
+ `);
99
+ expect(cor022ReceiptLeafRule.check(ctx)).toHaveLength(0);
100
+ });
101
+
102
+ test("collectReceiptVariables resolves aliases declared before the receipt", () => {
103
+ const sf = ts.createSourceFile(
104
+ "t.ts",
105
+ `
106
+ const early = late;
107
+ const late = EffectReceipt("r", { effect: "e", flavor: "existence" });
108
+ `,
109
+ ts.ScriptTarget.Latest,
110
+ true,
111
+ );
112
+ const vars = collectReceiptVariables(sf);
113
+ expect(vars.has("late")).toBe(true);
114
+ expect(vars.has("early")).toBe(true);
115
+ });
116
+ });
@@ -0,0 +1,130 @@
1
+ import * as ts from "typescript";
2
+ import type { LintRule, LintContext, LintDiagnostic } from "../rule";
3
+
4
+ /**
5
+ * COR022: Effect Receipt Is a Leaf (#1833, epic #1703)
6
+ *
7
+ * Nothing may reference an effect receipt's attributes. A receipt is the
8
+ * declared witness that an out-of-band effect ran — the `effect()` step
9
+ * (#1834) is its sole writer, on success, last (../../effect-receipt.ts).
10
+ * Any resource that derives a property from a receipt couples itself to a
11
+ * value only the effect controls: the coupling is invisible at synthesis,
12
+ * and the receipt's late write (or its absence after a crash) would ripple
13
+ * into resources that were supposed to be independent of whether the effect
14
+ * has fired yet.
15
+ *
16
+ * The rule walks the file's reference graph the way COR011 does: it collects
17
+ * every variable initialized from an `EffectReceipt(...)` call (aliases
18
+ * included — `const alias = receipt` is still the receipt), then flags every
19
+ * property or element access rooted at one of them. Const indirection fires:
20
+ *
21
+ * const r = EffectReceipt("seeded", { effect: "db-seed", flavor: "existence" });
22
+ * other.prop = r.something; // COR022
23
+ *
24
+ * Referencing the receipt VALUE (passing `r` itself around) is not flagged —
25
+ * the leaf constraint is about attributes, and lexicon materialization rows
26
+ * legitimately take the whole declaration.
27
+ *
28
+ * Core recognizes the factory call by name; a lexicon-materialized receipt
29
+ * row (#1835) is recognized at build time by its marker instead — this rule
30
+ * is the source-level half.
31
+ */
32
+
33
+ /** Simple callee name of a call: `EffectReceipt(...)` or `chant.EffectReceipt(...)`. */
34
+ function calleeName(expr: ts.CallExpression): string | undefined {
35
+ if (ts.isIdentifier(expr.expression)) return expr.expression.text;
36
+ if (ts.isPropertyAccessExpression(expr.expression)) return expr.expression.name.text;
37
+ return undefined;
38
+ }
39
+
40
+ /** A variable declaration's `EffectReceipt(...)` initializer, if that is what it is. */
41
+ export function receiptFactoryCall(decl: ts.VariableDeclaration): ts.CallExpression | undefined {
42
+ if (!decl.initializer || !ts.isCallExpression(decl.initializer)) return undefined;
43
+ return calleeName(decl.initializer) === "EffectReceipt" ? decl.initializer : undefined;
44
+ }
45
+
46
+ /**
47
+ * Every variable name bound to an effect receipt in this file: direct
48
+ * `EffectReceipt(...)` initializers plus identifier aliases, resolved to a
49
+ * fixpoint so declaration order does not matter. Scope-naive by design, the
50
+ * same trade COR011 makes — a chant source file is flat declarations.
51
+ */
52
+ export function collectReceiptVariables(sourceFile: ts.SourceFile): Set<string> {
53
+ const receipts = new Set<string>();
54
+ const aliases: Array<[string, string]> = [];
55
+
56
+ function visit(node: ts.Node): void {
57
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
58
+ if (receiptFactoryCall(node)) {
59
+ receipts.add(node.name.text);
60
+ } else if (ts.isIdentifier(node.initializer)) {
61
+ aliases.push([node.name.text, node.initializer.text]);
62
+ }
63
+ }
64
+ ts.forEachChild(node, visit);
65
+ }
66
+ visit(sourceFile);
67
+
68
+ let grew = true;
69
+ while (grew) {
70
+ grew = false;
71
+ for (const [name, source] of aliases) {
72
+ if (receipts.has(source) && !receipts.has(name)) {
73
+ receipts.add(name);
74
+ grew = true;
75
+ }
76
+ }
77
+ }
78
+ return receipts;
79
+ }
80
+
81
+ /** The accessed attribute's name, for the message: `r.something` → "something". */
82
+ function accessedAttribute(node: ts.PropertyAccessExpression | ts.ElementAccessExpression): string {
83
+ if (ts.isPropertyAccessExpression(node)) return node.name.text;
84
+ const arg = node.argumentExpression;
85
+ return ts.isStringLiteralLike(arg) ? arg.text : "<computed>";
86
+ }
87
+
88
+ export const cor022ReceiptLeafRule: LintRule = {
89
+ id: "COR022",
90
+ severity: "error",
91
+ category: "correctness",
92
+ description:
93
+ "An effect receipt is a leaf — nothing may reference its attributes; the effect() step is the receipt's sole writer",
94
+ check(context: LintContext): LintDiagnostic[] {
95
+ const receipts = collectReceiptVariables(context.sourceFile);
96
+ if (receipts.size === 0) return [];
97
+
98
+ const diagnostics: LintDiagnostic[] = [];
99
+
100
+ function visit(node: ts.Node): void {
101
+ if (
102
+ (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) &&
103
+ ts.isIdentifier(node.expression) &&
104
+ receipts.has(node.expression.text)
105
+ ) {
106
+ const receipt = node.expression.text;
107
+ const attribute = accessedAttribute(node);
108
+ const { line, character } = context.sourceFile.getLineAndCharacterOfPosition(
109
+ node.getStart(context.sourceFile),
110
+ );
111
+ diagnostics.push({
112
+ file: context.filePath,
113
+ line: line + 1,
114
+ column: character + 1,
115
+ ruleId: "COR022",
116
+ severity: "error",
117
+ message:
118
+ `"${receipt}" is an effect receipt — a receipt is a leaf, and nothing may reference its ` +
119
+ `attributes ("${attribute}" here). The effect() step is the receipt's sole writer, so a ` +
120
+ `property derived from it couples this resource to a value only the effect controls. ` +
121
+ `Reference the effect's inputs (or the resources they come from) directly instead.`,
122
+ });
123
+ }
124
+ ts.forEachChild(node, visit);
125
+ }
126
+ visit(context.sourceFile);
127
+
128
+ return diagnostics;
129
+ },
130
+ };
@@ -0,0 +1,121 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import * as ts from "typescript";
3
+ import { cor024ReceiptSecretPointerRule } from "./cor024-receipt-secret-pointer";
4
+ import type { LintContext } from "../rule";
5
+
6
+ function createContext(code: string, filePath = "test.ts"): LintContext {
7
+ const sourceFile = ts.createSourceFile(filePath, code, ts.ScriptTarget.Latest, true);
8
+ return { sourceFile, entities: [], filePath, lexicon: undefined };
9
+ }
10
+
11
+ describe("COR024: receipt inputs reference secrets by pointer (#1833)", () => {
12
+ test("rule metadata", () => {
13
+ expect(cor024ReceiptSecretPointerRule.id).toBe("COR024");
14
+ expect(cor024ReceiptSecretPointerRule.severity).toBe("error");
15
+ expect(cor024ReceiptSecretPointerRule.category).toBe("correctness");
16
+ });
17
+
18
+ test("flags a receipt input reading a Secret entity's data attribute", () => {
19
+ const ctx = createContext(`
20
+ export const dbSecret = new SecretManagerSecret({ name: "db-password" });
21
+ export const seeded = EffectReceipt("seeded", {
22
+ effect: "db-seed",
23
+ flavor: "hash",
24
+ inputs: { password: dbSecret.data.password },
25
+ });
26
+ `);
27
+ const diags = cor024ReceiptSecretPointerRule.check(ctx);
28
+ expect(diags).toHaveLength(1);
29
+ expect(diags[0].ruleId).toBe("COR024");
30
+ expect(diags[0].severity).toBe("error");
31
+ expect(diags[0].message).toContain("dbSecret.data.password");
32
+ expect(diags[0].message).toContain("name+version pointer");
33
+ expect(diags[0].message).toContain("secretVersion");
34
+ });
35
+
36
+ test("flags a .value read on a Secret entity", () => {
37
+ const ctx = createContext(`
38
+ const apiToken = new ExternalSecret({ name: "api-token" });
39
+ export const bootstrapped = EffectReceipt("bootstrapped", {
40
+ effect: "bootstrap",
41
+ flavor: "hash",
42
+ inputs: { token: apiToken.value },
43
+ });
44
+ `);
45
+ const diags = cor024ReceiptSecretPointerRule.check(ctx);
46
+ expect(diags).toHaveLength(1);
47
+ expect(diags[0].message).toContain("apiToken.value");
48
+ });
49
+
50
+ test("const indirection fires: material extracted into a variable first", () => {
51
+ const ctx = createContext(`
52
+ const dbSecret = new SecretManagerSecret({ name: "db-password" });
53
+ const material = dbSecret.data;
54
+ export const seeded = EffectReceipt("seeded", {
55
+ effect: "db-seed",
56
+ flavor: "hash",
57
+ inputs: { password: material.password },
58
+ });
59
+ `);
60
+ const diags = cor024ReceiptSecretPointerRule.check(ctx);
61
+ expect(diags).toHaveLength(1);
62
+ expect(diags[0].message).toContain("material");
63
+ });
64
+
65
+ test("bare material variable as an input fires", () => {
66
+ const ctx = createContext(`
67
+ const dbSecret = new SecretManagerSecret({ name: "db-password" });
68
+ const material = dbSecret.stringData;
69
+ export const seeded = EffectReceipt("seeded", {
70
+ effect: "db-seed",
71
+ flavor: "hash",
72
+ inputs: { blob: material },
73
+ });
74
+ `);
75
+ expect(cor024ReceiptSecretPointerRule.check(ctx)).toHaveLength(1);
76
+ });
77
+
78
+ test("passes: the name+version pointer form", () => {
79
+ const ctx = createContext(`
80
+ export const dbSecret = new SecretManagerSecret({ name: "db-password" });
81
+ export const seeded = EffectReceipt("seeded", {
82
+ effect: "db-seed",
83
+ flavor: "hash",
84
+ inputs: { secretName: dbSecret.name, secretVersion: 3 },
85
+ });
86
+ `);
87
+ expect(cor024ReceiptSecretPointerRule.check(ctx)).toHaveLength(0);
88
+ });
89
+
90
+ test("passes: non-secret entity attributes and static inputs", () => {
91
+ const ctx = createContext(`
92
+ export const db = new Database({ name: "main" });
93
+ export const seeded = EffectReceipt("seeded", {
94
+ effect: "db-seed",
95
+ flavor: "hash",
96
+ inputs: { endpoint: db.endpoint, schema: "v3", value: db.port },
97
+ });
98
+ `);
99
+ expect(cor024ReceiptSecretPointerRule.check(ctx)).toHaveLength(0);
100
+ });
101
+
102
+ test("passes: secret material read OUTSIDE a receipt's inputs is not this rule's concern", () => {
103
+ const ctx = createContext(`
104
+ const dbSecret = new SecretManagerSecret({ name: "db-password" });
105
+ export const app = new Service({ env: { DB_PASSWORD: dbSecret.data.password } });
106
+ `);
107
+ expect(cor024ReceiptSecretPointerRule.check(ctx)).toHaveLength(0);
108
+ });
109
+
110
+ test("declareSecret provenance declarations are secret-rooted", () => {
111
+ const ctx = createContext(`
112
+ const dbSecret = declareSecret("db-password", { provenance: "referenced" });
113
+ export const seeded = EffectReceipt("seeded", {
114
+ effect: "db-seed",
115
+ flavor: "hash",
116
+ inputs: { v: dbSecret.value },
117
+ });
118
+ `);
119
+ expect(cor024ReceiptSecretPointerRule.check(ctx)).toHaveLength(1);
120
+ });
121
+ });
@@ -0,0 +1,218 @@
1
+ import * as ts from "typescript";
2
+ import type { LintRule, LintContext, LintDiagnostic } from "../rule";
3
+ import { receiptFactoryCall } from "./cor022-receipt-leaf";
4
+
5
+ /**
6
+ * COR024: Receipt Inputs Reference Secrets by Pointer (#1833, epic #1703;
7
+ * composes with the secret-provenance work, #1365/#1828)
8
+ *
9
+ * A receipt's `inputs` feed its expectation hash (../../effect-receipt.ts) —
10
+ * whatever lands there is canonicalized, digested, and compared against a
11
+ * stored value for the rest of the receipt's life. chant's constitutional
12
+ * line on secrets is that no code path may hold, log, hash, or compare a
13
+ * secret value (../../secret-provenance.ts), so a receipt input that reads a
14
+ * Secret-kind entity's material (`.data`, `.value`, `.stringData`, ...)
15
+ * violates it twice over: the material enters the hash, and the serialized
16
+ * placeholder ties the receipt to the secret's value rather than its
17
+ * identity.
18
+ *
19
+ * The pointer form is fine — and is the point: reference the secret by name
20
+ * and version (`{ secretName: dbSecret.name, secretVersion: 3 }`), so a
21
+ * rotation is an explicit version bump that re-proposes the effect, and the
22
+ * hash only ever sees the pointer.
23
+ *
24
+ * Recognition is the same file-local reference walk COR022 uses (const
25
+ * indirection included): a variable is Secret-kind when its initializer is a
26
+ * constructor or factory whose name says so (`new SecretManagerSecret(...)`,
27
+ * `new ExternalSecret(...)`, `declareSecret(...)`); an input fires when a
28
+ * property chain rooted at one of those crosses a material field.
29
+ */
30
+
31
+ /** Fields that carry secret material. Mirrors secret-provenance.ts's
32
+ * FORBIDDEN_MATERIAL_FIELDS, plus the provider-attribute spellings
33
+ * (`secretString`/`secretValue`) a materialized secret exposes. */
34
+ const MATERIAL_FIELDS = new Set([
35
+ "value",
36
+ "data",
37
+ "stringdata",
38
+ "material",
39
+ "plaintext",
40
+ "ciphertext",
41
+ "secretstring",
42
+ "secretvalue",
43
+ ]);
44
+
45
+ function isMaterialField(name: string): boolean {
46
+ return MATERIAL_FIELDS.has(name.toLowerCase());
47
+ }
48
+
49
+ /** Simple name of a `new X(...)` / `X(...)` / `ns.X(...)` initializer. */
50
+ function initializerName(init: ts.Expression): string | undefined {
51
+ if (!ts.isNewExpression(init) && !ts.isCallExpression(init)) return undefined;
52
+ const callee = init.expression;
53
+ if (ts.isIdentifier(callee)) return callee.text;
54
+ if (ts.isPropertyAccessExpression(callee)) return callee.name.text;
55
+ return undefined;
56
+ }
57
+
58
+ /** Does this constructor/factory name declare a Secret-kind entity? */
59
+ function isSecretKindName(name: string): boolean {
60
+ return /secret/i.test(name) || name === "declareSecret";
61
+ }
62
+
63
+ /** One access chain: `sec.data.password` → root "sec", segments ["data","password"]. */
64
+ function decomposeChain(
65
+ node: ts.Expression,
66
+ ): { root: string; segments: string[] } | undefined {
67
+ const segments: string[] = [];
68
+ let current: ts.Expression = node;
69
+ while (ts.isPropertyAccessExpression(current) || ts.isElementAccessExpression(current)) {
70
+ if (ts.isPropertyAccessExpression(current)) {
71
+ segments.unshift(current.name.text);
72
+ } else {
73
+ const arg = current.argumentExpression;
74
+ segments.unshift(ts.isStringLiteralLike(arg) ? arg.text : "<computed>");
75
+ }
76
+ current = current.expression;
77
+ }
78
+ if (!ts.isIdentifier(current)) return undefined;
79
+ return { root: current.text, segments };
80
+ }
81
+
82
+ interface SecretVars {
83
+ /** Variables holding a Secret-kind entity (or an alias of one). */
84
+ secretRooted: Set<string>;
85
+ /** Variables that already crossed a material field (`const m = sec.data`). */
86
+ material: Set<string>;
87
+ }
88
+
89
+ /** File-local Secret-kind variables, aliases and material extractions, to a fixpoint. */
90
+ function collectSecretVariables(sourceFile: ts.SourceFile): SecretVars {
91
+ const secretRooted = new Set<string>();
92
+ const material = new Set<string>();
93
+ const derived: Array<{ name: string; root: string; crossesMaterial: boolean; isBareAlias: boolean }> = [];
94
+
95
+ function visit(node: ts.Node): void {
96
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
97
+ const init = node.initializer;
98
+ const ctorName = initializerName(init);
99
+ if (ctorName !== undefined && isSecretKindName(ctorName)) {
100
+ secretRooted.add(node.name.text);
101
+ } else if (ts.isIdentifier(init)) {
102
+ derived.push({ name: node.name.text, root: init.text, crossesMaterial: false, isBareAlias: true });
103
+ } else if (ts.isPropertyAccessExpression(init) || ts.isElementAccessExpression(init)) {
104
+ const chain = decomposeChain(init);
105
+ if (chain) {
106
+ derived.push({
107
+ name: node.name.text,
108
+ root: chain.root,
109
+ crossesMaterial: chain.segments.some(isMaterialField),
110
+ isBareAlias: false,
111
+ });
112
+ }
113
+ }
114
+ }
115
+ ts.forEachChild(node, visit);
116
+ }
117
+ visit(sourceFile);
118
+
119
+ let grew = true;
120
+ while (grew) {
121
+ grew = false;
122
+ for (const d of derived) {
123
+ const rootIsSecret = secretRooted.has(d.root);
124
+ const rootIsMaterial = material.has(d.root);
125
+ if (!rootIsSecret && !rootIsMaterial) continue;
126
+ const target = rootIsMaterial || d.crossesMaterial ? material : secretRooted;
127
+ if (d.isBareAlias && !d.crossesMaterial && rootIsSecret) {
128
+ if (!secretRooted.has(d.name)) {
129
+ secretRooted.add(d.name);
130
+ grew = true;
131
+ }
132
+ } else if (!target.has(d.name)) {
133
+ target.add(d.name);
134
+ grew = true;
135
+ }
136
+ }
137
+ }
138
+ return { secretRooted, material };
139
+ }
140
+
141
+ export const cor024ReceiptSecretPointerRule: LintRule = {
142
+ id: "COR024",
143
+ severity: "error",
144
+ category: "correctness",
145
+ description:
146
+ "A receipt's inputs may reference a secret only by name+version pointer, never by value — secret material must not enter the expectation hash",
147
+ check(context: LintContext): LintDiagnostic[] {
148
+ const diagnostics: LintDiagnostic[] = [];
149
+ const secrets = collectSecretVariables(context.sourceFile);
150
+ if (secrets.secretRooted.size === 0 && secrets.material.size === 0) return diagnostics;
151
+
152
+ const report = (node: ts.Node, rendered: string, root: string): void => {
153
+ const { line, character } = context.sourceFile.getLineAndCharacterOfPosition(
154
+ node.getStart(context.sourceFile),
155
+ );
156
+ diagnostics.push({
157
+ file: context.filePath,
158
+ line: line + 1,
159
+ column: character + 1,
160
+ ruleId: "COR024",
161
+ severity: "error",
162
+ message:
163
+ `Receipt input reads secret material (${rendered}) from "${root}" — a receipt's inputs may ` +
164
+ `reference a secret only by name+version pointer, never by value: the inputs are hashed into ` +
165
+ `the receipt's expectation, and no chant code path may hash or compare secret material. ` +
166
+ `Use the pointer form instead, e.g. { secretName: ${root}.name, secretVersion: 3 } — ` +
167
+ `a rotation then re-proposes the effect through an explicit version bump.`,
168
+ });
169
+ };
170
+
171
+ const checkInputs = (inputs: ts.Expression): void => {
172
+ const visit = (node: ts.Node): void => {
173
+ if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) {
174
+ const chain = decomposeChain(node);
175
+ if (chain) {
176
+ const rootMaterial = secrets.material.has(chain.root);
177
+ const crossesMaterial = chain.segments.some(isMaterialField);
178
+ if (rootMaterial || (secrets.secretRooted.has(chain.root) && crossesMaterial)) {
179
+ report(node, `\`${chain.root}.${chain.segments.join(".")}\``, chain.root);
180
+ return; // one diagnostic per chain, not one per link
181
+ }
182
+ }
183
+ } else if (ts.isIdentifier(node) && secrets.material.has(node.text)) {
184
+ const parent = node.parent;
185
+ const isChainBase =
186
+ (ts.isPropertyAccessExpression(parent) || ts.isElementAccessExpression(parent)) &&
187
+ parent.expression === node;
188
+ const isKey = ts.isPropertyAssignment(parent) && parent.name === node;
189
+ if (!isChainBase && !isKey) report(node, `\`${node.text}\``, node.text);
190
+ }
191
+ ts.forEachChild(node, visit);
192
+ };
193
+ visit(inputs);
194
+ };
195
+
196
+ const walk = (node: ts.Node): void => {
197
+ if (ts.isVariableDeclaration(node)) {
198
+ const call = receiptFactoryCall(node);
199
+ const options = call?.arguments[1];
200
+ if (options && ts.isObjectLiteralExpression(options)) {
201
+ for (const prop of options.properties) {
202
+ if (
203
+ ts.isPropertyAssignment(prop) &&
204
+ (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) &&
205
+ prop.name.text === "inputs"
206
+ ) {
207
+ checkInputs(prop.initializer);
208
+ }
209
+ }
210
+ }
211
+ }
212
+ ts.forEachChild(node, walk);
213
+ };
214
+ walk(context.sourceFile);
215
+
216
+ return diagnostics;
217
+ },
218
+ };
@@ -90,11 +90,16 @@ describe("EVL001: non-literal-expression", () => {
90
90
  expect(diags[0].message).toContain("statically evaluable");
91
91
  });
92
92
 
93
- test("flags method call", () => {
93
+ // chant #1966 — `fold()` widened to fold a method call when its receiver
94
+ // resolves (`github.actor.toString()`, `[...].join(",")`); `findSubsetViolation`
95
+ // (./subset.ts) accepts the SHAPE unconditionally, the same way it already
96
+ // does for `.step` and a nested `new` — whether the receiver actually
97
+ // resolves is resolution-dependent and out of reach here, so a method call
98
+ // no longer trips this rule on shape alone.
99
+ test("does not flag a method call — resolution-dependent, out of scope for a syntax-only rule (chant #1966)", () => {
94
100
  const ctx = createContext(`new Bucket({ name: config.getName() });`);
95
101
  const diags = evl001NonLiteralExpressionRule.check(ctx);
96
- expect(diags).toHaveLength(1);
97
- expect(diags[0].ruleId).toBe("EVL001");
102
+ expect(diags).toHaveLength(0);
98
103
  });
99
104
 
100
105
  test("flags await expression", () => {
@@ -104,6 +109,33 @@ describe("EVL001: non-literal-expression", () => {
104
109
  expect(diags[0].ruleId).toBe("EVL001");
105
110
  });
106
111
 
112
+ // chant #1544 — the single-action Composite() wrapper idiom
113
+ // (`Checkout({...}).step`) every lexicon's own docs/examples embed inline
114
+ // inside a `Job`'s `steps:` array must not trip EVL001.
115
+ test("allows a composite call's .step access embedded in an array", () => {
116
+ const ctx = createContext(`new Job({ "runs-on": "ubuntu-latest", steps: [Checkout({}).step] });`);
117
+ const diags = evl001NonLiteralExpressionRule.check(ctx);
118
+ expect(diags).toHaveLength(0);
119
+ });
120
+
121
+ test("allows a composite call's .step access as a direct property value", () => {
122
+ const ctx = createContext(`new Job({ step: Checkout({ ref: "main" }).step });`);
123
+ const diags = evl001NonLiteralExpressionRule.check(ctx);
124
+ expect(diags).toHaveLength(0);
125
+ });
126
+
127
+ test("still flags a call accessing a non-.step property", () => {
128
+ const ctx = createContext(`new Job({ steps: [Checkout({}).output] });`);
129
+ const diags = evl001NonLiteralExpressionRule.check(ctx);
130
+ expect(diags).toHaveLength(1);
131
+ });
132
+
133
+ test("still flags a bare call (no trailing .step access)", () => {
134
+ const ctx = createContext(`new Bucket({ name: getName() });`);
135
+ const diags = evl001NonLiteralExpressionRule.check(ctx);
136
+ expect(diags).toHaveLength(1);
137
+ });
138
+
107
139
  test("flags multiple violations", () => {
108
140
  const ctx = createContext(`new Bucket({ a: foo(), b: bar() });`);
109
141
  const diags = evl001NonLiteralExpressionRule.check(ctx);
@@ -31,6 +31,13 @@ function checkNode(node: ts.Node, context: LintContext, diagnostics: LintDiagnos
31
31
  const firstArg = node.arguments[0];
32
32
  if (ts.isObjectLiteralExpression(firstArg)) {
33
33
  for (const prop of firstArg.properties) {
34
+ // `Checkout({...}).step` — the single-action Composite()-wrapper
35
+ // idiom every lexicon's own docs/examples embed inline inside a
36
+ // Job's `steps:` array — is shape-valid unconditionally (chant
37
+ // #1544 introduced this as an EVL-only opt-in while `fold()`
38
+ // still rejected the shape; chant #1174 closed that divergence by
39
+ // making `fold()` accept it too, so `findSubsetViolation` no
40
+ // longer needs a flag to say so).
34
41
  const violation = checkObjectMember(prop, context.intrinsics);
35
42
  if (violation) {
36
43
  const { line, character } = context.sourceFile.getLineAndCharacterOfPosition(
@@ -26,6 +26,8 @@ export { evl010CompositeNoTransformRule } from "./evl010-composite-no-transform"
26
26
  export { cor017CompositeNameMatchRule } from "./cor017-composite-name-match";
27
27
  export { cor018CompositePreferLexiconTypeRule } from "./cor018-composite-prefer-lexicon-type";
28
28
  export { cor021EnvLiteralNameRule } from "./cor021-env-literal-name";
29
+ export { cor022ReceiptLeafRule, collectReceiptVariables, receiptFactoryCall } from "./cor022-receipt-leaf";
30
+ export { cor024ReceiptSecretPointerRule } from "./cor024-receipt-secret-pointer";
29
31
  export { isInsideCompositeFactory } from "./composite-scope";
30
32
 
31
33
  import { flatDeclarationsRule } from "./flat-declarations";
@@ -50,9 +52,11 @@ import { evl010CompositeNoTransformRule } from "./evl010-composite-no-transform"
50
52
  import { cor017CompositeNameMatchRule } from "./cor017-composite-name-match";
51
53
  import { cor018CompositePreferLexiconTypeRule } from "./cor018-composite-prefer-lexicon-type";
52
54
  import { cor021EnvLiteralNameRule } from "./cor021-env-literal-name";
55
+ import { cor022ReceiptLeafRule } from "./cor022-receipt-leaf";
56
+ import { cor024ReceiptSecretPointerRule } from "./cor024-receipt-secret-pointer";
53
57
 
54
58
  /**
55
- * Load all 22 core lint rules (COR + EVL).
59
+ * Load all 24 core lint rules (COR + EVL).
56
60
  */
57
61
  export function loadCoreRules(): LintRule[] {
58
62
  return [
@@ -78,5 +82,7 @@ export function loadCoreRules(): LintRule[] {
78
82
  cor017CompositeNameMatchRule,
79
83
  cor018CompositePreferLexiconTypeRule,
80
84
  cor021EnvLiteralNameRule,
85
+ cor022ReceiptLeafRule,
86
+ cor024ReceiptSecretPointerRule,
81
87
  ];
82
88
  }