@intentius/chant 0.57.0 → 0.59.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 (558) 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 +36 -1
  4. package/dist/audit/core.d.ts.map +1 -1
  5. package/dist/audit/discover.d.ts +26 -9
  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/carve-bridge.d.ts.map +1 -1
  34. package/dist/cli/commands/check-lexicon-docs.d.ts +1 -1
  35. package/dist/cli/commands/check-lexicon-examples.d.ts +16 -6
  36. package/dist/cli/commands/check-lexicon-examples.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 +13 -0
  44. package/dist/cli/commands/lint.d.ts.map +1 -1
  45. package/dist/cli/commands/onboard.d.ts +2 -0
  46. package/dist/cli/commands/onboard.d.ts.map +1 -1
  47. package/dist/cli/handlers/build.d.ts.map +1 -1
  48. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  49. package/dist/cli/handlers/operator.d.ts +82 -16
  50. package/dist/cli/handlers/operator.d.ts.map +1 -1
  51. package/dist/cli/handlers/run.d.ts +49 -42
  52. package/dist/cli/handlers/run.d.ts.map +1 -1
  53. package/dist/cli/main.d.ts +12 -1
  54. package/dist/cli/main.d.ts.map +1 -1
  55. package/dist/cli/mcp/op-tools.d.ts +16 -1
  56. package/dist/cli/mcp/op-tools.d.ts.map +1 -1
  57. package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
  58. package/dist/cli/mcp/resources/context.d.ts.map +1 -1
  59. package/dist/cli/plugins.d.ts +22 -0
  60. package/dist/cli/plugins.d.ts.map +1 -1
  61. package/dist/cli/registry.d.ts +40 -9
  62. package/dist/cli/registry.d.ts.map +1 -1
  63. package/dist/codegen/generate-post-synth-barrel.d.ts +1 -1
  64. package/dist/components/auto-release.d.ts +10 -20
  65. package/dist/components/auto-release.d.ts.map +1 -1
  66. package/dist/components/capability.d.ts +4 -4
  67. package/dist/components/cli-support.d.ts +24 -22
  68. package/dist/components/cli-support.d.ts.map +1 -1
  69. package/dist/components/component.d.ts +21 -6
  70. package/dist/components/component.d.ts.map +1 -1
  71. package/dist/components/driver-output.d.ts.map +1 -1
  72. package/dist/components/driver.d.ts +73 -37
  73. package/dist/components/driver.d.ts.map +1 -1
  74. package/dist/components/index.d.ts +1 -2
  75. package/dist/components/index.d.ts.map +1 -1
  76. package/dist/components/manifest-persistence.d.ts +1 -9
  77. package/dist/components/manifest-persistence.d.ts.map +1 -1
  78. package/dist/components/run-progress.d.ts +6 -6
  79. package/dist/components/run-progress.d.ts.map +1 -1
  80. package/dist/components/verbs/run-agent.d.ts +10 -22
  81. package/dist/components/verbs/run-agent.d.ts.map +1 -1
  82. package/dist/components/verbs/verify.d.ts +2 -2
  83. package/dist/deep-observation.d.ts +17 -0
  84. package/dist/deep-observation.d.ts.map +1 -1
  85. package/dist/detectLexicon.d.ts +13 -0
  86. package/dist/detectLexicon.d.ts.map +1 -1
  87. package/dist/discovery/files.d.ts +4 -4
  88. package/dist/discovery/fold-import.d.ts.map +1 -1
  89. package/dist/discovery/param-deps.d.ts +33 -0
  90. package/dist/discovery/param-deps.d.ts.map +1 -1
  91. package/dist/discovery/sandbox/bundle.d.ts.map +1 -1
  92. package/dist/discovery/sandbox/config-wire.d.ts +2 -2
  93. package/dist/fold/foldable-helpers.d.ts +5 -1
  94. package/dist/fold/foldable-helpers.d.ts.map +1 -1
  95. package/dist/fold-provenance.d.ts +236 -0
  96. package/dist/fold-provenance.d.ts.map +1 -0
  97. package/dist/graph-ops.d.ts +1 -1
  98. package/dist/held-elsewhere.d.ts +85 -0
  99. package/dist/held-elsewhere.d.ts.map +1 -0
  100. package/dist/index.d.ts +3 -0
  101. package/dist/index.d.ts.map +1 -1
  102. package/dist/kubectl-context.d.ts +3 -3
  103. package/dist/lexicon-config.d.ts +4 -4
  104. package/dist/lexicon-plugin-helpers.d.ts +1 -1
  105. package/dist/lexicon.d.ts +273 -16
  106. package/dist/lexicon.d.ts.map +1 -1
  107. package/dist/lifecycle/change-set.d.ts +13 -0
  108. package/dist/lifecycle/change-set.d.ts.map +1 -1
  109. package/dist/lifecycle/deep-diff.d.ts +133 -6
  110. package/dist/lifecycle/deep-diff.d.ts.map +1 -1
  111. package/dist/lifecycle/deep-observe.d.ts.map +1 -1
  112. package/dist/lifecycle/gate-ledger.d.ts +79 -3
  113. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  114. package/dist/lifecycle/index.d.ts +1 -0
  115. package/dist/lifecycle/index.d.ts.map +1 -1
  116. package/dist/lifecycle/release-ledger.d.ts +2 -2
  117. package/dist/lifecycle/run-ledger.d.ts +53 -0
  118. package/dist/lifecycle/run-ledger.d.ts.map +1 -0
  119. package/dist/lifecycle/symptoms.d.ts +3 -3
  120. package/dist/lifecycle/symptoms.d.ts.map +1 -1
  121. package/dist/lint/config.d.ts +81 -2
  122. package/dist/lint/config.d.ts.map +1 -1
  123. package/dist/lint/post-synth.d.ts +55 -2
  124. package/dist/lint/post-synth.d.ts.map +1 -1
  125. package/dist/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.d.ts +1 -1
  126. package/dist/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.d.ts.map +1 -1
  127. package/dist/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.d.ts +1 -1
  128. package/dist/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.d.ts.map +1 -1
  129. package/dist/lint/rules/comp/comp004-gate-needs-durable-runtime.d.ts +44 -0
  130. package/dist/lint/rules/comp/comp004-gate-needs-durable-runtime.d.ts.map +1 -0
  131. package/dist/lint/rules/comp/index.d.ts +1 -1
  132. package/dist/lint/rules/comp/index.d.ts.map +1 -1
  133. package/dist/lint/rules/op/index.d.ts +16 -0
  134. package/dist/lint/rules/op/index.d.ts.map +1 -0
  135. package/dist/lint/rules/op/ops012-activity-contract.d.ts +27 -0
  136. package/dist/lint/rules/op/ops012-activity-contract.d.ts.map +1 -0
  137. package/dist/lint/rules/op/ops013-step-output-ref.d.ts +61 -0
  138. package/dist/lint/rules/op/ops013-step-output-ref.d.ts.map +1 -0
  139. package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts +55 -0
  140. package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts.map +1 -0
  141. package/dist/lint/rules/op/support.d.ts +12 -0
  142. package/dist/lint/rules/op/support.d.ts.map +1 -0
  143. package/dist/lint/suppressions.d.ts +108 -0
  144. package/dist/lint/suppressions.d.ts.map +1 -0
  145. package/dist/live-endpoint.d.ts +1 -1
  146. package/dist/op/activities/activity-contracts.d.ts +54 -0
  147. package/dist/op/activities/activity-contracts.d.ts.map +1 -0
  148. package/dist/op/activities/apply.d.ts +262 -0
  149. package/dist/op/activities/apply.d.ts.map +1 -0
  150. package/dist/op/activities/build.d.ts +13 -0
  151. package/dist/op/activities/build.d.ts.map +1 -0
  152. package/dist/op/activities/converge.d.ts +110 -0
  153. package/dist/op/activities/converge.d.ts.map +1 -0
  154. package/dist/op/activities/env-teardown.d.ts +76 -0
  155. package/dist/op/activities/env-teardown.d.ts.map +1 -0
  156. package/dist/op/activities/guard-validate.d.ts +64 -0
  157. package/dist/op/activities/guard-validate.d.ts.map +1 -0
  158. package/dist/op/activities/http-check.d.ts +34 -0
  159. package/dist/op/activities/http-check.d.ts.map +1 -0
  160. package/dist/op/activities/index.d.ts +31 -0
  161. package/dist/op/activities/index.d.ts.map +1 -0
  162. package/dist/op/activities/lexicon-upgrade.d.ts +182 -0
  163. package/dist/op/activities/lexicon-upgrade.d.ts.map +1 -0
  164. package/dist/op/activities/lifecycle.d.ts +42 -0
  165. package/dist/op/activities/lifecycle.d.ts.map +1 -0
  166. package/dist/op/activities/pipeline-audit.d.ts +84 -0
  167. package/dist/op/activities/pipeline-audit.d.ts.map +1 -0
  168. package/dist/op/activities/policy.d.ts +17 -0
  169. package/dist/op/activities/policy.d.ts.map +1 -0
  170. package/dist/op/activities/reconcile.d.ts +149 -0
  171. package/dist/op/activities/reconcile.d.ts.map +1 -0
  172. package/dist/op/activities/shell.d.ts +13 -0
  173. package/dist/op/activities/shell.d.ts.map +1 -0
  174. package/dist/op/activities/teardown.d.ts +10 -0
  175. package/dist/op/activities/teardown.d.ts.map +1 -0
  176. package/dist/op/activities/util.d.ts +7 -0
  177. package/dist/op/activities/util.d.ts.map +1 -0
  178. package/dist/op/activities/wait.d.ts +28 -0
  179. package/dist/op/activities/wait.d.ts.map +1 -0
  180. package/dist/op/activities/workflow-audit.d.ts +92 -0
  181. package/dist/op/activities/workflow-audit.d.ts.map +1 -0
  182. package/dist/op/activity-contract-registry.d.ts +76 -0
  183. package/dist/op/activity-contract-registry.d.ts.map +1 -0
  184. package/dist/op/activity-contract.d.ts +25 -18
  185. package/dist/op/activity-contract.d.ts.map +1 -1
  186. package/dist/op/activity-failure.d.ts +28 -0
  187. package/dist/op/activity-failure.d.ts.map +1 -0
  188. package/dist/op/activity-profiles.d.ts +118 -0
  189. package/dist/op/activity-profiles.d.ts.map +1 -0
  190. package/dist/op/activity-registry.d.ts +22 -38
  191. package/dist/op/activity-registry.d.ts.map +1 -1
  192. package/dist/op/activity-runtime.d.ts +3 -14
  193. package/dist/op/activity-runtime.d.ts.map +1 -1
  194. package/dist/op/builders.d.ts +85 -30
  195. package/dist/op/builders.d.ts.map +1 -1
  196. package/dist/op/change-signal.d.ts +91 -0
  197. package/dist/op/change-signal.d.ts.map +1 -0
  198. package/dist/op/composites/apply-op.d.ts +118 -0
  199. package/dist/op/composites/apply-op.d.ts.map +1 -0
  200. package/dist/op/composites/converge-op.d.ts +120 -0
  201. package/dist/op/composites/converge-op.d.ts.map +1 -0
  202. package/dist/op/composites/index.d.ts +26 -0
  203. package/dist/op/composites/index.d.ts.map +1 -0
  204. package/dist/op/composites/lexicon-upgrade-op.d.ts +78 -0
  205. package/dist/op/composites/lexicon-upgrade-op.d.ts.map +1 -0
  206. package/dist/op/composites/pipeline-audit-op.d.ts +50 -0
  207. package/dist/op/composites/pipeline-audit-op.d.ts.map +1 -0
  208. package/dist/op/composites/reconcile-op.d.ts +57 -0
  209. package/dist/op/composites/reconcile-op.d.ts.map +1 -0
  210. package/dist/op/composites/watch-op.d.ts +60 -0
  211. package/dist/op/composites/watch-op.d.ts.map +1 -0
  212. package/dist/op/composites/workflow-audit-op.d.ts +57 -0
  213. package/dist/op/composites/workflow-audit-op.d.ts.map +1 -0
  214. package/dist/op/converge-rule.d.ts +12 -10
  215. package/dist/op/converge-rule.d.ts.map +1 -1
  216. package/dist/op/cron.d.ts +51 -0
  217. package/dist/op/cron.d.ts.map +1 -0
  218. package/dist/op/discover.d.ts +9 -0
  219. package/dist/op/discover.d.ts.map +1 -1
  220. package/dist/op/duration.d.ts +11 -0
  221. package/dist/op/duration.d.ts.map +1 -0
  222. package/dist/op/emulator-lifecycle.d.ts.map +1 -1
  223. package/dist/op/gate-name.d.ts +40 -0
  224. package/dist/op/gate-name.d.ts.map +1 -0
  225. package/dist/op/gate-summary.d.ts +43 -0
  226. package/dist/op/gate-summary.d.ts.map +1 -0
  227. package/dist/op/gate.d.ts +90 -0
  228. package/dist/op/gate.d.ts.map +1 -0
  229. package/dist/op/generate-pipeline.d.ts +9 -0
  230. package/dist/op/generate-pipeline.d.ts.map +1 -1
  231. package/dist/op/index.d.ts +29 -9
  232. package/dist/op/index.d.ts.map +1 -1
  233. package/dist/op/local-executor.d.ts +114 -28
  234. package/dist/op/local-executor.d.ts.map +1 -1
  235. package/dist/op/local-output.d.ts +11 -1
  236. package/dist/op/local-output.d.ts.map +1 -1
  237. package/dist/op/op-ir.d.ts +185 -0
  238. package/dist/op/op-ir.d.ts.map +1 -0
  239. package/dist/op/op-verb-class.d.ts +10 -2
  240. package/dist/op/op-verb-class.d.ts.map +1 -1
  241. package/dist/op/operator.d.ts +93 -5
  242. package/dist/op/operator.d.ts.map +1 -1
  243. package/dist/op/receipt-store.d.ts +1 -1
  244. package/dist/op/receipt-store.d.ts.map +1 -1
  245. package/dist/op/resource.d.ts +15 -1
  246. package/dist/op/resource.d.ts.map +1 -1
  247. package/dist/op/runtime.d.ts +200 -0
  248. package/dist/op/runtime.d.ts.map +1 -0
  249. package/dist/op/runtimes/local.d.ts +29 -0
  250. package/dist/op/runtimes/local.d.ts.map +1 -0
  251. package/dist/op/step-output-ref.d.ts +15 -13
  252. package/dist/op/step-output-ref.d.ts.map +1 -1
  253. package/dist/op/types.d.ts +96 -29
  254. package/dist/op/types.d.ts.map +1 -1
  255. package/dist/ownership.d.ts +1 -1
  256. package/dist/provenance.d.ts +21 -0
  257. package/dist/provenance.d.ts.map +1 -1
  258. package/dist/serializer-walker.d.ts.map +1 -1
  259. package/dist/terraform/bridge.d.ts +26 -7
  260. package/dist/terraform/bridge.d.ts.map +1 -1
  261. package/dist/terraform/carve-provider.d.ts +28 -2
  262. package/dist/terraform/carve-provider.d.ts.map +1 -1
  263. package/dist/terraform/data-source-shape.d.ts +77 -0
  264. package/dist/terraform/data-source-shape.d.ts.map +1 -0
  265. package/dist/terraform/graph.d.ts.map +1 -1
  266. package/dist/terraform/providers/kubernetes.d.ts +5 -0
  267. package/dist/terraform/providers/kubernetes.d.ts.map +1 -1
  268. package/dist/terraform/tier-map.d.ts +16 -4
  269. package/dist/terraform/tier-map.d.ts.map +1 -1
  270. package/dist/terraform/types.d.ts +8 -0
  271. package/dist/terraform/types.d.ts.map +1 -1
  272. package/package.json +6 -1
  273. package/src/audit/catalog.test.ts +79 -3
  274. package/src/audit/catalog.ts +111 -13
  275. package/src/audit/core.test.ts +26 -0
  276. package/src/audit/core.ts +84 -18
  277. package/src/audit/discover.test.ts +39 -0
  278. package/src/audit/discover.ts +202 -18
  279. package/src/audit/lineage.ts +100 -0
  280. package/src/audit/prior-art.ts +153 -0
  281. package/src/audit/proof.test.ts +67 -0
  282. package/src/audit/proof.ts +70 -0
  283. package/src/audit/report-html.ts +9 -4
  284. package/src/audit/report-model.ts +16 -2
  285. package/src/audit/report.test.ts +33 -0
  286. package/src/audit/report.ts +5 -3
  287. package/src/audit/rules-doc.test.ts +34 -2
  288. package/src/audit/rules-doc.ts +82 -9
  289. package/src/audit/secrets.ts +15 -3
  290. package/src/audit/terraform-state.test.ts +144 -0
  291. package/src/audit/terraform-state.ts +131 -0
  292. package/src/build.ts +31 -1
  293. package/src/claimed-fields.test.ts +145 -0
  294. package/src/claimed-fields.ts +208 -0
  295. package/src/cli/build-params-cli.ts +2 -2
  296. package/src/cli/command-group.ts +14 -0
  297. package/src/cli/commands/__fixtures__/audit-terraform/infra/.terraform/providers/lock.txt +1 -0
  298. package/src/cli/commands/__fixtures__/audit-terraform/infra/main.tf +1 -0
  299. package/src/cli/commands/__fixtures__/audit-terraform/infra/modules/foo/main.tf +1 -0
  300. package/src/cli/commands/__fixtures__/audit-terraform/infra/variables.tf +3 -0
  301. package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +13 -4
  302. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/docs-cli.ts +4 -0
  303. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/package.ts +1 -0
  304. package/src/cli/commands/__fixtures__/init-lexicon-output/src/package-cli.ts +21 -0
  305. package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +1 -0
  306. package/src/cli/commands/audit.test.ts +191 -11
  307. package/src/cli/commands/audit.ts +78 -28
  308. package/src/cli/commands/build.test.ts +330 -9
  309. package/src/cli/commands/build.ts +170 -24
  310. package/src/cli/commands/carve-bridge.test.ts +237 -14
  311. package/src/cli/commands/carve-bridge.ts +21 -17
  312. package/src/cli/commands/carve-emit-k8s.test.ts +17 -10
  313. package/src/cli/commands/check-lexicon-docs.ts +1 -1
  314. package/src/cli/commands/check-lexicon-examples.test.ts +29 -1
  315. package/src/cli/commands/check-lexicon-examples.ts +42 -10
  316. package/src/cli/commands/init-lexicon/templates/codegen.ts +9 -0
  317. package/src/cli/commands/init-lexicon/templates/project.ts +44 -4
  318. package/src/cli/commands/init-lexicon.test.ts +97 -2
  319. package/src/cli/commands/init-lexicon.ts +4 -2
  320. package/src/cli/commands/init.test.ts +35 -1
  321. package/src/cli/commands/init.ts +14 -3
  322. package/src/cli/commands/lint.test.ts +194 -1
  323. package/src/cli/commands/lint.ts +205 -7
  324. package/src/cli/commands/onboard.test.ts +75 -0
  325. package/src/cli/commands/onboard.ts +54 -23
  326. package/src/cli/handlers/build.test.ts +81 -1
  327. package/src/cli/handlers/build.ts +15 -1
  328. package/src/cli/handlers/graph.test.ts +3 -3
  329. package/src/cli/handlers/lifecycle.test.ts +195 -12
  330. package/src/cli/handlers/lifecycle.ts +123 -6
  331. package/src/cli/handlers/misc.ts +1 -1
  332. package/src/cli/handlers/operator.test.ts +183 -11
  333. package/src/cli/handlers/operator.ts +341 -55
  334. package/src/cli/handlers/run.test.ts +568 -1295
  335. package/src/cli/handlers/run.ts +446 -912
  336. package/src/cli/main.test.ts +22 -16
  337. package/src/cli/main.ts +66 -33
  338. package/src/cli/mcp/op-tools.ts +147 -88
  339. package/src/cli/mcp/resource-handlers.ts +10 -37
  340. package/src/cli/mcp/resources/context.ts +11 -12
  341. package/src/cli/mcp/server.test.ts +25 -27
  342. package/src/cli/mcp/server.ts +2 -2
  343. package/src/cli/plugins.test.ts +68 -2
  344. package/src/cli/plugins.ts +39 -0
  345. package/src/cli/registry.ts +40 -9
  346. package/src/codegen/docs.ts +1 -1
  347. package/src/codegen/generate-post-synth-barrel.test.ts +1 -1
  348. package/src/codegen/generate-post-synth-barrel.ts +1 -1
  349. package/src/components/README.md +4 -3
  350. package/src/components/__fixtures__/neo4j-fanout.json +1 -1
  351. package/src/components/auto-release.test.ts +4 -20
  352. package/src/components/auto-release.ts +17 -42
  353. package/src/components/capability.ts +4 -4
  354. package/src/components/cli-support.test.ts +28 -12
  355. package/src/components/cli-support.ts +60 -47
  356. package/src/components/component-schema.test.ts +18 -2
  357. package/src/components/component.schema.json +18 -5
  358. package/src/components/component.test.ts +2 -2
  359. package/src/components/component.ts +27 -7
  360. package/src/components/config-defaults.test.ts +2 -2
  361. package/src/components/driver-output.ts +14 -2
  362. package/src/components/driver.test.ts +109 -20
  363. package/src/components/driver.ts +201 -84
  364. package/src/components/index.ts +1 -6
  365. package/src/components/manifest-persistence.test.ts +2 -20
  366. package/src/components/manifest-persistence.ts +4 -24
  367. package/src/components/pilots/neo4j-fanout.pilot.ts +3 -3
  368. package/src/components/pilots/pilots-lint-clean.test.ts +11 -10
  369. package/src/components/run-progress.ts +6 -6
  370. package/src/components/verbs/run-agent.test.ts +19 -0
  371. package/src/components/verbs/run-agent.ts +12 -24
  372. package/src/components/verbs/verify.ts +2 -2
  373. package/src/deep-observation.test.ts +75 -0
  374. package/src/deep-observation.ts +48 -0
  375. package/src/detectLexicon.ts +18 -1
  376. package/src/discovery/collect.test.ts +3 -3
  377. package/src/discovery/files.ts +4 -4
  378. package/src/discovery/fold-import.test.ts +1 -1
  379. package/src/discovery/fold-import.ts +105 -2
  380. package/src/discovery/param-deps.test.ts +112 -1
  381. package/src/discovery/param-deps.ts +193 -0
  382. package/src/discovery/sandbox/bundle.ts +15 -1
  383. package/src/discovery/sandbox/config-wire.test.ts +2 -2
  384. package/src/discovery/sandbox/config-wire.ts +2 -2
  385. package/src/fold/fold.test.ts +56 -0
  386. package/src/fold/foldable-helpers.ts +82 -2
  387. package/src/fold-provenance-exclusion.test.ts +186 -0
  388. package/src/fold-provenance.test.ts +495 -0
  389. package/src/fold-provenance.ts +441 -0
  390. package/src/graph-ops.test.ts +3 -3
  391. package/src/graph-ops.ts +1 -1
  392. package/src/held-elsewhere.test.ts +102 -0
  393. package/src/held-elsewhere.ts +114 -0
  394. package/src/index.ts +3 -0
  395. package/src/kubectl-context.ts +3 -3
  396. package/src/lexicon-config.test.ts +5 -5
  397. package/src/lexicon-config.ts +4 -4
  398. package/src/lexicon-plugin-helpers.ts +1 -1
  399. package/src/lexicon.ts +296 -16
  400. package/src/lifecycle/change-set.test.ts +49 -0
  401. package/src/lifecycle/change-set.ts +67 -0
  402. package/src/lifecycle/converge-ledger.ts +1 -1
  403. package/src/lifecycle/deep-diff.test.ts +267 -14
  404. package/src/lifecycle/deep-diff.ts +217 -14
  405. package/src/lifecycle/deep-observe.test.ts +3 -1
  406. package/src/lifecycle/deep-observe.ts +16 -6
  407. package/src/lifecycle/gate-ledger.test.ts +74 -2
  408. package/src/lifecycle/gate-ledger.ts +154 -36
  409. package/src/lifecycle/index.ts +1 -0
  410. package/src/lifecycle/release-ledger.ts +2 -2
  411. package/src/lifecycle/run-ledger.test.ts +268 -0
  412. package/src/lifecycle/run-ledger.ts +201 -0
  413. package/src/lifecycle/symptoms.ts +3 -3
  414. package/src/lint/config.test.ts +82 -1
  415. package/src/lint/config.ts +112 -3
  416. package/src/lint/pipeline-change-gate.test.ts +2 -2
  417. package/src/lint/policy.test.ts +1 -1
  418. package/src/lint/post-synth.test.ts +31 -0
  419. package/src/lint/post-synth.ts +54 -1
  420. package/src/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.ts +2 -2
  421. package/src/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.ts +1 -1
  422. package/src/lint/rules/comp/comp.test.ts +27 -1
  423. package/src/lint/rules/comp/comp004-gate-needs-durable-runtime.ts +76 -0
  424. package/src/lint/rules/comp/index.ts +3 -3
  425. package/src/lint/rules/op/index.ts +23 -0
  426. package/src/lint/rules/op/ops012-activity-contract.test.ts +161 -0
  427. package/src/lint/rules/op/ops012-activity-contract.ts +64 -0
  428. package/src/lint/rules/op/ops013-step-output-ref.test.ts +237 -0
  429. package/src/lint/rules/op/ops013-step-output-ref.ts +98 -0
  430. package/src/lint/rules/op/ops014-converge-rule-refusals.test.ts +263 -0
  431. package/src/lint/rules/op/ops014-converge-rule-refusals.ts +179 -0
  432. package/src/lint/rules/op/support.ts +11 -0
  433. package/src/lint/suppressions.test.ts +163 -0
  434. package/src/lint/suppressions.ts +204 -0
  435. package/src/live-endpoint.ts +1 -1
  436. package/src/op/activities/activity-contracts.ts +66 -0
  437. package/src/op/activities/apply.test.ts +567 -0
  438. package/src/op/activities/apply.ts +662 -0
  439. package/src/op/activities/build.ts +26 -0
  440. package/src/op/activities/converge.test.ts +330 -0
  441. package/src/op/activities/converge.ts +449 -0
  442. package/src/op/activities/env-teardown.test.ts +187 -0
  443. package/src/op/activities/env-teardown.ts +171 -0
  444. package/src/op/activities/guard-validate.test.ts +139 -0
  445. package/src/op/activities/guard-validate.ts +223 -0
  446. package/src/op/activities/http-check.test.ts +48 -0
  447. package/src/op/activities/http-check.ts +55 -0
  448. package/src/op/activities/index.ts +119 -0
  449. package/src/op/activities/lexicon-upgrade.test.ts +492 -0
  450. package/src/op/activities/lexicon-upgrade.ts +673 -0
  451. package/src/op/activities/lifecycle.ts +85 -0
  452. package/src/op/activities/pipeline-audit.test.ts +67 -0
  453. package/src/op/activities/pipeline-audit.ts +201 -0
  454. package/src/op/activities/policy.test.ts +19 -0
  455. package/src/op/activities/policy.ts +37 -0
  456. package/src/op/activities/reconcile.test.ts +161 -0
  457. package/src/op/activities/reconcile.ts +354 -0
  458. package/src/op/activities/shell.ts +26 -0
  459. package/src/op/activities/teardown.ts +22 -0
  460. package/src/op/activities/util.ts +6 -0
  461. package/src/op/activities/wait.ts +69 -0
  462. package/src/op/activities/workflow-audit.test.ts +90 -0
  463. package/src/op/activities/workflow-audit.ts +203 -0
  464. package/src/op/activity-contract-registry.test.ts +98 -0
  465. package/src/op/activity-contract-registry.ts +114 -0
  466. package/src/op/activity-contract.test.ts +1 -1
  467. package/src/op/activity-contract.ts +37 -33
  468. package/src/op/activity-failure.ts +34 -0
  469. package/src/op/activity-profiles.test.ts +56 -0
  470. package/src/op/activity-profiles.ts +116 -0
  471. package/src/op/activity-registry.test.ts +37 -28
  472. package/src/op/activity-registry.ts +27 -56
  473. package/src/op/activity-runtime.ts +3 -47
  474. package/src/op/builders-exports.test.ts +0 -49
  475. package/src/op/builders-typed.test.ts +164 -0
  476. package/src/op/builders.ts +125 -41
  477. package/src/op/change-signal.test.ts +117 -0
  478. package/src/op/change-signal.ts +169 -0
  479. package/src/op/composites/apply-op.ts +218 -0
  480. package/src/op/composites/composites.test.ts +379 -0
  481. package/src/op/composites/converge-op.test.ts +168 -0
  482. package/src/op/composites/converge-op.ts +214 -0
  483. package/src/op/composites/index.ts +26 -0
  484. package/src/op/composites/lexicon-upgrade-op.test.ts +68 -0
  485. package/src/op/composites/lexicon-upgrade-op.ts +134 -0
  486. package/src/op/composites/pipeline-audit-op.test.ts +25 -0
  487. package/src/op/composites/pipeline-audit-op.ts +78 -0
  488. package/src/op/composites/reconcile-op.test.ts +51 -0
  489. package/src/op/composites/reconcile-op.ts +110 -0
  490. package/src/op/composites/watch-op.ts +108 -0
  491. package/src/op/composites/workflow-audit-op.test.ts +25 -0
  492. package/src/op/composites/workflow-audit-op.ts +88 -0
  493. package/src/op/converge-rule.ts +12 -10
  494. package/src/op/cron.test.ts +128 -0
  495. package/src/op/cron.ts +149 -0
  496. package/src/op/discover-root.test.ts +105 -1
  497. package/src/op/discover.test.ts +6 -2
  498. package/src/op/discover.ts +84 -16
  499. package/src/op/duration.ts +21 -0
  500. package/src/op/effect-step.test.ts +29 -14
  501. package/src/op/emulator-lifecycle.ts +1 -2
  502. package/src/op/gate-name.test.ts +65 -0
  503. package/src/op/gate-name.ts +60 -0
  504. package/src/op/gate-summary.ts +84 -0
  505. package/src/op/gate.ts +155 -0
  506. package/src/op/generate-pipeline.test.ts +65 -3
  507. package/src/op/generate-pipeline.ts +19 -2
  508. package/src/op/index.ts +48 -8
  509. package/src/op/local-executor.test.ts +178 -34
  510. package/src/op/local-executor.ts +371 -102
  511. package/src/op/local-output.test.ts +95 -11
  512. package/src/op/local-output.ts +33 -4
  513. package/src/op/op-ir.test.ts +292 -0
  514. package/src/op/op-ir.ts +419 -0
  515. package/src/op/op-verb-class.test.ts +2 -2
  516. package/src/op/op-verb-class.ts +19 -5
  517. package/src/op/op.test.ts +28 -7
  518. package/src/op/operator.test.ts +543 -6
  519. package/src/op/operator.ts +203 -20
  520. package/src/op/receipt-store.ts +1 -1
  521. package/src/op/resource.ts +20 -2
  522. package/src/op/runtime.ts +203 -0
  523. package/src/op/runtimes/local.test.ts +181 -0
  524. package/src/op/runtimes/local.ts +212 -0
  525. package/src/op/step-output-ref.ts +15 -13
  526. package/src/op/types.ts +106 -26
  527. package/src/ownership.ts +1 -1
  528. package/src/provenance.test.ts +6 -0
  529. package/src/provenance.ts +21 -1
  530. package/src/serializer-walker.test.ts +42 -0
  531. package/src/serializer-walker.ts +21 -2
  532. package/src/terraform/aws-resources.test.ts +13 -4
  533. package/src/terraform/bridge.test.ts +22 -9
  534. package/src/terraform/bridge.ts +89 -28
  535. package/src/terraform/carve-provider.ts +38 -2
  536. package/src/terraform/data-source-shape.ts +95 -0
  537. package/src/terraform/graph.ts +35 -7
  538. package/src/terraform/providers/kubernetes.ts +48 -2
  539. package/src/terraform/tier-map.ts +21 -6
  540. package/src/terraform/types.ts +8 -0
  541. package/dist/cli/handlers/op-progress.d.ts +0 -90
  542. package/dist/cli/handlers/op-progress.d.ts.map +0 -1
  543. package/dist/cli/handlers/run-client.d.ts +0 -127
  544. package/dist/cli/handlers/run-client.d.ts.map +0 -1
  545. package/dist/cli/handlers/run-report.d.ts +0 -5
  546. package/dist/cli/handlers/run-report.d.ts.map +0 -1
  547. package/dist/components/temporal-codegen-loader.d.ts +0 -27
  548. package/dist/components/temporal-codegen-loader.d.ts.map +0 -1
  549. package/dist/lint/rules/comp/comp004-gate-needs-temporal.d.ts +0 -37
  550. package/dist/lint/rules/comp/comp004-gate-needs-temporal.d.ts.map +0 -1
  551. package/src/cli/handlers/op-progress.test.ts +0 -268
  552. package/src/cli/handlers/op-progress.ts +0 -248
  553. package/src/cli/handlers/run-client.test.ts +0 -82
  554. package/src/cli/handlers/run-client.ts +0 -219
  555. package/src/cli/handlers/run-report.test.ts +0 -62
  556. package/src/cli/handlers/run-report.ts +0 -122
  557. package/src/components/temporal-codegen-loader.ts +0 -41
  558. package/src/lint/rules/comp/comp004-gate-needs-temporal.ts +0 -66
@@ -9,13 +9,35 @@ import {
9
9
  runOperatorRound,
10
10
  runOperatorForever,
11
11
  formatRoundLine,
12
+ formatSignalLine,
12
13
  DEFAULT_OPERATOR_INTERVAL_MS,
14
+ type ChangeSubscriber,
15
+ type OperatorSignalEvent,
16
+ type OperatorTickEvent,
13
17
  } from "./operator";
14
18
  import { readLease } from "../lifecycle/lease";
15
19
  import { appendConvergeRecord, readConvergeLedger } from "../lifecycle/converge-ledger";
20
+ import { readRunLedger } from "../lifecycle/run-ledger";
21
+ import type { OpRunRecord } from "./runtime";
16
22
 
17
23
  const PROFILES: Record<string, ActivityProfile> = {};
18
24
 
25
+ /** A minimal `OpRunRecord` for a result literal that never touches a ledger. */
26
+ function runRecord(op: string, status: OpRunRecord["status"] = "ok"): OpRunRecord {
27
+ return {
28
+ version: 1,
29
+ id: "00000000-0000-4000-8000-000000000000",
30
+ op,
31
+ env: "staging",
32
+ started: "2026-01-01T00:00:00.000Z",
33
+ ended: "2026-01-01T00:00:01.000Z",
34
+ status,
35
+ labels: {},
36
+ outcomes: {},
37
+ phases: [],
38
+ };
39
+ }
40
+
19
41
  function git(args: string[], cwd: string): { stdout: string; exitCode: number } {
20
42
  const r = spawnSync("git", args, { cwd, encoding: "utf-8" });
21
43
  return { stdout: r.stdout ?? "", exitCode: r.status ?? -1 };
@@ -34,7 +56,7 @@ async function initRepo(dir: string): Promise<void> {
34
56
  * Write a minimal fixture ConvergeOp — the same `{ props: OpConfig }` shape
35
57
  * `discoverOps` reads from a real `*.op.ts` file's default export
36
58
  * (`../op/discover.ts`), hand-built here rather than going through the real
37
- * `ConvergeOp()` composite (lexicons/temporal) so this suite tests the
59
+ * `ConvergeOp()` composite so this suite tests the
38
60
  * operator's own discovery/lease/tick-loop logic without depending on that
39
61
  * lexicon at all. One "Converge" phase calling a single fake activity —
40
62
  * `runOpLocally` doesn't care what the phases are named or how many there
@@ -50,7 +72,26 @@ function writeFixtureConvergeOp(dir: string, opName: string, env: string): void
50
72
  name: ${JSON.stringify(opName)},
51
73
  overview: "fixture converge op",
52
74
  phases: [{ name: "Converge", steps: [{ kind: "activity", fn: "fakeTick", args: {} }] }],
53
- searchAttributes: { Converge: "true", Env: ${JSON.stringify(env)}, Dial: "observe" },
75
+ labels: { Converge: "true", Env: ${JSON.stringify(env)}, Dial: "observe" },
76
+ },
77
+ };
78
+ `,
79
+ );
80
+ }
81
+
82
+ /** The same fixture with a cadence of its own (#2120) — what `ConvergeOp({ schedule })` produces. */
83
+ function writeFixtureScheduledConvergeOp(dir: string, opName: string, env: string, cron: string): void {
84
+ const opsDir = join(dir, "ops");
85
+ mkdirSync(opsDir, { recursive: true });
86
+ writeFileSync(
87
+ join(opsDir, `${opName}.op.ts`),
88
+ `export default {
89
+ props: {
90
+ name: ${JSON.stringify(opName)},
91
+ overview: "fixture scheduled converge op",
92
+ phases: [{ name: "Converge", steps: [{ kind: "activity", fn: "fakeTick", args: {} }] }],
93
+ labels: { Converge: "true", Env: ${JSON.stringify(env)}, Dial: "observe" },
94
+ schedule: { cron: ${JSON.stringify(cron)}, overlap: "skip" },
54
95
  },
55
96
  };
56
97
  `,
@@ -99,7 +140,7 @@ function fakeTickActivities(dir: string, env: string, opName: string, opts?: { t
99
140
  }
100
141
 
101
142
  describe("discoverConvergeOps", () => {
102
- test("finds only ops with searchAttributes.Converge === 'true'", async () => {
143
+ test("finds only ops with labels.Converge === 'true'", async () => {
103
144
  await withTestDir(async (dir) => {
104
145
  await initRepo(dir);
105
146
  writeFixtureConvergeOp(dir, "staging-converge", "staging");
@@ -132,6 +173,28 @@ describe("discoverConvergeOps", () => {
132
173
  expect(ops.map((d) => d.config.name)).toEqual(["prod-converge", "staging-converge"]);
133
174
  });
134
175
  });
176
+
177
+ test("a ConvergeOp declaring no labels.Env is discovered unfiltered but never matches --env", async () => {
178
+ await withTestDir(async (dir) => {
179
+ await initRepo(dir);
180
+ mkdirSync(join(dir, "ops"), { recursive: true });
181
+ writeFileSync(
182
+ join(dir, "ops", "envless-converge.op.ts"),
183
+ `export default {
184
+ props: {
185
+ name: "envless-converge",
186
+ overview: "a hand-built ConvergeOp with no Env label",
187
+ phases: [{ name: "Converge", steps: [{ kind: "activity", fn: "fakeTick", args: {} }] }],
188
+ labels: { Converge: "true" },
189
+ },
190
+ };
191
+ `,
192
+ );
193
+
194
+ expect((await discoverConvergeOps({ cwd: dir })).ops.map((d) => d.config.name)).toEqual(["envless-converge"]);
195
+ expect((await discoverConvergeOps({ cwd: dir, env: "staging" })).ops).toEqual([]);
196
+ });
197
+ });
135
198
  });
136
199
 
137
200
  describe("runOperatorRound — lease + tick execution over a fixture ConvergeOp (offline)", () => {
@@ -142,7 +205,7 @@ describe("runOperatorRound — lease + tick execution over a fixture ConvergeOp
142
205
  const activities = fakeTickActivities(dir, "staging", "staging-converge");
143
206
 
144
207
  const events = await runOperatorRound({ cwd: dir, holder: "op-a", activities, profiles: PROFILES });
145
- expect(events).toEqual([{ kind: "ticked", op: "staging-converge", env: "staging", result: expect.objectContaining({ ok: true }) }]);
208
+ expect(events).toEqual([{ kind: "ticked", op: "staging-converge", env: "staging", result: expect.objectContaining({ status: "ok" }) }]);
146
209
 
147
210
  const { records } = await readConvergeLedger("staging", { cwd: dir });
148
211
  expect(records).toHaveLength(1);
@@ -150,6 +213,27 @@ describe("runOperatorRound — lease + tick execution over a fixture ConvergeOp
150
213
  });
151
214
  });
152
215
 
216
+ test("the tick's own run lands on the run ledger, keyed by the op's labels.Env (#2118)", async () => {
217
+ await withTestDir(async (dir) => {
218
+ await initRepo(dir);
219
+ writeFixtureConvergeOp(dir, "staging-converge", "staging");
220
+ const activities = fakeTickActivities(dir, "staging", "staging-converge");
221
+
222
+ await runOperatorRound({ cwd: dir, holder: "op-a", activities, profiles: PROFILES });
223
+
224
+ const { records, malformed } = await readRunLedger("staging", "staging-converge", { cwd: dir });
225
+ expect(malformed).toBe(0);
226
+ expect(records).toHaveLength(1);
227
+ expect(records[0]).toMatchObject({
228
+ op: "staging-converge",
229
+ env: "staging",
230
+ status: "ok",
231
+ labels: { Converge: "true", Env: "staging", Dial: "observe" },
232
+ });
233
+ expect(records[0].phases.map((p) => p.name)).toEqual(["Converge"]);
234
+ });
235
+ });
236
+
153
237
  test("a second operator process (different holder) sees the lease held and skips — never queues", async () => {
154
238
  await withTestDir(async (dir) => {
155
239
  await initRepo(dir);
@@ -330,10 +414,97 @@ describe("runOperatorForever — interval/timer behavior", () => {
330
414
  });
331
415
  });
332
416
 
417
+ describe("runOperatorRound — an Op's own schedule is its tick cadence (#2120)", () => {
418
+ /** 2026-09-06T13:30 local — a minute `*​/10 * * * *` fires on. */
419
+ const firing = () => new Date(2026, 8, 6, 13, 30, 0, 0);
420
+ /** …and 13:33, which it does not. */
421
+ const quiet = () => new Date(2026, 8, 6, 13, 33, 0, 0);
422
+
423
+ test("a round on a firing minute ticks the op", async () => {
424
+ await withTestDir(async (dir) => {
425
+ await initRepo(dir);
426
+ writeFixtureScheduledConvergeOp(dir, "staging-converge", "staging", "*/10 * * * *");
427
+ const activities = fakeTickActivities(dir, "staging", "staging-converge");
428
+
429
+ const events = await runOperatorRound({ cwd: dir, holder: "op-a", activities, profiles: PROFILES, now: firing });
430
+ expect(events).toEqual([
431
+ { kind: "ticked", op: "staging-converge", env: "staging", result: expect.objectContaining({ status: "ok" }) },
432
+ ]);
433
+ });
434
+ });
435
+
436
+ test("a round off the cron skips without touching the lease, and writes no ledger record", async () => {
437
+ await withTestDir(async (dir) => {
438
+ await initRepo(dir);
439
+ writeFixtureScheduledConvergeOp(dir, "staging-converge", "staging", "*/10 * * * *");
440
+ const activities = fakeTickActivities(dir, "staging", "staging-converge");
441
+
442
+ const events = await runOperatorRound({ cwd: dir, holder: "op-a", activities, profiles: PROFILES, now: quiet });
443
+ expect(events).toEqual([
444
+ { kind: "skipped-not-due", op: "staging-converge", env: "staging", cron: "*/10 * * * *" },
445
+ ]);
446
+
447
+ const { records } = await readConvergeLedger("staging", { cwd: dir });
448
+ expect(records).toHaveLength(0);
449
+ expect((await readLease("staging-converge", { cwd: dir })).record).toBeUndefined();
450
+ });
451
+ });
452
+
453
+ test("a late round still owes the tick: a firing minute passed since this op was last considered", async () => {
454
+ await withTestDir(async (dir) => {
455
+ await initRepo(dir);
456
+ writeFixtureScheduledConvergeOp(dir, "staging-converge", "staging", "*/10 * * * *");
457
+ const activities = fakeTickActivities(dir, "staging", "staging-converge");
458
+ const scheduleState = new Map<string, Date>();
459
+
460
+ // 13:33 — first sight, not a firing minute: recorded, not ticked.
461
+ const first = await runOperatorRound({ cwd: dir, holder: "op-a", activities, profiles: PROFILES, now: quiet, scheduleState });
462
+ expect(first[0].kind).toBe("skipped-not-due");
463
+
464
+ // 13:42 — 13:40 fired while nobody was looking, so this round ticks.
465
+ const late = () => new Date(2026, 8, 6, 13, 42, 0, 0);
466
+ const second = await runOperatorRound({ cwd: dir, holder: "op-a", activities, profiles: PROFILES, now: late, scheduleState });
467
+ expect(second[0].kind).toBe("ticked");
468
+
469
+ // 13:43 — nothing has fired since 13:42.
470
+ const after = () => new Date(2026, 8, 6, 13, 43, 0, 0);
471
+ const third = await runOperatorRound({ cwd: dir, holder: "op-a", activities, profiles: PROFILES, now: after, scheduleState });
472
+ expect(third[0].kind).toBe("skipped-not-due");
473
+ });
474
+ });
475
+
476
+ test("an op with no cadence of its own still ticks every round — `--interval` is the fallback", async () => {
477
+ await withTestDir(async (dir) => {
478
+ await initRepo(dir);
479
+ writeFixtureConvergeOp(dir, "staging-converge", "staging");
480
+ const activities = fakeTickActivities(dir, "staging", "staging-converge");
481
+
482
+ const events = await runOperatorRound({ cwd: dir, holder: "op-a", activities, profiles: PROFILES, now: quiet });
483
+ expect(events[0].kind).toBe("ticked");
484
+ });
485
+ });
486
+ });
487
+
333
488
  describe("formatRoundLine", () => {
334
489
  test("renders each event kind as one line", () => {
335
- expect(formatRoundLine({ kind: "ticked", op: "x", env: "staging", result: { op: "x", records: [], totalMs: 1, ok: true } }))
336
- .toContain("ticked=1 ok=true");
490
+ expect(formatRoundLine({
491
+ kind: "ticked", op: "x", env: "staging",
492
+ result: {
493
+ op: "x", records: [], totalMs: 1, status: "ok", startedAt: "2026-09-05T12:00:00.000Z",
494
+ record: runRecord("x", "ok"),
495
+ },
496
+ })).toContain("ticked=1 status=ok");
497
+ expect(formatRoundLine({
498
+ kind: "ticked", op: "x", env: "staging",
499
+ result: {
500
+ op: "x", records: [], totalMs: 1, status: "gated", startedAt: "2026-09-05T12:00:00.000Z",
501
+ gate: {
502
+ version: 1, kind: "pending", op: "x", gate: "rollout-gate",
503
+ timestamp: "2026-09-05T12:00:00.000Z", expiresAt: "2026-09-07T12:00:00.000Z",
504
+ },
505
+ record: runRecord("x", "gated"),
506
+ },
507
+ })).toContain('status=gated gate="rollout-gate"');
337
508
  expect(formatRoundLine({ kind: "skipped-lease-held", op: "x", env: "staging", heldBy: "holder-a" }))
338
509
  .toContain("lease-held:holder-a");
339
510
  expect(formatRoundLine({ kind: "tick-failed", op: "x", env: "staging", error: "boom" }))
@@ -342,5 +513,371 @@ describe("formatRoundLine", () => {
342
513
  .toContain("fenced=1");
343
514
  expect(formatRoundLine({ kind: "lease-error", op: "x", env: "staging", error: "stale lock at .../x.lock" }))
344
515
  .toContain("stale lock at .../x.lock");
516
+ expect(formatRoundLine({ kind: "skipped-not-due", op: "x", env: "staging", cron: "*/10 * * * *" }))
517
+ .toContain("not-due:*/10 * * * *");
518
+ });
519
+ });
520
+
521
+ describe("runOperatorForever: a substrate change signal wakes a tick (#1981)", () => {
522
+ async function waitFor(predicate: () => boolean, maxWaitMs = 5_000): Promise<void> {
523
+ const deadline = Date.now() + maxWaitMs;
524
+ while (!predicate() && Date.now() < deadline) {
525
+ await new Promise((r) => setTimeout(r, 5));
526
+ }
527
+ }
528
+
529
+ /**
530
+ * A stub `subscribeChanges` seam. Hands the loop's `onChange`/`onError` back
531
+ * to the test so it can fabricate signals and kill the subscription, with no
532
+ * substrate anywhere near it, which is the whole point of the seam being a
533
+ * plain callback pair.
534
+ */
535
+ function stubSubscriber(lexicon = "stub") {
536
+ const handle = {
537
+ lexicon,
538
+ subscribes: 0,
539
+ closes: 0,
540
+ onChange: undefined as undefined | (() => void),
541
+ onError: undefined as undefined | ((message: string) => void),
542
+ signal: undefined as undefined | AbortSignal,
543
+ failNextSubscribe: undefined as undefined | string,
544
+ };
545
+ const subscriber: ChangeSubscriber = {
546
+ lexicon,
547
+ async subscribe(ctx) {
548
+ handle.subscribes++;
549
+ if (handle.failNextSubscribe) {
550
+ const message = handle.failNextSubscribe;
551
+ handle.failNextSubscribe = undefined;
552
+ throw new Error(message);
553
+ }
554
+ handle.onChange = ctx.onChange;
555
+ handle.onError = ctx.onError;
556
+ handle.signal = ctx.signal;
557
+ return {
558
+ async close() {
559
+ handle.closes++;
560
+ },
561
+ };
562
+ },
563
+ };
564
+ return { handle, subscriber };
565
+ }
566
+
567
+ test("a signal wakes the next round early, well inside the interval", async () => {
568
+ await withTestDir(async (dir) => {
569
+ await initRepo(dir);
570
+ writeFixtureConvergeOp(dir, "staging-converge", "staging");
571
+ const activities = fakeTickActivities(dir, "staging", "staging-converge");
572
+ const { handle, subscriber } = stubSubscriber("k8s");
573
+
574
+ const controller = new AbortController();
575
+ const rounds: number[] = [];
576
+ const signalEvents: OperatorSignalEvent[] = [];
577
+ const loop = runOperatorForever({
578
+ cwd: dir,
579
+ holder: "op-a",
580
+ intervalMs: 60_000, // a full minute: nothing but a signal can produce a second round
581
+ signalFloorMs: 0,
582
+ activities,
583
+ profiles: PROFILES,
584
+ signal: controller.signal,
585
+ subscribers: [subscriber],
586
+ onRound: () => rounds.push(Date.now()),
587
+ onSignalEvent: (e) => signalEvents.push(e),
588
+ });
589
+
590
+ await waitFor(() => handle.onChange !== undefined);
591
+ expect(rounds.length).toBe(1);
592
+ handle.onChange!();
593
+ await waitFor(() => rounds.length >= 2);
594
+ expect(rounds.length).toBeGreaterThanOrEqual(2);
595
+
596
+ controller.abort();
597
+ await loop;
598
+ expect(signalEvents.some((e) => e.kind === "subscribed" && e.lexicon === "k8s")).toBe(true);
599
+ expect(signalEvents.some((e) => e.kind === "woken")).toBe(true);
600
+ });
601
+ }, 15_000);
602
+
603
+ test("a fabricated signal cannot change what the tick reports, only when it runs", async () => {
604
+ await withTestDir(async (dir) => {
605
+ await initRepo(dir);
606
+ writeFixtureConvergeOp(dir, "staging-converge", "staging");
607
+
608
+ /** One round's events, run to completion on a timer alone. */
609
+ async function roundsWithoutSignal(): Promise<string> {
610
+ const controller = new AbortController();
611
+ const seen: OperatorTickEvent[][] = [];
612
+ const loop = runOperatorForever({
613
+ cwd: dir,
614
+ holder: "op-a",
615
+ intervalMs: 60_000,
616
+ activities: fakeTickActivities(dir, "staging", "staging-converge"),
617
+ profiles: PROFILES,
618
+ signal: controller.signal,
619
+ onRound: (events) => {
620
+ seen.push(events);
621
+ controller.abort();
622
+ },
623
+ });
624
+ await loop;
625
+ return JSON.stringify(seen[0].map((e) => ({ kind: e.kind, op: e.op, env: e.env })));
626
+ }
627
+
628
+ /** The same, woken by a signal carrying a fabricated payload. */
629
+ async function roundsWithFabricatedSignal(): Promise<string> {
630
+ const { handle, subscriber } = stubSubscriber("liar");
631
+ const controller = new AbortController();
632
+ const seen: OperatorTickEvent[][] = [];
633
+ const loop = runOperatorForever({
634
+ cwd: dir,
635
+ holder: "op-a",
636
+ intervalMs: 60_000,
637
+ signalFloorMs: 0,
638
+ activities: fakeTickActivities(dir, "staging", "staging-converge"),
639
+ profiles: PROFILES,
640
+ signal: controller.signal,
641
+ subscribers: [subscriber],
642
+ onRound: (events) => {
643
+ seen.push(events);
644
+ if (seen.length >= 2) controller.abort();
645
+ },
646
+ });
647
+ await waitFor(() => handle.onChange !== undefined);
648
+ // `onChange` takes no arguments; a caller that fabricates a resource
649
+ // has nowhere to put it, and the extra arguments go nowhere.
650
+ (handle.onChange as unknown as (...args: unknown[]) => void)(
651
+ { kind: "Deployment", name: "ghost", status: "Drifted" },
652
+ "fabricated",
653
+ );
654
+ await waitFor(() => seen.length >= 2);
655
+ controller.abort();
656
+ await loop;
657
+ return JSON.stringify(seen[1].map((e) => ({ kind: e.kind, op: e.op, env: e.env })));
658
+ }
659
+
660
+ const timerOnly = await roundsWithoutSignal();
661
+ const signalled = await roundsWithFabricatedSignal();
662
+ // Byte for byte the same verdict: the signal moved the clock, nothing else.
663
+ expect(signalled).toBe(timerOnly);
664
+ // And the seam itself carries no payload channel to begin with.
665
+ const { handle } = stubSubscriber();
666
+ expect(handle.onChange?.length ?? 0).toBe(0);
667
+ });
668
+ }, 20_000);
669
+
670
+ test("a storm of signals wakes at most one round per floor", async () => {
671
+ await withTestDir(async (dir) => {
672
+ await initRepo(dir);
673
+ writeFixtureConvergeOp(dir, "staging-converge", "staging");
674
+ const { handle, subscriber } = stubSubscriber();
675
+
676
+ const controller = new AbortController();
677
+ const rounds: number[] = [];
678
+ const loop = runOperatorForever({
679
+ cwd: dir,
680
+ holder: "op-a",
681
+ intervalMs: 60_000,
682
+ signalFloorMs: 400,
683
+ activities: fakeTickActivities(dir, "staging", "staging-converge"),
684
+ profiles: PROFILES,
685
+ signal: controller.signal,
686
+ subscribers: [subscriber],
687
+ onRound: () => rounds.push(Date.now()),
688
+ });
689
+
690
+ await waitFor(() => handle.onChange !== undefined);
691
+ const before = rounds.length;
692
+ for (let i = 0; i < 300; i++) handle.onChange!();
693
+ // Well past a floor, but nowhere near the 60s interval: at most one
694
+ // extra round may have happened, and a per-signal wake would have
695
+ // produced hundreds.
696
+ await waitFor(() => rounds.length > before, 2_000);
697
+ expect(rounds.length - before).toBeLessThanOrEqual(1);
698
+
699
+ controller.abort();
700
+ await loop;
701
+ });
702
+ }, 15_000);
703
+
704
+ test("a killed subscription is reported once, the loop keeps ticking, and the next round re-subscribes", async () => {
705
+ await withTestDir(async (dir) => {
706
+ await initRepo(dir);
707
+ writeFixtureConvergeOp(dir, "staging-converge", "staging");
708
+ const { handle, subscriber } = stubSubscriber("k8s");
709
+
710
+ const controller = new AbortController();
711
+ const rounds: number[] = [];
712
+ const signalEvents: OperatorSignalEvent[] = [];
713
+ const loop = runOperatorForever({
714
+ cwd: dir,
715
+ holder: "op-a",
716
+ intervalMs: 60,
717
+ signalFloorMs: 0,
718
+ activities: fakeTickActivities(dir, "staging", "staging-converge"),
719
+ profiles: PROFILES,
720
+ signal: controller.signal,
721
+ subscribers: [subscriber],
722
+ onRound: () => rounds.push(Date.now()),
723
+ onSignalEvent: (e) => signalEvents.push(e),
724
+ });
725
+
726
+ await waitFor(() => handle.onError !== undefined);
727
+ const subscribesBefore = handle.subscribes;
728
+ // The watch dies. Twice, because a dead subscription reports once.
729
+ handle.onError!("connection reset by peer");
730
+ handle.onError!("connection reset by peer");
731
+
732
+ await waitFor(() => handle.subscribes > subscribesBefore);
733
+ const roundsAtDeath = rounds.length;
734
+ await waitFor(() => rounds.length > roundsAtDeath + 1);
735
+
736
+ controller.abort();
737
+ await loop;
738
+
739
+ const lost = signalEvents.filter((e) => e.kind === "subscription-lost");
740
+ expect(lost).toHaveLength(1);
741
+ expect(formatSignalLine(lost[0])).toContain("polling on the timer");
742
+ // The loop kept ticking on the timer, and re-subscribed.
743
+ expect(rounds.length).toBeGreaterThan(roundsAtDeath + 1);
744
+ expect(handle.subscribes).toBeGreaterThan(subscribesBefore);
745
+ });
746
+ }, 15_000);
747
+
748
+ test("a subscription that cannot be established degrades to polling with one line, never a crash", async () => {
749
+ await withTestDir(async (dir) => {
750
+ await initRepo(dir);
751
+ writeFixtureConvergeOp(dir, "staging-converge", "staging");
752
+ const failing: ChangeSubscriber = {
753
+ lexicon: "k8s",
754
+ async subscribe() {
755
+ throw new Error("no cluster binding for env staging");
756
+ },
757
+ };
758
+
759
+ const controller = new AbortController();
760
+ const rounds: number[] = [];
761
+ const signalEvents: OperatorSignalEvent[] = [];
762
+ const loop = runOperatorForever({
763
+ cwd: dir,
764
+ holder: "op-a",
765
+ intervalMs: 40,
766
+ activities: fakeTickActivities(dir, "staging", "staging-converge"),
767
+ profiles: PROFILES,
768
+ signal: controller.signal,
769
+ subscribers: [failing],
770
+ onRound: () => rounds.push(Date.now()),
771
+ onSignalEvent: (e) => signalEvents.push(e),
772
+ });
773
+
774
+ await waitFor(() => rounds.length >= 3);
775
+ controller.abort();
776
+ await loop; // resolves: a failing subscription never rejects the loop
777
+
778
+ const failed = signalEvents.filter((e) => e.kind === "subscribe-failed");
779
+ expect(failed.length).toBeGreaterThanOrEqual(1);
780
+ expect(formatSignalLine(failed[0])).toContain("no cluster binding for env staging");
781
+ expect(rounds.length).toBeGreaterThanOrEqual(3);
782
+ });
783
+ }, 15_000);
784
+
785
+ test("stopping the operator closes every subscription and aborts its signal", async () => {
786
+ await withTestDir(async (dir) => {
787
+ await initRepo(dir);
788
+ writeFixtureConvergeOp(dir, "staging-converge", "staging");
789
+ const a = stubSubscriber("k8s");
790
+ const b = stubSubscriber("docker");
791
+
792
+ const controller = new AbortController();
793
+ const loop = runOperatorForever({
794
+ cwd: dir,
795
+ holder: "op-a",
796
+ intervalMs: 60_000,
797
+ activities: fakeTickActivities(dir, "staging", "staging-converge"),
798
+ profiles: PROFILES,
799
+ signal: controller.signal,
800
+ subscribers: [a.subscriber, b.subscriber],
801
+ });
802
+
803
+ await waitFor(() => a.handle.onChange !== undefined && b.handle.onChange !== undefined);
804
+ expect(a.handle.signal!.aborted).toBe(false);
805
+
806
+ controller.abort();
807
+ await loop;
808
+
809
+ expect(a.handle.closes).toBe(1);
810
+ expect(b.handle.closes).toBe(1);
811
+ expect(a.handle.signal!.aborted).toBe(true);
812
+ expect(b.handle.signal!.aborted).toBe(true);
813
+ });
814
+ }, 15_000);
815
+
816
+ test("a close() that throws still unwinds the loop cleanly", async () => {
817
+ await withTestDir(async (dir) => {
818
+ await initRepo(dir);
819
+ writeFixtureConvergeOp(dir, "staging-converge", "staging");
820
+ let subscribed = false;
821
+ const hostile: ChangeSubscriber = {
822
+ lexicon: "k8s",
823
+ async subscribe() {
824
+ subscribed = true;
825
+ return { close: async () => { throw new Error("close blew up"); } };
826
+ },
827
+ };
828
+
829
+ const controller = new AbortController();
830
+ const loop = runOperatorForever({
831
+ cwd: dir,
832
+ holder: "op-a",
833
+ intervalMs: 60_000,
834
+ activities: fakeTickActivities(dir, "staging", "staging-converge"),
835
+ profiles: PROFILES,
836
+ signal: controller.signal,
837
+ subscribers: [hostile],
838
+ });
839
+
840
+ await waitFor(() => subscribed);
841
+ controller.abort();
842
+ await expect(loop).resolves.toBeUndefined();
843
+ });
844
+ }, 15_000);
845
+
846
+ test("no subscribers: the loop is exactly the timer it always was", async () => {
847
+ await withTestDir(async (dir) => {
848
+ await initRepo(dir);
849
+ writeFixtureConvergeOp(dir, "staging-converge", "staging");
850
+ const controller = new AbortController();
851
+ const rounds: number[] = [];
852
+ const signalEvents: OperatorSignalEvent[] = [];
853
+ const loop = runOperatorForever({
854
+ cwd: dir,
855
+ holder: "op-a",
856
+ intervalMs: 40,
857
+ activities: fakeTickActivities(dir, "staging", "staging-converge"),
858
+ profiles: PROFILES,
859
+ signal: controller.signal,
860
+ subscribers: [], // a project whose lexicons implement no signal seam
861
+ onRound: () => rounds.push(Date.now()),
862
+ onSignalEvent: (e) => signalEvents.push(e),
863
+ });
864
+
865
+ await waitFor(() => rounds.length >= 3);
866
+ controller.abort();
867
+ await loop;
868
+ expect(rounds.length).toBeGreaterThanOrEqual(3);
869
+ expect(signalEvents).toEqual([]);
870
+ });
871
+ }, 15_000);
872
+ });
873
+
874
+ describe("formatSignalLine", () => {
875
+ test("renders each signal event kind as one line, none of them an observation", () => {
876
+ expect(formatSignalLine({ kind: "subscribed", lexicon: "k8s" })).toContain("the timer still runs");
877
+ expect(formatSignalLine({ kind: "subscribe-failed", lexicon: "k8s", error: "no binding" }))
878
+ .toContain("polling on the timer");
879
+ expect(formatSignalLine({ kind: "subscription-lost", lexicon: "k8s", error: "410 Gone" }))
880
+ .toContain("re-subscribes");
881
+ expect(formatSignalLine({ kind: "woken", afterMs: 120 })).toContain("running an ordinary round");
345
882
  });
346
883
  });