@intentius/chant 0.57.0 → 0.59.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audit/catalog.d.ts +51 -1
- package/dist/audit/catalog.d.ts.map +1 -1
- package/dist/audit/core.d.ts +36 -1
- package/dist/audit/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts +26 -9
- 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/carve-bridge.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/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 +13 -0
- 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 +12 -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/plugins.d.ts +22 -0
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +40 -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 +24 -22
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/component.d.ts +21 -6
- 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 +73 -37
- 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 +10 -22
- 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/detectLexicon.d.ts +13 -0
- package/dist/detectLexicon.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/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 +273 -16
- 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 +79 -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 +149 -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 +85 -30
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/change-signal.d.ts +91 -0
- package/dist/op/change-signal.d.ts.map +1 -0
- package/dist/op/composites/apply-op.d.ts +118 -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-name.d.ts +40 -0
- package/dist/op/gate-name.d.ts.map +1 -0
- package/dist/op/gate-summary.d.ts +43 -0
- package/dist/op/gate-summary.d.ts.map +1 -0
- 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 +29 -9
- 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 +185 -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 +93 -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 +96 -29
- 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/terraform/bridge.d.ts +26 -7
- package/dist/terraform/bridge.d.ts.map +1 -1
- package/dist/terraform/carve-provider.d.ts +28 -2
- package/dist/terraform/carve-provider.d.ts.map +1 -1
- package/dist/terraform/data-source-shape.d.ts +77 -0
- package/dist/terraform/data-source-shape.d.ts.map +1 -0
- package/dist/terraform/graph.d.ts.map +1 -1
- package/dist/terraform/providers/kubernetes.d.ts +5 -0
- package/dist/terraform/providers/kubernetes.d.ts.map +1 -1
- package/dist/terraform/tier-map.d.ts +16 -4
- package/dist/terraform/tier-map.d.ts.map +1 -1
- package/dist/terraform/types.d.ts +8 -0
- package/dist/terraform/types.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 +84 -18
- package/src/audit/discover.test.ts +39 -0
- package/src/audit/discover.ts +202 -18
- 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 +191 -11
- package/src/cli/commands/audit.ts +78 -28
- package/src/cli/commands/build.test.ts +330 -9
- package/src/cli/commands/build.ts +170 -24
- package/src/cli/commands/carve-bridge.test.ts +237 -14
- package/src/cli/commands/carve-bridge.ts +21 -17
- package/src/cli/commands/carve-emit-k8s.test.ts +17 -10
- 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/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 +194 -1
- package/src/cli/commands/lint.ts +205 -7
- 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 +341 -55
- package/src/cli/handlers/run.test.ts +568 -1295
- package/src/cli/handlers/run.ts +446 -912
- package/src/cli/main.test.ts +22 -16
- package/src/cli/main.ts +66 -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/plugins.test.ts +68 -2
- package/src/cli/plugins.ts +39 -0
- package/src/cli/registry.ts +40 -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 +4 -3
- package/src/components/__fixtures__/neo4j-fanout.json +1 -1
- 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 +28 -12
- package/src/components/cli-support.ts +60 -47
- package/src/components/component-schema.test.ts +18 -2
- package/src/components/component.schema.json +18 -5
- package/src/components/component.test.ts +2 -2
- package/src/components/component.ts +27 -7
- package/src/components/config-defaults.test.ts +2 -2
- package/src/components/driver-output.ts +14 -2
- package/src/components/driver.test.ts +109 -20
- package/src/components/driver.ts +201 -84
- 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/neo4j-fanout.pilot.ts +3 -3
- 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.test.ts +19 -0
- package/src/components/verbs/run-agent.ts +12 -24
- 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/detectLexicon.ts +18 -1
- package/src/discovery/collect.test.ts +3 -3
- package/src/discovery/files.ts +4 -4
- package/src/discovery/fold-import.test.ts +1 -1
- 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 +82 -2
- 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 +3 -3
- package/src/graph-ops.ts +1 -1
- package/src/held-elsewhere.test.ts +102 -0
- package/src/held-elsewhere.ts +114 -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 +296 -16
- 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 +154 -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/pipeline-change-gate.test.ts +2 -2
- 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 +27 -1
- package/src/lint/rules/comp/comp004-gate-needs-durable-runtime.ts +76 -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 +161 -0
- package/src/op/activities/reconcile.ts +354 -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 +125 -41
- package/src/op/change-signal.test.ts +117 -0
- package/src/op/change-signal.ts +169 -0
- package/src/op/composites/apply-op.ts +218 -0
- package/src/op/composites/composites.test.ts +379 -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 +110 -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 +29 -14
- package/src/op/emulator-lifecycle.ts +1 -2
- package/src/op/gate-name.test.ts +65 -0
- package/src/op/gate-name.ts +60 -0
- package/src/op/gate-summary.ts +84 -0
- 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 +48 -8
- package/src/op/local-executor.test.ts +178 -34
- package/src/op/local-executor.ts +371 -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 +292 -0
- package/src/op/op-ir.ts +419 -0
- package/src/op/op-verb-class.test.ts +2 -2
- package/src/op/op-verb-class.ts +19 -5
- package/src/op/op.test.ts +28 -7
- package/src/op/operator.test.ts +543 -6
- package/src/op/operator.ts +203 -20
- 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 +106 -26
- 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/terraform/aws-resources.test.ts +13 -4
- package/src/terraform/bridge.test.ts +22 -9
- package/src/terraform/bridge.ts +89 -28
- package/src/terraform/carve-provider.ts +38 -2
- package/src/terraform/data-source-shape.ts +95 -0
- package/src/terraform/graph.ts +35 -7
- package/src/terraform/providers/kubernetes.ts +48 -2
- package/src/terraform/tier-map.ts +21 -6
- package/src/terraform/types.ts +8 -0
- 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
package/src/op/operator.test.ts
CHANGED
|
@@ -9,13 +9,35 @@ import {
|
|
|
9
9
|
runOperatorRound,
|
|
10
10
|
runOperatorForever,
|
|
11
11
|
formatRoundLine,
|
|
12
|
+
formatSignalLine,
|
|
12
13
|
DEFAULT_OPERATOR_INTERVAL_MS,
|
|
14
|
+
type ChangeSubscriber,
|
|
15
|
+
type OperatorSignalEvent,
|
|
16
|
+
type OperatorTickEvent,
|
|
13
17
|
} from "./operator";
|
|
14
18
|
import { readLease } from "../lifecycle/lease";
|
|
15
19
|
import { appendConvergeRecord, readConvergeLedger } from "../lifecycle/converge-ledger";
|
|
20
|
+
import { readRunLedger } from "../lifecycle/run-ledger";
|
|
21
|
+
import type { OpRunRecord } from "./runtime";
|
|
16
22
|
|
|
17
23
|
const PROFILES: Record<string, ActivityProfile> = {};
|
|
18
24
|
|
|
25
|
+
/** A minimal `OpRunRecord` for a result literal that never touches a ledger. */
|
|
26
|
+
function runRecord(op: string, status: OpRunRecord["status"] = "ok"): OpRunRecord {
|
|
27
|
+
return {
|
|
28
|
+
version: 1,
|
|
29
|
+
id: "00000000-0000-4000-8000-000000000000",
|
|
30
|
+
op,
|
|
31
|
+
env: "staging",
|
|
32
|
+
started: "2026-01-01T00:00:00.000Z",
|
|
33
|
+
ended: "2026-01-01T00:00:01.000Z",
|
|
34
|
+
status,
|
|
35
|
+
labels: {},
|
|
36
|
+
outcomes: {},
|
|
37
|
+
phases: [],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
19
41
|
function git(args: string[], cwd: string): { stdout: string; exitCode: number } {
|
|
20
42
|
const r = spawnSync("git", args, { cwd, encoding: "utf-8" });
|
|
21
43
|
return { stdout: r.stdout ?? "", exitCode: r.status ?? -1 };
|
|
@@ -34,7 +56,7 @@ async function initRepo(dir: string): Promise<void> {
|
|
|
34
56
|
* Write a minimal fixture ConvergeOp — the same `{ props: OpConfig }` shape
|
|
35
57
|
* `discoverOps` reads from a real `*.op.ts` file's default export
|
|
36
58
|
* (`../op/discover.ts`), hand-built here rather than going through the real
|
|
37
|
-
* `ConvergeOp()` composite
|
|
59
|
+
* `ConvergeOp()` composite so this suite tests the
|
|
38
60
|
* operator's own discovery/lease/tick-loop logic without depending on that
|
|
39
61
|
* lexicon at all. One "Converge" phase calling a single fake activity —
|
|
40
62
|
* `runOpLocally` doesn't care what the phases are named or how many there
|
|
@@ -50,7 +72,26 @@ function writeFixtureConvergeOp(dir: string, opName: string, env: string): void
|
|
|
50
72
|
name: ${JSON.stringify(opName)},
|
|
51
73
|
overview: "fixture converge op",
|
|
52
74
|
phases: [{ name: "Converge", steps: [{ kind: "activity", fn: "fakeTick", args: {} }] }],
|
|
53
|
-
|
|
75
|
+
labels: { Converge: "true", Env: ${JSON.stringify(env)}, Dial: "observe" },
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The same fixture with a cadence of its own (#2120) — what `ConvergeOp({ schedule })` produces. */
|
|
83
|
+
function writeFixtureScheduledConvergeOp(dir: string, opName: string, env: string, cron: string): void {
|
|
84
|
+
const opsDir = join(dir, "ops");
|
|
85
|
+
mkdirSync(opsDir, { recursive: true });
|
|
86
|
+
writeFileSync(
|
|
87
|
+
join(opsDir, `${opName}.op.ts`),
|
|
88
|
+
`export default {
|
|
89
|
+
props: {
|
|
90
|
+
name: ${JSON.stringify(opName)},
|
|
91
|
+
overview: "fixture scheduled converge op",
|
|
92
|
+
phases: [{ name: "Converge", steps: [{ kind: "activity", fn: "fakeTick", args: {} }] }],
|
|
93
|
+
labels: { Converge: "true", Env: ${JSON.stringify(env)}, Dial: "observe" },
|
|
94
|
+
schedule: { cron: ${JSON.stringify(cron)}, overlap: "skip" },
|
|
54
95
|
},
|
|
55
96
|
};
|
|
56
97
|
`,
|
|
@@ -99,7 +140,7 @@ function fakeTickActivities(dir: string, env: string, opName: string, opts?: { t
|
|
|
99
140
|
}
|
|
100
141
|
|
|
101
142
|
describe("discoverConvergeOps", () => {
|
|
102
|
-
test("finds only ops with
|
|
143
|
+
test("finds only ops with labels.Converge === 'true'", async () => {
|
|
103
144
|
await withTestDir(async (dir) => {
|
|
104
145
|
await initRepo(dir);
|
|
105
146
|
writeFixtureConvergeOp(dir, "staging-converge", "staging");
|
|
@@ -132,6 +173,28 @@ describe("discoverConvergeOps", () => {
|
|
|
132
173
|
expect(ops.map((d) => d.config.name)).toEqual(["prod-converge", "staging-converge"]);
|
|
133
174
|
});
|
|
134
175
|
});
|
|
176
|
+
|
|
177
|
+
test("a ConvergeOp declaring no labels.Env is discovered unfiltered but never matches --env", async () => {
|
|
178
|
+
await withTestDir(async (dir) => {
|
|
179
|
+
await initRepo(dir);
|
|
180
|
+
mkdirSync(join(dir, "ops"), { recursive: true });
|
|
181
|
+
writeFileSync(
|
|
182
|
+
join(dir, "ops", "envless-converge.op.ts"),
|
|
183
|
+
`export default {
|
|
184
|
+
props: {
|
|
185
|
+
name: "envless-converge",
|
|
186
|
+
overview: "a hand-built ConvergeOp with no Env label",
|
|
187
|
+
phases: [{ name: "Converge", steps: [{ kind: "activity", fn: "fakeTick", args: {} }] }],
|
|
188
|
+
labels: { Converge: "true" },
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
`,
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
expect((await discoverConvergeOps({ cwd: dir })).ops.map((d) => d.config.name)).toEqual(["envless-converge"]);
|
|
195
|
+
expect((await discoverConvergeOps({ cwd: dir, env: "staging" })).ops).toEqual([]);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
135
198
|
});
|
|
136
199
|
|
|
137
200
|
describe("runOperatorRound — lease + tick execution over a fixture ConvergeOp (offline)", () => {
|
|
@@ -142,7 +205,7 @@ describe("runOperatorRound — lease + tick execution over a fixture ConvergeOp
|
|
|
142
205
|
const activities = fakeTickActivities(dir, "staging", "staging-converge");
|
|
143
206
|
|
|
144
207
|
const events = await runOperatorRound({ cwd: dir, holder: "op-a", activities, profiles: PROFILES });
|
|
145
|
-
expect(events).toEqual([{ kind: "ticked", op: "staging-converge", env: "staging", result: expect.objectContaining({
|
|
208
|
+
expect(events).toEqual([{ kind: "ticked", op: "staging-converge", env: "staging", result: expect.objectContaining({ status: "ok" }) }]);
|
|
146
209
|
|
|
147
210
|
const { records } = await readConvergeLedger("staging", { cwd: dir });
|
|
148
211
|
expect(records).toHaveLength(1);
|
|
@@ -150,6 +213,27 @@ describe("runOperatorRound — lease + tick execution over a fixture ConvergeOp
|
|
|
150
213
|
});
|
|
151
214
|
});
|
|
152
215
|
|
|
216
|
+
test("the tick's own run lands on the run ledger, keyed by the op's labels.Env (#2118)", async () => {
|
|
217
|
+
await withTestDir(async (dir) => {
|
|
218
|
+
await initRepo(dir);
|
|
219
|
+
writeFixtureConvergeOp(dir, "staging-converge", "staging");
|
|
220
|
+
const activities = fakeTickActivities(dir, "staging", "staging-converge");
|
|
221
|
+
|
|
222
|
+
await runOperatorRound({ cwd: dir, holder: "op-a", activities, profiles: PROFILES });
|
|
223
|
+
|
|
224
|
+
const { records, malformed } = await readRunLedger("staging", "staging-converge", { cwd: dir });
|
|
225
|
+
expect(malformed).toBe(0);
|
|
226
|
+
expect(records).toHaveLength(1);
|
|
227
|
+
expect(records[0]).toMatchObject({
|
|
228
|
+
op: "staging-converge",
|
|
229
|
+
env: "staging",
|
|
230
|
+
status: "ok",
|
|
231
|
+
labels: { Converge: "true", Env: "staging", Dial: "observe" },
|
|
232
|
+
});
|
|
233
|
+
expect(records[0].phases.map((p) => p.name)).toEqual(["Converge"]);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
153
237
|
test("a second operator process (different holder) sees the lease held and skips — never queues", async () => {
|
|
154
238
|
await withTestDir(async (dir) => {
|
|
155
239
|
await initRepo(dir);
|
|
@@ -330,10 +414,97 @@ describe("runOperatorForever — interval/timer behavior", () => {
|
|
|
330
414
|
});
|
|
331
415
|
});
|
|
332
416
|
|
|
417
|
+
describe("runOperatorRound — an Op's own schedule is its tick cadence (#2120)", () => {
|
|
418
|
+
/** 2026-09-06T13:30 local — a minute `*/10 * * * *` fires on. */
|
|
419
|
+
const firing = () => new Date(2026, 8, 6, 13, 30, 0, 0);
|
|
420
|
+
/** …and 13:33, which it does not. */
|
|
421
|
+
const quiet = () => new Date(2026, 8, 6, 13, 33, 0, 0);
|
|
422
|
+
|
|
423
|
+
test("a round on a firing minute ticks the op", async () => {
|
|
424
|
+
await withTestDir(async (dir) => {
|
|
425
|
+
await initRepo(dir);
|
|
426
|
+
writeFixtureScheduledConvergeOp(dir, "staging-converge", "staging", "*/10 * * * *");
|
|
427
|
+
const activities = fakeTickActivities(dir, "staging", "staging-converge");
|
|
428
|
+
|
|
429
|
+
const events = await runOperatorRound({ cwd: dir, holder: "op-a", activities, profiles: PROFILES, now: firing });
|
|
430
|
+
expect(events).toEqual([
|
|
431
|
+
{ kind: "ticked", op: "staging-converge", env: "staging", result: expect.objectContaining({ status: "ok" }) },
|
|
432
|
+
]);
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
test("a round off the cron skips without touching the lease, and writes no ledger record", async () => {
|
|
437
|
+
await withTestDir(async (dir) => {
|
|
438
|
+
await initRepo(dir);
|
|
439
|
+
writeFixtureScheduledConvergeOp(dir, "staging-converge", "staging", "*/10 * * * *");
|
|
440
|
+
const activities = fakeTickActivities(dir, "staging", "staging-converge");
|
|
441
|
+
|
|
442
|
+
const events = await runOperatorRound({ cwd: dir, holder: "op-a", activities, profiles: PROFILES, now: quiet });
|
|
443
|
+
expect(events).toEqual([
|
|
444
|
+
{ kind: "skipped-not-due", op: "staging-converge", env: "staging", cron: "*/10 * * * *" },
|
|
445
|
+
]);
|
|
446
|
+
|
|
447
|
+
const { records } = await readConvergeLedger("staging", { cwd: dir });
|
|
448
|
+
expect(records).toHaveLength(0);
|
|
449
|
+
expect((await readLease("staging-converge", { cwd: dir })).record).toBeUndefined();
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
test("a late round still owes the tick: a firing minute passed since this op was last considered", async () => {
|
|
454
|
+
await withTestDir(async (dir) => {
|
|
455
|
+
await initRepo(dir);
|
|
456
|
+
writeFixtureScheduledConvergeOp(dir, "staging-converge", "staging", "*/10 * * * *");
|
|
457
|
+
const activities = fakeTickActivities(dir, "staging", "staging-converge");
|
|
458
|
+
const scheduleState = new Map<string, Date>();
|
|
459
|
+
|
|
460
|
+
// 13:33 — first sight, not a firing minute: recorded, not ticked.
|
|
461
|
+
const first = await runOperatorRound({ cwd: dir, holder: "op-a", activities, profiles: PROFILES, now: quiet, scheduleState });
|
|
462
|
+
expect(first[0].kind).toBe("skipped-not-due");
|
|
463
|
+
|
|
464
|
+
// 13:42 — 13:40 fired while nobody was looking, so this round ticks.
|
|
465
|
+
const late = () => new Date(2026, 8, 6, 13, 42, 0, 0);
|
|
466
|
+
const second = await runOperatorRound({ cwd: dir, holder: "op-a", activities, profiles: PROFILES, now: late, scheduleState });
|
|
467
|
+
expect(second[0].kind).toBe("ticked");
|
|
468
|
+
|
|
469
|
+
// 13:43 — nothing has fired since 13:42.
|
|
470
|
+
const after = () => new Date(2026, 8, 6, 13, 43, 0, 0);
|
|
471
|
+
const third = await runOperatorRound({ cwd: dir, holder: "op-a", activities, profiles: PROFILES, now: after, scheduleState });
|
|
472
|
+
expect(third[0].kind).toBe("skipped-not-due");
|
|
473
|
+
});
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
test("an op with no cadence of its own still ticks every round — `--interval` is the fallback", async () => {
|
|
477
|
+
await withTestDir(async (dir) => {
|
|
478
|
+
await initRepo(dir);
|
|
479
|
+
writeFixtureConvergeOp(dir, "staging-converge", "staging");
|
|
480
|
+
const activities = fakeTickActivities(dir, "staging", "staging-converge");
|
|
481
|
+
|
|
482
|
+
const events = await runOperatorRound({ cwd: dir, holder: "op-a", activities, profiles: PROFILES, now: quiet });
|
|
483
|
+
expect(events[0].kind).toBe("ticked");
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
|
|
333
488
|
describe("formatRoundLine", () => {
|
|
334
489
|
test("renders each event kind as one line", () => {
|
|
335
|
-
expect(formatRoundLine({
|
|
336
|
-
|
|
490
|
+
expect(formatRoundLine({
|
|
491
|
+
kind: "ticked", op: "x", env: "staging",
|
|
492
|
+
result: {
|
|
493
|
+
op: "x", records: [], totalMs: 1, status: "ok", startedAt: "2026-09-05T12:00:00.000Z",
|
|
494
|
+
record: runRecord("x", "ok"),
|
|
495
|
+
},
|
|
496
|
+
})).toContain("ticked=1 status=ok");
|
|
497
|
+
expect(formatRoundLine({
|
|
498
|
+
kind: "ticked", op: "x", env: "staging",
|
|
499
|
+
result: {
|
|
500
|
+
op: "x", records: [], totalMs: 1, status: "gated", startedAt: "2026-09-05T12:00:00.000Z",
|
|
501
|
+
gate: {
|
|
502
|
+
version: 1, kind: "pending", op: "x", gate: "rollout-gate",
|
|
503
|
+
timestamp: "2026-09-05T12:00:00.000Z", expiresAt: "2026-09-07T12:00:00.000Z",
|
|
504
|
+
},
|
|
505
|
+
record: runRecord("x", "gated"),
|
|
506
|
+
},
|
|
507
|
+
})).toContain('status=gated gate="rollout-gate"');
|
|
337
508
|
expect(formatRoundLine({ kind: "skipped-lease-held", op: "x", env: "staging", heldBy: "holder-a" }))
|
|
338
509
|
.toContain("lease-held:holder-a");
|
|
339
510
|
expect(formatRoundLine({ kind: "tick-failed", op: "x", env: "staging", error: "boom" }))
|
|
@@ -342,5 +513,371 @@ describe("formatRoundLine", () => {
|
|
|
342
513
|
.toContain("fenced=1");
|
|
343
514
|
expect(formatRoundLine({ kind: "lease-error", op: "x", env: "staging", error: "stale lock at .../x.lock" }))
|
|
344
515
|
.toContain("stale lock at .../x.lock");
|
|
516
|
+
expect(formatRoundLine({ kind: "skipped-not-due", op: "x", env: "staging", cron: "*/10 * * * *" }))
|
|
517
|
+
.toContain("not-due:*/10 * * * *");
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
describe("runOperatorForever: a substrate change signal wakes a tick (#1981)", () => {
|
|
522
|
+
async function waitFor(predicate: () => boolean, maxWaitMs = 5_000): Promise<void> {
|
|
523
|
+
const deadline = Date.now() + maxWaitMs;
|
|
524
|
+
while (!predicate() && Date.now() < deadline) {
|
|
525
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* A stub `subscribeChanges` seam. Hands the loop's `onChange`/`onError` back
|
|
531
|
+
* to the test so it can fabricate signals and kill the subscription, with no
|
|
532
|
+
* substrate anywhere near it, which is the whole point of the seam being a
|
|
533
|
+
* plain callback pair.
|
|
534
|
+
*/
|
|
535
|
+
function stubSubscriber(lexicon = "stub") {
|
|
536
|
+
const handle = {
|
|
537
|
+
lexicon,
|
|
538
|
+
subscribes: 0,
|
|
539
|
+
closes: 0,
|
|
540
|
+
onChange: undefined as undefined | (() => void),
|
|
541
|
+
onError: undefined as undefined | ((message: string) => void),
|
|
542
|
+
signal: undefined as undefined | AbortSignal,
|
|
543
|
+
failNextSubscribe: undefined as undefined | string,
|
|
544
|
+
};
|
|
545
|
+
const subscriber: ChangeSubscriber = {
|
|
546
|
+
lexicon,
|
|
547
|
+
async subscribe(ctx) {
|
|
548
|
+
handle.subscribes++;
|
|
549
|
+
if (handle.failNextSubscribe) {
|
|
550
|
+
const message = handle.failNextSubscribe;
|
|
551
|
+
handle.failNextSubscribe = undefined;
|
|
552
|
+
throw new Error(message);
|
|
553
|
+
}
|
|
554
|
+
handle.onChange = ctx.onChange;
|
|
555
|
+
handle.onError = ctx.onError;
|
|
556
|
+
handle.signal = ctx.signal;
|
|
557
|
+
return {
|
|
558
|
+
async close() {
|
|
559
|
+
handle.closes++;
|
|
560
|
+
},
|
|
561
|
+
};
|
|
562
|
+
},
|
|
563
|
+
};
|
|
564
|
+
return { handle, subscriber };
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
test("a signal wakes the next round early, well inside the interval", async () => {
|
|
568
|
+
await withTestDir(async (dir) => {
|
|
569
|
+
await initRepo(dir);
|
|
570
|
+
writeFixtureConvergeOp(dir, "staging-converge", "staging");
|
|
571
|
+
const activities = fakeTickActivities(dir, "staging", "staging-converge");
|
|
572
|
+
const { handle, subscriber } = stubSubscriber("k8s");
|
|
573
|
+
|
|
574
|
+
const controller = new AbortController();
|
|
575
|
+
const rounds: number[] = [];
|
|
576
|
+
const signalEvents: OperatorSignalEvent[] = [];
|
|
577
|
+
const loop = runOperatorForever({
|
|
578
|
+
cwd: dir,
|
|
579
|
+
holder: "op-a",
|
|
580
|
+
intervalMs: 60_000, // a full minute: nothing but a signal can produce a second round
|
|
581
|
+
signalFloorMs: 0,
|
|
582
|
+
activities,
|
|
583
|
+
profiles: PROFILES,
|
|
584
|
+
signal: controller.signal,
|
|
585
|
+
subscribers: [subscriber],
|
|
586
|
+
onRound: () => rounds.push(Date.now()),
|
|
587
|
+
onSignalEvent: (e) => signalEvents.push(e),
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
await waitFor(() => handle.onChange !== undefined);
|
|
591
|
+
expect(rounds.length).toBe(1);
|
|
592
|
+
handle.onChange!();
|
|
593
|
+
await waitFor(() => rounds.length >= 2);
|
|
594
|
+
expect(rounds.length).toBeGreaterThanOrEqual(2);
|
|
595
|
+
|
|
596
|
+
controller.abort();
|
|
597
|
+
await loop;
|
|
598
|
+
expect(signalEvents.some((e) => e.kind === "subscribed" && e.lexicon === "k8s")).toBe(true);
|
|
599
|
+
expect(signalEvents.some((e) => e.kind === "woken")).toBe(true);
|
|
600
|
+
});
|
|
601
|
+
}, 15_000);
|
|
602
|
+
|
|
603
|
+
test("a fabricated signal cannot change what the tick reports, only when it runs", async () => {
|
|
604
|
+
await withTestDir(async (dir) => {
|
|
605
|
+
await initRepo(dir);
|
|
606
|
+
writeFixtureConvergeOp(dir, "staging-converge", "staging");
|
|
607
|
+
|
|
608
|
+
/** One round's events, run to completion on a timer alone. */
|
|
609
|
+
async function roundsWithoutSignal(): Promise<string> {
|
|
610
|
+
const controller = new AbortController();
|
|
611
|
+
const seen: OperatorTickEvent[][] = [];
|
|
612
|
+
const loop = runOperatorForever({
|
|
613
|
+
cwd: dir,
|
|
614
|
+
holder: "op-a",
|
|
615
|
+
intervalMs: 60_000,
|
|
616
|
+
activities: fakeTickActivities(dir, "staging", "staging-converge"),
|
|
617
|
+
profiles: PROFILES,
|
|
618
|
+
signal: controller.signal,
|
|
619
|
+
onRound: (events) => {
|
|
620
|
+
seen.push(events);
|
|
621
|
+
controller.abort();
|
|
622
|
+
},
|
|
623
|
+
});
|
|
624
|
+
await loop;
|
|
625
|
+
return JSON.stringify(seen[0].map((e) => ({ kind: e.kind, op: e.op, env: e.env })));
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/** The same, woken by a signal carrying a fabricated payload. */
|
|
629
|
+
async function roundsWithFabricatedSignal(): Promise<string> {
|
|
630
|
+
const { handle, subscriber } = stubSubscriber("liar");
|
|
631
|
+
const controller = new AbortController();
|
|
632
|
+
const seen: OperatorTickEvent[][] = [];
|
|
633
|
+
const loop = runOperatorForever({
|
|
634
|
+
cwd: dir,
|
|
635
|
+
holder: "op-a",
|
|
636
|
+
intervalMs: 60_000,
|
|
637
|
+
signalFloorMs: 0,
|
|
638
|
+
activities: fakeTickActivities(dir, "staging", "staging-converge"),
|
|
639
|
+
profiles: PROFILES,
|
|
640
|
+
signal: controller.signal,
|
|
641
|
+
subscribers: [subscriber],
|
|
642
|
+
onRound: (events) => {
|
|
643
|
+
seen.push(events);
|
|
644
|
+
if (seen.length >= 2) controller.abort();
|
|
645
|
+
},
|
|
646
|
+
});
|
|
647
|
+
await waitFor(() => handle.onChange !== undefined);
|
|
648
|
+
// `onChange` takes no arguments; a caller that fabricates a resource
|
|
649
|
+
// has nowhere to put it, and the extra arguments go nowhere.
|
|
650
|
+
(handle.onChange as unknown as (...args: unknown[]) => void)(
|
|
651
|
+
{ kind: "Deployment", name: "ghost", status: "Drifted" },
|
|
652
|
+
"fabricated",
|
|
653
|
+
);
|
|
654
|
+
await waitFor(() => seen.length >= 2);
|
|
655
|
+
controller.abort();
|
|
656
|
+
await loop;
|
|
657
|
+
return JSON.stringify(seen[1].map((e) => ({ kind: e.kind, op: e.op, env: e.env })));
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const timerOnly = await roundsWithoutSignal();
|
|
661
|
+
const signalled = await roundsWithFabricatedSignal();
|
|
662
|
+
// Byte for byte the same verdict: the signal moved the clock, nothing else.
|
|
663
|
+
expect(signalled).toBe(timerOnly);
|
|
664
|
+
// And the seam itself carries no payload channel to begin with.
|
|
665
|
+
const { handle } = stubSubscriber();
|
|
666
|
+
expect(handle.onChange?.length ?? 0).toBe(0);
|
|
667
|
+
});
|
|
668
|
+
}, 20_000);
|
|
669
|
+
|
|
670
|
+
test("a storm of signals wakes at most one round per floor", async () => {
|
|
671
|
+
await withTestDir(async (dir) => {
|
|
672
|
+
await initRepo(dir);
|
|
673
|
+
writeFixtureConvergeOp(dir, "staging-converge", "staging");
|
|
674
|
+
const { handle, subscriber } = stubSubscriber();
|
|
675
|
+
|
|
676
|
+
const controller = new AbortController();
|
|
677
|
+
const rounds: number[] = [];
|
|
678
|
+
const loop = runOperatorForever({
|
|
679
|
+
cwd: dir,
|
|
680
|
+
holder: "op-a",
|
|
681
|
+
intervalMs: 60_000,
|
|
682
|
+
signalFloorMs: 400,
|
|
683
|
+
activities: fakeTickActivities(dir, "staging", "staging-converge"),
|
|
684
|
+
profiles: PROFILES,
|
|
685
|
+
signal: controller.signal,
|
|
686
|
+
subscribers: [subscriber],
|
|
687
|
+
onRound: () => rounds.push(Date.now()),
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
await waitFor(() => handle.onChange !== undefined);
|
|
691
|
+
const before = rounds.length;
|
|
692
|
+
for (let i = 0; i < 300; i++) handle.onChange!();
|
|
693
|
+
// Well past a floor, but nowhere near the 60s interval: at most one
|
|
694
|
+
// extra round may have happened, and a per-signal wake would have
|
|
695
|
+
// produced hundreds.
|
|
696
|
+
await waitFor(() => rounds.length > before, 2_000);
|
|
697
|
+
expect(rounds.length - before).toBeLessThanOrEqual(1);
|
|
698
|
+
|
|
699
|
+
controller.abort();
|
|
700
|
+
await loop;
|
|
701
|
+
});
|
|
702
|
+
}, 15_000);
|
|
703
|
+
|
|
704
|
+
test("a killed subscription is reported once, the loop keeps ticking, and the next round re-subscribes", async () => {
|
|
705
|
+
await withTestDir(async (dir) => {
|
|
706
|
+
await initRepo(dir);
|
|
707
|
+
writeFixtureConvergeOp(dir, "staging-converge", "staging");
|
|
708
|
+
const { handle, subscriber } = stubSubscriber("k8s");
|
|
709
|
+
|
|
710
|
+
const controller = new AbortController();
|
|
711
|
+
const rounds: number[] = [];
|
|
712
|
+
const signalEvents: OperatorSignalEvent[] = [];
|
|
713
|
+
const loop = runOperatorForever({
|
|
714
|
+
cwd: dir,
|
|
715
|
+
holder: "op-a",
|
|
716
|
+
intervalMs: 60,
|
|
717
|
+
signalFloorMs: 0,
|
|
718
|
+
activities: fakeTickActivities(dir, "staging", "staging-converge"),
|
|
719
|
+
profiles: PROFILES,
|
|
720
|
+
signal: controller.signal,
|
|
721
|
+
subscribers: [subscriber],
|
|
722
|
+
onRound: () => rounds.push(Date.now()),
|
|
723
|
+
onSignalEvent: (e) => signalEvents.push(e),
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
await waitFor(() => handle.onError !== undefined);
|
|
727
|
+
const subscribesBefore = handle.subscribes;
|
|
728
|
+
// The watch dies. Twice, because a dead subscription reports once.
|
|
729
|
+
handle.onError!("connection reset by peer");
|
|
730
|
+
handle.onError!("connection reset by peer");
|
|
731
|
+
|
|
732
|
+
await waitFor(() => handle.subscribes > subscribesBefore);
|
|
733
|
+
const roundsAtDeath = rounds.length;
|
|
734
|
+
await waitFor(() => rounds.length > roundsAtDeath + 1);
|
|
735
|
+
|
|
736
|
+
controller.abort();
|
|
737
|
+
await loop;
|
|
738
|
+
|
|
739
|
+
const lost = signalEvents.filter((e) => e.kind === "subscription-lost");
|
|
740
|
+
expect(lost).toHaveLength(1);
|
|
741
|
+
expect(formatSignalLine(lost[0])).toContain("polling on the timer");
|
|
742
|
+
// The loop kept ticking on the timer, and re-subscribed.
|
|
743
|
+
expect(rounds.length).toBeGreaterThan(roundsAtDeath + 1);
|
|
744
|
+
expect(handle.subscribes).toBeGreaterThan(subscribesBefore);
|
|
745
|
+
});
|
|
746
|
+
}, 15_000);
|
|
747
|
+
|
|
748
|
+
test("a subscription that cannot be established degrades to polling with one line, never a crash", async () => {
|
|
749
|
+
await withTestDir(async (dir) => {
|
|
750
|
+
await initRepo(dir);
|
|
751
|
+
writeFixtureConvergeOp(dir, "staging-converge", "staging");
|
|
752
|
+
const failing: ChangeSubscriber = {
|
|
753
|
+
lexicon: "k8s",
|
|
754
|
+
async subscribe() {
|
|
755
|
+
throw new Error("no cluster binding for env staging");
|
|
756
|
+
},
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
const controller = new AbortController();
|
|
760
|
+
const rounds: number[] = [];
|
|
761
|
+
const signalEvents: OperatorSignalEvent[] = [];
|
|
762
|
+
const loop = runOperatorForever({
|
|
763
|
+
cwd: dir,
|
|
764
|
+
holder: "op-a",
|
|
765
|
+
intervalMs: 40,
|
|
766
|
+
activities: fakeTickActivities(dir, "staging", "staging-converge"),
|
|
767
|
+
profiles: PROFILES,
|
|
768
|
+
signal: controller.signal,
|
|
769
|
+
subscribers: [failing],
|
|
770
|
+
onRound: () => rounds.push(Date.now()),
|
|
771
|
+
onSignalEvent: (e) => signalEvents.push(e),
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
await waitFor(() => rounds.length >= 3);
|
|
775
|
+
controller.abort();
|
|
776
|
+
await loop; // resolves: a failing subscription never rejects the loop
|
|
777
|
+
|
|
778
|
+
const failed = signalEvents.filter((e) => e.kind === "subscribe-failed");
|
|
779
|
+
expect(failed.length).toBeGreaterThanOrEqual(1);
|
|
780
|
+
expect(formatSignalLine(failed[0])).toContain("no cluster binding for env staging");
|
|
781
|
+
expect(rounds.length).toBeGreaterThanOrEqual(3);
|
|
782
|
+
});
|
|
783
|
+
}, 15_000);
|
|
784
|
+
|
|
785
|
+
test("stopping the operator closes every subscription and aborts its signal", async () => {
|
|
786
|
+
await withTestDir(async (dir) => {
|
|
787
|
+
await initRepo(dir);
|
|
788
|
+
writeFixtureConvergeOp(dir, "staging-converge", "staging");
|
|
789
|
+
const a = stubSubscriber("k8s");
|
|
790
|
+
const b = stubSubscriber("docker");
|
|
791
|
+
|
|
792
|
+
const controller = new AbortController();
|
|
793
|
+
const loop = runOperatorForever({
|
|
794
|
+
cwd: dir,
|
|
795
|
+
holder: "op-a",
|
|
796
|
+
intervalMs: 60_000,
|
|
797
|
+
activities: fakeTickActivities(dir, "staging", "staging-converge"),
|
|
798
|
+
profiles: PROFILES,
|
|
799
|
+
signal: controller.signal,
|
|
800
|
+
subscribers: [a.subscriber, b.subscriber],
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
await waitFor(() => a.handle.onChange !== undefined && b.handle.onChange !== undefined);
|
|
804
|
+
expect(a.handle.signal!.aborted).toBe(false);
|
|
805
|
+
|
|
806
|
+
controller.abort();
|
|
807
|
+
await loop;
|
|
808
|
+
|
|
809
|
+
expect(a.handle.closes).toBe(1);
|
|
810
|
+
expect(b.handle.closes).toBe(1);
|
|
811
|
+
expect(a.handle.signal!.aborted).toBe(true);
|
|
812
|
+
expect(b.handle.signal!.aborted).toBe(true);
|
|
813
|
+
});
|
|
814
|
+
}, 15_000);
|
|
815
|
+
|
|
816
|
+
test("a close() that throws still unwinds the loop cleanly", async () => {
|
|
817
|
+
await withTestDir(async (dir) => {
|
|
818
|
+
await initRepo(dir);
|
|
819
|
+
writeFixtureConvergeOp(dir, "staging-converge", "staging");
|
|
820
|
+
let subscribed = false;
|
|
821
|
+
const hostile: ChangeSubscriber = {
|
|
822
|
+
lexicon: "k8s",
|
|
823
|
+
async subscribe() {
|
|
824
|
+
subscribed = true;
|
|
825
|
+
return { close: async () => { throw new Error("close blew up"); } };
|
|
826
|
+
},
|
|
827
|
+
};
|
|
828
|
+
|
|
829
|
+
const controller = new AbortController();
|
|
830
|
+
const loop = runOperatorForever({
|
|
831
|
+
cwd: dir,
|
|
832
|
+
holder: "op-a",
|
|
833
|
+
intervalMs: 60_000,
|
|
834
|
+
activities: fakeTickActivities(dir, "staging", "staging-converge"),
|
|
835
|
+
profiles: PROFILES,
|
|
836
|
+
signal: controller.signal,
|
|
837
|
+
subscribers: [hostile],
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
await waitFor(() => subscribed);
|
|
841
|
+
controller.abort();
|
|
842
|
+
await expect(loop).resolves.toBeUndefined();
|
|
843
|
+
});
|
|
844
|
+
}, 15_000);
|
|
845
|
+
|
|
846
|
+
test("no subscribers: the loop is exactly the timer it always was", async () => {
|
|
847
|
+
await withTestDir(async (dir) => {
|
|
848
|
+
await initRepo(dir);
|
|
849
|
+
writeFixtureConvergeOp(dir, "staging-converge", "staging");
|
|
850
|
+
const controller = new AbortController();
|
|
851
|
+
const rounds: number[] = [];
|
|
852
|
+
const signalEvents: OperatorSignalEvent[] = [];
|
|
853
|
+
const loop = runOperatorForever({
|
|
854
|
+
cwd: dir,
|
|
855
|
+
holder: "op-a",
|
|
856
|
+
intervalMs: 40,
|
|
857
|
+
activities: fakeTickActivities(dir, "staging", "staging-converge"),
|
|
858
|
+
profiles: PROFILES,
|
|
859
|
+
signal: controller.signal,
|
|
860
|
+
subscribers: [], // a project whose lexicons implement no signal seam
|
|
861
|
+
onRound: () => rounds.push(Date.now()),
|
|
862
|
+
onSignalEvent: (e) => signalEvents.push(e),
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
await waitFor(() => rounds.length >= 3);
|
|
866
|
+
controller.abort();
|
|
867
|
+
await loop;
|
|
868
|
+
expect(rounds.length).toBeGreaterThanOrEqual(3);
|
|
869
|
+
expect(signalEvents).toEqual([]);
|
|
870
|
+
});
|
|
871
|
+
}, 15_000);
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
describe("formatSignalLine", () => {
|
|
875
|
+
test("renders each signal event kind as one line, none of them an observation", () => {
|
|
876
|
+
expect(formatSignalLine({ kind: "subscribed", lexicon: "k8s" })).toContain("the timer still runs");
|
|
877
|
+
expect(formatSignalLine({ kind: "subscribe-failed", lexicon: "k8s", error: "no binding" }))
|
|
878
|
+
.toContain("polling on the timer");
|
|
879
|
+
expect(formatSignalLine({ kind: "subscription-lost", lexicon: "k8s", error: "410 Gone" }))
|
|
880
|
+
.toContain("re-subscribes");
|
|
881
|
+
expect(formatSignalLine({ kind: "woken", afterMs: 120 })).toContain("running an ordinary round");
|
|
345
882
|
});
|
|
346
883
|
});
|