@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
@@ -16,7 +16,8 @@ import { discoverOps } from "./discover";
16
16
  * symlink and the walk compares resolved paths).
17
17
  */
18
18
  let fakeGitRoot = "";
19
- vi.mock("../runtime-adapter", () => ({
19
+ vi.mock("../runtime-adapter", async (importOriginal) => ({
20
+ ...(await importOriginal<typeof import("../runtime-adapter")>()),
20
21
  getRuntime: () => ({
21
22
  spawn: async (cmd: string[]) =>
22
23
  cmd[0] === "git" && cmd[1] === "rev-parse"
@@ -66,3 +67,106 @@ describe("discoverOps — the scan root is the project, not the checkout (#2058)
66
67
  expect([...ops.keys()].sort()).toEqual(["mine", "root", "theirs"]);
67
68
  });
68
69
  });
70
+
71
+ /**
72
+ * #2171. An Op may be exported by name, not only as the file's default.
73
+ *
74
+ * Discovery used to read `mod.default` alone, which forced every runnable Op
75
+ * into the one export shape the fold path refuses, so an Op under a project's
76
+ * `sourceDir` cost that project its fold coverage. The default export is still
77
+ * accepted, unchanged; these cases are the widening around it.
78
+ *
79
+ * Same fake-git-root harness as the suite above: the Op files here are written
80
+ * as plain object literals with a `props` field, which is the shape discovery
81
+ * actually validates, so the fixtures stay free of the Op builders.
82
+ */
83
+ describe("discoverOps: an Op may be exported by name (#2171)", () => {
84
+ const OP_VALUE = (name: string): string =>
85
+ `{ props: { name: ${JSON.stringify(name)}, overview: "t", phases: [{ name: "Run", steps: [] }] } }`;
86
+
87
+ let checkout: string;
88
+ const write = (file: string, body: string): void => {
89
+ writeFileSync(join(checkout, "ops", file), body);
90
+ };
91
+
92
+ beforeEach(() => {
93
+ checkout = realpathSync(mkdtempSync(join(tmpdir(), "chant-op-named-")));
94
+ fakeGitRoot = checkout;
95
+ mkdirSync(join(checkout, "ops"), { recursive: true });
96
+ writeFileSync(join(checkout, "chant.config.json"), JSON.stringify({ lexicons: ["aws"] }));
97
+ });
98
+ afterEach(() => rmSync(checkout, { recursive: true, force: true }));
99
+
100
+ test("a named export is discovered", async () => {
101
+ write("named.op.ts", `export const deploy = ${OP_VALUE("deploy")};\n`);
102
+ const { ops, errors } = await discoverOps({ cwd: checkout });
103
+ expect(errors).toEqual([]);
104
+ expect([...ops.keys()]).toEqual(["deploy"]);
105
+ expect(ops.get("deploy")!.exportName).toBe("deploy");
106
+ });
107
+
108
+ test("the default export still works, and reports itself as the default", async () => {
109
+ write("legacy.op.ts", `export default ${OP_VALUE("legacy")};\n`);
110
+ const { ops, errors } = await discoverOps({ cwd: checkout });
111
+ expect(errors).toEqual([]);
112
+ expect([...ops.keys()]).toEqual(["legacy"]);
113
+ expect(ops.get("legacy")!.exportName).toBe("default");
114
+ });
115
+
116
+ test("one file may declare several Ops, and each is registered on its own", async () => {
117
+ write("many.op.ts", `export const a = ${OP_VALUE("alpha")};\nexport const b = ${OP_VALUE("beta")};\n`);
118
+ const { ops, errors } = await discoverOps({ cwd: checkout });
119
+ expect(errors).toEqual([]);
120
+ expect([...ops.keys()].sort()).toEqual(["alpha", "beta"]);
121
+ expect(ops.get("alpha")!.filePath).toBe(ops.get("beta")!.filePath);
122
+ });
123
+
124
+ test("a default and a named Op in one file are both registered", async () => {
125
+ write("both.op.ts", `export default ${OP_VALUE("first")};\nexport const other = ${OP_VALUE("second")};\n`);
126
+ const { ops, errors } = await discoverOps({ cwd: checkout });
127
+ expect(errors).toEqual([]);
128
+ expect([...ops.keys()].sort()).toEqual(["first", "second"]);
129
+ });
130
+
131
+ test("the same Op exported twice is one Op, not a self-collision", async () => {
132
+ write("aliased.op.ts", `const op = ${OP_VALUE("aliased")};\nexport default op;\nexport { op };\n`);
133
+ const { ops, errors } = await discoverOps({ cwd: checkout });
134
+ expect(errors).toEqual([]);
135
+ expect([...ops.keys()]).toEqual(["aliased"]);
136
+ expect(ops.get("aliased")!.exportName).toBe("default");
137
+ });
138
+
139
+ test("exports that are not Ops are skipped in silence", async () => {
140
+ write("mixed.op.ts", `export const meta = { team: "infra" };\nexport const notQuite = { props: { name: "x" } };\nexport const real = ${OP_VALUE("real")};\n`);
141
+ const { ops, errors } = await discoverOps({ cwd: checkout });
142
+ expect(errors).toEqual([]);
143
+ expect([...ops.keys()]).toEqual(["real"]);
144
+ });
145
+
146
+ test("a file exporting no Op at all is the error, and names both accepted shapes", async () => {
147
+ write("empty.op.ts", `export const meta = { team: "infra" };\n`);
148
+ const { ops, errors } = await discoverOps({ cwd: checkout });
149
+ expect(ops.size).toBe(0);
150
+ expect(errors).toHaveLength(1);
151
+ expect(errors[0]).toContain("exports no Op");
152
+ expect(errors[0]).toContain("export default Op({...})");
153
+ expect(errors[0]).toContain("export const deploy = Op({...})");
154
+ });
155
+
156
+ test("two files declaring the same Op name still collide", async () => {
157
+ write("one.op.ts", `export const a = ${OP_VALUE("same")};\n`);
158
+ write("two.op.ts", `export const b = ${OP_VALUE("same")};\n`);
159
+ const { ops, errors } = await discoverOps({ cwd: checkout });
160
+ expect(ops.size).toBe(1);
161
+ expect(errors).toHaveLength(1);
162
+ expect(errors[0]).toMatch(/^Duplicate Op name "same" in .+ and .+$/);
163
+ });
164
+
165
+ test("two Ops sharing a name inside one file collide, and the message says so", async () => {
166
+ write("clash.op.ts", `export const a = ${OP_VALUE("twice")};\nexport const b = ${OP_VALUE("twice")};\n`);
167
+ const { ops, errors } = await discoverOps({ cwd: checkout });
168
+ expect(ops.size).toBe(1);
169
+ expect(errors).toHaveLength(1);
170
+ expect(errors[0]).toContain(`Duplicate Op name "twice" declared twice in`);
171
+ });
172
+ });
@@ -1,8 +1,12 @@
1
1
  import { describe, test, expect, vi, beforeEach } from "vitest";
2
2
  import { discoverOps } from "./discover";
3
3
 
4
- // Mock getRuntime to return git root pointing at the repo root
5
- vi.mock("../runtime-adapter", () => ({
4
+ // Mock getRuntime to return git root pointing at the repo root. Partial, via
5
+ // `importOriginal`: the modules a discovered Op file pulls in reach the rest of
6
+ // this module (`moduleDir`, for the lint presets), and a wholesale replacement
7
+ // would make them fail to load rather than fail an assertion.
8
+ vi.mock("../runtime-adapter", async (importOriginal) => ({
9
+ ...(await importOriginal<typeof import("../runtime-adapter")>()),
6
10
  getRuntime: () => ({
7
11
  spawn: async (cmd: string[]) => {
8
12
  if (cmd[0] === "git" && cmd[1] === "rev-parse") {
@@ -7,6 +7,12 @@ import type { OpConfig } from "./types";
7
7
  export interface DiscoveredOp {
8
8
  config: OpConfig;
9
9
  filePath: string;
10
+ /**
11
+ * The export this Op arrived on: `"default"`, or the named export's own
12
+ * name (#2171). Diagnostic only, and the reason a file may carry more than
13
+ * one Op without either becoming ambiguous.
14
+ */
15
+ exportName: string;
10
16
  }
11
17
 
12
18
  export interface OpDiscoveryResult {
@@ -70,11 +76,71 @@ async function collectOpFiles(dir: string): Promise<string[]> {
70
76
  return files;
71
77
  }
72
78
 
79
+ /** The `OpConfig` behind an exported value, or `undefined` when the value is not an Op. An Op entity carries its config on `.props` (`./resource.ts`); `name` and `phases` are what every consumer of a discovered Op reads. */
80
+ function opConfigOf(value: unknown): OpConfig | undefined {
81
+ if (!value || typeof value !== "object") return undefined;
82
+ const config = (value as { props?: unknown }).props as OpConfig | undefined;
83
+ if (!config || typeof config.name !== "string" || !Array.isArray(config.phases)) return undefined;
84
+ return config;
85
+ }
86
+
87
+ /**
88
+ * Every Op a module exports, default first (#2171).
89
+ *
90
+ * Discovery used to read `mod.default` and nothing else, which made
91
+ * `export default op` the only shape a runnable Op could take. That is also
92
+ * the one export shape the fold path refuses (`../discovery/fold-import.ts`
93
+ * scans for it by name and falls the whole file back to run), so an Op living
94
+ * under a project's `sourceDir` cost that project its fold coverage, and took
95
+ * every file importing it down too. Accepting a named export removes the
96
+ * conflict without moving the fold's rule: the default export still works, so
97
+ * every Op file written against the documented shape keeps running unchanged.
98
+ *
99
+ * Non-Op exports are skipped in silence, not reported. An Op file is ordinary
100
+ * TypeScript and may export a helper, a config literal or a type alongside its
101
+ * Op; only a file exporting no Op at all is worth an error.
102
+ *
103
+ * A file may carry more than one Op, and each is registered on its own. Ops
104
+ * are keyed by their declared `config.name`, never by the export name or the
105
+ * file, so two Ops in one file are no more ambiguous to `chant run` than two
106
+ * Ops in two files, and the duplicate-name check in {@link discoverOps}
107
+ * covers both the same way. The same object exported twice (`export default
108
+ * op` alongside `export { op }`) is one Op, deduplicated by identity, not a
109
+ * self-collision.
110
+ *
111
+ * Export order is the module namespace's own: `default` first by construction
112
+ * here, then the named exports, which the ECMAScript specification requires a
113
+ * namespace object to enumerate in sorted order. So the discovered set is
114
+ * deterministic across runs and platforms.
115
+ */
116
+ function opsExportedBy(mod: Record<string, unknown>): Array<{ exportName: string; config: OpConfig }> {
117
+ const found: Array<{ exportName: string; config: OpConfig }> = [];
118
+ const seen = new Set<unknown>();
119
+
120
+ const consider = (exportName: string, value: unknown): void => {
121
+ if (seen.has(value)) return;
122
+ const config = opConfigOf(value);
123
+ if (!config) return;
124
+ seen.add(value);
125
+ found.push({ exportName, config });
126
+ };
127
+
128
+ consider("default", mod.default);
129
+ for (const exportName of Object.keys(mod)) {
130
+ if (exportName === "default") continue;
131
+ consider(exportName, mod[exportName]);
132
+ }
133
+ return found;
134
+ }
135
+
73
136
  /**
74
137
  * Discover all Op definitions from `*.op.ts` files under the nearest chant
75
138
  * project root (the directory holding `chant.config.ts`/`.json`, walking up
76
139
  * from `cwd`), or under the git root when no config exists — see
77
140
  * {@link findDiscoveryRoot} (#2058).
141
+ *
142
+ * An Op may be the file's default export or a named one, and a file may hold
143
+ * several. See {@link opsExportedBy} (#2171).
78
144
  */
79
145
  export async function discoverOps(opts?: { cwd?: string }): Promise<OpDiscoveryResult> {
80
146
  const errors: string[] = [];
@@ -87,28 +153,30 @@ export async function discoverOps(opts?: { cwd?: string }): Promise<OpDiscoveryR
87
153
 
88
154
  for (const filePath of files) {
89
155
  try {
90
- const mod = await import(filePath);
91
- const entity = mod.default;
156
+ const mod = (await import(filePath)) as Record<string, unknown>;
157
+ const exported = opsExportedBy(mod);
92
158
 
93
- if (!entity || typeof entity !== "object") {
94
- errors.push(`${filePath}: default export is not an object`);
159
+ if (exported.length === 0) {
160
+ errors.push(
161
+ `${filePath}: exports no Op. Expected \`export default Op({...})\` or a named export such as \`export const deploy = Op({...})\``,
162
+ );
95
163
  continue;
96
164
  }
97
165
 
98
- const config = entity.props as OpConfig | undefined;
166
+ for (const { exportName, config } of exported) {
167
+ const priorFile = nameToFile.get(config.name);
168
+ if (priorFile !== undefined) {
169
+ errors.push(
170
+ priorFile === filePath
171
+ ? `Duplicate Op name "${config.name}" declared twice in ${filePath}`
172
+ : `Duplicate Op name "${config.name}" in ${filePath} and ${priorFile}`,
173
+ );
174
+ continue;
175
+ }
99
176
 
100
- if (!config || typeof config.name !== "string" || !Array.isArray(config.phases)) {
101
- errors.push(`${filePath}: default export is not a valid Op (missing name or phases)`);
102
- continue;
177
+ nameToFile.set(config.name, filePath);
178
+ ops.set(config.name, { config, filePath, exportName });
103
179
  }
104
-
105
- if (nameToFile.has(config.name)) {
106
- errors.push(`Duplicate Op name "${config.name}" in ${filePath} and ${nameToFile.get(config.name)}`);
107
- continue;
108
- }
109
-
110
- nameToFile.set(config.name, filePath);
111
- ops.set(config.name, { config, filePath });
112
180
  } catch (err) {
113
181
  errors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`);
114
182
  }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The duration-string parser the Op model uses everywhere a timeout is
3
+ * authored ("5m", "30s", "1h30m", "100ms"). Its own leaf module so both
4
+ * `./local-executor.ts` (activity start-to-close timeouts) and `./gate.ts`
5
+ * (a gate's pending-fact expiry) can reach it without importing each other.
6
+ * Re-exported from `./local-executor.ts`, which is where it lived and where
7
+ * every existing caller still imports it from.
8
+ */
9
+
10
+ /** Parse a duration string ("5m", "30s", "1h30m", "100ms") to milliseconds. Throws when nothing in `s` parses as a quantity+unit. */
11
+ export function parseDuration(s: string): number {
12
+ const units: Record<string, number> = { ms: 1, s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000 };
13
+ let total = 0;
14
+ let matched = false;
15
+ for (const m of s.matchAll(/(\d+)(ms|s|m|h|d)/g)) {
16
+ total += Number(m[1]) * units[m[2]];
17
+ matched = true;
18
+ }
19
+ if (!matched) throw new Error(`unparseable duration: "${s}"`);
20
+ return total;
21
+ }
@@ -11,7 +11,7 @@
11
11
  * - `receiptStaleness` (WatchOp's phase) is read-only: it reports absent and
12
12
  * differing receipts as findings and never writes.
13
13
  */
14
- import { describe, test, expect } from "vitest";
14
+ import { describe, test, expect, vi } from "vitest";
15
15
  import { EffectReceipt, receiptExpectation, EXISTENCE_EXPECTATION } from "../effect-receipt";
16
16
  import { INTRINSIC_MARKER, type Intrinsic } from "../intrinsic";
17
17
  import { effect, phase } from "./builders";
@@ -23,7 +23,8 @@ import {
23
23
  } from "./receipt-store";
24
24
  import type { ActivityFn, ActivityProfile } from "./activity-registry";
25
25
  import type { OpConfig, ActivityStep } from "./types";
26
- import { runOpLocally, findGate, LocalGateUnsupportedError, OpRunFailure } from "./local-executor";
26
+ import { runOpLocally, OpRunFailure } from "./local-executor";
27
+ import { memoryGateLedgerPort } from "./gate";
27
28
 
28
29
  function fakeIntrinsic(json: unknown): Intrinsic {
29
30
  return {
@@ -187,7 +188,7 @@ describe("receiptActivities (the injectable store seam)", () => {
187
188
  // ── Local executor: read-compare-run-write ────────────────────────────────────
188
189
 
189
190
  const PROFILES: Record<string, ActivityProfile> = {
190
- fastIdempotent: { startToCloseTimeout: "5m", retry: { maximumAttempts: 1 } },
191
+ fastIdempotent: { timeout: "5m", retry: { maximumAttempts: 1 } },
191
192
  };
192
193
 
193
194
  function activityMap(
@@ -216,7 +217,7 @@ describe("runOpLocally — effect steps", () => {
216
217
  const ran: string[] = [];
217
218
  const activities = activityMap(store, { runSeed: async () => void ran.push("runSeed") });
218
219
  const result = await runOpLocally(seedOp(), activities, PROFILES);
219
- expect(result.ok).toBe(true);
220
+ expect(result.status).toBe("ok");
220
221
  expect(ran).toEqual([]);
221
222
  expect(writes).toEqual([]);
222
223
  // The read records the effect as already applied; the nested step is skipped.
@@ -235,7 +236,7 @@ describe("runOpLocally — effect steps", () => {
235
236
  { runSeed: async () => void order.push("runSeed") },
236
237
  );
237
238
  const result = await runOpLocally(seedOp(), activities, PROFILES);
238
- expect(result.ok).toBe(true);
239
+ expect(result.status).toBe("ok");
239
240
  expect(order).toEqual(["runSeed", "write"]);
240
241
  expect(writes).toEqual([{ name: "seeded", expectation: receiptExpectation(seeded) }]);
241
242
  expect(result.records.map((r) => [r.fn, r.status])).toEqual([
@@ -262,7 +263,7 @@ describe("runOpLocally — effect steps", () => {
262
263
  // then writes.
263
264
  healthy = true;
264
265
  const rerun = await runOpLocally(seedOp(), activities, PROFILES);
265
- expect(rerun.ok).toBe(true);
266
+ expect(rerun.status).toBe("ok");
266
267
  expect(writes).toEqual([{ name: "seeded", expectation: receiptExpectation(seeded) }]);
267
268
  });
268
269
 
@@ -282,7 +283,7 @@ describe("runOpLocally — effect steps", () => {
282
283
  ]);
283
284
  });
284
285
 
285
- test("a gate nested in an effect step is rejected up front, like any other gate", () => {
286
+ test("a gate nested in an effect step stops before the receipt is written (#2119)", async () => {
286
287
  const config: OpConfig = {
287
288
  name: "gated",
288
289
  overview: "",
@@ -290,11 +291,25 @@ describe("runOpLocally — effect steps", () => {
290
291
  phase("Seed", [effect(seeded, [{ kind: "gate", signalName: "approve-seed" }, step("runSeed")])]),
291
292
  ],
292
293
  };
293
- expect(findGate(config)?.signalName).toBe("approve-seed");
294
294
  const { store } = memStore();
295
- return expect(
296
- runOpLocally(config, activityMap(store, { runSeed: async () => {} }), PROFILES),
297
- ).rejects.toThrow(LocalGateUnsupportedError);
295
+ const ran = vi.fn();
296
+ const result = await runOpLocally(
297
+ config,
298
+ activityMap(store, { runSeed: async () => { ran(); } }),
299
+ PROFILES,
300
+ undefined,
301
+ { gates: memoryGateLedgerPort(), now: "2026-09-05T12:00:00.000Z" },
302
+ );
303
+ expect(result.status).toBe("gated");
304
+ expect(result.gate?.gate).toBe("approve-seed");
305
+ expect(ran).not.toHaveBeenCalled();
306
+ // The receipt is untouched, so the next run re-proposes the effect.
307
+ expect(result.records.map((r) => [r.fn, r.status])).toEqual([
308
+ ["receiptRead", "ok"],
309
+ ["gate:approve-seed", "skipped"],
310
+ ["runSeed", "skipped"],
311
+ ["receiptWrite", "skipped"],
312
+ ]);
298
313
  });
299
314
 
300
315
  test("an effect step in a parallel phase is refused — read-compare-run-write is ordered", async () => {
@@ -1,6 +1,6 @@
1
1
  import { exec } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
- import { safeHeartbeat, sleep } from "./activity-runtime";
3
+ import { sleep } from "./activity-runtime";
4
4
 
5
5
  const execAsync = promisify(exec);
6
6
 
@@ -158,7 +158,6 @@ export function emulatorLifecycle(spec: EmulatorSpec): EmulatorLifecycle {
158
158
  let ok = false;
159
159
  while (Date.now() < deadline) {
160
160
  if (signal?.aborted) throw new Error(`emulator "${name}" wait aborted`);
161
- safeHeartbeat({ step: "emulatorUp", container: name });
162
161
  try {
163
162
  const res = await fetch(url, { signal });
164
163
  if (res.ok && ready(await res.text())) {
package/src/op/gate.ts ADDED
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Gate-as-fact (#2119, epic #2114) — the one place that decides what happens
3
+ * when a run reaches a `gate`, shared by the Op executor
4
+ * (`./local-executor.ts`) and the component driver
5
+ * (`../components/driver.ts`) so the two can never drift.
6
+ *
7
+ * A gate used to be a wait, which meant a runtime that cannot wait had to
8
+ * refuse the whole op before its first step (`LocalGateUnsupportedError`,
9
+ * deleted with this). It is a fact now: the run reads the gate ledger
10
+ * (`../lifecycle/gate-ledger.ts`) for a resolution someone wrote with `chant
11
+ * approve`, and either walks through carrying the approver, or records a
12
+ * pending fact and ends. Nothing blocks, nothing is queued, nothing has to be
13
+ * resumed — the next run re-evaluates from the ledger, which is exactly the
14
+ * level-triggered shape a converge tick already has.
15
+ *
16
+ * The staleness rule is the same one `chant operator status` has applied since
17
+ * #1485: a resolution counts only if it is newer than the newest pending fact
18
+ * for that gate. Approving before the gate was ever reached does not
19
+ * pre-authorize a future run's gate that has since been recorded pending; a
20
+ * resolution from last month does not answer the fact this run just wrote.
21
+ *
22
+ * Ledger access goes through {@link GateLedgerPort} rather than straight to
23
+ * git, so a test (and the operator's own in-memory paths) can drive the
24
+ * decision without an orphan branch on disk.
25
+ */
26
+
27
+ import {
28
+ appendPendingGate,
29
+ isPendingGateExpired,
30
+ latestPendingGate,
31
+ latestResolutionSince,
32
+ readGateLedger,
33
+ resolveApprovalUrl,
34
+ DEFAULT_GATE_EXPIRY,
35
+ type GateResolutionRecord,
36
+ type PendingGateInput,
37
+ type PendingGateRecord,
38
+ } from "../lifecycle/gate-ledger";
39
+ import { pushLifecycle } from "../lifecycle/git";
40
+ import { parseDuration } from "./duration";
41
+
42
+ /** The gate ledger, as the two executors need it: read both kinds of line, append a pending fact. */
43
+ export interface GateLedgerPort {
44
+ read(op: string): Promise<{ resolutions: GateResolutionRecord[]; pending: PendingGateRecord[] }>;
45
+ appendPending(input: PendingGateInput): Promise<PendingGateRecord>;
46
+ }
47
+
48
+ /**
49
+ * The real port: the `chant/lifecycle` orphan branch. Pushes best-effort after
50
+ * appending, the same two-step-collapsed-into-one shape `chant approve` uses
51
+ * (`../cli/handlers/operator.ts` calls `pushLifecycle().catch(...)` right after
52
+ * its append) — a pending fact that only ever reaches the local branch is still
53
+ * a correct local answer, so a missing remote never fails the run.
54
+ */
55
+ export function gitGateLedgerPort(opts?: { cwd?: string }): GateLedgerPort {
56
+ return {
57
+ async read(op) {
58
+ const { resolutions, pending } = await readGateLedger(op, opts);
59
+ return { resolutions, pending };
60
+ },
61
+ async appendPending(input) {
62
+ const { record } = await appendPendingGate(input, opts);
63
+ await pushLifecycle(opts).catch(() => undefined);
64
+ return record;
65
+ },
66
+ };
67
+ }
68
+
69
+ /** A gate ledger held in memory — what a test, or a caller that has already read the ledger, hands {@link evaluateGate}. */
70
+ export function memoryGateLedgerPort(
71
+ seed: { resolutions?: GateResolutionRecord[]; pending?: PendingGateRecord[] } = {},
72
+ ): GateLedgerPort & { appended: PendingGateRecord[] } {
73
+ const resolutions = [...(seed.resolutions ?? [])];
74
+ const pending = [...(seed.pending ?? [])];
75
+ const appended: PendingGateRecord[] = [];
76
+ return {
77
+ appended,
78
+ async read() {
79
+ return { resolutions: [...resolutions], pending: [...pending] };
80
+ },
81
+ async appendPending(input) {
82
+ const record: PendingGateRecord = { version: 1, kind: "pending", ...input };
83
+ pending.push(record);
84
+ appended.push(record);
85
+ return record;
86
+ },
87
+ };
88
+ }
89
+
90
+ /** What a caller has to say about the gate it just reached. */
91
+ export interface GateCheckInput {
92
+ /** The op the gate belongs to — the component's name, on the driver. */
93
+ op: string;
94
+ /** The gate's signal name. */
95
+ gate: string;
96
+ description?: string;
97
+ /** The gate's authored `timeout`, which becomes the pending fact's expiry. Default {@link DEFAULT_GATE_EXPIRY}. */
98
+ timeout?: string;
99
+ /** Identifies the run that reached the gate, when the caller has one. */
100
+ runId?: string;
101
+ /** ISO-8601 "now" — supplied by the caller, so the decision is deterministic under test. */
102
+ now?: string;
103
+ }
104
+
105
+ /** Either the gate is answered, or it is a standing fact. */
106
+ export type GateCheck =
107
+ | { satisfied: true; resolution: GateResolutionRecord }
108
+ | { satisfied: false; pending: PendingGateRecord; recorded: boolean };
109
+
110
+ /** The beginning of time — the anchor for a gate that has never been recorded pending, so any resolution for it counts. */
111
+ const EPOCH = new Date(0).toISOString();
112
+
113
+ /**
114
+ * Decide a gate against the ledger, recording the pending fact when it isn't
115
+ * answered.
116
+ *
117
+ * - A resolution newer than the newest pending fact satisfies the gate.
118
+ * - Otherwise, a pending fact that hasn't expired stands as it is: the run
119
+ * ends `gated` and the ledger is left alone, so an operator ticking every
120
+ * minute against an unapproved gate does not append a line a minute.
121
+ * - Otherwise (no pending fact, or the newest one has expired) a fresh pending
122
+ * fact is appended and the run ends `gated`. `recorded` says which of the
123
+ * two happened, so a renderer can tell "just recorded" from "still standing".
124
+ */
125
+ export async function evaluateGate(port: GateLedgerPort, input: GateCheckInput): Promise<GateCheck> {
126
+ const now = input.now ?? new Date().toISOString();
127
+ const { resolutions, pending } = await port.read(input.op);
128
+
129
+ const standing = latestPendingGate(pending, input.gate);
130
+ const resolution = latestResolutionSince(resolutions, input.gate, standing?.timestamp ?? EPOCH);
131
+ if (resolution) return { satisfied: true, resolution };
132
+
133
+ if (standing && !isPendingGateExpired(standing, now)) {
134
+ return { satisfied: false, pending: standing, recorded: false };
135
+ }
136
+
137
+ const url = resolveApprovalUrl();
138
+ const record = await port.appendPending({
139
+ op: input.op,
140
+ gate: input.gate,
141
+ timestamp: now,
142
+ expiresAt: new Date(
143
+ new Date(now).getTime() + parseDuration(input.timeout ?? DEFAULT_GATE_EXPIRY),
144
+ ).toISOString(),
145
+ ...(input.description ? { description: input.description } : {}),
146
+ ...(input.runId ? { runId: input.runId } : {}),
147
+ ...(url ? { url } : {}),
148
+ });
149
+ return { satisfied: false, pending: record, recorded: true };
150
+ }
151
+
152
+ /** The line every renderer prints to say how a pending gate is cleared. */
153
+ export function approveCommand(op: string, gate: string): string {
154
+ return `chant approve ${op} ${gate}`;
155
+ }
@@ -9,7 +9,9 @@
9
9
  */
10
10
 
11
11
  import { describe, test, expect, vi } from "vitest";
12
- import { generateOpsPipeline } from "./generate-pipeline";
12
+ import { generateOpsPipeline, withOpSchedules } from "./generate-pipeline";
13
+ import type { DiscoveredOp } from "./discover";
14
+ import type { OpConfig } from "./types";
13
15
  import type { ScheduledOpSpec, OpPipelineResult } from "../lexicon";
14
16
 
15
17
  vi.mock("@intentius/chant-lexicon-gitlab", () => ({
@@ -22,7 +24,12 @@ vi.mock("@intentius/chant-lexicon-gitlab", () => ({
22
24
  package: () => "gitlab",
23
25
  generateOpPipeline: (ops: ScheduledOpSpec[]): OpPipelineResult => ({
24
26
  files: ops.map((o) => ({ name: `${o.name}.yml`, yaml: `# ${o.name} @ ${o.schedule}` })),
25
- jobs: ops.map((o) => ({ jobName: o.name, op: o.name, schedule: o.schedule, findingMode: o.findingMode ?? "report" })),
27
+ jobs: ops.map((o) => ({
28
+ jobName: o.name,
29
+ op: o.name,
30
+ trigger: o.trigger ?? { kind: "cron" as const, schedule: o.schedule! },
31
+ findingMode: o.findingMode ?? "report",
32
+ })),
26
33
  }),
27
34
  },
28
35
  }));
@@ -48,6 +55,61 @@ describe("generateOpsPipeline", () => {
48
55
  const result = await generateOpsPipeline(specs, "gitlab");
49
56
  expect(result.success).toBe(true);
50
57
  expect(result.files).toEqual([{ name: "alb-deploy.yml", yaml: "# alb-deploy @ 0 6 * * *" }]);
51
- expect(result.jobs).toEqual([{ jobName: "alb-deploy", op: "alb-deploy", schedule: "0 6 * * *", findingMode: "issue" }]);
58
+ expect(result.jobs).toEqual([
59
+ { jobName: "alb-deploy", op: "alb-deploy", trigger: { kind: "cron", schedule: "0 6 * * *" }, findingMode: "issue" },
60
+ ]);
61
+ });
62
+
63
+ // #2084: `trigger` is new and optional — a spec built the old way (`schedule`
64
+ // only, no `trigger`) must still round-trip through `generateOpsPipeline`
65
+ // unchanged, since every pre-#2084 caller (`WorkflowAuditOp`,
66
+ // `PipelineAuditOp`, `ReconcileOp`) only ever sets `schedule`.
67
+ test("a legacy `{ schedule }` spec with no `trigger` round-trips unchanged", async () => {
68
+ const specs: ScheduledOpSpec[] = [{ name: "alb-deploy", schedule: "0 6 * * *" }];
69
+ const result = await generateOpsPipeline(specs, "gitlab");
70
+ expect(result.success).toBe(true);
71
+ expect(result.files).toEqual([{ name: "alb-deploy.yml", yaml: "# alb-deploy @ 0 6 * * *" }]);
72
+ expect(result.jobs).toEqual([
73
+ { jobName: "alb-deploy", op: "alb-deploy", trigger: { kind: "cron", schedule: "0 6 * * *" }, findingMode: "report" },
74
+ ]);
75
+ });
76
+ });
77
+
78
+ describe("withOpSchedules — the Op's own cadence reaches the CI generator (#2120)", () => {
79
+ function discovered(entries: Array<[string, OpConfig]>): Map<string, DiscoveredOp> {
80
+ return new Map(entries.map(([name, config]) => [name, { config, filePath: `${name}.op.ts`, exportName: "default" }]));
81
+ }
82
+
83
+ const scheduled: OpConfig = {
84
+ name: "prod-watch",
85
+ overview: "watch prod",
86
+ phases: [],
87
+ schedule: { cron: "*/10 * * * *", overlap: "skip" },
88
+ };
89
+ const unscheduled: OpConfig = { name: "one-shot", overview: "no cadence", phases: [] };
90
+
91
+ test("a scheduled Op's cadence lands on its spec as opSchedule", () => {
92
+ const [spec] = withOpSchedules([{ name: "prod-watch" }], discovered([["prod-watch", scheduled]]));
93
+ expect(spec.opSchedule).toEqual({ cron: "*/10 * * * *", overlap: "skip" });
94
+ });
95
+
96
+ test("an Op with no cadence leaves its spec untouched", () => {
97
+ const input: ScheduledOpSpec[] = [{ name: "one-shot", schedule: "0 6 * * *" }];
98
+ expect(withOpSchedules(input, discovered([["one-shot", unscheduled]]))).toEqual(input);
99
+ });
100
+
101
+ test("an explicit trigger or schedule on the spec is preserved alongside it — resolveOpTrigger picks", () => {
102
+ const [spec] = withOpSchedules(
103
+ [{ name: "prod-watch", trigger: { kind: "push", branches: ["main"] } }],
104
+ discovered([["prod-watch", scheduled]]),
105
+ );
106
+ expect(spec.trigger).toEqual({ kind: "push", branches: ["main"] });
107
+ expect(spec.opSchedule).toEqual({ cron: "*/10 * * * *", overlap: "skip" });
108
+ });
109
+
110
+ test("the input specs are not mutated", () => {
111
+ const input: ScheduledOpSpec[] = [{ name: "prod-watch" }];
112
+ withOpSchedules(input, discovered([["prod-watch", scheduled]]));
113
+ expect(input[0].opSchedule).toBeUndefined();
52
114
  });
53
115
  });