@intentius/chant 0.56.0 → 0.58.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 (515) hide show
  1. package/dist/audit/catalog.d.ts +51 -1
  2. package/dist/audit/catalog.d.ts.map +1 -1
  3. package/dist/audit/core.d.ts +20 -1
  4. package/dist/audit/core.d.ts.map +1 -1
  5. package/dist/audit/discover.d.ts +14 -8
  6. package/dist/audit/discover.d.ts.map +1 -1
  7. package/dist/audit/lineage.d.ts +10 -0
  8. package/dist/audit/lineage.d.ts.map +1 -0
  9. package/dist/audit/prior-art.d.ts +385 -0
  10. package/dist/audit/prior-art.d.ts.map +1 -0
  11. package/dist/audit/proof.d.ts +16 -0
  12. package/dist/audit/proof.d.ts.map +1 -1
  13. package/dist/audit/report-html.d.ts.map +1 -1
  14. package/dist/audit/report-model.d.ts +8 -1
  15. package/dist/audit/report-model.d.ts.map +1 -1
  16. package/dist/audit/report.d.ts.map +1 -1
  17. package/dist/audit/rules-doc.d.ts +3 -0
  18. package/dist/audit/rules-doc.d.ts.map +1 -1
  19. package/dist/audit/secrets.d.ts +15 -0
  20. package/dist/audit/secrets.d.ts.map +1 -1
  21. package/dist/audit/terraform-state.d.ts +46 -0
  22. package/dist/audit/terraform-state.d.ts.map +1 -0
  23. package/dist/build.d.ts +17 -1
  24. package/dist/build.d.ts.map +1 -1
  25. package/dist/claimed-fields.d.ts +161 -0
  26. package/dist/claimed-fields.d.ts.map +1 -0
  27. package/dist/cli/build-params-cli.d.ts +2 -2
  28. package/dist/cli/command-group.d.ts +14 -0
  29. package/dist/cli/command-group.d.ts.map +1 -1
  30. package/dist/cli/commands/audit.d.ts +17 -1
  31. package/dist/cli/commands/audit.d.ts.map +1 -1
  32. package/dist/cli/commands/build.d.ts.map +1 -1
  33. package/dist/cli/commands/check-lexicon-docs.d.ts +1 -1
  34. package/dist/cli/commands/check-lexicon-examples.d.ts +16 -6
  35. package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
  36. package/dist/cli/commands/import.d.ts.map +1 -1
  37. package/dist/cli/commands/init-lexicon/templates/codegen.d.ts +1 -0
  38. package/dist/cli/commands/init-lexicon/templates/codegen.d.ts.map +1 -1
  39. package/dist/cli/commands/init-lexicon/templates/project.d.ts +7 -0
  40. package/dist/cli/commands/init-lexicon/templates/project.d.ts.map +1 -1
  41. package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
  42. package/dist/cli/commands/init.d.ts.map +1 -1
  43. package/dist/cli/commands/lint.d.ts.map +1 -1
  44. package/dist/cli/commands/onboard.d.ts +2 -0
  45. package/dist/cli/commands/onboard.d.ts.map +1 -1
  46. package/dist/cli/handlers/build.d.ts.map +1 -1
  47. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  48. package/dist/cli/handlers/operator.d.ts +82 -16
  49. package/dist/cli/handlers/operator.d.ts.map +1 -1
  50. package/dist/cli/handlers/run.d.ts +49 -42
  51. package/dist/cli/handlers/run.d.ts.map +1 -1
  52. package/dist/cli/main.d.ts +28 -1
  53. package/dist/cli/main.d.ts.map +1 -1
  54. package/dist/cli/mcp/op-tools.d.ts +16 -1
  55. package/dist/cli/mcp/op-tools.d.ts.map +1 -1
  56. package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
  57. package/dist/cli/mcp/resources/context.d.ts.map +1 -1
  58. package/dist/cli/registry.d.ts +26 -9
  59. package/dist/cli/registry.d.ts.map +1 -1
  60. package/dist/codegen/generate-post-synth-barrel.d.ts +1 -1
  61. package/dist/components/auto-release.d.ts +10 -20
  62. package/dist/components/auto-release.d.ts.map +1 -1
  63. package/dist/components/capability.d.ts +4 -4
  64. package/dist/components/cli-support.d.ts +20 -21
  65. package/dist/components/cli-support.d.ts.map +1 -1
  66. package/dist/components/component.d.ts +3 -3
  67. package/dist/components/component.d.ts.map +1 -1
  68. package/dist/components/driver-output.d.ts.map +1 -1
  69. package/dist/components/driver.d.ts +66 -36
  70. package/dist/components/driver.d.ts.map +1 -1
  71. package/dist/components/index.d.ts +1 -2
  72. package/dist/components/index.d.ts.map +1 -1
  73. package/dist/components/manifest-persistence.d.ts +1 -9
  74. package/dist/components/manifest-persistence.d.ts.map +1 -1
  75. package/dist/components/run-progress.d.ts +6 -6
  76. package/dist/components/run-progress.d.ts.map +1 -1
  77. package/dist/components/verbs/run-agent.d.ts +9 -15
  78. package/dist/components/verbs/run-agent.d.ts.map +1 -1
  79. package/dist/components/verbs/verify.d.ts +2 -2
  80. package/dist/deep-observation.d.ts +17 -0
  81. package/dist/deep-observation.d.ts.map +1 -1
  82. package/dist/discovery/files.d.ts +4 -4
  83. package/dist/discovery/fold-import.d.ts.map +1 -1
  84. package/dist/discovery/param-deps.d.ts +33 -0
  85. package/dist/discovery/param-deps.d.ts.map +1 -1
  86. package/dist/discovery/sandbox/bundle.d.ts.map +1 -1
  87. package/dist/discovery/sandbox/config-wire.d.ts +2 -2
  88. package/dist/fold/foldable-helpers.d.ts +5 -1
  89. package/dist/fold/foldable-helpers.d.ts.map +1 -1
  90. package/dist/fold-provenance.d.ts +236 -0
  91. package/dist/fold-provenance.d.ts.map +1 -0
  92. package/dist/graph-ops.d.ts +1 -1
  93. package/dist/held-elsewhere.d.ts +85 -0
  94. package/dist/held-elsewhere.d.ts.map +1 -0
  95. package/dist/import/ir-utils.d.ts +1 -1
  96. package/dist/import/ir-utils.d.ts.map +1 -1
  97. package/dist/import/parser.d.ts +33 -0
  98. package/dist/import/parser.d.ts.map +1 -1
  99. package/dist/index.d.ts +3 -0
  100. package/dist/index.d.ts.map +1 -1
  101. package/dist/kubectl-context.d.ts +3 -3
  102. package/dist/lexicon-config.d.ts +4 -4
  103. package/dist/lexicon-plugin-helpers.d.ts +1 -1
  104. package/dist/lexicon.d.ts +104 -14
  105. package/dist/lexicon.d.ts.map +1 -1
  106. package/dist/lifecycle/change-set.d.ts +13 -0
  107. package/dist/lifecycle/change-set.d.ts.map +1 -1
  108. package/dist/lifecycle/deep-diff.d.ts +133 -6
  109. package/dist/lifecycle/deep-diff.d.ts.map +1 -1
  110. package/dist/lifecycle/deep-observe.d.ts.map +1 -1
  111. package/dist/lifecycle/gate-ledger.d.ts +71 -3
  112. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  113. package/dist/lifecycle/index.d.ts +1 -0
  114. package/dist/lifecycle/index.d.ts.map +1 -1
  115. package/dist/lifecycle/release-ledger.d.ts +2 -2
  116. package/dist/lifecycle/run-ledger.d.ts +53 -0
  117. package/dist/lifecycle/run-ledger.d.ts.map +1 -0
  118. package/dist/lifecycle/symptoms.d.ts +3 -3
  119. package/dist/lifecycle/symptoms.d.ts.map +1 -1
  120. package/dist/lint/config.d.ts +81 -2
  121. package/dist/lint/config.d.ts.map +1 -1
  122. package/dist/lint/post-synth.d.ts +55 -2
  123. package/dist/lint/post-synth.d.ts.map +1 -1
  124. package/dist/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.d.ts +1 -1
  125. package/dist/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.d.ts.map +1 -1
  126. package/dist/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.d.ts +1 -1
  127. package/dist/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.d.ts.map +1 -1
  128. package/dist/lint/rules/comp/comp004-gate-needs-durable-runtime.d.ts +44 -0
  129. package/dist/lint/rules/comp/comp004-gate-needs-durable-runtime.d.ts.map +1 -0
  130. package/dist/lint/rules/comp/index.d.ts +1 -1
  131. package/dist/lint/rules/comp/index.d.ts.map +1 -1
  132. package/dist/lint/rules/op/index.d.ts +16 -0
  133. package/dist/lint/rules/op/index.d.ts.map +1 -0
  134. package/dist/lint/rules/op/ops012-activity-contract.d.ts +27 -0
  135. package/dist/lint/rules/op/ops012-activity-contract.d.ts.map +1 -0
  136. package/dist/lint/rules/op/ops013-step-output-ref.d.ts +61 -0
  137. package/dist/lint/rules/op/ops013-step-output-ref.d.ts.map +1 -0
  138. package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts +55 -0
  139. package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts.map +1 -0
  140. package/dist/lint/rules/op/support.d.ts +12 -0
  141. package/dist/lint/rules/op/support.d.ts.map +1 -0
  142. package/dist/lint/suppressions.d.ts +108 -0
  143. package/dist/lint/suppressions.d.ts.map +1 -0
  144. package/dist/live-endpoint.d.ts +1 -1
  145. package/dist/op/activities/activity-contracts.d.ts +54 -0
  146. package/dist/op/activities/activity-contracts.d.ts.map +1 -0
  147. package/dist/op/activities/apply.d.ts +262 -0
  148. package/dist/op/activities/apply.d.ts.map +1 -0
  149. package/dist/op/activities/build.d.ts +13 -0
  150. package/dist/op/activities/build.d.ts.map +1 -0
  151. package/dist/op/activities/converge.d.ts +110 -0
  152. package/dist/op/activities/converge.d.ts.map +1 -0
  153. package/dist/op/activities/env-teardown.d.ts +76 -0
  154. package/dist/op/activities/env-teardown.d.ts.map +1 -0
  155. package/dist/op/activities/guard-validate.d.ts +64 -0
  156. package/dist/op/activities/guard-validate.d.ts.map +1 -0
  157. package/dist/op/activities/http-check.d.ts +34 -0
  158. package/dist/op/activities/http-check.d.ts.map +1 -0
  159. package/dist/op/activities/index.d.ts +31 -0
  160. package/dist/op/activities/index.d.ts.map +1 -0
  161. package/dist/op/activities/lexicon-upgrade.d.ts +182 -0
  162. package/dist/op/activities/lexicon-upgrade.d.ts.map +1 -0
  163. package/dist/op/activities/lifecycle.d.ts +42 -0
  164. package/dist/op/activities/lifecycle.d.ts.map +1 -0
  165. package/dist/op/activities/pipeline-audit.d.ts +84 -0
  166. package/dist/op/activities/pipeline-audit.d.ts.map +1 -0
  167. package/dist/op/activities/policy.d.ts +17 -0
  168. package/dist/op/activities/policy.d.ts.map +1 -0
  169. package/dist/op/activities/reconcile.d.ts +89 -0
  170. package/dist/op/activities/reconcile.d.ts.map +1 -0
  171. package/dist/op/activities/shell.d.ts +13 -0
  172. package/dist/op/activities/shell.d.ts.map +1 -0
  173. package/dist/op/activities/teardown.d.ts +10 -0
  174. package/dist/op/activities/teardown.d.ts.map +1 -0
  175. package/dist/op/activities/util.d.ts +7 -0
  176. package/dist/op/activities/util.d.ts.map +1 -0
  177. package/dist/op/activities/wait.d.ts +28 -0
  178. package/dist/op/activities/wait.d.ts.map +1 -0
  179. package/dist/op/activities/workflow-audit.d.ts +92 -0
  180. package/dist/op/activities/workflow-audit.d.ts.map +1 -0
  181. package/dist/op/activity-contract-registry.d.ts +76 -0
  182. package/dist/op/activity-contract-registry.d.ts.map +1 -0
  183. package/dist/op/activity-contract.d.ts +25 -18
  184. package/dist/op/activity-contract.d.ts.map +1 -1
  185. package/dist/op/activity-failure.d.ts +28 -0
  186. package/dist/op/activity-failure.d.ts.map +1 -0
  187. package/dist/op/activity-profiles.d.ts +118 -0
  188. package/dist/op/activity-profiles.d.ts.map +1 -0
  189. package/dist/op/activity-registry.d.ts +22 -38
  190. package/dist/op/activity-registry.d.ts.map +1 -1
  191. package/dist/op/activity-runtime.d.ts +3 -14
  192. package/dist/op/activity-runtime.d.ts.map +1 -1
  193. package/dist/op/builders.d.ts +84 -29
  194. package/dist/op/builders.d.ts.map +1 -1
  195. package/dist/op/composites/apply-op.d.ts +113 -0
  196. package/dist/op/composites/apply-op.d.ts.map +1 -0
  197. package/dist/op/composites/converge-op.d.ts +120 -0
  198. package/dist/op/composites/converge-op.d.ts.map +1 -0
  199. package/dist/op/composites/index.d.ts +26 -0
  200. package/dist/op/composites/index.d.ts.map +1 -0
  201. package/dist/op/composites/lexicon-upgrade-op.d.ts +78 -0
  202. package/dist/op/composites/lexicon-upgrade-op.d.ts.map +1 -0
  203. package/dist/op/composites/pipeline-audit-op.d.ts +50 -0
  204. package/dist/op/composites/pipeline-audit-op.d.ts.map +1 -0
  205. package/dist/op/composites/reconcile-op.d.ts +57 -0
  206. package/dist/op/composites/reconcile-op.d.ts.map +1 -0
  207. package/dist/op/composites/watch-op.d.ts +60 -0
  208. package/dist/op/composites/watch-op.d.ts.map +1 -0
  209. package/dist/op/composites/workflow-audit-op.d.ts +57 -0
  210. package/dist/op/composites/workflow-audit-op.d.ts.map +1 -0
  211. package/dist/op/converge-rule.d.ts +12 -10
  212. package/dist/op/converge-rule.d.ts.map +1 -1
  213. package/dist/op/cron.d.ts +51 -0
  214. package/dist/op/cron.d.ts.map +1 -0
  215. package/dist/op/discover.d.ts +9 -0
  216. package/dist/op/discover.d.ts.map +1 -1
  217. package/dist/op/duration.d.ts +11 -0
  218. package/dist/op/duration.d.ts.map +1 -0
  219. package/dist/op/emulator-lifecycle.d.ts.map +1 -1
  220. package/dist/op/gate.d.ts +90 -0
  221. package/dist/op/gate.d.ts.map +1 -0
  222. package/dist/op/generate-pipeline.d.ts +9 -0
  223. package/dist/op/generate-pipeline.d.ts.map +1 -1
  224. package/dist/op/index.d.ts +23 -7
  225. package/dist/op/index.d.ts.map +1 -1
  226. package/dist/op/local-executor.d.ts +114 -28
  227. package/dist/op/local-executor.d.ts.map +1 -1
  228. package/dist/op/local-output.d.ts +11 -1
  229. package/dist/op/local-output.d.ts.map +1 -1
  230. package/dist/op/op-ir.d.ts +184 -0
  231. package/dist/op/op-ir.d.ts.map +1 -0
  232. package/dist/op/op-verb-class.d.ts +10 -2
  233. package/dist/op/op-verb-class.d.ts.map +1 -1
  234. package/dist/op/operator.d.ts +30 -5
  235. package/dist/op/operator.d.ts.map +1 -1
  236. package/dist/op/receipt-store.d.ts +1 -1
  237. package/dist/op/receipt-store.d.ts.map +1 -1
  238. package/dist/op/resource.d.ts +15 -1
  239. package/dist/op/resource.d.ts.map +1 -1
  240. package/dist/op/runtime.d.ts +200 -0
  241. package/dist/op/runtime.d.ts.map +1 -0
  242. package/dist/op/runtimes/local.d.ts +29 -0
  243. package/dist/op/runtimes/local.d.ts.map +1 -0
  244. package/dist/op/step-output-ref.d.ts +15 -13
  245. package/dist/op/step-output-ref.d.ts.map +1 -1
  246. package/dist/op/types.d.ts +79 -27
  247. package/dist/op/types.d.ts.map +1 -1
  248. package/dist/ownership.d.ts +1 -1
  249. package/dist/provenance.d.ts +21 -0
  250. package/dist/provenance.d.ts.map +1 -1
  251. package/dist/serializer-walker.d.ts.map +1 -1
  252. package/dist/stack-output.d.ts +5 -0
  253. package/dist/stack-output.d.ts.map +1 -1
  254. package/package.json +6 -1
  255. package/src/audit/catalog.test.ts +79 -3
  256. package/src/audit/catalog.ts +111 -13
  257. package/src/audit/core.test.ts +26 -0
  258. package/src/audit/core.ts +61 -17
  259. package/src/audit/discover.test.ts +39 -0
  260. package/src/audit/discover.ts +91 -15
  261. package/src/audit/lineage.ts +100 -0
  262. package/src/audit/prior-art.ts +153 -0
  263. package/src/audit/proof.test.ts +67 -0
  264. package/src/audit/proof.ts +70 -0
  265. package/src/audit/report-html.ts +9 -4
  266. package/src/audit/report-model.ts +16 -2
  267. package/src/audit/report.test.ts +33 -0
  268. package/src/audit/report.ts +5 -3
  269. package/src/audit/rules-doc.test.ts +34 -2
  270. package/src/audit/rules-doc.ts +82 -9
  271. package/src/audit/secrets.ts +15 -3
  272. package/src/audit/terraform-state.test.ts +144 -0
  273. package/src/audit/terraform-state.ts +131 -0
  274. package/src/build.ts +31 -1
  275. package/src/claimed-fields.test.ts +145 -0
  276. package/src/claimed-fields.ts +208 -0
  277. package/src/cli/build-params-cli.ts +2 -2
  278. package/src/cli/command-group.ts +14 -0
  279. package/src/cli/commands/__fixtures__/audit-terraform/infra/.terraform/providers/lock.txt +1 -0
  280. package/src/cli/commands/__fixtures__/audit-terraform/infra/main.tf +1 -0
  281. package/src/cli/commands/__fixtures__/audit-terraform/infra/modules/foo/main.tf +1 -0
  282. package/src/cli/commands/__fixtures__/audit-terraform/infra/variables.tf +3 -0
  283. package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +13 -4
  284. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/docs-cli.ts +4 -0
  285. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/package.ts +1 -0
  286. package/src/cli/commands/__fixtures__/init-lexicon-output/src/package-cli.ts +21 -0
  287. package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +1 -0
  288. package/src/cli/commands/audit.test.ts +189 -11
  289. package/src/cli/commands/audit.ts +73 -27
  290. package/src/cli/commands/build.test.ts +292 -9
  291. package/src/cli/commands/build.ts +156 -24
  292. package/src/cli/commands/check-lexicon-docs.ts +1 -1
  293. package/src/cli/commands/check-lexicon-examples.test.ts +29 -1
  294. package/src/cli/commands/check-lexicon-examples.ts +42 -10
  295. package/src/cli/commands/import.ts +6 -0
  296. package/src/cli/commands/init-lexicon/templates/codegen.ts +9 -0
  297. package/src/cli/commands/init-lexicon/templates/project.ts +44 -4
  298. package/src/cli/commands/init-lexicon.test.ts +97 -2
  299. package/src/cli/commands/init-lexicon.ts +4 -2
  300. package/src/cli/commands/init.test.ts +35 -1
  301. package/src/cli/commands/init.ts +14 -3
  302. package/src/cli/commands/lint.test.ts +48 -0
  303. package/src/cli/commands/lint.ts +123 -0
  304. package/src/cli/commands/onboard.test.ts +75 -0
  305. package/src/cli/commands/onboard.ts +54 -23
  306. package/src/cli/handlers/build.test.ts +81 -1
  307. package/src/cli/handlers/build.ts +15 -1
  308. package/src/cli/handlers/graph.test.ts +3 -3
  309. package/src/cli/handlers/lifecycle.test.ts +195 -12
  310. package/src/cli/handlers/lifecycle.ts +123 -6
  311. package/src/cli/handlers/misc.ts +1 -1
  312. package/src/cli/handlers/operator.test.ts +183 -11
  313. package/src/cli/handlers/operator.ts +260 -55
  314. package/src/cli/handlers/run.test.ts +431 -1287
  315. package/src/cli/handlers/run.ts +365 -915
  316. package/src/cli/main.test.ts +23 -15
  317. package/src/cli/main.ts +88 -33
  318. package/src/cli/mcp/op-tools.ts +147 -88
  319. package/src/cli/mcp/resource-handlers.ts +10 -37
  320. package/src/cli/mcp/resources/context.ts +11 -12
  321. package/src/cli/mcp/server.test.ts +25 -27
  322. package/src/cli/mcp/server.ts +2 -2
  323. package/src/cli/registry.ts +26 -9
  324. package/src/codegen/docs.ts +1 -1
  325. package/src/codegen/generate-post-synth-barrel.test.ts +1 -1
  326. package/src/codegen/generate-post-synth-barrel.ts +1 -1
  327. package/src/components/README.md +3 -2
  328. package/src/components/auto-release.test.ts +4 -20
  329. package/src/components/auto-release.ts +17 -42
  330. package/src/components/capability.ts +4 -4
  331. package/src/components/cli-support.test.ts +10 -5
  332. package/src/components/cli-support.ts +52 -44
  333. package/src/components/component.schema.json +2 -2
  334. package/src/components/component.ts +3 -3
  335. package/src/components/driver-output.ts +14 -2
  336. package/src/components/driver.test.ts +92 -22
  337. package/src/components/driver.ts +193 -83
  338. package/src/components/index.ts +1 -6
  339. package/src/components/manifest-persistence.test.ts +2 -20
  340. package/src/components/manifest-persistence.ts +4 -24
  341. package/src/components/pilots/pilots-lint-clean.test.ts +11 -10
  342. package/src/components/run-progress.ts +6 -6
  343. package/src/components/verbs/run-agent.ts +11 -17
  344. package/src/components/verbs/verify.ts +2 -2
  345. package/src/deep-observation.test.ts +75 -0
  346. package/src/deep-observation.ts +48 -0
  347. package/src/discovery/collect.test.ts +3 -3
  348. package/src/discovery/files.ts +4 -4
  349. package/src/discovery/fold-import.ts +105 -2
  350. package/src/discovery/param-deps.test.ts +112 -1
  351. package/src/discovery/param-deps.ts +193 -0
  352. package/src/discovery/sandbox/bundle.ts +15 -1
  353. package/src/discovery/sandbox/config-wire.test.ts +2 -2
  354. package/src/discovery/sandbox/config-wire.ts +2 -2
  355. package/src/fold/fold.test.ts +56 -0
  356. package/src/fold/foldable-helpers.ts +81 -1
  357. package/src/fold-provenance-exclusion.test.ts +186 -0
  358. package/src/fold-provenance.test.ts +495 -0
  359. package/src/fold-provenance.ts +441 -0
  360. package/src/graph-ops.test.ts +2 -2
  361. package/src/graph-ops.ts +1 -1
  362. package/src/held-elsewhere.test.ts +102 -0
  363. package/src/held-elsewhere.ts +114 -0
  364. package/src/import/ir-utils.ts +11 -1
  365. package/src/import/parser.ts +35 -0
  366. package/src/index.ts +3 -0
  367. package/src/kubectl-context.ts +3 -3
  368. package/src/lexicon-config.test.ts +5 -5
  369. package/src/lexicon-config.ts +4 -4
  370. package/src/lexicon-plugin-helpers.ts +1 -1
  371. package/src/lexicon.ts +120 -14
  372. package/src/lifecycle/change-set.test.ts +49 -0
  373. package/src/lifecycle/change-set.ts +67 -0
  374. package/src/lifecycle/converge-ledger.ts +1 -1
  375. package/src/lifecycle/deep-diff.test.ts +267 -14
  376. package/src/lifecycle/deep-diff.ts +217 -14
  377. package/src/lifecycle/deep-observe.test.ts +3 -1
  378. package/src/lifecycle/deep-observe.ts +16 -6
  379. package/src/lifecycle/gate-ledger.test.ts +74 -2
  380. package/src/lifecycle/gate-ledger.ts +143 -36
  381. package/src/lifecycle/index.ts +1 -0
  382. package/src/lifecycle/release-ledger.ts +2 -2
  383. package/src/lifecycle/run-ledger.test.ts +268 -0
  384. package/src/lifecycle/run-ledger.ts +201 -0
  385. package/src/lifecycle/symptoms.ts +3 -3
  386. package/src/lint/config.test.ts +82 -1
  387. package/src/lint/config.ts +112 -3
  388. package/src/lint/policy.test.ts +1 -1
  389. package/src/lint/post-synth.test.ts +31 -0
  390. package/src/lint/post-synth.ts +54 -1
  391. package/src/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.ts +2 -2
  392. package/src/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.ts +1 -1
  393. package/src/lint/rules/comp/comp.test.ts +1 -1
  394. package/src/lint/rules/comp/comp004-gate-needs-durable-runtime.ts +74 -0
  395. package/src/lint/rules/comp/index.ts +3 -3
  396. package/src/lint/rules/op/index.ts +23 -0
  397. package/src/lint/rules/op/ops012-activity-contract.test.ts +161 -0
  398. package/src/lint/rules/op/ops012-activity-contract.ts +64 -0
  399. package/src/lint/rules/op/ops013-step-output-ref.test.ts +237 -0
  400. package/src/lint/rules/op/ops013-step-output-ref.ts +98 -0
  401. package/src/lint/rules/op/ops014-converge-rule-refusals.test.ts +263 -0
  402. package/src/lint/rules/op/ops014-converge-rule-refusals.ts +179 -0
  403. package/src/lint/rules/op/support.ts +11 -0
  404. package/src/lint/suppressions.test.ts +163 -0
  405. package/src/lint/suppressions.ts +204 -0
  406. package/src/live-endpoint.ts +1 -1
  407. package/src/op/activities/activity-contracts.ts +66 -0
  408. package/src/op/activities/apply.test.ts +567 -0
  409. package/src/op/activities/apply.ts +662 -0
  410. package/src/op/activities/build.ts +26 -0
  411. package/src/op/activities/converge.test.ts +330 -0
  412. package/src/op/activities/converge.ts +449 -0
  413. package/src/op/activities/env-teardown.test.ts +187 -0
  414. package/src/op/activities/env-teardown.ts +171 -0
  415. package/src/op/activities/guard-validate.test.ts +139 -0
  416. package/src/op/activities/guard-validate.ts +223 -0
  417. package/src/op/activities/http-check.test.ts +48 -0
  418. package/src/op/activities/http-check.ts +55 -0
  419. package/src/op/activities/index.ts +119 -0
  420. package/src/op/activities/lexicon-upgrade.test.ts +492 -0
  421. package/src/op/activities/lexicon-upgrade.ts +673 -0
  422. package/src/op/activities/lifecycle.ts +85 -0
  423. package/src/op/activities/pipeline-audit.test.ts +67 -0
  424. package/src/op/activities/pipeline-audit.ts +201 -0
  425. package/src/op/activities/policy.test.ts +19 -0
  426. package/src/op/activities/policy.ts +37 -0
  427. package/src/op/activities/reconcile.test.ts +81 -0
  428. package/src/op/activities/reconcile.ts +187 -0
  429. package/src/op/activities/shell.ts +26 -0
  430. package/src/op/activities/teardown.ts +22 -0
  431. package/src/op/activities/util.ts +6 -0
  432. package/src/op/activities/wait.ts +69 -0
  433. package/src/op/activities/workflow-audit.test.ts +90 -0
  434. package/src/op/activities/workflow-audit.ts +203 -0
  435. package/src/op/activity-contract-registry.test.ts +98 -0
  436. package/src/op/activity-contract-registry.ts +114 -0
  437. package/src/op/activity-contract.test.ts +1 -1
  438. package/src/op/activity-contract.ts +37 -33
  439. package/src/op/activity-failure.ts +34 -0
  440. package/src/op/activity-profiles.test.ts +56 -0
  441. package/src/op/activity-profiles.ts +116 -0
  442. package/src/op/activity-registry.test.ts +37 -28
  443. package/src/op/activity-registry.ts +27 -56
  444. package/src/op/activity-runtime.ts +3 -47
  445. package/src/op/builders-exports.test.ts +0 -49
  446. package/src/op/builders-typed.test.ts +164 -0
  447. package/src/op/builders.ts +123 -39
  448. package/src/op/composites/apply-op.ts +208 -0
  449. package/src/op/composites/composites.test.ts +366 -0
  450. package/src/op/composites/converge-op.test.ts +168 -0
  451. package/src/op/composites/converge-op.ts +214 -0
  452. package/src/op/composites/index.ts +26 -0
  453. package/src/op/composites/lexicon-upgrade-op.test.ts +68 -0
  454. package/src/op/composites/lexicon-upgrade-op.ts +134 -0
  455. package/src/op/composites/pipeline-audit-op.test.ts +25 -0
  456. package/src/op/composites/pipeline-audit-op.ts +78 -0
  457. package/src/op/composites/reconcile-op.test.ts +51 -0
  458. package/src/op/composites/reconcile-op.ts +107 -0
  459. package/src/op/composites/watch-op.ts +108 -0
  460. package/src/op/composites/workflow-audit-op.test.ts +25 -0
  461. package/src/op/composites/workflow-audit-op.ts +88 -0
  462. package/src/op/converge-rule.ts +12 -10
  463. package/src/op/cron.test.ts +128 -0
  464. package/src/op/cron.ts +149 -0
  465. package/src/op/discover-root.test.ts +105 -1
  466. package/src/op/discover.test.ts +6 -2
  467. package/src/op/discover.ts +84 -16
  468. package/src/op/duration.ts +21 -0
  469. package/src/op/effect-step.test.ts +26 -11
  470. package/src/op/emulator-lifecycle.ts +1 -2
  471. package/src/op/gate.ts +155 -0
  472. package/src/op/generate-pipeline.test.ts +65 -3
  473. package/src/op/generate-pipeline.ts +19 -2
  474. package/src/op/index.ts +39 -6
  475. package/src/op/local-executor.test.ts +163 -34
  476. package/src/op/local-executor.ts +370 -102
  477. package/src/op/local-output.test.ts +95 -11
  478. package/src/op/local-output.ts +33 -4
  479. package/src/op/op-ir.test.ts +281 -0
  480. package/src/op/op-ir.ts +417 -0
  481. package/src/op/op-verb-class.ts +19 -5
  482. package/src/op/op.test.ts +26 -5
  483. package/src/op/operator.test.ts +175 -6
  484. package/src/op/operator.ts +63 -16
  485. package/src/op/receipt-store.ts +1 -1
  486. package/src/op/resource.ts +20 -2
  487. package/src/op/runtime.ts +203 -0
  488. package/src/op/runtimes/local.test.ts +181 -0
  489. package/src/op/runtimes/local.ts +212 -0
  490. package/src/op/step-output-ref.ts +15 -13
  491. package/src/op/types.ts +84 -24
  492. package/src/ownership.ts +1 -1
  493. package/src/provenance.test.ts +6 -0
  494. package/src/provenance.ts +21 -1
  495. package/src/serializer-walker.test.ts +42 -0
  496. package/src/serializer-walker.ts +21 -2
  497. package/src/stack-output.ts +6 -1
  498. package/dist/cli/handlers/op-progress.d.ts +0 -90
  499. package/dist/cli/handlers/op-progress.d.ts.map +0 -1
  500. package/dist/cli/handlers/run-client.d.ts +0 -127
  501. package/dist/cli/handlers/run-client.d.ts.map +0 -1
  502. package/dist/cli/handlers/run-report.d.ts +0 -5
  503. package/dist/cli/handlers/run-report.d.ts.map +0 -1
  504. package/dist/components/temporal-codegen-loader.d.ts +0 -27
  505. package/dist/components/temporal-codegen-loader.d.ts.map +0 -1
  506. package/dist/lint/rules/comp/comp004-gate-needs-temporal.d.ts +0 -37
  507. package/dist/lint/rules/comp/comp004-gate-needs-temporal.d.ts.map +0 -1
  508. package/src/cli/handlers/op-progress.test.ts +0 -268
  509. package/src/cli/handlers/op-progress.ts +0 -248
  510. package/src/cli/handlers/run-client.test.ts +0 -82
  511. package/src/cli/handlers/run-client.ts +0 -219
  512. package/src/cli/handlers/run-report.test.ts +0 -62
  513. package/src/cli/handlers/run-report.ts +0 -122
  514. package/src/components/temporal-codegen-loader.ts +0 -41
  515. package/src/lint/rules/comp/comp004-gate-needs-temporal.ts +0 -66
@@ -0,0 +1,449 @@
1
+ /**
2
+ * `convergeTick` — the activity a `ConvergeOp`-generated Op's Converge
3
+ * phase runs (../../composites/converge-op.ts). One call is one tick:
4
+ * observe (shell to `chant lifecycle plan`/`chant components status`, the
5
+ * same CLI surface `reconcilePr`/`lifecycleDiff` already shell to — see
6
+ * ./reconcile.ts, ./lifecycle.ts) -> classify (the pure rule table
7
+ * evaluator, core's `evaluatePredicate`, then this
8
+ * activity's own runtime backstop re-classifying each dispatch target's verb
9
+ * class before it runs — see `verbClassAllowedToDispatch` below) -> dispatch
10
+ * within budget (`chant run <op>`, the existing local runner — a gated
11
+ * dispatched Op ends `gated` with its pending fact on the ledger, exit code 3,
12
+ * which this tick turns into a `"gated"` outcome) -> record (one line to the
13
+ * converge ledger, `../../lifecycle/converge-ledger`).
14
+ *
15
+ * Deliberately monolithic, matching `reconcilePr`'s shape (one activity that
16
+ * derives the change set, regenerates, and opens the PR) rather than
17
+ * `ApplyOp`'s Build/Plan/Approve/Apply phase split — a converge tick's
18
+ * "classify against a rule table, then dispatch" step can't be decomposed
19
+ * into separate declared steps, because *which* rules fire is a runtime fact
20
+ * discovered only once live symptoms are read, while an Op's phase structure
21
+ * is fully static at build time. Splitting `Observe` into its own phase (the
22
+ * composite still does, for the per-phase visibility WatchOp/ApplyOp/
23
+ * ReconcileOp all get) and threading its result into this activity's `args` via a
24
+ * step-output reference (#1290) rather than a search-attribute round-trip is
25
+ * the seam this module *does* use the new dataflow for.
26
+ *
27
+ * `unknown` never remediates: whatever a rule's own predicate says, a tick
28
+ * whose derived symptom is `status: "unknown"` forces every fired `run`
29
+ * action to `report` instead — the honesty requirement is enforced here, at
30
+ * the one place a bad rule table can't route around it, not only trusted to
31
+ * rule authors.
32
+ */
33
+ import { exec } from "node:child_process";
34
+ import { promisify } from "node:util";
35
+ import {
36
+ evaluatePredicate,
37
+ type ConvergeRule,
38
+ type RuleAction,
39
+ DEFAULT_FLAP_THRESHOLD,
40
+ } from "../converge-rule";
41
+ import { classifyOpVerbClass, type OpVerbClass } from "../op-verb-class";
42
+ import { discoverOps } from "../discover";
43
+ import { deriveSymptoms, type ConvergeSymptom } from "../../lifecycle/symptoms";
44
+ import {
45
+ appendConvergeRecord,
46
+ readConvergeLedger,
47
+ consecutiveRuleFires,
48
+ componentVerdicts,
49
+ sanitizeLedgerText,
50
+ type ConvergeRuleOutcome,
51
+ type ConvergeTickRecord,
52
+ } from "../../lifecycle/converge-ledger";
53
+ import { resolveApprovalUrl } from "../../lifecycle/gate-ledger";
54
+ import { fetchLifecycle, pushLifecycle } from "../../lifecycle/git";
55
+ import type { ChangeSet } from "../../lifecycle/change-set";
56
+ import type { ComponentStatusRow } from "../../lifecycle/status";
57
+
58
+ const execAsync = promisify(exec);
59
+
60
+ /** A `ConvergeRule<ConvergeSymptom>`, restated with `S = ConvergeSymptom` fixed — the JSON shape `ConvergeOp` bakes into the Op's `convergeTick` step args. */
61
+ export type SerializedConvergeRule = ConvergeRule<ConvergeSymptom>;
62
+
63
+ export interface ConvergeTickArgs {
64
+ /** Op name (`ConvergeOp`'s own `config.name`) — carried into the ledger record. */
65
+ opName: string;
66
+ env: string;
67
+ /**
68
+ * Authority level for this environment (the issue's dial × verb-class
69
+ * matrix): `"observe"` never dispatches a mutating/destructive op
70
+ * (report-only); `"reconcile"` free-runs a read-only op but never a
71
+ * mutating one — the issue's table answer for `reconcile` × mutating is
72
+ * "open PR", which v1 doesn't implement (epic #1487's onDrift-channel open
73
+ * question), so `reconcile` refuses that dispatch (`OPS014`, build time)
74
+ * rather than silently escalating it to "run directly"; `"apply"` is the
75
+ * only dial that free-runs a mutating op. A destructive op is refused
76
+ * under every dial, `"apply"` included, in v1 — see `OPS014`'s doc on why
77
+ * "destructive + apply + gated" can never actually dispatch.
78
+ */
79
+ dial: "observe" | "reconcile" | "apply";
80
+ /** Max number of `run` dispatches this tick may perform; remaining matched rules are recorded `skipped-budget`. */
81
+ budget: number;
82
+ /** The rule table, in authored order. */
83
+ rules: SerializedConvergeRule[];
84
+ /**
85
+ * `Observe` phase's quick drift signal, threaded in via a step-output
86
+ * reference rather than a search-attribute round-trip (#1290) — a
87
+ * diagnostic cross-check folded into the tick record, not a control input
88
+ * (this activity re-derives its own symptom independently).
89
+ */
90
+ preflightDrift?: boolean;
91
+ }
92
+
93
+ export interface ConvergeTickResult {
94
+ /** The ledger record's tick id (#2027) — what a caller correlates this tick's outcomes, gate facts and remediations against. */
95
+ id: string;
96
+ drifted: boolean;
97
+ remediated: number;
98
+ reported: number;
99
+ skippedBudget: number;
100
+ skippedFlap: number;
101
+ unobserved: number;
102
+ adopted: number;
103
+ /** Rules whose dispatch hit a gate this tick (#1485) — see `ConvergeRuleOutcome.action`'s doc. */
104
+ gated: number;
105
+ /** The one human-readable summary line this tick produced. */
106
+ log: string;
107
+ }
108
+
109
+ function shellQuote(s: string): string {
110
+ return `'${s.replace(/'/g, "'\\''")}'`;
111
+ }
112
+
113
+ /**
114
+ * Cap a subprocess failure's raw output to one sanitized line — a ledger
115
+ * record (`converge-ledger.ts`'s `appendConvergeRecord`) is one line of JSON,
116
+ * and a multi-line/oversized `stderr` folded straight into `outcome.reason`
117
+ * would break that. Not a fix for the ledger-durability bug (#1936,
118
+ * `writeBlobToPath`'s corruption under concurrent writes — fixed at the root
119
+ * in a separate PR); this only keeps one dispatch failure's own message from
120
+ * corrupting the line it's written into.
121
+ *
122
+ * The rule now lives next to the record it protects (`sanitizeLedgerText`,
123
+ * #2027), since a component verdict's `detail` needs the same cap; this stays
124
+ * as the dispatch path's name for it.
125
+ */
126
+ export const sanitizeOneLine = sanitizeLedgerText;
127
+
128
+ /** Run `chant lifecycle plan <env> --live --json` and parse its `ChangeSet`. */
129
+ async function observeChangeSet(env: string, signal?: AbortSignal): Promise<ChangeSet> {
130
+ const { stdout } = await execAsync(`chant lifecycle plan ${shellQuote(env)} --live --json`, { signal });
131
+ return JSON.parse(stdout) as ChangeSet;
132
+ }
133
+
134
+ /** Run `chant components status <env> --live --json` and parse its rows. */
135
+ async function observeStatusRows(env: string, signal?: AbortSignal): Promise<ComponentStatusRow[]> {
136
+ const { stdout } = await execAsync(`chant components status ${shellQuote(env)} --live --json`, { signal });
137
+ const parsed = JSON.parse(stdout) as ComponentStatusRow[] | { rows: ComponentStatusRow[] };
138
+ return Array.isArray(parsed) ? parsed : parsed.rows;
139
+ }
140
+
141
+ /**
142
+ * `chant run <op>`'s exit code for a run that stopped at an unapproved gate
143
+ * (#2119 — `../../../../../packages/core/src/cli/handlers/run.ts`'s
144
+ * `GATED_EXIT_CODE`). The typed signal that replaced the old string match
145
+ * against `LocalGateUnsupportedError`'s message: since #2119 the executor
146
+ * records the pending fact itself and ends with `status: "gated"`, so a tick
147
+ * no longer has to reconstruct a gate from an error string, and no longer
148
+ * writes the pending fact on the executor's behalf.
149
+ */
150
+ const GATED_EXIT_CODE = 3;
151
+
152
+ /**
153
+ * The gate a gated `chant run` stopped at, read off its JSON result (#2119) —
154
+ * pure, unit-tested directly rather than only through `dispatchOp`'s
155
+ * subprocess plumbing. `undefined` when the output says nothing about a gate.
156
+ */
157
+ export function classifyDispatchFailure(raw: string): { gateName: string } | undefined {
158
+ for (const line of raw.split("\n")) {
159
+ const trimmed = line.trim();
160
+ if (!trimmed.startsWith("{")) continue;
161
+ try {
162
+ const parsed = JSON.parse(trimmed) as { status?: string; gate?: { gate?: string } };
163
+ if (parsed.status === "gated" && typeof parsed.gate?.gate === "string") {
164
+ return { gateName: parsed.gate.gate };
165
+ }
166
+ } catch {
167
+ // Not this line's JSON — keep looking.
168
+ }
169
+ }
170
+ const match = /is gated on "([^"]+)"/.exec(raw);
171
+ return match ? { gateName: match[1] } : undefined;
172
+ }
173
+
174
+ /**
175
+ * Dispatch one matched `run` action via the existing local runner (`chant run
176
+ * <op> --json`). A gated target Op is gate-as-fact (#1485, closed by #2119):
177
+ * the target's own executor reads the gate ledger, records the pending fact,
178
+ * and exits {@link GATED_EXIT_CODE} — so this tick reads the gate off that
179
+ * result rather than parsing a refusal message, and records the `"gated"`
180
+ * outcome that names it. The pending fact itself is the executor's to write;
181
+ * duplicating it here is what "call the same executor code" rules out.
182
+ */
183
+ async function dispatchOp(
184
+ opName: string,
185
+ signal?: AbortSignal,
186
+ ): Promise<{ ok: true } | { ok: false; error: string; gateName?: string }> {
187
+ try {
188
+ await execAsync(`chant run ${shellQuote(opName)} --json`, { signal });
189
+ return { ok: true };
190
+ } catch (err) {
191
+ const e = err as { code?: number; stdout?: string; stderr?: string; message?: string };
192
+ const raw = e.stderr ?? e.message ?? String(err);
193
+ if (e.code === GATED_EXIT_CODE) {
194
+ const gate = classifyDispatchFailure(`${e.stdout ?? ""}\n${raw}`);
195
+ if (gate) return { ok: false, error: sanitizeOneLine(raw), gateName: gate.gateName };
196
+ }
197
+ return { ok: false, error: sanitizeOneLine(raw) };
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Verb class allowed to free-run for a given dial — the coarse first-pass
203
+ * gate: `"observe"` never dispatches anything (report only), `"reconcile"`
204
+ * and `"apply"` both allow *some* dispatch. Which verb classes each of those
205
+ * two actually permits is `OPS014`'s job at build time
206
+ * (`../../lint/post-synth/tmp014-converge-rule-refusals.ts`) and
207
+ * {@link verbClassAllowedToDispatch}'s job as this tick's own runtime
208
+ * backstop, below.
209
+ */
210
+ function dialAllowsDispatch(dial: ConvergeTickArgs["dial"]): boolean {
211
+ return dial === "reconcile" || dial === "apply";
212
+ }
213
+
214
+ /**
215
+ * Runtime backstop for issue #1484's Autonomy table (pre-merge review of
216
+ * #1954): `OPS014` already refuses, at build time, a rule table shaped to
217
+ * reach this point with a mutating dispatch outside `"apply"` or any
218
+ * destructive dispatch at all. This is the defense-in-depth check for a rule
219
+ * table that reached `convergeTick` without going through that build — the
220
+ * same "not authored through the checked path" concern
221
+ * `isWellFormedPredicate` already guards for a hand-assembled predicate (see
222
+ * `../../../../packages/core/src/op/converge-rule.ts`'s doc). Fail-closed:
223
+ * an unclassifiable target (`verbClass === undefined`, e.g. the target op
224
+ * couldn't be discovered/read) is never treated as safe to free-run — same
225
+ * fail-closed stance `classifyOpVerbClass` itself takes for an unrecognized
226
+ * activity `fn`.
227
+ */
228
+ export function verbClassAllowedToDispatch(dial: ConvergeTickArgs["dial"], verbClass: OpVerbClass | undefined): boolean {
229
+ const effective = verbClass ?? "mutating";
230
+ if (effective === "read-only") return true;
231
+ if (effective === "mutating") return dial === "apply";
232
+ return false; // destructive: refused at runtime, unconditionally — see OPS014's doc on why the gate can never actually run.
233
+ }
234
+
235
+ /**
236
+ * Apply {@link verbClassAllowedToDispatch} to one planned "ran" outcome,
237
+ * downgrading it to "reported" (never silently dropped, never silently run)
238
+ * when the dial/verb-class pairing isn't one the tick may actually free-run.
239
+ * Pure — the impure part (discovering and classifying the target op) is
240
+ * {@link classifyDispatchTarget}, called by `convergeTick` before this.
241
+ */
242
+ export function enforceVerbClassAtDispatch(
243
+ outcome: ConvergeRuleOutcome,
244
+ dial: ConvergeTickArgs["dial"],
245
+ verbClass: OpVerbClass | undefined,
246
+ ): ConvergeRuleOutcome {
247
+ if (outcome.action !== "ran") return outcome;
248
+ if (verbClassAllowedToDispatch(dial, verbClass)) return outcome;
249
+ return {
250
+ ...outcome,
251
+ action: "reported",
252
+ reason:
253
+ `runtime backstop: dial "${dial}" does not permit dispatching "${outcome.op}" ` +
254
+ `(${verbClass ?? "unclassifiable — treated as mutating, fail-closed"}) — OPS014 should already refuse ` +
255
+ `this at build; reporting instead of risking a silent authority escalation`,
256
+ };
257
+ }
258
+
259
+ /** Discover and classify a dispatch target's own verb class. Never throws: an op that can't be discovered/read classifies as `undefined` (fail-closed via {@link verbClassAllowedToDispatch}), not silently as `"read-only"`. */
260
+ async function classifyDispatchTarget(opName: string): Promise<OpVerbClass | undefined> {
261
+ try {
262
+ const { ops } = await discoverOps();
263
+ const target = ops.get(opName);
264
+ return target ? classifyOpVerbClass(target.config) : undefined;
265
+ } catch {
266
+ return undefined;
267
+ }
268
+ }
269
+
270
+ /**
271
+ * The pure planning core: given a symptom, a rule table, prior-tick fire
272
+ * history, the dial, and the remaining budget, decide what each rule does
273
+ * this tick. Exported and unit-tested directly — the activity function
274
+ * around it is the thin I/O shell (gather inputs, execute the plan, append
275
+ * the ledger record), matching `reconcilePr`'s split between its pure
276
+ * `reconcileSummary`/`entriesFromPlan` and its I/O body.
277
+ */
278
+ export interface ConvergeTickPlan {
279
+ firedRuleIds: string[];
280
+ outcomes: ConvergeRuleOutcome[];
281
+ }
282
+
283
+ export function planConvergeTick(
284
+ symptom: ConvergeSymptom,
285
+ rules: SerializedConvergeRule[],
286
+ priorRecords: ConvergeTickRecord[],
287
+ dial: ConvergeTickArgs["dial"],
288
+ budget: number,
289
+ ): ConvergeTickPlan {
290
+ const firedRuleIds: string[] = [];
291
+ const outcomes: ConvergeRuleOutcome[] = [];
292
+ let dispatched = 0;
293
+
294
+ for (const rule of rules) {
295
+ if (!evaluatePredicate(rule.when, symptom)) continue;
296
+ firedRuleIds.push(rule.id);
297
+
298
+ const threshold = rule.flapThreshold ?? DEFAULT_FLAP_THRESHOLD;
299
+ const consecutive = consecutiveRuleFires(priorRecords, rule.id);
300
+ if (consecutive >= threshold) {
301
+ outcomes.push({
302
+ ruleId: rule.id,
303
+ action: "skipped-flap",
304
+ reason: `fired ${consecutive + 1} consecutive ticks without clearing (threshold ${threshold}) — escalated to report`,
305
+ });
306
+ continue;
307
+ }
308
+
309
+ const action: RuleAction = rule.then;
310
+
311
+ // `unknown` never remediates, regardless of what the rule says.
312
+ if (symptom.status === "unknown" && action.kind === "run") {
313
+ outcomes.push({
314
+ ruleId: rule.id,
315
+ action: "reported",
316
+ reason: `symptom is unknown (${symptom.unobservedReasons.join(", ") || "unobserved"}) — unknown never remediates`,
317
+ });
318
+ continue;
319
+ }
320
+
321
+ if (action.kind === "report") {
322
+ outcomes.push({ ruleId: rule.id, action: "reported", reason: action.reason });
323
+ continue;
324
+ }
325
+
326
+ // action.kind === "run"
327
+ if (!dialAllowsDispatch(dial)) {
328
+ outcomes.push({ ruleId: rule.id, action: "reported", reason: `dial "${dial}" does not permit dispatch — report only`, op: action.op });
329
+ continue;
330
+ }
331
+ if (dispatched >= budget) {
332
+ outcomes.push({ ruleId: rule.id, action: "skipped-budget", op: action.op });
333
+ continue;
334
+ }
335
+ dispatched++;
336
+ outcomes.push({ ruleId: rule.id, action: "ran", op: action.op });
337
+ }
338
+
339
+ return { firedRuleIds, outcomes };
340
+ }
341
+
342
+ function summarizeOutcomes(outcomes: ConvergeRuleOutcome[]) {
343
+ return {
344
+ remediated: outcomes.filter((o) => o.action === "ran").length,
345
+ reported: outcomes.filter((o) => o.action === "reported").length,
346
+ skippedBudget: outcomes.filter((o) => o.action === "skipped-budget").length,
347
+ skippedFlap: outcomes.filter((o) => o.action === "skipped-flap").length,
348
+ gated: outcomes.filter((o) => o.action === "gated").length,
349
+ };
350
+ }
351
+
352
+ function renderLog(env: string, s: ConvergeSymptom, counts: ReturnType<typeof summarizeOutcomes>): string {
353
+ return (
354
+ `converge(${env}): drifted=${s.updateCount + s.deleteCount} remediated=${counts.remediated} ` +
355
+ `reported=${counts.reported} skipped-budget=${counts.skippedBudget} skipped-flap=${counts.skippedFlap} ` +
356
+ `gated=${counts.gated} unobserved=${s.unobservedCount} adopted=${s.adoptCount}`
357
+ );
358
+ }
359
+
360
+ export async function convergeTick(args: ConvergeTickArgs, signal?: AbortSignal): Promise<ConvergeTickResult> {
361
+ await fetchLifecycle().catch(() => undefined);
362
+
363
+ const [cs, statusRows, ledger] = await Promise.all([
364
+ observeChangeSet(args.env, signal),
365
+ observeStatusRows(args.env, signal),
366
+ readConvergeLedger(args.env),
367
+ ]);
368
+
369
+ const symptom = deriveSymptoms(args.env, cs, statusRows);
370
+ const plan = planConvergeTick(symptom, args.rules, ledger.records, args.dial, args.budget);
371
+
372
+ // Execute: only "ran" outcomes cause a subprocess dispatch. Before that
373
+ // dispatch, the runtime backstop (Finding A, #1954 pre-merge review)
374
+ // re-classifies the target and downgrades to "reported" if this dial/verb
375
+ // class pairing was never supposed to reach dispatch — OPS014 (build time)
376
+ // is the primary defense; this is what catches a rule table that reached
377
+ // `convergeTick` without going through it.
378
+ for (const outcome of plan.outcomes) {
379
+ if (outcome.action !== "ran" || !outcome.op) continue;
380
+
381
+ const verbClass = await classifyDispatchTarget(outcome.op);
382
+ const backstopped = enforceVerbClassAtDispatch(outcome, args.dial, verbClass);
383
+ outcome.action = backstopped.action;
384
+ outcome.reason = backstopped.reason;
385
+ if (outcome.action !== "ran") continue;
386
+
387
+ const result = await dispatchOp(outcome.op, signal);
388
+ if (!result.ok && result.gateName) {
389
+ // Gate-as-fact (#1485, closed by #2119): a terminal, durable,
390
+ // non-blocking fact — not an ordinary dispatch failure the operator
391
+ // keeps retrying. The target's own executor already wrote the pending
392
+ // record to `_gates/<op>.jsonl`; this outcome is the tick's account of
393
+ // the same event. See ../../../../../packages/core/src/lifecycle/
394
+ // gate-ledger.ts for how it resolves (`chant approve`, or a merged PR).
395
+ outcome.action = "gated";
396
+ outcome.gateName = result.gateName;
397
+ // #2028: the pending fact carries its address when this tick is running
398
+ // somewhere that has one (a PR/MR job). Absent otherwise — never guessed.
399
+ const url = resolveApprovalUrl();
400
+ if (url) outcome.url = url;
401
+ outcome.reason =
402
+ `dispatch of "${outcome.op}" hit gate "${result.gateName}" — recorded as a pending fact, not retried; ` +
403
+ `resolve with \`chant approve ${outcome.op} ${result.gateName}\`${url ? ` or by merging ${url}` : " or a merged PR"}`;
404
+ } else if (!result.ok) {
405
+ outcome.action = "reported";
406
+ outcome.reason = `dispatch of "${outcome.op}" failed: ${result.error}`;
407
+ }
408
+ }
409
+
410
+ const counts = summarizeOutcomes(plan.outcomes);
411
+ const log = renderLog(args.env, symptom, counts);
412
+ const timestamp = new Date().toISOString();
413
+
414
+ const { record } = await appendConvergeRecord({
415
+ op: args.opName,
416
+ env: args.env,
417
+ timestamp,
418
+ firedRuleIds: plan.firedRuleIds,
419
+ outcomes: plan.outcomes,
420
+ // #2027: the per-component verdicts this tick already observed, kept
421
+ // instead of discarded once `summary`'s counts were derived from them.
422
+ components: componentVerdicts(statusRows),
423
+ summary: {
424
+ drifted: symptom.updateCount + symptom.deleteCount,
425
+ remediated: counts.remediated,
426
+ reported: counts.reported,
427
+ skippedBudget: counts.skippedBudget,
428
+ skippedFlap: counts.skippedFlap,
429
+ gated: counts.gated,
430
+ unobserved: symptom.unobservedCount,
431
+ adopted: symptom.adoptCount,
432
+ },
433
+ log,
434
+ });
435
+ await pushLifecycle().catch(() => undefined);
436
+
437
+ return {
438
+ id: record.id,
439
+ drifted: symptom.status === "drifted",
440
+ remediated: record.summary.remediated,
441
+ reported: record.summary.reported,
442
+ skippedBudget: record.summary.skippedBudget,
443
+ skippedFlap: record.summary.skippedFlap,
444
+ gated: record.summary.gated ?? 0,
445
+ unobserved: record.summary.unobserved,
446
+ adopted: record.summary.adopted,
447
+ log: record.log,
448
+ };
449
+ }
@@ -0,0 +1,187 @@
1
+ import { describe, test, expect, vi } from "vitest";
2
+ import { envTeardown } from "./env-teardown";
3
+ import type { ObservationLexicon, TeardownEnumeration, TeardownExecution } from "../../lexicon";
4
+ import type { ChantConfig } from "../../config";
5
+
6
+ /**
7
+ * The activity is exercised through the REAL core engine
8
+ * (`executeTeardown` from @intentius/chant/lifecycle/teardown) — only the
9
+ * config and the lexicon plugin are injected. What's asserted is therefore
10
+ * the whole path an Op step takes: guards, plan, execution, retry, report.
11
+ */
12
+
13
+ const projectConfig = (overrides: Partial<ChantConfig> = {}): ChantConfig =>
14
+ ({
15
+ environments: ["dev", "prod"],
16
+ ownership: { stack: "shop" },
17
+ ...overrides,
18
+ }) as ChantConfig;
19
+
20
+ const mockPlugin = (overrides: Record<string, unknown>): ObservationLexicon =>
21
+ ({ name: "mock", ...overrides }) as unknown as ObservationLexicon;
22
+
23
+ const enumeration: TeardownEnumeration = {
24
+ candidates: [
25
+ { name: "web", type: "K8s::Apps::Deployment", physicalId: "web-1", marker: { stack: "shop", env: "dev" } },
26
+ { name: "cache", type: "K8s::Core::Service", marker: { stack: "shop", env: "dev" } },
27
+ ],
28
+ };
29
+
30
+ describe("envTeardown — the happy path through the real core engine", () => {
31
+ test("plans by marker, deletes through the plugin, and reports counts", async () => {
32
+ const teardownOwned = vi.fn(async () => enumeration);
33
+ const executeTeardown = vi.fn(
34
+ async (args: { candidates: Array<{ name: string }> }): Promise<TeardownExecution> => ({
35
+ outcomes: args.candidates.map((c) => ({ name: c.name, outcome: "deleted" as const })),
36
+ }),
37
+ );
38
+ const plugin = mockPlugin({ teardownOwned, executeTeardown });
39
+
40
+ const result = await envTeardown({ env: "dev" }, undefined, {
41
+ config: projectConfig(),
42
+ plugins: [plugin],
43
+ });
44
+
45
+ expect(teardownOwned).toHaveBeenCalledWith({
46
+ environment: "dev",
47
+ marker: { stack: "shop", env: "dev" },
48
+ });
49
+ expect(executeTeardown).toHaveBeenCalledTimes(1);
50
+ expect(executeTeardown.mock.calls[0]![0]).toMatchObject({
51
+ environment: "dev",
52
+ marker: { stack: "shop", env: "dev" },
53
+ });
54
+ expect(result.environment).toBe("dev");
55
+ expect(result.stack).toBe("shop");
56
+ expect(result.deleted).toBe(2);
57
+ expect(result.notPrunable).toBe(0);
58
+ expect(result.skipped).toBe(0);
59
+ expect(result.holes).toBe(0);
60
+ expect(result.report.outcomes.map((o) => o.outcome)).toEqual(["deleted", "deleted"]);
61
+ });
62
+
63
+ test("a candidate whose marker is foreign never reaches execution — core drops it", async () => {
64
+ const teardownOwned = vi.fn(async (): Promise<TeardownEnumeration> => ({
65
+ candidates: [
66
+ { name: "mine", type: "T", marker: { stack: "shop", env: "dev" } },
67
+ { name: "other-env", type: "T", marker: { stack: "shop", env: "prod" } },
68
+ ],
69
+ }));
70
+ const executeTeardown = vi.fn(
71
+ async (args: { candidates: Array<{ name: string }> }): Promise<TeardownExecution> => ({
72
+ outcomes: args.candidates.map((c) => ({ name: c.name, outcome: "deleted" as const })),
73
+ }),
74
+ );
75
+
76
+ const result = await envTeardown({ env: "dev" }, undefined, {
77
+ config: projectConfig(),
78
+ plugins: [mockPlugin({ teardownOwned, executeTeardown })],
79
+ });
80
+
81
+ expect(executeTeardown.mock.calls[0]![0].candidates.map((c) => c.name)).toEqual(["mine"]);
82
+ expect(result.deleted).toBe(1);
83
+ });
84
+ });
85
+
86
+ describe("envTeardown — guards (the CLI's guards, unchanged)", () => {
87
+ test("refuses a prod-like environment without confirmProd: true, before any live read", async () => {
88
+ const teardownOwned = vi.fn(async () => enumeration);
89
+ await expect(
90
+ envTeardown({ env: "prod" }, undefined, {
91
+ config: projectConfig(),
92
+ plugins: [mockPlugin({ teardownOwned })],
93
+ }),
94
+ ).rejects.toThrow(/looks like a production environment.*confirmProd: true/);
95
+ expect(teardownOwned).not.toHaveBeenCalled();
96
+ });
97
+
98
+ test("confirmProd: true releases the prod guard", async () => {
99
+ const teardownOwned = vi.fn(async (): Promise<TeardownEnumeration> => ({
100
+ candidates: [{ name: "web", type: "T", marker: { stack: "shop", env: "prod" } }],
101
+ }));
102
+ const executeTeardown = vi.fn(async (): Promise<TeardownExecution> => ({
103
+ outcomes: [{ name: "web", outcome: "deleted" }],
104
+ }));
105
+
106
+ const result = await envTeardown({ env: "prod", confirmProd: true }, undefined, {
107
+ config: projectConfig(),
108
+ plugins: [mockPlugin({ teardownOwned, executeTeardown })],
109
+ });
110
+
111
+ expect(result.deleted).toBe(1);
112
+ });
113
+
114
+ test("a truthy-but-not-true confirmProd does not count", async () => {
115
+ await expect(
116
+ envTeardown({ env: "prod", confirmProd: "yes" as unknown as boolean }, undefined, {
117
+ config: projectConfig(),
118
+ plugins: [],
119
+ }),
120
+ ).rejects.toThrow(/confirmProd: true/);
121
+ });
122
+
123
+ test("refuses an environment the project does not declare", async () => {
124
+ const teardownOwned = vi.fn(async () => enumeration);
125
+ await expect(
126
+ envTeardown({ env: "qa" }, undefined, {
127
+ config: projectConfig(),
128
+ plugins: [mockPlugin({ teardownOwned })],
129
+ }),
130
+ ).rejects.toThrow(/Unknown environment "qa"/);
131
+ expect(teardownOwned).not.toHaveBeenCalled();
132
+ });
133
+
134
+ test("refuses a project with no ownership.stack — nothing to select on", async () => {
135
+ await expect(
136
+ envTeardown({ env: "dev" }, undefined, {
137
+ config: projectConfig({ ownership: undefined }),
138
+ plugins: [],
139
+ }),
140
+ ).rejects.toThrow(/ownership\.stack/);
141
+ });
142
+ });
143
+
144
+ describe("envTeardown — failures fail the activity", () => {
145
+ test("a candidate still failed after core's retry pass throws, naming it", async () => {
146
+ const teardownOwned = vi.fn(async (): Promise<TeardownEnumeration> => ({
147
+ candidates: [
148
+ { name: "web", type: "T", marker: { stack: "shop", env: "dev" } },
149
+ { name: "cache", type: "T", marker: { stack: "shop", env: "dev" } },
150
+ ],
151
+ }));
152
+ const executeTeardown = vi.fn(
153
+ async (args: { candidates: Array<{ name: string }> }): Promise<TeardownExecution> => ({
154
+ outcomes: args.candidates.map((c) =>
155
+ c.name === "web"
156
+ ? { name: c.name, outcome: "failed" as const, detail: "409 conflict" }
157
+ : { name: c.name, outcome: "deleted" as const },
158
+ ),
159
+ }),
160
+ );
161
+
162
+ await expect(
163
+ envTeardown({ env: "dev" }, undefined, {
164
+ config: projectConfig(),
165
+ plugins: [mockPlugin({ teardownOwned, executeTeardown })],
166
+ }),
167
+ ).rejects.toThrow(/1 of 2 candidate\(s\) still failed.*mock\/web.*409 conflict/);
168
+ // First pass over both, then core's one bounded retry pass over the failure.
169
+ expect(executeTeardown).toHaveBeenCalledTimes(2);
170
+ expect(executeTeardown.mock.calls[1]![0].candidates.map((c) => c.name)).toEqual(["web"]);
171
+ });
172
+
173
+ test("a lexicon that enumerates but cannot execute reports skipped, not clean", async () => {
174
+ const teardownOwned = vi.fn(async (): Promise<TeardownEnumeration> => ({
175
+ candidates: [{ name: "web", type: "T", marker: { stack: "shop", env: "dev" } }],
176
+ }));
177
+
178
+ const result = await envTeardown({ env: "dev" }, undefined, {
179
+ config: projectConfig(),
180
+ plugins: [mockPlugin({ teardownOwned })],
181
+ });
182
+
183
+ expect(result.deleted).toBe(0);
184
+ expect(result.skipped).toBe(1);
185
+ expect(result.report.unimplemented).toEqual(["mock"]);
186
+ });
187
+ });