@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
@@ -122,6 +122,25 @@ describe("buildProvenanceStatement — SLSA in-toto statement from #614 provenan
122
122
  }),
123
123
  ).toThrow(SignTargetNotDigestError);
124
124
  });
125
+
126
+ it("reserved parameter keys win over caller-supplied collisions (#1943)", () => {
127
+ const statement = buildProvenanceStatement({
128
+ imageRef: DIGEST_REF,
129
+ provenance: { sourceRef: "abc123def", artifactDigest: DIGEST_REF.split("@")[1]! },
130
+ builderId: "builder",
131
+ externalParameters: { sourceRef: "spoofed", agent: "reviewer" },
132
+ internalParameters: { artifactDigest: "spoofed", spriteId: "s-1" },
133
+ });
134
+
135
+ expect(statement.predicate.buildDefinition.externalParameters).toEqual({
136
+ sourceRef: "abc123def",
137
+ agent: "reviewer",
138
+ });
139
+ expect(statement.predicate.buildDefinition.internalParameters).toEqual({
140
+ artifactDigest: DIGEST_REF.split("@")[1]!,
141
+ spriteId: "s-1",
142
+ });
143
+ });
125
144
  });
126
145
 
127
146
  describe("attest-provenance — keyless cosign attest (#622)", () => {
@@ -40,6 +40,19 @@
40
40
  * `requireTool` throws `ToolNotAvailableError` with an actionable message if
41
41
  * `cosign` is absent, and no test here ever spawns a real process or talks
42
42
  * to Rekor/Fulcio.
43
+ *
44
+ * **#1943 extension (`run-agent` interop).** `BuildProvenanceStatementInput`
45
+ * gained two optional merge fields, `externalParameters`/`internalParameters`
46
+ * — additive only, every existing call site's statement is byte-identical
47
+ * without them. `run-agent` (./run-agent.ts's `buildRunAgentProvenanceStatement`)
48
+ * is the first caller to use them, plus `buildType` (already overridable,
49
+ * unchanged here) set to a `run-agent`-specific recipe URI. `predicateType`
50
+ * stays `"https://slsa.dev/provenance/v1"` for every caller, `run-agent`
51
+ * included — see ./run-agent.ts's own doc comment ("Attestation interop")
52
+ * for why a turn is attested as an SLSA-shaped provenance predicate rather
53
+ * than under a minted `run-agent`-specific `predicateType`, and why that
54
+ * decision means `sign`/`attestProvenance`/`../verbs/verify.ts` needed zero
55
+ * changes to their `cosign` invocations to attest/verify a turn.
43
56
  */
44
57
 
45
58
  import type { Capability } from "../capability";
@@ -235,6 +248,23 @@ export interface BuildProvenanceStatementInput {
235
248
  finishedOn?: string;
236
249
  /** CI invocation/run id, when available (e.g. a GitHub Actions run id). */
237
250
  invocationId?: string;
251
+ /**
252
+ * Additional `buildDefinition.externalParameters` merged in alongside the
253
+ * `{ sourceRef }` default (#1943: `run-agent`'s provenance statement folds
254
+ * in the fountain-supplied *declared* facts — the agent selector today,
255
+ * more once fountain `Agent` resolution lands — without run-agent needing
256
+ * its own predicate builder). `sourceRef` is reserved by this function: a
257
+ * caller-supplied `sourceRef` is overridden, never signed; any other key
258
+ * name is the caller's choice.
259
+ */
260
+ externalParameters?: Record<string, unknown>;
261
+ /**
262
+ * Additional `buildDefinition.internalParameters` merged in alongside the
263
+ * `{ artifactDigest }` default (#1943: `run-agent` folds in what actually
264
+ * happened — sprite id, checkpoint id, turn status/exit code — the
265
+ * *imperative* facts a build's `internalParameters` are for).
266
+ */
267
+ internalParameters?: Record<string, unknown>;
238
268
  }
239
269
 
240
270
  const DEFAULT_BUILD_TYPE = "https://chant.dev/build-archive/v1";
@@ -263,8 +293,10 @@ export function buildProvenanceStatement(input: BuildProvenanceStatementInput):
263
293
  predicate: {
264
294
  buildDefinition: {
265
295
  buildType: input.buildType ?? DEFAULT_BUILD_TYPE,
266
- externalParameters: { sourceRef: input.provenance.sourceRef },
267
- internalParameters: { artifactDigest: input.provenance.artifactDigest },
296
+ // Reserved keys spread LAST so a caller-supplied key can never clobber
297
+ // them into a signed statement that disagrees with resolvedDependencies.
298
+ externalParameters: { ...input.externalParameters, sourceRef: input.provenance.sourceRef },
299
+ internalParameters: { ...input.internalParameters, artifactDigest: input.provenance.artifactDigest },
268
300
  resolvedDependencies: [{ uri: input.provenance.sourceRef, digest: { sha256: digest } }],
269
301
  },
270
302
  runDetails: {
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Tests `wrangler-deploy`/`wrangler-versions-promote` (#1293, epic #1296,
3
+ * ./wrangler.ts) against a `MockProcessRunner` — no live `wrangler`, no
4
+ * network call, ever. Asserts the constructed invocations, the version-id
5
+ * extraction, the wired output -> promote input handoff, and the
6
+ * captured-previous-version rollback (the "no hand-written compensation"
7
+ * claim from #1293's verification section).
8
+ */
9
+
10
+ import { describe, expect, it } from "vitest";
11
+ import {
12
+ createWranglerDeployCapability,
13
+ createWranglerVersionsPromoteCapability,
14
+ parseWranglerVersionId,
15
+ WranglerVersionIdNotFoundError,
16
+ } from "./wrangler";
17
+ import { ToolNotAvailableError } from "./process-runner";
18
+ import { createMockProcessRunner } from "./__tests__/mock-process-runner";
19
+
20
+ const ctx = { env: "prod", component: "api-worker" };
21
+ const VERSION_A = "07bcb198-9633-4172-a1f0-b09f6f1a1a11";
22
+ const VERSION_B = "aa11bb22-cc33-dd44-ee55-ff6677889900";
23
+
24
+ describe("wrangler-deploy", () => {
25
+ it("shells `wrangler deploy` with --config, extracts the Version ID from stdout", async () => {
26
+ const mock = createMockProcessRunner({
27
+ responses: { "wrangler deploy": `Uploaded api-worker (1.23 sec)\nVersion ID: ${VERSION_A}\n` },
28
+ });
29
+ const capability = createWranglerDeployCapability(mock.runner);
30
+
31
+ const output = await capability.run(ctx, { config: "api/wrangler.jsonc" });
32
+
33
+ expect(output).toEqual({ versionId: VERSION_A });
34
+ const deployCall = mock.calls.find((c) => c.command.startsWith("wrangler deploy"))!;
35
+ expect(deployCall.command).toBe(`wrangler deploy --config 'api/wrangler.jsonc'`);
36
+ });
37
+
38
+ it("passes --env through when supplied", async () => {
39
+ const mock = createMockProcessRunner({
40
+ responses: { "wrangler deploy": `Version ID: ${VERSION_A}\n` },
41
+ });
42
+ const capability = createWranglerDeployCapability(mock.runner);
43
+
44
+ await capability.run(ctx, { config: "api/wrangler.jsonc", env: "staging" });
45
+
46
+ const deployCall = mock.calls.find((c) => c.command.startsWith("wrangler deploy"))!;
47
+ expect(deployCall.command).toBe(`wrangler deploy --config 'api/wrangler.jsonc' --env 'staging'`);
48
+ });
49
+
50
+ it("throws WranglerVersionIdNotFoundError when stdout carries no Version ID", async () => {
51
+ const mock = createMockProcessRunner({ responses: { "wrangler deploy": "Uploaded, nothing else.\n" } });
52
+ const capability = createWranglerDeployCapability(mock.runner);
53
+
54
+ await expect(capability.run(ctx, { config: "api/wrangler.jsonc" })).rejects.toBeInstanceOf(
55
+ WranglerVersionIdNotFoundError,
56
+ );
57
+ });
58
+
59
+ it("throws ToolNotAvailableError when wrangler is absent, rather than silently no-op'ing", async () => {
60
+ const mock = createMockProcessRunner({ tools: { wrangler: false } });
61
+ const capability = createWranglerDeployCapability(mock.runner);
62
+
63
+ await expect(capability.run(ctx, { config: "api/wrangler.jsonc" })).rejects.toBeInstanceOf(
64
+ ToolNotAvailableError,
65
+ );
66
+ });
67
+
68
+ it("declares a rollback (native rollback disposition, no rollbackPolicy override needed)", () => {
69
+ const capability = createWranglerDeployCapability(createMockProcessRunner().runner);
70
+ expect(typeof capability.rollback).toBe("function");
71
+ expect(capability.rollbackPolicy).toBeUndefined();
72
+ });
73
+
74
+ it("rollback is a no-op on a first deploy — nothing was live before it", async () => {
75
+ const mock = createMockProcessRunner({
76
+ responses: { "versions list": "[]", "wrangler deploy": `Version ID: ${VERSION_A}\n` },
77
+ });
78
+ const capability = createWranglerDeployCapability(mock.runner);
79
+ await capability.run(ctx, { config: "api/wrangler.jsonc" });
80
+
81
+ await capability.rollback!(ctx, { config: "api/wrangler.jsonc" });
82
+
83
+ expect(mock.calls.some((c) => c.command.includes("versions deploy"))).toBe(false);
84
+ });
85
+
86
+ it("rollback promotes back to whichever version was live before this deploy — the wrangler-versions-promote back-to-prior-version claim, with no hand-written compensation", async () => {
87
+ const mock = createMockProcessRunner({
88
+ responses: {
89
+ "versions list": JSON.stringify([{ id: VERSION_A, percentage: 100 }]),
90
+ "wrangler deploy": `Version ID: ${VERSION_B}\n`,
91
+ },
92
+ });
93
+ const capability = createWranglerDeployCapability(mock.runner);
94
+ await capability.run(ctx, { config: "api/wrangler.jsonc" });
95
+
96
+ await capability.rollback!(ctx, { config: "api/wrangler.jsonc" });
97
+
98
+ const promoteCall = mock.calls.find((c) => c.command.startsWith("wrangler versions deploy"))!;
99
+ expect(promoteCall.command).toBe(
100
+ `wrangler versions deploy '${VERSION_A}@100' --config 'api/wrangler.jsonc' --yes`,
101
+ );
102
+ });
103
+ });
104
+
105
+ describe("wrangler-versions-promote", () => {
106
+ it("shells `wrangler versions deploy <id>@<pct> --yes`, defaulting percentage to 100", async () => {
107
+ const mock = createMockProcessRunner();
108
+ const capability = createWranglerVersionsPromoteCapability(mock.runner);
109
+
110
+ const output = await capability.run(ctx, { config: "api/wrangler.jsonc", versionId: VERSION_A });
111
+
112
+ expect(output).toEqual({ versionId: VERSION_A, percentage: 100 });
113
+ const promoteCall = mock.calls.find((c) => c.command.startsWith("wrangler versions deploy"))!;
114
+ expect(promoteCall.command).toBe(
115
+ `wrangler versions deploy '${VERSION_A}@100' --config 'api/wrangler.jsonc' --yes`,
116
+ );
117
+ });
118
+
119
+ it("consumes a wired wrangler-deploy output as its versionId input", async () => {
120
+ const mock = createMockProcessRunner({
121
+ responses: { "wrangler deploy": `Version ID: ${VERSION_A}\n` },
122
+ });
123
+ const deploy = createWranglerDeployCapability(mock.runner);
124
+ const promote = createWranglerVersionsPromoteCapability(mock.runner);
125
+
126
+ const { versionId } = await deploy.run(ctx, { config: "api/wrangler.jsonc" });
127
+ const promoted = await promote.run(ctx, { config: "api/wrangler.jsonc", versionId, percentage: 10 });
128
+
129
+ expect(promoted).toEqual({ versionId: VERSION_A, percentage: 10 });
130
+ const promoteCall = mock.calls.find((c) => c.command.startsWith("wrangler versions deploy"))!;
131
+ expect(promoteCall.command).toContain(`'${VERSION_A}@10'`);
132
+ });
133
+
134
+ it("declares a rollback (native): re-promotes to whichever version was live before this promote call", async () => {
135
+ const mock = createMockProcessRunner({
136
+ responses: { "versions list": JSON.stringify([{ id: VERSION_A, percentage: 100 }]) },
137
+ });
138
+ const capability = createWranglerVersionsPromoteCapability(mock.runner);
139
+ await capability.run(ctx, { config: "api/wrangler.jsonc", versionId: VERSION_B });
140
+
141
+ await capability.rollback!(ctx, { config: "api/wrangler.jsonc", versionId: VERSION_B });
142
+
143
+ const calls = mock.calls.filter((c) => c.command.startsWith("wrangler versions deploy"));
144
+ expect(calls).toHaveLength(2);
145
+ expect(calls[0]!.command).toContain(`'${VERSION_B}@100'`);
146
+ expect(calls[1]!.command).toContain(`'${VERSION_A}@100'`);
147
+ });
148
+
149
+ it("throws ToolNotAvailableError when wrangler is absent", async () => {
150
+ const mock = createMockProcessRunner({ tools: { wrangler: false } });
151
+ const capability = createWranglerVersionsPromoteCapability(mock.runner);
152
+
153
+ await expect(
154
+ capability.run(ctx, { config: "api/wrangler.jsonc", versionId: VERSION_A }),
155
+ ).rejects.toBeInstanceOf(ToolNotAvailableError);
156
+ });
157
+ });
158
+
159
+ describe("parseWranglerVersionId", () => {
160
+ it("accepts both 'Version ID:' and bare 'Version:' wording", () => {
161
+ expect(parseWranglerVersionId(`Version ID: ${VERSION_A}`)).toBe(VERSION_A);
162
+ expect(parseWranglerVersionId(`Version: ${VERSION_A}`)).toBe(VERSION_A);
163
+ });
164
+
165
+ it("throws on stdout with no UUID-shaped version token", () => {
166
+ expect(() => parseWranglerVersionId("Version: 2 (a plain counter, not a version id)")).toThrow(
167
+ WranglerVersionIdNotFoundError,
168
+ );
169
+ });
170
+ });
@@ -0,0 +1,241 @@
1
+ /**
2
+ * `wrangler-deploy` / `wrangler-versions-promote` — the Cloudflare Workers
3
+ * apply leaves (chant #1293, epic #1296). The epic cedes the Workers plane
4
+ * entirely to `wrangler` (first-party, schema-backed, already owns the
5
+ * config format, bindings model, local dev, and deploy) — these two verbs
6
+ * are a typed wrapper over that CLI, not a reimplementation of it.
7
+ *
8
+ * Placement: core, not a cloudflare lexicon. #1293's own analysis weighed
9
+ * three options — core (generic verb, fastest, costs a little conceptual
10
+ * tidiness since `wrangler-deploy` shells a specific vendor CLI), a
11
+ * capability-only cloudflare lexicon (the "correct" home, but `LexiconPlugin`
12
+ * tier-1 completeness — serializer, lint rules, post-synth, LSP, examples,
13
+ * docs — is heavy ceremony for three verbs and no resource types), or
14
+ * generalizing to a `cli-deploy` verb parameterised by tool (tempting, but
15
+ * premature and it re-opens the verb set the bounded-primitives thesis
16
+ * depends on staying closed). Core wins on the same precedent `sign`/
17
+ * `attest-provenance` (./sign.ts) already set: a specific vendor CLI
18
+ * (`cosign`) shelled out through the injectable `ProcessRunner`
19
+ * (./process-runner.ts), no cloud-specific SDK client needed the way AWS's
20
+ * `CloudExecutor` (./cloud-executor.ts) models CloudFormation/ECS/Lambda.
21
+ * `wrangler` fits that exact shape. Revisit if/when the zone-plane lexicon
22
+ * (#1294) lands and gives these three verbs a natural cloudflare-owned home.
23
+ *
24
+ * `wrangler-versions-promote` is also the direct evidence for #1296's
25
+ * "three new verbs cover a whole new cloud" claim: Cloudflare's native
26
+ * Worker version rollback maps onto the existing `RollbackPolicy: "native"`
27
+ * (../capability.ts) with no compensation code to hand-write — both verbs
28
+ * here declare a `rollback` (auto-derived "native" per ../capability.ts's
29
+ * `Capability.rollbackPolicy` doc) that re-promotes to whichever version was
30
+ * live before the step ran, the same best-effort captured-previous-state
31
+ * pattern lexicons/aws/src/components/apply.ts's `lambda-deploy` uses for
32
+ * its alias rollback.
33
+ *
34
+ * No real Cloudflare control-plane emulator exists yet (#1295, epic #1296),
35
+ * so — like ./sign.ts — every real path here shells out through the
36
+ * injectable `ProcessRunner` and every test substitutes `MockProcessRunner`
37
+ * (./__tests__/mock-process-runner.ts): no live `wrangler`, no network, ever,
38
+ * in a test run. CI coverage is plan-shape/invocation-shape assertions only,
39
+ * per #1293's own "Verification" section.
40
+ */
41
+
42
+ import type { Capability } from "../capability";
43
+ import { defaultProcessRunner, q, requireTool, type ProcessRunner } from "./process-runner";
44
+
45
+ const WRANGLER_TOOL = "wrangler";
46
+
47
+ /** Distinguishes one deploy target (a wrangler config + optional named environment) from another, so the best-effort previous-version tracking below never conflates two different Workers sharing a process. */
48
+ function targetKey(config: string, env?: string): string {
49
+ return env ? `${config}#${env}` : config;
50
+ }
51
+
52
+ function buildWranglerDeployArgs(input: WranglerDeployInput): string {
53
+ const args = ["wrangler", "deploy", "--config", q(input.config)];
54
+ if (input.env) args.push("--env", q(input.env));
55
+ return args.join(" ");
56
+ }
57
+
58
+ function buildVersionsListArgs(config: string, env?: string): string {
59
+ const args = ["wrangler", "versions", "list", "--config", q(config), "--json"];
60
+ if (env) args.push("--env", q(env));
61
+ return args.join(" ");
62
+ }
63
+
64
+ function buildVersionsPromoteArgs(config: string, versionId: string, percentage: number, env?: string): string {
65
+ const args = ["wrangler", "versions", "deploy", q(`${versionId}@${percentage}`), "--config", q(config), "--yes"];
66
+ if (env) args.push("--env", q(env));
67
+ return args.join(" ");
68
+ }
69
+
70
+ /** Thrown when `wrangler deploy`'s stdout carries no parseable Version ID — fail-closed rather than returning a capability output downstream steps (`wrangler-versions-promote`, wired via `"@Deploy.versionId"`) would silently receive as `undefined`. */
71
+ export class WranglerVersionIdNotFoundError extends Error {
72
+ constructor(public readonly stdout: string) {
73
+ super(`wrangler-deploy: could not find a Version ID in wrangler's output. Got:\n${stdout}`);
74
+ this.name = "WranglerVersionIdNotFoundError";
75
+ }
76
+ }
77
+
78
+ // wrangler's deploy/versions-upload output names the new version as e.g.
79
+ // "Version ID: 07bcb198-... " (gradual deployments) — accept either "Version
80
+ // ID:" or the bare "Version:" wording across wrangler versions, and require a
81
+ // UUID-shaped token so a stray "Version: 2" summary line can't be mistaken
82
+ // for it.
83
+ const VERSION_ID_RE = /Version(?: ID)?:\s*([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})/;
84
+
85
+ /** Extract the deployed Worker version id from `wrangler deploy`'s stdout. Exported so tests can assert on it directly, mirroring ./sign.ts's `buildSignArgs`. */
86
+ export function parseWranglerVersionId(stdout: string): string {
87
+ const match = VERSION_ID_RE.exec(stdout);
88
+ if (!match) throw new WranglerVersionIdNotFoundError(stdout);
89
+ return match[1]!;
90
+ }
91
+
92
+ interface WranglerVersionsListEntry {
93
+ id: string;
94
+ percentage?: number;
95
+ }
96
+
97
+ /**
98
+ * Best-effort: the currently-live (100%) version id, from `wrangler versions
99
+ * list --json` — `undefined` for a first deploy (no versions yet) or if
100
+ * listing fails/parses oddly. Never throws: capturing "what to roll back to"
101
+ * must not block the deploy/promote itself, the same fail-soft stance
102
+ * `lambda-deploy`'s alias-version capture takes.
103
+ */
104
+ async function currentLiveVersionId(
105
+ runner: ProcessRunner,
106
+ config: string,
107
+ env?: string,
108
+ ): Promise<string | undefined> {
109
+ try {
110
+ const { stdout } = await runner.run(buildVersionsListArgs(config, env));
111
+ const entries = JSON.parse(stdout) as WranglerVersionsListEntry[];
112
+ return entries.find((entry) => entry.percentage === 100)?.id;
113
+ } catch {
114
+ return undefined;
115
+ }
116
+ }
117
+
118
+ // ── wrangler-deploy ──────────────────────────────────────────────────────────
119
+
120
+ export interface WranglerDeployInput {
121
+ /** Path to the Worker's `wrangler.jsonc`/`wrangler.toml`. */
122
+ config: string;
123
+ /** Named environment within the config (`wrangler deploy --env <env>`). */
124
+ env?: string;
125
+ }
126
+
127
+ export interface WranglerDeployOutput {
128
+ /** The version id wrangler assigned this deploy — wire into `wrangler-versions-promote` (e.g. `"@Deploy.versionId"`). */
129
+ versionId: string;
130
+ }
131
+
132
+ /**
133
+ * Deploy a Worker from a `wrangler.jsonc`/`wrangler.toml` via `wrangler
134
+ * deploy`, and return the version id it published — the analogue of
135
+ * `lambda-deploy` (../../lexicons/aws/src/components/apply.ts) for the
136
+ * Workers plane.
137
+ *
138
+ * Rollback: `wrangler-versions-promote` back to whichever version was live
139
+ * before this step ran (captured up front via `wrangler versions list`, the
140
+ * same best-effort captured-previous pattern `lambda-deploy` uses for its
141
+ * alias). A no-op on a first deploy — nothing was live to restore.
142
+ */
143
+ export function createWranglerDeployCapability(
144
+ processRunner: ProcessRunner = defaultProcessRunner(),
145
+ ): Capability<WranglerDeployInput, WranglerDeployOutput> {
146
+ const previousVersionByTarget = new Map<string, string | undefined>();
147
+
148
+ return {
149
+ kind: "wrangler-deploy",
150
+ async run(_ctx, input) {
151
+ await requireTool(processRunner, WRANGLER_TOOL, `deploy the Worker in ${input.config}`);
152
+ const target = targetKey(input.config, input.env);
153
+ if (!previousVersionByTarget.has(target)) {
154
+ previousVersionByTarget.set(target, await currentLiveVersionId(processRunner, input.config, input.env));
155
+ }
156
+
157
+ const { stdout } = await processRunner.run(buildWranglerDeployArgs(input));
158
+ return { versionId: parseWranglerVersionId(stdout) };
159
+ },
160
+ async rollback(_ctx, input) {
161
+ const target = targetKey(input.config, input.env);
162
+ const previousVersionId = previousVersionByTarget.get(target);
163
+ if (!previousVersionId) return; // first deploy — nothing was live before it.
164
+ await requireTool(processRunner, WRANGLER_TOOL, `roll back the Worker in ${input.config}`);
165
+ await processRunner.run(buildVersionsPromoteArgs(input.config, previousVersionId, 100, input.env));
166
+ },
167
+ };
168
+ }
169
+
170
+ /** Default `wrangler-deploy` capability, backed by the real `ProcessRunner`. */
171
+ export const wranglerDeployCapability: Capability<WranglerDeployInput, WranglerDeployOutput> =
172
+ createWranglerDeployCapability();
173
+
174
+ // ── wrangler-versions-promote ─────────────────────────────────────────────────
175
+
176
+ export interface WranglerVersionsPromoteInput {
177
+ /** Path to the Worker's `wrangler.jsonc`/`wrangler.toml`. */
178
+ config: string;
179
+ /** Version id to promote — typically wired from a prior `wrangler-deploy` step (`"@Deploy.versionId"`) or a prior version id when this step composes as an explicit rollback. */
180
+ versionId: string;
181
+ /** Traffic percentage to route to `versionId`. Default: 100 (full promote/rollback). Below 100 is the gradual-deployment lever. */
182
+ percentage?: number;
183
+ /** Named environment within the config (`wrangler versions deploy --env <env>`). */
184
+ env?: string;
185
+ }
186
+
187
+ export interface WranglerVersionsPromoteOutput {
188
+ /** The version id that was promoted. */
189
+ versionId: string;
190
+ /** The traffic percentage actually routed to it. */
191
+ percentage: number;
192
+ }
193
+
194
+ /**
195
+ * Promote a Worker version to (some percentage of) live traffic via
196
+ * `wrangler versions deploy <version-id>@<percentage> --yes` — both the
197
+ * gradual-deployment lever (`percentage` < 100) and, at `percentage: 100`,
198
+ * the rollback mechanism: Cloudflare's native version rollback is a promote
199
+ * to a prior version id, not a redeploy, so this same verb composes as the
200
+ * explicit compensation step a component wires up (#1293's "verification"
201
+ * example: forced failure after `wrangler-deploy` -> `wrangler-versions-promote`
202
+ * back to the prior version, no hand-written compensation needed).
203
+ *
204
+ * Also declares its own `rollback` (native, no `rollbackPolicy` override
205
+ * needed — see ../capability.ts): re-promotes to whichever version was live
206
+ * before *this* promote call, for the case where the promote step itself is
207
+ * composed directly (not just as `wrangler-deploy`'s compensation).
208
+ */
209
+ export function createWranglerVersionsPromoteCapability(
210
+ processRunner: ProcessRunner = defaultProcessRunner(),
211
+ ): Capability<WranglerVersionsPromoteInput, WranglerVersionsPromoteOutput> {
212
+ const previousVersionByTarget = new Map<string, string | undefined>();
213
+
214
+ return {
215
+ kind: "wrangler-versions-promote",
216
+ async run(_ctx, input) {
217
+ await requireTool(processRunner, WRANGLER_TOOL, `promote version ${input.versionId} in ${input.config}`);
218
+ const percentage = input.percentage ?? 100;
219
+ const target = targetKey(input.config, input.env);
220
+ if (!previousVersionByTarget.has(target)) {
221
+ previousVersionByTarget.set(target, await currentLiveVersionId(processRunner, input.config, input.env));
222
+ }
223
+
224
+ await processRunner.run(buildVersionsPromoteArgs(input.config, input.versionId, percentage, input.env));
225
+ return { versionId: input.versionId, percentage };
226
+ },
227
+ async rollback(_ctx, input) {
228
+ const target = targetKey(input.config, input.env);
229
+ const previousVersionId = previousVersionByTarget.get(target);
230
+ if (!previousVersionId) return; // nothing was live before this promote.
231
+ await requireTool(processRunner, WRANGLER_TOOL, `roll back version promotion in ${input.config}`);
232
+ await processRunner.run(buildVersionsPromoteArgs(input.config, previousVersionId, 100, input.env));
233
+ },
234
+ };
235
+ }
236
+
237
+ /** Default `wrangler-versions-promote` capability, backed by the real `ProcessRunner`. */
238
+ export const wranglerVersionsPromoteCapability: Capability<
239
+ WranglerVersionsPromoteInput,
240
+ WranglerVersionsPromoteOutput
241
+ > = createWranglerVersionsPromoteCapability();
package/src/composite.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { isDeclarable, type Declarable } from "./declarable";
2
- import { setProvenance } from "./provenance";
2
+ import { setPathProvenance, setProvenance } from "./provenance";
3
3
 
4
4
  /**
5
5
  * Marker symbol for Composite type identification.
@@ -150,10 +150,16 @@ export function Composite<P, M extends CompositeFactoryMembers = CompositeMember
150
150
  /**
151
151
  * Expands a CompositeInstance into a flat Map of prefixed entity names to Declarables.
152
152
  * Handles nested composites recursively.
153
+ *
154
+ * @param instanceName - The composite INSTANCE every member belongs to (chant
155
+ * #1443), which at the top call is the export name and stays that name
156
+ * through nesting — `prefix` grows per member and is not an instance
157
+ * identity. Defaults to `prefix` so the top call needs no extra argument.
153
158
  */
154
159
  export function expandComposite(
155
160
  prefix: string,
156
161
  instance: CompositeInstance,
162
+ instanceName: string = prefix,
157
163
  ): Map<string, Declarable> {
158
164
  const result = new Map<string, Declarable>();
159
165
  const shared = (instance as unknown as Record<symbol, unknown>)[SHARED_PROPS] as Record<string, unknown> | undefined;
@@ -164,7 +170,7 @@ export function expandComposite(
164
170
  const fullName = `${prefix}${memberName[0].toUpperCase()}${memberName.slice(1)}`;
165
171
 
166
172
  if (isCompositeInstance(member)) {
167
- const nested = expandComposite(fullName, member);
173
+ const nested = expandComposite(fullName, member, instanceName);
168
174
  for (const [nestedName, nestedEntity] of nested) {
169
175
  // Inner composite already stamped; `??=` keeps the most-specific one.
170
176
  if (compositeName) setProvenance(nestedEntity, { composite: compositeName });
@@ -176,6 +182,15 @@ export function expandComposite(
176
182
  }
177
183
  }
178
184
 
185
+ // Whole-entity origin (chant #1443). Recorded after the loop so a nested
186
+ // member carries its INNER composite here — the recursive call above already
187
+ // ran and `setPathProvenance` keeps the first writer.
188
+ if (compositeName) {
189
+ for (const entity of result.values()) {
190
+ setPathProvenance(entity, "", { kind: "composite", composite: compositeName, instance: instanceName });
191
+ }
192
+ }
193
+
179
194
  if (shared) {
180
195
  for (const entity of result.values()) {
181
196
  if ("props" in entity) {
@@ -211,6 +226,20 @@ export function expandComposite(
211
226
  Object.defineProperty(entity, "props", {
212
227
  value: merged, enumerable: false, configurable: true,
213
228
  });
229
+ // A key the member did not set is the propagating instance's alone, so
230
+ // it is attributable exactly (chant #1443). A key both sides wrote —
231
+ // an override, or the array concatenation above — is left to the
232
+ // whole-entity origin rather than credited to one contributor.
233
+ if (compositeName) {
234
+ for (const k of Object.keys(shared)) {
235
+ if (shared[k] === undefined || existing[k] !== undefined) continue;
236
+ setPathProvenance(entity, k, {
237
+ kind: "composite",
238
+ composite: compositeName,
239
+ instance: instanceName,
240
+ });
241
+ }
242
+ }
214
243
  }
215
244
  }
216
245
  }
@@ -6,6 +6,7 @@ import {
6
6
  resolveAutoReleaseDisabled,
7
7
  resolveFoldEnabled,
8
8
  resolveSbomFormat,
9
+ resolveKnowledgeDir,
9
10
  environmentName,
10
11
  environmentNames,
11
12
  environmentEndpoint,
@@ -433,3 +434,17 @@ describe("resolveSbomFormat (#606)", () => {
433
434
  expect(resolveSbomFormat({}, "cyclonedx")).toBe("cyclonedx");
434
435
  });
435
436
  });
437
+
438
+ describe("resolveKnowledgeDir (#1864, design #1059)", () => {
439
+ test("convention: knowledge/ beside the project root when config is silent", () => {
440
+ expect(resolveKnowledgeDir({}, "/proj")).toBe(join("/proj", "knowledge"));
441
+ });
442
+
443
+ test("config override honored", () => {
444
+ expect(resolveKnowledgeDir({ knowledge: { dir: "docs/knowledge" } }, "/proj")).toBe(join("/proj", "docs/knowledge"));
445
+ });
446
+
447
+ test("an empty knowledge object still falls back to the convention name", () => {
448
+ expect(resolveKnowledgeDir({ knowledge: {} }, "/proj")).toBe(join("/proj", "knowledge"));
449
+ });
450
+ });
package/src/config.ts CHANGED
@@ -181,6 +181,9 @@ export const ChantConfigSchema = z.object({
181
181
  scanner: z.enum(["grype", "trivy"]).optional(),
182
182
  vexSources: z.array(z.string()).optional(),
183
183
  }).optional(),
184
+ knowledge: z.object({
185
+ dir: z.string().min(1).optional(),
186
+ }).optional(),
184
187
  }).passthrough();
185
188
 
186
189
  /**
@@ -400,6 +403,21 @@ export interface ChantConfig {
400
403
  /** Default VEX document paths (OpenVEX/CycloneDX) applied to every gate. Read where the gate step is composed. */
401
404
  vexSources?: string[];
402
405
  };
406
+
407
+ /**
408
+ * OKF knowledge bundle location (#1864, design #1059, epic #1057) — the
409
+ * *input* side of `chant explain --format okf` (#1058): a project may
410
+ * author knowledge as an OKF v0.2 bundle (a directory of markdown files
411
+ * with YAML frontmatter) that `./okf-read.ts`'s `loadOkfBundle` reads and
412
+ * binds to discovered entities via each concept's `binds` frontmatter key.
413
+ * Convention-first: `knowledge/` beside `chant.config.ts` is used when this
414
+ * is unset. Set `dir` only when that name is already taken by something
415
+ * else in the project. See {@link resolveKnowledgeDir}.
416
+ */
417
+ knowledge?: {
418
+ /** Bundle directory, relative to the project root. Defaults to `"knowledge"`. */
419
+ dir?: string;
420
+ };
403
421
  }
404
422
 
405
423
  /**
@@ -783,6 +801,18 @@ export function resolveVulnPolicy(config: ChantConfig): Partial<VulnPolicy> {
783
801
  return out;
784
802
  }
785
803
 
804
+ /**
805
+ * Resolve the OKF knowledge bundle directory (#1864, design #1059):
806
+ * `knowledge.dir` relative to `projectPath` when configured, else the
807
+ * `knowledge/` convention beside `chant.config.ts`. Never checks existence —
808
+ * a project with no bundle yet resolves a path all the same, and
809
+ * `okf-read.ts`'s `loadOkfBundle` treats a missing directory as an empty
810
+ * bundle rather than an error.
811
+ */
812
+ export function resolveKnowledgeDir(config: ChantConfig, projectPath: string): string {
813
+ return join(projectPath, config.knowledge?.dir ?? "knowledge");
814
+ }
815
+
786
816
  /**
787
817
  * Validate and normalize a raw config object into ChantConfig shape.
788
818
  */
@@ -48,6 +48,25 @@ describe("normalizeDeepProperties", () => {
48
48
  expect(Object.keys(out.mid as Record<string, unknown>)).toEqual(["a", "z"]);
49
49
  });
50
50
 
51
+ test("the mask hook collapses a structural secret path to MASKED, on either side (#1830)", () => {
52
+ const hooks: DeepNormalizationHooks = {
53
+ mask: (n) => n.entityType === "K8s::Core::Secret" && n.pattern.startsWith("data."),
54
+ };
55
+ for (const side of ["declared", "live"] as const) {
56
+ const out = normalizeDeepProperties(
57
+ { data: { "app.conf": "c2VjcmV0LWJ5dGVz" }, type: "Opaque" },
58
+ { entityType: "K8s::Core::Secret", side, hooks },
59
+ );
60
+ expect(out).toEqual({ data: { "app.conf": MASKED }, type: "Opaque" });
61
+ }
62
+ // Another entity type sails through the same hook untouched.
63
+ const other = normalizeDeepProperties(
64
+ { data: { "app.conf": "plain" } },
65
+ { entityType: "K8s::Core::ConfigMap", side: "live", hooks },
66
+ );
67
+ expect(other).toEqual({ data: { "app.conf": "plain" } });
68
+ });
69
+
51
70
  test("leaves array order alone with no ordering hook", () => {
52
71
  const out = normalizeDeepProperties({ Tags: [{ Key: "z" }, { Key: "a" }] }, { entityType: "T", side: "live" });
53
72
  expect(out.Tags).toEqual([{ Key: "z" }, { Key: "a" }]);