@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.
- package/dist/audit/catalog.d.ts +51 -1
- package/dist/audit/catalog.d.ts.map +1 -1
- package/dist/audit/core.d.ts +20 -1
- package/dist/audit/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts +14 -8
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/lineage.d.ts +10 -0
- package/dist/audit/lineage.d.ts.map +1 -0
- package/dist/audit/prior-art.d.ts +385 -0
- package/dist/audit/prior-art.d.ts.map +1 -0
- package/dist/audit/proof.d.ts +16 -0
- package/dist/audit/proof.d.ts.map +1 -1
- package/dist/audit/report-html.d.ts.map +1 -1
- package/dist/audit/report-model.d.ts +8 -1
- package/dist/audit/report-model.d.ts.map +1 -1
- package/dist/audit/report.d.ts.map +1 -1
- package/dist/audit/rules-doc.d.ts +3 -0
- package/dist/audit/rules-doc.d.ts.map +1 -1
- package/dist/audit/secrets.d.ts +15 -0
- package/dist/audit/secrets.d.ts.map +1 -1
- package/dist/audit/terraform-state.d.ts +46 -0
- package/dist/audit/terraform-state.d.ts.map +1 -0
- package/dist/build.d.ts +17 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/claimed-fields.d.ts +161 -0
- package/dist/claimed-fields.d.ts.map +1 -0
- package/dist/cli/build-params-cli.d.ts +2 -2
- package/dist/cli/command-group.d.ts +14 -0
- package/dist/cli/command-group.d.ts.map +1 -1
- package/dist/cli/commands/audit.d.ts +17 -1
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-docs.d.ts +1 -1
- package/dist/cli/commands/check-lexicon-examples.d.ts +16 -6
- package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
- package/dist/cli/commands/import.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon/templates/codegen.d.ts +1 -0
- package/dist/cli/commands/init-lexicon/templates/codegen.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon/templates/project.d.ts +7 -0
- package/dist/cli/commands/init-lexicon/templates/project.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/commands/onboard.d.ts +2 -0
- package/dist/cli/commands/onboard.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts +82 -16
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts +49 -42
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts +28 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/op-tools.d.ts +16 -1
- package/dist/cli/mcp/op-tools.d.ts.map +1 -1
- package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
- package/dist/cli/mcp/resources/context.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +26 -9
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/generate-post-synth-barrel.d.ts +1 -1
- package/dist/components/auto-release.d.ts +10 -20
- package/dist/components/auto-release.d.ts.map +1 -1
- package/dist/components/capability.d.ts +4 -4
- package/dist/components/cli-support.d.ts +20 -21
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/component.d.ts +3 -3
- package/dist/components/component.d.ts.map +1 -1
- package/dist/components/driver-output.d.ts.map +1 -1
- package/dist/components/driver.d.ts +66 -36
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/index.d.ts +1 -2
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/manifest-persistence.d.ts +1 -9
- package/dist/components/manifest-persistence.d.ts.map +1 -1
- package/dist/components/run-progress.d.ts +6 -6
- package/dist/components/run-progress.d.ts.map +1 -1
- package/dist/components/verbs/run-agent.d.ts +9 -15
- package/dist/components/verbs/run-agent.d.ts.map +1 -1
- package/dist/components/verbs/verify.d.ts +2 -2
- package/dist/deep-observation.d.ts +17 -0
- package/dist/deep-observation.d.ts.map +1 -1
- package/dist/discovery/files.d.ts +4 -4
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/param-deps.d.ts +33 -0
- package/dist/discovery/param-deps.d.ts.map +1 -1
- package/dist/discovery/sandbox/bundle.d.ts.map +1 -1
- package/dist/discovery/sandbox/config-wire.d.ts +2 -2
- package/dist/fold/foldable-helpers.d.ts +5 -1
- package/dist/fold/foldable-helpers.d.ts.map +1 -1
- package/dist/fold-provenance.d.ts +236 -0
- package/dist/fold-provenance.d.ts.map +1 -0
- package/dist/graph-ops.d.ts +1 -1
- package/dist/held-elsewhere.d.ts +85 -0
- package/dist/held-elsewhere.d.ts.map +1 -0
- package/dist/import/ir-utils.d.ts +1 -1
- package/dist/import/ir-utils.d.ts.map +1 -1
- package/dist/import/parser.d.ts +33 -0
- package/dist/import/parser.d.ts.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/kubectl-context.d.ts +3 -3
- package/dist/lexicon-config.d.ts +4 -4
- package/dist/lexicon-plugin-helpers.d.ts +1 -1
- package/dist/lexicon.d.ts +104 -14
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/change-set.d.ts +13 -0
- package/dist/lifecycle/change-set.d.ts.map +1 -1
- package/dist/lifecycle/deep-diff.d.ts +133 -6
- package/dist/lifecycle/deep-diff.d.ts.map +1 -1
- package/dist/lifecycle/deep-observe.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +71 -3
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lifecycle/index.d.ts +1 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.d.ts +2 -2
- package/dist/lifecycle/run-ledger.d.ts +53 -0
- package/dist/lifecycle/run-ledger.d.ts.map +1 -0
- package/dist/lifecycle/symptoms.d.ts +3 -3
- package/dist/lifecycle/symptoms.d.ts.map +1 -1
- package/dist/lint/config.d.ts +81 -2
- package/dist/lint/config.d.ts.map +1 -1
- package/dist/lint/post-synth.d.ts +55 -2
- package/dist/lint/post-synth.d.ts.map +1 -1
- package/dist/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.d.ts +1 -1
- package/dist/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.d.ts.map +1 -1
- package/dist/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.d.ts +1 -1
- package/dist/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.d.ts.map +1 -1
- package/dist/lint/rules/comp/comp004-gate-needs-durable-runtime.d.ts +44 -0
- package/dist/lint/rules/comp/comp004-gate-needs-durable-runtime.d.ts.map +1 -0
- package/dist/lint/rules/comp/index.d.ts +1 -1
- package/dist/lint/rules/comp/index.d.ts.map +1 -1
- package/dist/lint/rules/op/index.d.ts +16 -0
- package/dist/lint/rules/op/index.d.ts.map +1 -0
- package/dist/lint/rules/op/ops012-activity-contract.d.ts +27 -0
- package/dist/lint/rules/op/ops012-activity-contract.d.ts.map +1 -0
- package/dist/lint/rules/op/ops013-step-output-ref.d.ts +61 -0
- package/dist/lint/rules/op/ops013-step-output-ref.d.ts.map +1 -0
- package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts +55 -0
- package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts.map +1 -0
- package/dist/lint/rules/op/support.d.ts +12 -0
- package/dist/lint/rules/op/support.d.ts.map +1 -0
- package/dist/lint/suppressions.d.ts +108 -0
- package/dist/lint/suppressions.d.ts.map +1 -0
- package/dist/live-endpoint.d.ts +1 -1
- package/dist/op/activities/activity-contracts.d.ts +54 -0
- package/dist/op/activities/activity-contracts.d.ts.map +1 -0
- package/dist/op/activities/apply.d.ts +262 -0
- package/dist/op/activities/apply.d.ts.map +1 -0
- package/dist/op/activities/build.d.ts +13 -0
- package/dist/op/activities/build.d.ts.map +1 -0
- package/dist/op/activities/converge.d.ts +110 -0
- package/dist/op/activities/converge.d.ts.map +1 -0
- package/dist/op/activities/env-teardown.d.ts +76 -0
- package/dist/op/activities/env-teardown.d.ts.map +1 -0
- package/dist/op/activities/guard-validate.d.ts +64 -0
- package/dist/op/activities/guard-validate.d.ts.map +1 -0
- package/dist/op/activities/http-check.d.ts +34 -0
- package/dist/op/activities/http-check.d.ts.map +1 -0
- package/dist/op/activities/index.d.ts +31 -0
- package/dist/op/activities/index.d.ts.map +1 -0
- package/dist/op/activities/lexicon-upgrade.d.ts +182 -0
- package/dist/op/activities/lexicon-upgrade.d.ts.map +1 -0
- package/dist/op/activities/lifecycle.d.ts +42 -0
- package/dist/op/activities/lifecycle.d.ts.map +1 -0
- package/dist/op/activities/pipeline-audit.d.ts +84 -0
- package/dist/op/activities/pipeline-audit.d.ts.map +1 -0
- package/dist/op/activities/policy.d.ts +17 -0
- package/dist/op/activities/policy.d.ts.map +1 -0
- package/dist/op/activities/reconcile.d.ts +89 -0
- package/dist/op/activities/reconcile.d.ts.map +1 -0
- package/dist/op/activities/shell.d.ts +13 -0
- package/dist/op/activities/shell.d.ts.map +1 -0
- package/dist/op/activities/teardown.d.ts +10 -0
- package/dist/op/activities/teardown.d.ts.map +1 -0
- package/dist/op/activities/util.d.ts +7 -0
- package/dist/op/activities/util.d.ts.map +1 -0
- package/dist/op/activities/wait.d.ts +28 -0
- package/dist/op/activities/wait.d.ts.map +1 -0
- package/dist/op/activities/workflow-audit.d.ts +92 -0
- package/dist/op/activities/workflow-audit.d.ts.map +1 -0
- package/dist/op/activity-contract-registry.d.ts +76 -0
- package/dist/op/activity-contract-registry.d.ts.map +1 -0
- package/dist/op/activity-contract.d.ts +25 -18
- package/dist/op/activity-contract.d.ts.map +1 -1
- package/dist/op/activity-failure.d.ts +28 -0
- package/dist/op/activity-failure.d.ts.map +1 -0
- package/dist/op/activity-profiles.d.ts +118 -0
- package/dist/op/activity-profiles.d.ts.map +1 -0
- package/dist/op/activity-registry.d.ts +22 -38
- package/dist/op/activity-registry.d.ts.map +1 -1
- package/dist/op/activity-runtime.d.ts +3 -14
- package/dist/op/activity-runtime.d.ts.map +1 -1
- package/dist/op/builders.d.ts +84 -29
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/composites/apply-op.d.ts +113 -0
- package/dist/op/composites/apply-op.d.ts.map +1 -0
- package/dist/op/composites/converge-op.d.ts +120 -0
- package/dist/op/composites/converge-op.d.ts.map +1 -0
- package/dist/op/composites/index.d.ts +26 -0
- package/dist/op/composites/index.d.ts.map +1 -0
- package/dist/op/composites/lexicon-upgrade-op.d.ts +78 -0
- package/dist/op/composites/lexicon-upgrade-op.d.ts.map +1 -0
- package/dist/op/composites/pipeline-audit-op.d.ts +50 -0
- package/dist/op/composites/pipeline-audit-op.d.ts.map +1 -0
- package/dist/op/composites/reconcile-op.d.ts +57 -0
- package/dist/op/composites/reconcile-op.d.ts.map +1 -0
- package/dist/op/composites/watch-op.d.ts +60 -0
- package/dist/op/composites/watch-op.d.ts.map +1 -0
- package/dist/op/composites/workflow-audit-op.d.ts +57 -0
- package/dist/op/composites/workflow-audit-op.d.ts.map +1 -0
- package/dist/op/converge-rule.d.ts +12 -10
- package/dist/op/converge-rule.d.ts.map +1 -1
- package/dist/op/cron.d.ts +51 -0
- package/dist/op/cron.d.ts.map +1 -0
- package/dist/op/discover.d.ts +9 -0
- package/dist/op/discover.d.ts.map +1 -1
- package/dist/op/duration.d.ts +11 -0
- package/dist/op/duration.d.ts.map +1 -0
- package/dist/op/emulator-lifecycle.d.ts.map +1 -1
- package/dist/op/gate.d.ts +90 -0
- package/dist/op/gate.d.ts.map +1 -0
- package/dist/op/generate-pipeline.d.ts +9 -0
- package/dist/op/generate-pipeline.d.ts.map +1 -1
- package/dist/op/index.d.ts +23 -7
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts +114 -28
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-output.d.ts +11 -1
- package/dist/op/local-output.d.ts.map +1 -1
- package/dist/op/op-ir.d.ts +184 -0
- package/dist/op/op-ir.d.ts.map +1 -0
- package/dist/op/op-verb-class.d.ts +10 -2
- package/dist/op/op-verb-class.d.ts.map +1 -1
- package/dist/op/operator.d.ts +30 -5
- package/dist/op/operator.d.ts.map +1 -1
- package/dist/op/receipt-store.d.ts +1 -1
- package/dist/op/receipt-store.d.ts.map +1 -1
- package/dist/op/resource.d.ts +15 -1
- package/dist/op/resource.d.ts.map +1 -1
- package/dist/op/runtime.d.ts +200 -0
- package/dist/op/runtime.d.ts.map +1 -0
- package/dist/op/runtimes/local.d.ts +29 -0
- package/dist/op/runtimes/local.d.ts.map +1 -0
- package/dist/op/step-output-ref.d.ts +15 -13
- package/dist/op/step-output-ref.d.ts.map +1 -1
- package/dist/op/types.d.ts +79 -27
- package/dist/op/types.d.ts.map +1 -1
- package/dist/ownership.d.ts +1 -1
- package/dist/provenance.d.ts +21 -0
- package/dist/provenance.d.ts.map +1 -1
- package/dist/serializer-walker.d.ts.map +1 -1
- package/dist/stack-output.d.ts +5 -0
- package/dist/stack-output.d.ts.map +1 -1
- package/package.json +6 -1
- package/src/audit/catalog.test.ts +79 -3
- package/src/audit/catalog.ts +111 -13
- package/src/audit/core.test.ts +26 -0
- package/src/audit/core.ts +61 -17
- package/src/audit/discover.test.ts +39 -0
- package/src/audit/discover.ts +91 -15
- package/src/audit/lineage.ts +100 -0
- package/src/audit/prior-art.ts +153 -0
- package/src/audit/proof.test.ts +67 -0
- package/src/audit/proof.ts +70 -0
- package/src/audit/report-html.ts +9 -4
- package/src/audit/report-model.ts +16 -2
- package/src/audit/report.test.ts +33 -0
- package/src/audit/report.ts +5 -3
- package/src/audit/rules-doc.test.ts +34 -2
- package/src/audit/rules-doc.ts +82 -9
- package/src/audit/secrets.ts +15 -3
- package/src/audit/terraform-state.test.ts +144 -0
- package/src/audit/terraform-state.ts +131 -0
- package/src/build.ts +31 -1
- package/src/claimed-fields.test.ts +145 -0
- package/src/claimed-fields.ts +208 -0
- package/src/cli/build-params-cli.ts +2 -2
- package/src/cli/command-group.ts +14 -0
- package/src/cli/commands/__fixtures__/audit-terraform/infra/.terraform/providers/lock.txt +1 -0
- package/src/cli/commands/__fixtures__/audit-terraform/infra/main.tf +1 -0
- package/src/cli/commands/__fixtures__/audit-terraform/infra/modules/foo/main.tf +1 -0
- package/src/cli/commands/__fixtures__/audit-terraform/infra/variables.tf +3 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +13 -4
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/docs-cli.ts +4 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/package.ts +1 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/package-cli.ts +21 -0
- package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +1 -0
- package/src/cli/commands/audit.test.ts +189 -11
- package/src/cli/commands/audit.ts +73 -27
- package/src/cli/commands/build.test.ts +292 -9
- package/src/cli/commands/build.ts +156 -24
- package/src/cli/commands/check-lexicon-docs.ts +1 -1
- package/src/cli/commands/check-lexicon-examples.test.ts +29 -1
- package/src/cli/commands/check-lexicon-examples.ts +42 -10
- package/src/cli/commands/import.ts +6 -0
- package/src/cli/commands/init-lexicon/templates/codegen.ts +9 -0
- package/src/cli/commands/init-lexicon/templates/project.ts +44 -4
- package/src/cli/commands/init-lexicon.test.ts +97 -2
- package/src/cli/commands/init-lexicon.ts +4 -2
- package/src/cli/commands/init.test.ts +35 -1
- package/src/cli/commands/init.ts +14 -3
- package/src/cli/commands/lint.test.ts +48 -0
- package/src/cli/commands/lint.ts +123 -0
- package/src/cli/commands/onboard.test.ts +75 -0
- package/src/cli/commands/onboard.ts +54 -23
- package/src/cli/handlers/build.test.ts +81 -1
- package/src/cli/handlers/build.ts +15 -1
- package/src/cli/handlers/graph.test.ts +3 -3
- package/src/cli/handlers/lifecycle.test.ts +195 -12
- package/src/cli/handlers/lifecycle.ts +123 -6
- package/src/cli/handlers/misc.ts +1 -1
- package/src/cli/handlers/operator.test.ts +183 -11
- package/src/cli/handlers/operator.ts +260 -55
- package/src/cli/handlers/run.test.ts +431 -1287
- package/src/cli/handlers/run.ts +365 -915
- package/src/cli/main.test.ts +23 -15
- package/src/cli/main.ts +88 -33
- package/src/cli/mcp/op-tools.ts +147 -88
- package/src/cli/mcp/resource-handlers.ts +10 -37
- package/src/cli/mcp/resources/context.ts +11 -12
- package/src/cli/mcp/server.test.ts +25 -27
- package/src/cli/mcp/server.ts +2 -2
- package/src/cli/registry.ts +26 -9
- package/src/codegen/docs.ts +1 -1
- package/src/codegen/generate-post-synth-barrel.test.ts +1 -1
- package/src/codegen/generate-post-synth-barrel.ts +1 -1
- package/src/components/README.md +3 -2
- package/src/components/auto-release.test.ts +4 -20
- package/src/components/auto-release.ts +17 -42
- package/src/components/capability.ts +4 -4
- package/src/components/cli-support.test.ts +10 -5
- package/src/components/cli-support.ts +52 -44
- package/src/components/component.schema.json +2 -2
- package/src/components/component.ts +3 -3
- package/src/components/driver-output.ts +14 -2
- package/src/components/driver.test.ts +92 -22
- package/src/components/driver.ts +193 -83
- package/src/components/index.ts +1 -6
- package/src/components/manifest-persistence.test.ts +2 -20
- package/src/components/manifest-persistence.ts +4 -24
- package/src/components/pilots/pilots-lint-clean.test.ts +11 -10
- package/src/components/run-progress.ts +6 -6
- package/src/components/verbs/run-agent.ts +11 -17
- package/src/components/verbs/verify.ts +2 -2
- package/src/deep-observation.test.ts +75 -0
- package/src/deep-observation.ts +48 -0
- package/src/discovery/collect.test.ts +3 -3
- package/src/discovery/files.ts +4 -4
- package/src/discovery/fold-import.ts +105 -2
- package/src/discovery/param-deps.test.ts +112 -1
- package/src/discovery/param-deps.ts +193 -0
- package/src/discovery/sandbox/bundle.ts +15 -1
- package/src/discovery/sandbox/config-wire.test.ts +2 -2
- package/src/discovery/sandbox/config-wire.ts +2 -2
- package/src/fold/fold.test.ts +56 -0
- package/src/fold/foldable-helpers.ts +81 -1
- package/src/fold-provenance-exclusion.test.ts +186 -0
- package/src/fold-provenance.test.ts +495 -0
- package/src/fold-provenance.ts +441 -0
- package/src/graph-ops.test.ts +2 -2
- package/src/graph-ops.ts +1 -1
- package/src/held-elsewhere.test.ts +102 -0
- package/src/held-elsewhere.ts +114 -0
- package/src/import/ir-utils.ts +11 -1
- package/src/import/parser.ts +35 -0
- package/src/index.ts +3 -0
- package/src/kubectl-context.ts +3 -3
- package/src/lexicon-config.test.ts +5 -5
- package/src/lexicon-config.ts +4 -4
- package/src/lexicon-plugin-helpers.ts +1 -1
- package/src/lexicon.ts +120 -14
- package/src/lifecycle/change-set.test.ts +49 -0
- package/src/lifecycle/change-set.ts +67 -0
- package/src/lifecycle/converge-ledger.ts +1 -1
- package/src/lifecycle/deep-diff.test.ts +267 -14
- package/src/lifecycle/deep-diff.ts +217 -14
- package/src/lifecycle/deep-observe.test.ts +3 -1
- package/src/lifecycle/deep-observe.ts +16 -6
- package/src/lifecycle/gate-ledger.test.ts +74 -2
- package/src/lifecycle/gate-ledger.ts +143 -36
- package/src/lifecycle/index.ts +1 -0
- package/src/lifecycle/release-ledger.ts +2 -2
- package/src/lifecycle/run-ledger.test.ts +268 -0
- package/src/lifecycle/run-ledger.ts +201 -0
- package/src/lifecycle/symptoms.ts +3 -3
- package/src/lint/config.test.ts +82 -1
- package/src/lint/config.ts +112 -3
- package/src/lint/policy.test.ts +1 -1
- package/src/lint/post-synth.test.ts +31 -0
- package/src/lint/post-synth.ts +54 -1
- package/src/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.ts +2 -2
- package/src/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.ts +1 -1
- package/src/lint/rules/comp/comp.test.ts +1 -1
- package/src/lint/rules/comp/comp004-gate-needs-durable-runtime.ts +74 -0
- package/src/lint/rules/comp/index.ts +3 -3
- package/src/lint/rules/op/index.ts +23 -0
- package/src/lint/rules/op/ops012-activity-contract.test.ts +161 -0
- package/src/lint/rules/op/ops012-activity-contract.ts +64 -0
- package/src/lint/rules/op/ops013-step-output-ref.test.ts +237 -0
- package/src/lint/rules/op/ops013-step-output-ref.ts +98 -0
- package/src/lint/rules/op/ops014-converge-rule-refusals.test.ts +263 -0
- package/src/lint/rules/op/ops014-converge-rule-refusals.ts +179 -0
- package/src/lint/rules/op/support.ts +11 -0
- package/src/lint/suppressions.test.ts +163 -0
- package/src/lint/suppressions.ts +204 -0
- package/src/live-endpoint.ts +1 -1
- package/src/op/activities/activity-contracts.ts +66 -0
- package/src/op/activities/apply.test.ts +567 -0
- package/src/op/activities/apply.ts +662 -0
- package/src/op/activities/build.ts +26 -0
- package/src/op/activities/converge.test.ts +330 -0
- package/src/op/activities/converge.ts +449 -0
- package/src/op/activities/env-teardown.test.ts +187 -0
- package/src/op/activities/env-teardown.ts +171 -0
- package/src/op/activities/guard-validate.test.ts +139 -0
- package/src/op/activities/guard-validate.ts +223 -0
- package/src/op/activities/http-check.test.ts +48 -0
- package/src/op/activities/http-check.ts +55 -0
- package/src/op/activities/index.ts +119 -0
- package/src/op/activities/lexicon-upgrade.test.ts +492 -0
- package/src/op/activities/lexicon-upgrade.ts +673 -0
- package/src/op/activities/lifecycle.ts +85 -0
- package/src/op/activities/pipeline-audit.test.ts +67 -0
- package/src/op/activities/pipeline-audit.ts +201 -0
- package/src/op/activities/policy.test.ts +19 -0
- package/src/op/activities/policy.ts +37 -0
- package/src/op/activities/reconcile.test.ts +81 -0
- package/src/op/activities/reconcile.ts +187 -0
- package/src/op/activities/shell.ts +26 -0
- package/src/op/activities/teardown.ts +22 -0
- package/src/op/activities/util.ts +6 -0
- package/src/op/activities/wait.ts +69 -0
- package/src/op/activities/workflow-audit.test.ts +90 -0
- package/src/op/activities/workflow-audit.ts +203 -0
- package/src/op/activity-contract-registry.test.ts +98 -0
- package/src/op/activity-contract-registry.ts +114 -0
- package/src/op/activity-contract.test.ts +1 -1
- package/src/op/activity-contract.ts +37 -33
- package/src/op/activity-failure.ts +34 -0
- package/src/op/activity-profiles.test.ts +56 -0
- package/src/op/activity-profiles.ts +116 -0
- package/src/op/activity-registry.test.ts +37 -28
- package/src/op/activity-registry.ts +27 -56
- package/src/op/activity-runtime.ts +3 -47
- package/src/op/builders-exports.test.ts +0 -49
- package/src/op/builders-typed.test.ts +164 -0
- package/src/op/builders.ts +123 -39
- package/src/op/composites/apply-op.ts +208 -0
- package/src/op/composites/composites.test.ts +366 -0
- package/src/op/composites/converge-op.test.ts +168 -0
- package/src/op/composites/converge-op.ts +214 -0
- package/src/op/composites/index.ts +26 -0
- package/src/op/composites/lexicon-upgrade-op.test.ts +68 -0
- package/src/op/composites/lexicon-upgrade-op.ts +134 -0
- package/src/op/composites/pipeline-audit-op.test.ts +25 -0
- package/src/op/composites/pipeline-audit-op.ts +78 -0
- package/src/op/composites/reconcile-op.test.ts +51 -0
- package/src/op/composites/reconcile-op.ts +107 -0
- package/src/op/composites/watch-op.ts +108 -0
- package/src/op/composites/workflow-audit-op.test.ts +25 -0
- package/src/op/composites/workflow-audit-op.ts +88 -0
- package/src/op/converge-rule.ts +12 -10
- package/src/op/cron.test.ts +128 -0
- package/src/op/cron.ts +149 -0
- package/src/op/discover-root.test.ts +105 -1
- package/src/op/discover.test.ts +6 -2
- package/src/op/discover.ts +84 -16
- package/src/op/duration.ts +21 -0
- package/src/op/effect-step.test.ts +26 -11
- package/src/op/emulator-lifecycle.ts +1 -2
- package/src/op/gate.ts +155 -0
- package/src/op/generate-pipeline.test.ts +65 -3
- package/src/op/generate-pipeline.ts +19 -2
- package/src/op/index.ts +39 -6
- package/src/op/local-executor.test.ts +163 -34
- package/src/op/local-executor.ts +370 -102
- package/src/op/local-output.test.ts +95 -11
- package/src/op/local-output.ts +33 -4
- package/src/op/op-ir.test.ts +281 -0
- package/src/op/op-ir.ts +417 -0
- package/src/op/op-verb-class.ts +19 -5
- package/src/op/op.test.ts +26 -5
- package/src/op/operator.test.ts +175 -6
- package/src/op/operator.ts +63 -16
- package/src/op/receipt-store.ts +1 -1
- package/src/op/resource.ts +20 -2
- package/src/op/runtime.ts +203 -0
- package/src/op/runtimes/local.test.ts +181 -0
- package/src/op/runtimes/local.ts +212 -0
- package/src/op/step-output-ref.ts +15 -13
- package/src/op/types.ts +84 -24
- package/src/ownership.ts +1 -1
- package/src/provenance.test.ts +6 -0
- package/src/provenance.ts +21 -1
- package/src/serializer-walker.test.ts +42 -0
- package/src/serializer-walker.ts +21 -2
- package/src/stack-output.ts +6 -1
- package/dist/cli/handlers/op-progress.d.ts +0 -90
- package/dist/cli/handlers/op-progress.d.ts.map +0 -1
- package/dist/cli/handlers/run-client.d.ts +0 -127
- package/dist/cli/handlers/run-client.d.ts.map +0 -1
- package/dist/cli/handlers/run-report.d.ts +0 -5
- package/dist/cli/handlers/run-report.d.ts.map +0 -1
- package/dist/components/temporal-codegen-loader.d.ts +0 -27
- package/dist/components/temporal-codegen-loader.d.ts.map +0 -1
- package/dist/lint/rules/comp/comp004-gate-needs-temporal.d.ts +0 -37
- package/dist/lint/rules/comp/comp004-gate-needs-temporal.d.ts.map +0 -1
- package/src/cli/handlers/op-progress.test.ts +0 -268
- package/src/cli/handlers/op-progress.ts +0 -248
- package/src/cli/handlers/run-client.test.ts +0 -82
- package/src/cli/handlers/run-client.ts +0 -219
- package/src/cli/handlers/run-report.test.ts +0 -62
- package/src/cli/handlers/run-report.ts +0 -122
- package/src/components/temporal-codegen-loader.ts +0 -41
- 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
|
+
});
|
package/src/op/discover.test.ts
CHANGED
|
@@ -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
|
-
|
|
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") {
|
package/src/op/discover.ts
CHANGED
|
@@ -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
|
|
156
|
+
const mod = (await import(filePath)) as Record<string, unknown>;
|
|
157
|
+
const exported = opsExportedBy(mod);
|
|
92
158
|
|
|
93
|
-
if (
|
|
94
|
-
errors.push(
|
|
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
|
|
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
|
-
|
|
101
|
-
|
|
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,
|
|
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: {
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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 {
|
|
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) => ({
|
|
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([
|
|
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
|
});
|