@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,153 @@
1
+ /**
2
+ * The pipeline-change gate class (#1569): changes to the pipeline itself
3
+ * route to a stricter gate.
4
+ *
5
+ * The honest concession in the PR-flow teardown (rubric-product-research,
6
+ * 06-objections §1): chant Ops have a `shell` step, so a governed plane is
7
+ * not automatically immune to "the change edits the machinery that applies
8
+ * changes." What chant *can* claim — and a plain PR flow structurally cannot
9
+ * — is that pipeline changes are distinguishable: a composition is data over
10
+ * a bounded verb registry, and the COMP rules already police the escape
11
+ * hatches (COMP006 — every `shell` step carries a declared reason;
12
+ * COMP005 — no noun-shaped escape hatches, `./rules/comp/`). What was missing
13
+ * was the routing: nothing classified a change as "touches the pipeline" and
14
+ * required a stricter gate for it, so a change to a composition rode through
15
+ * the same gate as a replica-count bump.
16
+ *
17
+ * This is the cheap first cut #1569 asks for: the registry-only predicate.
18
+ * A composition that contains only registry verbs is ordinary; a `shell`
19
+ * step (the deliberate escape hatch) or a change to the set of verbs a
20
+ * composition invokes trips the stricter class. comp005/comp006 already make
21
+ * both decidable from the text — this module reuses the same walk
22
+ * (`./rules/comp/support.ts`) rather than re-deriving it.
23
+ *
24
+ * Like #1568's `evaluateUnobservedGate`
25
+ * (../lifecycle/unobserved-gate.ts), this is a reusable predicate, not a
26
+ * runtime: classify a change and get back a verdict; the caller (a
27
+ * `lint.policies` check routed through `policyGate`, an Op step, #1487's
28
+ * gate-as-fact once it lands) decides what "stricter" means in its own
29
+ * context. The stricter class composes with #1487's principle that
30
+ * destructive verbs always wait for a person — pipeline changes are the
31
+ * second category that should never free-run, because they change what
32
+ * "gated" means for everything after them. That is why the default policy
33
+ * below is `"human-always"`, not `"stricter-gate"`.
34
+ */
35
+ import type { Component } from "../components/component";
36
+ import { STARTER_VERB_FAMILIES } from "../components/starter-plugin";
37
+ import { walkComponent } from "./rules/comp/support";
38
+
39
+ /**
40
+ * Fallback registry when no project registry is supplied: core's own starter
41
+ * verbs, the same fallback COMP005 uses for a direct unit test
42
+ * (`./rules/comp/comp005-capability-kind-is-noun.ts`). A real `chant lint` /
43
+ * gate run passes the project's resolved `knownKinds` (core's starter set
44
+ * plus the active lexicons' leaves) so a real project's cloud verbs are never
45
+ * mistaken for an out-of-registry escape hatch.
46
+ */
47
+ const CORE_STARTER_KINDS: ReadonlySet<string> = new Set(Object.values(STARTER_VERB_FAMILIES).flat());
48
+
49
+ /** Why a composition (or a change to one) tripped the pipeline-change class. */
50
+ export type PipelineChangeReason =
51
+ | { kind: "shell-step"; phaseName: string }
52
+ | { kind: "unregistered-verb"; stepKind: string; phaseName: string }
53
+ | { kind: "verb-set-changed"; added: string[]; removed: string[] };
54
+
55
+ export interface PipelineChangeClassification {
56
+ /** True when any reason below applies. */
57
+ pipelineChange: boolean;
58
+ reasons: PipelineChangeReason[];
59
+ }
60
+
61
+ /** Every non-gate step kind a component's composition invokes, across `deploy` and `rollback` (nested fan-out phases included, via `walkComponent`). `gate` steps are excluded — they are not a capability verb. */
62
+ export function componentVerbSet(component: Component): Set<string> {
63
+ const { steps } = walkComponent(component);
64
+ return new Set(steps.map((s) => s.step.kind));
65
+ }
66
+
67
+ /**
68
+ * Classify a single component's composition (the "cheap first cut", no
69
+ * before/after needed): a `shell` step trips the class unconditionally — it
70
+ * is the escape hatch by construction (COMP006) — and any step whose `kind`
71
+ * is outside `knownKinds` trips it too, since an unregistered verb is,
72
+ * structurally, the same kind of hole a `shell` step is: something the
73
+ * bounded registry does not account for.
74
+ */
75
+ export function classifyComponentPipelineChange(
76
+ component: Component,
77
+ opts?: { knownKinds?: ReadonlySet<string> },
78
+ ): PipelineChangeClassification {
79
+ const knownKinds = opts?.knownKinds ?? CORE_STARTER_KINDS;
80
+ const { steps } = walkComponent(component);
81
+
82
+ const reasons: PipelineChangeReason[] = [];
83
+ for (const { step, phaseName } of steps) {
84
+ if (step.kind === "shell") {
85
+ reasons.push({ kind: "shell-step", phaseName });
86
+ } else if (!knownKinds.has(step.kind)) {
87
+ reasons.push({ kind: "unregistered-verb", stepKind: step.kind, phaseName });
88
+ }
89
+ }
90
+
91
+ return { pipelineChange: reasons.length > 0, reasons };
92
+ }
93
+
94
+ /**
95
+ * Classify a *change* to a component's composition between two revisions
96
+ * (#1569's "add or modify a shell step ... or the composition graph
97
+ * itself"): on top of `classifyComponentPipelineChange`'s single-composition
98
+ * check on `after`, a change also trips the class when the verb set itself
99
+ * changed — a verb added or removed changes what the composition can invoke,
100
+ * which is the routing signal even when every individual verb, before and
101
+ * after, is registry-clean. `before: undefined` (a brand-new component) never
102
+ * reports a verb-set change — there is nothing to diff against, and the
103
+ * single-composition check already covers a new component's own shell/
104
+ * unregistered-verb findings.
105
+ */
106
+ export function classifyPipelineChange(
107
+ before: Component | undefined,
108
+ after: Component,
109
+ opts?: { knownKinds?: ReadonlySet<string> },
110
+ ): PipelineChangeClassification {
111
+ const base = classifyComponentPipelineChange(after, opts);
112
+ if (!before) return base;
113
+
114
+ const beforeVerbs = componentVerbSet(before);
115
+ const afterVerbs = componentVerbSet(after);
116
+ const added = [...afterVerbs].filter((v) => !beforeVerbs.has(v)).sort();
117
+ const removed = [...beforeVerbs].filter((v) => !afterVerbs.has(v)).sort();
118
+
119
+ if (added.length === 0 && removed.length === 0) return base;
120
+
121
+ return {
122
+ pipelineChange: true,
123
+ reasons: [...base.reasons, { kind: "verb-set-changed", added, removed }],
124
+ };
125
+ }
126
+
127
+ /**
128
+ * How the gate routes a classified pipeline change.
129
+ *
130
+ * - `"human-always"` (the recommended default — see this module's doc
131
+ * comment) — a pipeline change never free-runs, mirroring #1487's "a
132
+ * destructive verb always waits for a person."
133
+ * - `"stricter-gate"` — route to a named stricter gate class rather than
134
+ * requiring a person on every run; the caller supplies what "stricter"
135
+ * means (an additional approval, a narrower blast-radius budget, ...).
136
+ */
137
+ export type PipelineChangeGatePolicy = "stricter-gate" | "human-always";
138
+
139
+ export interface PipelineChangeGateVerdict extends PipelineChangeClassification {
140
+ /** What the policy says to do about it. `"none"` when the change is not a pipeline change at all. */
141
+ route: PipelineChangeGatePolicy | "none";
142
+ }
143
+
144
+ /** Evaluate a component composition change against a routing policy. Pure — no I/O. */
145
+ export function evaluatePipelineChangeGate(
146
+ before: Component | undefined,
147
+ after: Component,
148
+ policy: PipelineChangeGatePolicy = "human-always",
149
+ opts?: { knownKinds?: ReadonlySet<string> },
150
+ ): PipelineChangeGateVerdict {
151
+ const classification = classifyPipelineChange(before, after, opts);
152
+ return { ...classification, route: classification.pipelineChange ? policy : "none" };
153
+ }
@@ -145,3 +145,100 @@ describe("isPostSynthCheck", () => {
145
145
  // chant #1138 — `applyConfiguredSeverity` (the `lint.rules` severity-override
146
146
  // pass over `PostSynthDiagnostic`s) is tested in `./config.test.ts`, where the
147
147
  // function itself now lives — see `./config.ts`'s doc comment for why.
148
+
149
+ // ── chant #975 — ctx.docs: lazy, memoized, shared across every check ───────
150
+ describe("ctx.docs (chant #975)", () => {
151
+ /** A Map subclass that counts how many times it was iterated — the only
152
+ * way `parseOutputDocs` reads its outputs — so the tests below can prove
153
+ * "parsed once" without reaching into module internals. */
154
+ class CountingOutputs extends Map<string, string> {
155
+ iterations = 0;
156
+ [Symbol.iterator](): IterableIterator<[string, string]> {
157
+ this.iterations++;
158
+ return super[Symbol.iterator]();
159
+ }
160
+ }
161
+
162
+ test("is not computed until first accessed", () => {
163
+ const outputs = new CountingOutputs([["k8s", "kind: Namespace"]]);
164
+ const check: PostSynthCheck = {
165
+ id: "PS-DOCS-1",
166
+ description: "never touches ctx.docs",
167
+ check() {
168
+ return [];
169
+ },
170
+ };
171
+ runPostSynthChecks([check], createBuildResult({ outputs: outputs as never }));
172
+ expect(outputs.iterations).toBe(0);
173
+ });
174
+
175
+ test("is parsed exactly once even when read by multiple checks", () => {
176
+ const outputs = new CountingOutputs([
177
+ ["k8s", "apiVersion: v1\nkind: Namespace\nmetadata:\n name: ns-a"],
178
+ ]);
179
+ let seenByFirst: unknown;
180
+ let seenBySecond: unknown;
181
+ const checks: PostSynthCheck[] = [
182
+ {
183
+ id: "PS-DOCS-2A",
184
+ description: "reads ctx.docs once",
185
+ check(ctx) {
186
+ seenByFirst = ctx.docs;
187
+ return [];
188
+ },
189
+ },
190
+ {
191
+ id: "PS-DOCS-2B",
192
+ description: "reads ctx.docs again, and a second time in the same check",
193
+ check(ctx) {
194
+ seenBySecond = ctx.docs;
195
+ void ctx.docs; // a second read within the same check — still no reparse
196
+ return [];
197
+ },
198
+ },
199
+ ];
200
+ runPostSynthChecks(checks, createBuildResult({ outputs: outputs as never }));
201
+ expect(outputs.iterations).toBe(1);
202
+ // Every reader gets the exact same array instance, not an equal copy.
203
+ expect(seenByFirst).toBe(seenBySecond);
204
+ });
205
+
206
+ test("parses ctx.outputs into the expected OutputDoc shape", () => {
207
+ const outputs = new Map<string, string>([
208
+ ["k8s", "apiVersion: v1\nkind: Namespace\nmetadata:\n name: ns-a"],
209
+ ]);
210
+ const check: PostSynthCheck = {
211
+ id: "PS-DOCS-3",
212
+ description: "reads a manifest field off ctx.docs",
213
+ check(ctx) {
214
+ const doc = (ctx.docs ?? [])[0];
215
+ return [
216
+ {
217
+ checkId: "PS-DOCS-3",
218
+ severity: "info",
219
+ message: `kind=${(doc.value as { kind?: string }).kind}`,
220
+ },
221
+ ];
222
+ },
223
+ };
224
+ const diags = runPostSynthChecks([check], createBuildResult({ outputs: outputs as never }));
225
+ expect(diags[0].message).toBe("kind=Namespace");
226
+ });
227
+
228
+ test("two independent runPostSynthChecks calls each get their own cache", () => {
229
+ const outputsA = new CountingOutputs([["k8s", "kind: Namespace"]]);
230
+ const outputsB = new CountingOutputs([["k8s", "kind: Deployment"]]);
231
+ const reader: PostSynthCheck = {
232
+ id: "PS-DOCS-4",
233
+ description: "reads ctx.docs",
234
+ check(ctx) {
235
+ void ctx.docs;
236
+ return [];
237
+ },
238
+ };
239
+ runPostSynthChecks([reader], createBuildResult({ outputs: outputsA as never }));
240
+ runPostSynthChecks([reader], createBuildResult({ outputs: outputsB as never }));
241
+ expect(outputsA.iterations).toBe(1);
242
+ expect(outputsB.iterations).toBe(1);
243
+ });
244
+ });
@@ -1,6 +1,9 @@
1
1
  import type { Declarable } from "../declarable";
2
2
  import type { SerializerResult } from "../serializer";
3
3
  import type { Severity } from "./rule";
4
+ import { parseOutputDocs, type OutputDoc } from "./output-docs";
5
+
6
+ export { parseOutputDocs, pick, get, type OutputDoc } from "./output-docs";
4
7
 
5
8
  /**
6
9
  * Context provided to post-synthesis checks.
@@ -16,6 +19,25 @@ export interface PostSynthContext {
16
19
  * e.g. "no public buckets in prod". Undefined when no environment is set.
17
20
  */
18
21
  env?: string;
22
+ /**
23
+ * Parsed output documents (chant #975) — `ctx.outputs` run through
24
+ * `parseOutputDocs` once and cached. A lazy `readonly` getter, not a plain
25
+ * field: computed on first access and shared across every check in the
26
+ * run, so a check that only reads `entities` pays nothing, and no two
27
+ * checks re-parse the same YAML/JSON. See `./output-docs.ts`.
28
+ *
29
+ * Optional at the type level — NOT because it can be absent from a real
30
+ * build. Every context chant itself constructs (`runPostSynthChecks`
31
+ * below, `@intentius/chant-test-utils`'s `createPostSynthContext` and
32
+ * `makePostSynthCtx*`) wires it up via `createDocsAccessor` and it is
33
+ * always present there. It is typed optional only so the many lexicon
34
+ * tests that build a `PostSynthContext` object literal by hand (predating
35
+ * this field) keep compiling unchanged, per this issue's own "existing
36
+ * checks compile unchanged" constraint — a new check that wants `ctx.docs`
37
+ * should still get a real array from every context chant builds; guard
38
+ * with `ctx.docs ?? []` only when a context's provenance is unknown.
39
+ */
40
+ readonly docs?: OutputDoc[];
19
41
  /** Raw build result object */
20
42
  buildResult: {
21
43
  outputs: Map<string, string | SerializerResult>;
@@ -26,6 +48,25 @@ export interface PostSynthContext {
26
48
  };
27
49
  }
28
50
 
51
+ /**
52
+ * Build the lazy, memoized `docs` accessor shared by `runPostSynthChecks`
53
+ * (below) and `@intentius/chant-test-utils`'s `createPostSynthContext` — the
54
+ * two places a `PostSynthContext` gets constructed. Returns a zero-arg
55
+ * function suitable for a `get docs()` object-literal accessor; the first
56
+ * call parses, every later call returns the same cached array.
57
+ */
58
+ export function createDocsAccessor(
59
+ outputs: Map<string, string | SerializerResult>,
60
+ ): () => OutputDoc[] {
61
+ let cached: OutputDoc[] | undefined;
62
+ return () => {
63
+ if (cached === undefined) {
64
+ cached = parseOutputDocs(outputs);
65
+ }
66
+ return cached;
67
+ };
68
+ }
69
+
29
70
  /**
30
71
  * Extract the primary content string from a serializer output.
31
72
  */
@@ -33,6 +74,21 @@ export function getPrimaryOutput(output: string | SerializerResult): string {
33
74
  return typeof output === "string" ? output : output.primary;
34
75
  }
35
76
 
77
+ /**
78
+ * Extract the ADDITIONAL files from a serializer output — everything
79
+ * {@link getPrimaryOutput} discards.
80
+ *
81
+ * Every post-synth check shipped before this one reads the primary output
82
+ * only, which means a sidecar file (a nested stack template, committed SOPS
83
+ * ciphertext) is invisible to all of them. That is the right default — the
84
+ * primary output is what appliers read — but a rule ABOUT a sidecar has to
85
+ * be able to see it, and `PostSynthContext.outputs` has carried the data all
86
+ * along. WK8504 (k8s) is the first caller.
87
+ */
88
+ export function getAdditionalFiles(output: string | SerializerResult): Record<string, string> {
89
+ return typeof output === "string" ? {} : (output.files ?? {});
90
+ }
91
+
36
92
  /**
37
93
  * A diagnostic from a post-synthesis check.
38
94
  *
@@ -100,11 +156,15 @@ export function runPostSynthChecks(
100
156
  buildResult: PostSynthContext["buildResult"],
101
157
  env?: string,
102
158
  ): PostSynthDiagnostic[] {
159
+ const getDocs = createDocsAccessor(buildResult.outputs);
103
160
  const ctx: PostSynthContext = {
104
161
  outputs: buildResult.outputs,
105
162
  entities: buildResult.entities,
106
163
  env,
107
164
  buildResult,
165
+ get docs(): OutputDoc[] {
166
+ return getDocs();
167
+ },
108
168
  };
109
169
 
110
170
  const diagnostics: PostSynthDiagnostic[] = [];
@@ -0,0 +1,101 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { coreReceiptChecks, RECEIPT_PLAIN_STORE_CHECK_ID } from "./receipt-checks";
3
+ import { runPostSynthChecks } from "./post-synth";
4
+ import { EffectReceipt, EFFECT_RECEIPT_MARKER } from "../effect-receipt";
5
+ import { DECLARABLE_MARKER, type Declarable } from "../declarable";
6
+
7
+ /** A lexicon-materialized receipt row: its own entityType, the receipt marker. */
8
+ function materializedReceipt(opts: {
9
+ lexicon: string;
10
+ entityType: string;
11
+ props?: Record<string, unknown>;
12
+ }): Declarable {
13
+ return {
14
+ [DECLARABLE_MARKER]: true,
15
+ [EFFECT_RECEIPT_MARKER]: true,
16
+ lexicon: opts.lexicon,
17
+ entityType: opts.entityType,
18
+ ...(opts.props ? { props: opts.props } : {}),
19
+ } as unknown as Declarable;
20
+ }
21
+
22
+ function run(entities: Map<string, Declarable>) {
23
+ return runPostSynthChecks(coreReceiptChecks(), {
24
+ outputs: new Map(),
25
+ entities,
26
+ warnings: [],
27
+ errors: [],
28
+ sourceFileCount: 1,
29
+ });
30
+ }
31
+
32
+ describe("COR023: receipt materializes into a plain store (#1833)", () => {
33
+ test("check id", () => {
34
+ expect(RECEIPT_PLAIN_STORE_CHECK_ID).toBe("COR023");
35
+ expect(coreReceiptChecks().map((c) => c.id)).toContain("COR023");
36
+ });
37
+
38
+ test("fires when a receipt is materialized into a Secret kind", () => {
39
+ const entities = new Map<string, Declarable>([
40
+ ["seededReceipt", materializedReceipt({ lexicon: "k8s", entityType: "K8s::Core::Secret" })],
41
+ ]);
42
+ const diags = run(entities);
43
+ expect(diags).toHaveLength(1);
44
+ expect(diags[0].checkId).toBe("COR023");
45
+ expect(diags[0].severity).toBe("error");
46
+ expect(diags[0].entity).toBe("seededReceipt");
47
+ expect(diags[0].message).toContain("K8s::Core::Secret");
48
+ expect(diags[0].message).toContain("plain store");
49
+ });
50
+
51
+ test("fires when a plain kind selects a secret-capable variant (SSM SecureString)", () => {
52
+ const entities = new Map<string, Declarable>([
53
+ [
54
+ "migratedReceipt",
55
+ materializedReceipt({
56
+ lexicon: "aws",
57
+ entityType: "AWS::SSM::Parameter",
58
+ props: { Name: "/receipts/migrated", Type: "SecureString" },
59
+ }),
60
+ ],
61
+ ]);
62
+ const diags = run(entities);
63
+ expect(diags).toHaveLength(1);
64
+ expect(diags[0].message).toContain('Type: "SecureString"');
65
+ expect(diags[0].entity).toBe("migratedReceipt");
66
+ });
67
+
68
+ test("passes: a receipt materialized into a plain store", () => {
69
+ const entities = new Map<string, Declarable>([
70
+ [
71
+ "migratedReceipt",
72
+ materializedReceipt({
73
+ lexicon: "aws",
74
+ entityType: "AWS::SSM::Parameter",
75
+ props: { Name: "/receipts/migrated", Type: "String" },
76
+ }),
77
+ ],
78
+ ]);
79
+ expect(run(entities)).toHaveLength(0);
80
+ });
81
+
82
+ test("passes: the core Chant::EffectReceipt declaration itself", () => {
83
+ const receipt = EffectReceipt("seeded", {
84
+ effect: "db-seed",
85
+ flavor: "hash",
86
+ inputs: { schema: "v3" },
87
+ });
88
+ const entities = new Map<string, Declarable>([["seeded", receipt]]);
89
+ expect(run(entities)).toHaveLength(0);
90
+ });
91
+
92
+ test("passes: a Secret-kind entity that is NOT a receipt is not this check's concern", () => {
93
+ const secret: Declarable = {
94
+ [DECLARABLE_MARKER]: true,
95
+ lexicon: "k8s",
96
+ entityType: "K8s::Core::Secret",
97
+ } as unknown as Declarable;
98
+ const entities = new Map<string, Declarable>([["dbSecret", secret]]);
99
+ expect(run(entities)).toHaveLength(0);
100
+ });
101
+ });
@@ -0,0 +1,93 @@
1
+ import type { PostSynthCheck, PostSynthDiagnostic } from "./post-synth";
2
+ import { isEffectReceipt } from "../effect-receipt";
3
+ import { isSensitiveKey } from "../deep-observation";
4
+ import { isResourceDeclarable } from "../declarable";
5
+
6
+ /**
7
+ * COR023: Receipt Materializes Into a Plain Store (#1833, epic #1703)
8
+ *
9
+ * A receipt's materialization target must be a plain store — never a
10
+ * SecureString, a Secret, or any secret-capable kind. A receipt value is a
11
+ * witness (an existence marker or a `sha256:` digest,
12
+ * ../effect-receipt.ts), not a secret: parking it in a secret-capable store
13
+ * invites masking, rotation, and access-control semantics that defeat the
14
+ * observe-and-compare loop the receipt exists for, and quietly teaches
15
+ * readers that the value is sensitive when it must never be.
16
+ *
17
+ * This is the honest core half of the guard. Core recognizes receipts
18
+ * lexicon-independently through the marker (#1831) but cannot know what any
19
+ * lexicon's kinds mean, so it checks the two signals it does have, both
20
+ * reusing the secret-kind knowledge core already carries
21
+ * (SENSITIVE_KEY_PATTERNS, ../deep-observation.ts):
22
+ *
23
+ * - the materialized `entityType` names a secret-capable kind
24
+ * (`K8s::Core::Secret`, `AWS::SecretsManager::Secret`, ...);
25
+ * - the declared props select a secret-capable variant of an otherwise
26
+ * plain kind (a `Type`/`Kind` prop whose value matches secret/secure —
27
+ * SSM's `Type: "SecureString"` is the canonical case).
28
+ *
29
+ * The concrete per-kind enforcement is each materialization row's job — the
30
+ * aws row (#1835) pins SSM to plain `String` at the source.
31
+ */
32
+
33
+ /** Prop names that select a store variant. */
34
+ const VARIANT_PROP = /^(type|kind)$/i;
35
+ /** Variant values that make the store secret-capable. */
36
+ const SECRET_VARIANT = /secret|secure/i;
37
+
38
+ export const RECEIPT_PLAIN_STORE_CHECK_ID = "COR023";
39
+
40
+ const receiptPlainStoreCheck: PostSynthCheck = {
41
+ id: RECEIPT_PLAIN_STORE_CHECK_ID,
42
+ description:
43
+ "An effect receipt's materialization target must be a plain store — never a secret-capable kind or variant",
44
+ check(ctx) {
45
+ const diagnostics: PostSynthDiagnostic[] = [];
46
+ for (const [name, entity] of ctx.entities) {
47
+ if (!isEffectReceipt(entity)) continue;
48
+
49
+ if (isSensitiveKey(entity.entityType)) {
50
+ diagnostics.push({
51
+ checkId: RECEIPT_PLAIN_STORE_CHECK_ID,
52
+ severity: "error",
53
+ entity: name,
54
+ lexicon: entity.lexicon,
55
+ message:
56
+ `Effect receipt "${name}" materializes into secret-capable kind "${entity.entityType}" — ` +
57
+ `a receipt's target must be a plain store. The receipt value is a witness ` +
58
+ `(an existence marker or a sha256 digest), not a secret; a secret-capable store adds ` +
59
+ `masking and rotation semantics that defeat the receipt's observe-and-compare loop.`,
60
+ });
61
+ continue;
62
+ }
63
+
64
+ if (isResourceDeclarable(entity) && typeof entity.props === "object" && entity.props !== null) {
65
+ for (const [key, value] of Object.entries(entity.props as Record<string, unknown>)) {
66
+ if (VARIANT_PROP.test(key) && typeof value === "string" && SECRET_VARIANT.test(value)) {
67
+ diagnostics.push({
68
+ checkId: RECEIPT_PLAIN_STORE_CHECK_ID,
69
+ severity: "error",
70
+ entity: name,
71
+ lexicon: entity.lexicon,
72
+ message:
73
+ `Effect receipt "${name}" (${entity.entityType}) declares ${key}: "${value}" — ` +
74
+ `a secret-capable store variant. A receipt's target must be a plain store; ` +
75
+ `use the plain variant (e.g. SSM Type: "String") so the witness value stays ` +
76
+ `readable to the observe-and-compare loop.`,
77
+ });
78
+ }
79
+ }
80
+ }
81
+ }
82
+ return diagnostics;
83
+ },
84
+ };
85
+
86
+ /**
87
+ * Core's own post-synth checks over effect receipts, run by `chant build`
88
+ * over the full build result (plugin checks are lexicon-scoped; receipts are
89
+ * recognized by marker, so this set is deliberately not).
90
+ */
91
+ export function coreReceiptChecks(): PostSynthCheck[] {
92
+ return [receiptPlainStoreCheck];
93
+ }
@@ -0,0 +1,26 @@
1
+ import type { Component } from "../../../../../../components/component";
2
+ import { phase } from "../../../../../../components/component";
3
+
4
+ /**
5
+ * COMP003 pass case (#1944): a bare "run-agent" step with no "noRollback"
6
+ * opt-out and no component-level "rollback" — must pass COMP003 because
7
+ * "run-agent"'s rollbackPolicy is "native" (#1941), not "needs-opt-out". This
8
+ * is the regression test proving the registry's declared rollbackPolicy for
9
+ * "run-agent" is wired correctly into ctx.rollbackPolicies, the same seam
10
+ * COMP005 uses for ctx.knownKinds — see comp.test.ts's FIXTURE_ROLLBACK_POLICIES.
11
+ */
12
+ export const agentTurn: Component = {
13
+ name: "agent-turn",
14
+ archetype: "infra",
15
+ dependsOn: [],
16
+ deploy: [
17
+ phase("Run", [
18
+ {
19
+ kind: "run-agent",
20
+ agent: "claude",
21
+ task: { prompt: "run the migration script and report the result" },
22
+ workspace: {},
23
+ },
24
+ ]),
25
+ ],
26
+ };
@@ -23,11 +23,14 @@ const checks = loadComponentChecks();
23
23
  /**
24
24
  * Rollback dispositions the COMP003 fixtures reference, standing in for what
25
25
  * `chant lint` derives from the project's registry (`cfn-deploy` has a native
26
- * `rollback`; `run-migration` declares `needs-opt-out` in the aws lexicon).
26
+ * `rollback`; `run-migration` declares `needs-opt-out` in the aws lexicon;
27
+ * `run-agent` declares `native` from phase 1 (#1941) — see
28
+ * `../../../components/verbs/run-agent.ts`'s `rollbackPolicy: "native"`).
27
29
  */
28
30
  const FIXTURE_ROLLBACK_POLICIES = new Map<string, RollbackPolicy>([
29
31
  ["cfn-deploy", "native"],
30
32
  ["run-migration", "needs-opt-out"],
33
+ ["run-agent", "native"],
31
34
  ]);
32
35
 
33
36
  async function lintFixture(
@@ -124,6 +127,16 @@ describe("COMP003: mutating-no-rollback", () => {
124
127
  expect(diagnostics.filter((d) => d.checkId === "COMP003")).toHaveLength(0);
125
128
  });
126
129
 
130
+ it("does not flag a bare run-agent step — its registry-declared rollbackPolicy is \"native\" (#1941), never needing a noRollback opt-out (#1944)", async () => {
131
+ // Regression test: proves the registry's rollbackPolicy for "run-agent"
132
+ // is wired correctly into ctx.rollbackPolicies (the same seam COMP005
133
+ // uses for ctx.knownKinds), not just that COMP003 happens to stay quiet.
134
+ // See ../../../components/verbs/run-agent.ts's "rollbackPolicy: 'native'".
135
+ const diagnostics = await lintFixture("comp003", "pass", { rollbackPolicies: FIXTURE_ROLLBACK_POLICIES });
136
+ const hits = diagnostics.filter((d) => d.checkId === "COMP003" && d.component === "agent-turn");
137
+ expect(hits).toHaveLength(0);
138
+ });
139
+
127
140
  it("flags a needs-opt-out step nested inside a fan-out phase, not just top-level phases", () => {
128
141
  // Regression test: the rule used to iterate component.deploy directly and
129
142
  // never recursed into nested Phase entries (a fan-out unit, e.g. the
@@ -163,6 +176,41 @@ describe("COMP003: mutating-no-rollback", () => {
163
176
  expect(diagnostics[0].message).toContain("run-migration");
164
177
  });
165
178
 
179
+ it("DOES flag a \"run-agent\"-kind step when a synthetic registry declares it needs-opt-out — proves the rule is conditional on the registry, not a hard-coded allowance for this one verb name (#1944)", () => {
180
+ // Companion negative case to the "does not flag a bare run-agent step"
181
+ // test above: same step kind, same shape, but with a synthetic
182
+ // rollbackPolicies map that declares "run-agent" as "needs-opt-out"
183
+ // instead of the real registry's "native". If this rule silently
184
+ // special-cased "run-agent" by name, this would incorrectly pass; it must
185
+ // flag exactly like any other needs-opt-out verb.
186
+ const [comp003] = checks.filter((c) => c.id === "COMP003");
187
+ const ctx = {
188
+ rollbackPolicies: new Map<string, RollbackPolicy>([["run-agent", "needs-opt-out"]]),
189
+ components: new Map([
190
+ [
191
+ "agent-turn",
192
+ {
193
+ component: {
194
+ name: "agent-turn",
195
+ dependsOn: [],
196
+ deploy: [
197
+ {
198
+ phase: "Run",
199
+ steps: [{ kind: "run-agent", agent: "claude", task: { prompt: "..." }, workspace: {} }],
200
+ },
201
+ ],
202
+ },
203
+ filePath: "agent-turn.component.ts",
204
+ },
205
+ ],
206
+ ]),
207
+ };
208
+ const diagnostics = comp003.check(ctx as never);
209
+ expect(diagnostics).toHaveLength(1);
210
+ expect(diagnostics[0].component).toBe("agent-turn");
211
+ expect(diagnostics[0].message).toContain("run-agent");
212
+ });
213
+
166
214
  it("a compensation sibling only counts within the same nested phase, not a same-named sibling phase elsewhere in the component", () => {
167
215
  // Two different "Node" phases both contain a run-migration step; only the
168
216
  // second one has a rollback-previous sibling. The first must still be