@intentius/chant 0.46.0 → 0.50.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 +13 -3
- package/dist/audit/catalog.d.ts.map +1 -1
- package/dist/audit/core.d.ts +30 -3
- package/dist/audit/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts +9 -2
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/fetch.d.ts.map +1 -1
- package/dist/audit/report-html.d.ts.map +1 -1
- package/dist/audit/report-model.d.ts +6 -0
- 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.map +1 -1
- package/dist/audit/secrets.d.ts +95 -0
- package/dist/audit/secrets.d.ts.map +1 -0
- package/dist/audit/wrangler.d.ts +33 -0
- package/dist/audit/wrangler.d.ts.map +1 -0
- package/dist/build.d.ts +3 -3
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/commands/audit.d.ts +7 -0
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts +23 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/components.d.ts +31 -0
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts +12 -1
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts +32 -0
- package/dist/cli/handlers/operator.d.ts.map +1 -0
- package/dist/cli/handlers/scenario.d.ts +39 -0
- package/dist/cli/handlers/scenario.d.ts.map +1 -0
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
- package/dist/cli/mcp/server.d.ts +35 -2
- package/dist/cli/mcp/server.d.ts.map +1 -1
- package/dist/cli/mcp/tools/explain.d.ts +6 -0
- package/dist/cli/mcp/tools/explain.d.ts.map +1 -1
- package/dist/cli/mcp/types.d.ts +29 -1
- package/dist/cli/mcp/types.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +1 -1
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +14 -2
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/cli/reporters/stylish.d.ts +15 -1
- package/dist/cli/reporters/stylish.d.ts.map +1 -1
- package/dist/codegen/docs-rule-scanning.d.ts.map +1 -1
- package/dist/components/auto-release.d.ts +4 -0
- package/dist/components/auto-release.d.ts.map +1 -1
- package/dist/components/capability.d.ts +17 -2
- package/dist/components/capability.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +7 -0
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/component.d.ts +15 -0
- package/dist/components/component.d.ts.map +1 -1
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/starter-plugin.d.ts +2 -0
- package/dist/components/starter-plugin.d.ts.map +1 -1
- package/dist/components/verbs/ensure-secret.d.ts +50 -0
- package/dist/components/verbs/ensure-secret.d.ts.map +1 -0
- package/dist/components/verbs/index.d.ts +13 -0
- package/dist/components/verbs/index.d.ts.map +1 -1
- package/dist/components/verbs/r2-sync.d.ts +76 -0
- package/dist/components/verbs/r2-sync.d.ts.map +1 -0
- package/dist/components/verbs/run-agent.d.ts +499 -0
- package/dist/components/verbs/run-agent.d.ts.map +1 -0
- package/dist/components/verbs/sign.d.ts +30 -0
- package/dist/components/verbs/sign.d.ts.map +1 -1
- package/dist/components/verbs/wrangler.d.ts +108 -0
- package/dist/components/verbs/wrangler.d.ts.map +1 -0
- package/dist/composite.d.ts +6 -1
- package/dist/composite.d.ts.map +1 -1
- package/dist/config.d.ts +26 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/deep-observation.d.ts +14 -0
- package/dist/deep-observation.d.ts.map +1 -1
- package/dist/discovery/collect.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts +15 -1
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/fold-rank.d.ts +66 -0
- package/dist/discovery/fold-rank.d.ts.map +1 -0
- package/dist/discovery/index.d.ts +15 -0
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/param-deps.d.ts +17 -0
- package/dist/discovery/param-deps.d.ts.map +1 -0
- package/dist/effect-receipt.d.ts +177 -0
- package/dist/effect-receipt.d.ts.map +1 -0
- package/dist/fold/fold.d.ts +55 -2
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/fold/subset.d.ts +20 -0
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lexicon-schema.d.ts +2 -0
- package/dist/lexicon-schema.d.ts.map +1 -1
- package/dist/lexicon.d.ts +137 -3
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/change-set.d.ts +33 -5
- package/dist/lifecycle/change-set.d.ts.map +1 -1
- package/dist/lifecycle/converge-ledger.d.ts +90 -0
- package/dist/lifecycle/converge-ledger.d.ts.map +1 -0
- package/dist/lifecycle/deep-diff.d.ts +18 -0
- package/dist/lifecycle/deep-diff.d.ts.map +1 -1
- package/dist/lifecycle/deep-observe.d.ts +9 -1
- package/dist/lifecycle/deep-observe.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +33 -0
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -0
- package/dist/lifecycle/git.d.ts +145 -21
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/index.d.ts +6 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/lease.d.ts +113 -0
- package/dist/lifecycle/lease.d.ts.map +1 -0
- package/dist/lifecycle/observation-baseline.d.ts +21 -3
- package/dist/lifecycle/observation-baseline.d.ts.map +1 -1
- package/dist/lifecycle/receipt-plan.d.ts +62 -0
- package/dist/lifecycle/receipt-plan.d.ts.map +1 -0
- package/dist/lifecycle/release-ledger.d.ts +20 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/scenario-eval.d.ts +42 -0
- package/dist/lifecycle/scenario-eval.d.ts.map +1 -0
- package/dist/lifecycle/scenario.d.ts +163 -0
- package/dist/lifecycle/scenario.d.ts.map +1 -0
- package/dist/lifecycle/symptoms.d.ts +63 -0
- package/dist/lifecycle/symptoms.d.ts.map +1 -0
- package/dist/lifecycle/teardown.d.ts +6 -4
- package/dist/lifecycle/teardown.d.ts.map +1 -1
- package/dist/lifecycle/unobserved-gate.d.ts +67 -0
- package/dist/lifecycle/unobserved-gate.d.ts.map +1 -0
- package/dist/lint/knowledge-checks.d.ts +48 -0
- package/dist/lint/knowledge-checks.d.ts.map +1 -0
- package/dist/lint/output-checks.d.ts +5 -0
- package/dist/lint/output-checks.d.ts.map +1 -0
- package/dist/lint/output-docs.d.ts +94 -0
- package/dist/lint/output-docs.d.ts.map +1 -0
- package/dist/lint/pipeline-change-gate.d.ts +101 -0
- package/dist/lint/pipeline-change-gate.d.ts.map +1 -0
- package/dist/lint/post-synth.d.ts +41 -0
- package/dist/lint/post-synth.d.ts.map +1 -1
- package/dist/lint/receipt-checks.d.ts +9 -0
- package/dist/lint/receipt-checks.d.ts.map +1 -0
- package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.d.ts +11 -0
- package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.d.ts.map +1 -0
- package/dist/lint/rules/cor022-receipt-leaf.d.ts +13 -0
- package/dist/lint/rules/cor022-receipt-leaf.d.ts.map +1 -0
- package/dist/lint/rules/cor024-receipt-secret-pointer.d.ts +3 -0
- package/dist/lint/rules/cor024-receipt-secret-pointer.d.ts.map +1 -0
- package/dist/lint/rules/evl001-non-literal-expression.d.ts.map +1 -1
- package/dist/lint/rules/index.d.ts +3 -1
- package/dist/lint/rules/index.d.ts.map +1 -1
- package/dist/lsp/lexicon-providers.d.ts +7 -0
- package/dist/lsp/lexicon-providers.d.ts.map +1 -1
- package/dist/okf-read.d.ts +78 -0
- package/dist/okf-read.d.ts.map +1 -0
- package/dist/op/activity-contract.d.ts +139 -0
- package/dist/op/activity-contract.d.ts.map +1 -0
- package/dist/op/builders.d.ts +140 -3
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/converge-rule.d.ts +161 -0
- package/dist/op/converge-rule.d.ts.map +1 -0
- package/dist/op/generate-pipeline.d.ts +39 -0
- package/dist/op/generate-pipeline.d.ts.map +1 -0
- package/dist/op/index.d.ts +18 -2
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts +2 -1
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/op-verb-class.d.ts +42 -0
- package/dist/op/op-verb-class.d.ts.map +1 -0
- package/dist/op/operator.d.ts +128 -0
- package/dist/op/operator.d.ts.map +1 -0
- package/dist/op/receipt-store.d.ts +138 -0
- package/dist/op/receipt-store.d.ts.map +1 -0
- package/dist/op/step-output-ref.d.ts +187 -0
- package/dist/op/step-output-ref.d.ts.map +1 -0
- package/dist/op/types.d.ts +49 -2
- package/dist/op/types.d.ts.map +1 -1
- package/dist/provenance.d.ts +73 -3
- package/dist/provenance.d.ts.map +1 -1
- package/dist/runtime-adapter.d.ts +7 -1
- package/dist/runtime-adapter.d.ts.map +1 -1
- package/dist/secret-materialization.d.ts +138 -0
- package/dist/secret-materialization.d.ts.map +1 -0
- package/dist/secret-provenance.d.ts +218 -0
- package/dist/secret-provenance.d.ts.map +1 -0
- package/dist/serializer.d.ts +29 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/toml.d.ts +40 -5
- package/dist/toml.d.ts.map +1 -1
- package/dist/yaml.d.ts.map +1 -1
- package/package.json +4 -1
- package/src/audit/catalog.test.ts +1 -1
- package/src/audit/catalog.ts +75 -3
- package/src/audit/core.test.ts +57 -0
- package/src/audit/core.ts +0 -0
- package/src/audit/detect-bundle.test.ts +1 -1
- package/src/audit/discover.test.ts +24 -0
- package/src/audit/discover.ts +40 -4
- package/src/audit/fetch.test.ts +216 -3
- package/src/audit/fetch.ts +270 -59
- package/src/audit/report-html.ts +5 -2
- package/src/audit/report-model.ts +9 -0
- package/src/audit/report.test.ts +22 -0
- package/src/audit/report.ts +3 -2
- package/src/audit/rules-doc.ts +13 -1
- package/src/audit/secrets.test.ts +303 -0
- package/src/audit/secrets.ts +406 -0
- package/src/audit/wrangler.test.ts +230 -0
- package/src/audit/wrangler.ts +290 -0
- package/src/build.test.ts +41 -0
- package/src/build.ts +39 -6
- package/src/cli/command-group.ts +1 -1
- package/src/cli/commands/__fixtures__/audit-fountain/agents/fleet.yaml +27 -0
- package/src/cli/commands/__fixtures__/audit-fountain/k8s/deploy.yaml +16 -0
- package/src/cli/commands/__fixtures__/audit-fountain-clean/fleet.yaml +20 -0
- package/src/cli/commands/__fixtures__/schemas/sarif-2.1.0.schema.json +2882 -0
- package/src/cli/commands/audit.test.ts +268 -1
- package/src/cli/commands/audit.ts +87 -18
- package/src/cli/commands/build.test.ts +245 -0
- package/src/cli/commands/build.ts +210 -21
- package/src/cli/commands/lint.ts +15 -3
- package/src/cli/handlers/build.ts +2 -0
- package/src/cli/handlers/components.test.ts +199 -1
- package/src/cli/handlers/components.ts +160 -3
- package/src/cli/handlers/explain.test.ts +70 -1
- package/src/cli/handlers/graph.test.ts +20 -0
- package/src/cli/handlers/graph.ts +12 -3
- package/src/cli/handlers/lifecycle.test.ts +115 -1
- package/src/cli/handlers/lifecycle.ts +96 -15
- package/src/cli/handlers/operator.test.ts +255 -0
- package/src/cli/handlers/operator.ts +240 -0
- package/src/cli/handlers/scenario.test.ts +456 -0
- package/src/cli/handlers/scenario.ts +330 -0
- package/src/cli/main.test.ts +23 -0
- package/src/cli/main.ts +72 -1
- package/src/cli/mcp/resource-handlers.ts +38 -1
- package/src/cli/mcp/server.test.ts +323 -3
- package/src/cli/mcp/server.ts +84 -7
- package/src/cli/mcp/tools/explain.ts +51 -2
- package/src/cli/mcp/types.ts +27 -1
- package/src/cli/plugins.ts +4 -2
- package/src/cli/registry.ts +14 -2
- package/src/cli/reporters/stylish.test.ts +154 -0
- package/src/cli/reporters/stylish.ts +154 -33
- package/src/codegen/docs-rule-scanning.test.ts +42 -0
- package/src/codegen/docs-rule-scanning.ts +25 -2
- package/src/components/README.md +7 -0
- package/src/components/auto-release.ts +6 -0
- package/src/components/capability.ts +17 -2
- package/src/components/cli-support.test.ts +17 -0
- package/src/components/cli-support.ts +13 -1
- package/src/components/component-schema.test.ts +32 -0
- package/src/components/component.schema.json +6 -0
- package/src/components/component.test.ts +21 -0
- package/src/components/component.ts +15 -0
- package/src/components/driver.ts +12 -4
- package/src/components/registry.test.ts +7 -2
- package/src/components/starter-plugin.ts +17 -0
- package/src/components/verbs/ensure-secret.test.ts +130 -0
- package/src/components/verbs/ensure-secret.ts +79 -0
- package/src/components/verbs/index.ts +13 -0
- package/src/components/verbs/r2-sync.test.ts +107 -0
- package/src/components/verbs/r2-sync.ts +124 -0
- package/src/components/verbs/run-agent.test.ts +683 -0
- package/src/components/verbs/run-agent.ts +786 -0
- package/src/components/verbs/sign.test.ts +19 -0
- package/src/components/verbs/sign.ts +34 -2
- package/src/components/verbs/wrangler.test.ts +170 -0
- package/src/components/verbs/wrangler.ts +241 -0
- package/src/composite.ts +31 -2
- package/src/config.test.ts +15 -0
- package/src/config.ts +30 -0
- package/src/deep-observation.test.ts +19 -0
- package/src/deep-observation.ts +17 -0
- package/src/discovery/collect.ts +11 -2
- package/src/discovery/fold-import.test.ts +54 -0
- package/src/discovery/fold-import.ts +178 -38
- package/src/discovery/fold-rank.test.ts +197 -0
- package/src/discovery/fold-rank.ts +346 -0
- package/src/discovery/index.ts +16 -1
- package/src/discovery/param-deps.test.ts +118 -0
- package/src/discovery/param-deps.ts +170 -0
- package/src/effect-receipt-exclusion.test.ts +190 -0
- package/src/effect-receipt.test.ts +419 -0
- package/src/effect-receipt.ts +412 -0
- package/src/fold/fold.test.ts +6 -2
- package/src/fold/fold.ts +184 -3
- package/src/fold/subset.test.ts +95 -6
- package/src/fold/subset.ts +66 -2
- package/src/index.ts +4 -0
- package/src/lexicon-schema.ts +3 -0
- package/src/lexicon.ts +151 -5
- package/src/lifecycle/change-set.ts +46 -7
- package/src/lifecycle/converge-ledger.test.ts +199 -0
- package/src/lifecycle/converge-ledger.ts +179 -0
- package/src/lifecycle/deep-diff.test.ts +79 -1
- package/src/lifecycle/deep-diff.ts +23 -0
- package/src/lifecycle/deep-observe.ts +13 -2
- package/src/lifecycle/gate-ledger.test.ts +103 -0
- package/src/lifecycle/gate-ledger.ts +140 -0
- package/src/lifecycle/git.test.ts +430 -0
- package/src/lifecycle/git.ts +446 -84
- package/src/lifecycle/index.ts +6 -0
- package/src/lifecycle/lease.test.ts +343 -0
- package/src/lifecycle/lease.ts +270 -0
- package/src/lifecycle/observation-baseline.test.ts +46 -0
- package/src/lifecycle/observation-baseline.ts +33 -1
- package/src/lifecycle/receipt-plan.test.ts +250 -0
- package/src/lifecycle/receipt-plan.ts +249 -0
- package/src/lifecycle/release-ledger.ts +20 -0
- package/src/lifecycle/scenario-eval.test.ts +199 -0
- package/src/lifecycle/scenario-eval.ts +158 -0
- package/src/lifecycle/scenario.test.ts +195 -0
- package/src/lifecycle/scenario.ts +321 -0
- package/src/lifecycle/symptoms.test.ts +116 -0
- package/src/lifecycle/symptoms.ts +126 -0
- package/src/lifecycle/teardown.test.ts +31 -0
- package/src/lifecycle/teardown.ts +6 -4
- package/src/lifecycle/unobserved-gate.test.ts +109 -0
- package/src/lifecycle/unobserved-gate.ts +102 -0
- package/src/lint/knowledge-checks.test.ts +80 -0
- package/src/lint/knowledge-checks.ts +74 -0
- package/src/lint/output-checks.test.ts +85 -0
- package/src/lint/output-checks.ts +99 -0
- package/src/lint/output-docs.test.ts +220 -0
- package/src/lint/output-docs.ts +204 -0
- package/src/lint/pipeline-change-gate.test.ts +144 -0
- package/src/lint/pipeline-change-gate.ts +153 -0
- package/src/lint/post-synth.test.ts +97 -0
- package/src/lint/post-synth.ts +60 -0
- package/src/lint/receipt-checks.test.ts +101 -0
- package/src/lint/receipt-checks.ts +93 -0
- package/src/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.ts +26 -0
- package/src/lint/rules/comp/comp.test.ts +49 -1
- package/src/lint/rules/cor022-receipt-leaf.test.ts +116 -0
- package/src/lint/rules/cor022-receipt-leaf.ts +130 -0
- package/src/lint/rules/cor024-receipt-secret-pointer.test.ts +121 -0
- package/src/lint/rules/cor024-receipt-secret-pointer.ts +218 -0
- package/src/lint/rules/evl001-non-literal-expression.test.ts +35 -3
- package/src/lint/rules/evl001-non-literal-expression.ts +7 -0
- package/src/lint/rules/index.ts +7 -1
- package/src/lsp/lexicon-providers.test.ts +44 -0
- package/src/lsp/lexicon-providers.ts +11 -1
- package/src/okf-read.test.ts +149 -0
- package/src/okf-read.ts +197 -0
- package/src/op/activity-contract.test.ts +180 -0
- package/src/op/activity-contract.ts +278 -0
- package/src/op/builders-exports.test.ts +17 -1
- package/src/op/builders.ts +198 -6
- package/src/op/converge-rule.test.ts +179 -0
- package/src/op/converge-rule.ts +311 -0
- package/src/op/effect-step.test.ts +311 -0
- package/src/op/generate-pipeline.test.ts +53 -0
- package/src/op/generate-pipeline.ts +99 -0
- package/src/op/index.ts +40 -3
- package/src/op/local-executor.test.ts +92 -0
- package/src/op/local-executor.ts +212 -29
- package/src/op/op-verb-class.test.ts +126 -0
- package/src/op/op-verb-class.ts +115 -0
- package/src/op/op.test.ts +25 -2
- package/src/op/operator.test.ts +346 -0
- package/src/op/operator.ts +213 -0
- package/src/op/receipt-store.ts +211 -0
- package/src/op/step-output-ref.test.ts +334 -0
- package/src/op/step-output-ref.ts +453 -0
- package/src/op/types.ts +51 -2
- package/src/provenance.test.ts +151 -4
- package/src/provenance.ts +118 -4
- package/src/runtime-adapter.ts +31 -10
- package/src/secret-materialization.test.ts +199 -0
- package/src/secret-materialization.ts +235 -0
- package/src/secret-provenance.test.ts +388 -0
- package/src/secret-provenance.ts +475 -0
- package/src/serializer.ts +30 -0
- package/src/toml.test.ts +157 -384
- package/src/toml.ts +371 -5
- package/src/yaml.test.ts +88 -0
- package/src/yaml.ts +76 -6
package/src/op/local-executor.ts
CHANGED
|
@@ -11,8 +11,10 @@
|
|
|
11
11
|
* so core never statically depends on `@intentius/chant-lexicon-temporal`.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import type { OpConfig, PhaseDefinition, ActivityStep, GateStep, StepDefinition } from "./types";
|
|
14
|
+
import type { OpConfig, PhaseDefinition, ActivityStep, GateStep, EffectStep, StepDefinition } from "./types";
|
|
15
15
|
import { resolveActivity, type ActivityFn, type ActivityProfile } from "./activity-registry";
|
|
16
|
+
import type { ReceiptReadResult } from "./receipt-store";
|
|
17
|
+
import { isStepOutputRef } from "./step-output-ref";
|
|
16
18
|
|
|
17
19
|
// ── Records ─────────────────────────────────────────────────────────────────
|
|
18
20
|
|
|
@@ -69,6 +71,7 @@ const FALLBACK_TIMEOUT_MS = 5 * 60_000;
|
|
|
69
71
|
|
|
70
72
|
const isActivity = (s: StepDefinition): s is ActivityStep => s.kind === "activity";
|
|
71
73
|
const isGate = (s: StepDefinition): s is GateStep => s.kind === "gate";
|
|
74
|
+
const isEffect = (s: StepDefinition): s is EffectStep => s.kind === "effect";
|
|
72
75
|
|
|
73
76
|
/** Parse a Temporal duration string ("5m", "30s", "1h30m", "100ms") to ms. */
|
|
74
77
|
export function parseDuration(s: string): number {
|
|
@@ -92,12 +95,37 @@ function resolvePath(value: unknown, path?: string): unknown {
|
|
|
92
95
|
);
|
|
93
96
|
}
|
|
94
97
|
|
|
95
|
-
/**
|
|
98
|
+
/**
|
|
99
|
+
* Deep-walk `value`, replacing every {@link StepOutputRef} with the recorded
|
|
100
|
+
* result of its producer step, resolved through the ref's optional dot-path
|
|
101
|
+
* exactly like the serializer's compiled `__rN?.path?.segments` (chant #1290)
|
|
102
|
+
* — `resolvePath` gives the same "undefined intermediate resolves to
|
|
103
|
+
* undefined, never throws" semantics. A reference to a step whose result
|
|
104
|
+
* isn't in `resultsById` (never ran, or ran out of the scope
|
|
105
|
+
* `validateStepOutputRefs` allows) resolves to `undefined`, the same way a
|
|
106
|
+
* producer whose declared field is absent would.
|
|
107
|
+
*/
|
|
108
|
+
function resolveStepOutputRefs(value: unknown, resultsById: ReadonlyMap<string, unknown>): unknown {
|
|
109
|
+
if (isStepOutputRef(value)) return resolvePath(resultsById.get(value.step), value.path);
|
|
110
|
+
if (Array.isArray(value)) return value.map((v) => resolveStepOutputRefs(v, resultsById));
|
|
111
|
+
if (value && typeof value === "object") {
|
|
112
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, resolveStepOutputRefs(v, resultsById)]));
|
|
113
|
+
}
|
|
114
|
+
return value;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Find the first gate step anywhere in the Op (phases + onFailure, including
|
|
118
|
+
* gates nested inside effect steps), if any. */
|
|
96
119
|
export function findGate(config: OpConfig): GateStep | undefined {
|
|
97
120
|
const all = [...config.phases, ...(config.onFailure ?? [])];
|
|
98
121
|
for (const phase of all) {
|
|
99
|
-
const
|
|
100
|
-
|
|
122
|
+
for (const step of phase.steps) {
|
|
123
|
+
if (isGate(step)) return step;
|
|
124
|
+
if (isEffect(step)) {
|
|
125
|
+
const nested = step.steps.find(isGate);
|
|
126
|
+
if (nested) return nested;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
101
129
|
}
|
|
102
130
|
return undefined;
|
|
103
131
|
}
|
|
@@ -145,15 +173,29 @@ async function callWithTimeout(
|
|
|
145
173
|
|
|
146
174
|
// ── Step + phase execution ────────────────────────────────────────────────��─
|
|
147
175
|
|
|
148
|
-
/**
|
|
176
|
+
/** A finished step: its record plus (on success) the activity's return value —
|
|
177
|
+
* the effect-step path needs `receiptRead`'s result, not just its status. */
|
|
178
|
+
interface RanStep {
|
|
179
|
+
record: StepRecord;
|
|
180
|
+
result?: unknown;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Run one activity step with retry + timeout. Never throws — returns a
|
|
185
|
+
* record. Any {@link StepOutputRef} in `step.args` is resolved against
|
|
186
|
+
* `resultsById` before the activity is called (#1290) — the local executor
|
|
187
|
+
* is a first-class peer of the Temporal path, so an unresolved placeholder
|
|
188
|
+
* must never reach an activity function; see `resolveStepOutputRefs`.
|
|
189
|
+
*/
|
|
149
190
|
async function runStep(
|
|
150
191
|
step: ActivityStep,
|
|
151
192
|
phaseName: string,
|
|
152
193
|
activities: Map<string, ActivityFn>,
|
|
153
194
|
profiles: Record<string, ActivityProfile>,
|
|
195
|
+
resultsById: Map<string, unknown>,
|
|
154
196
|
signal?: AbortSignal,
|
|
155
|
-
): Promise<
|
|
156
|
-
const args = step.args ?? {}
|
|
197
|
+
): Promise<RanStep> {
|
|
198
|
+
const args = resolveStepOutputRefs(step.args ?? {}, resultsById) as Record<string, unknown>;
|
|
157
199
|
const base = { phase: phaseName, fn: step.fn, args };
|
|
158
200
|
const start = Date.now();
|
|
159
201
|
|
|
@@ -161,7 +203,7 @@ async function runStep(
|
|
|
161
203
|
try {
|
|
162
204
|
fn = resolveActivity(activities, step.fn);
|
|
163
205
|
} catch (err) {
|
|
164
|
-
return { ...base, status: "fail", durationMs: 0, error: errMessage(err) };
|
|
206
|
+
return { record: { ...base, status: "fail", durationMs: 0, error: errMessage(err) } };
|
|
165
207
|
}
|
|
166
208
|
|
|
167
209
|
const profile = profiles[step.profile ?? DEFAULT_PROFILE] ?? {};
|
|
@@ -183,6 +225,7 @@ async function runStep(
|
|
|
183
225
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
184
226
|
try {
|
|
185
227
|
const result = await callWithTimeout(fn, args, timeoutMs, signal);
|
|
228
|
+
if (step.id) resultsById.set(step.id, result);
|
|
186
229
|
const record: StepRecord = { ...base, status: "ok", durationMs: Date.now() - start };
|
|
187
230
|
if (step.outcomeAttribute) {
|
|
188
231
|
record.outcome = {
|
|
@@ -190,7 +233,7 @@ async function runStep(
|
|
|
190
233
|
value: resolvePath(result, step.outcomeAttribute.from),
|
|
191
234
|
};
|
|
192
235
|
}
|
|
193
|
-
return record;
|
|
236
|
+
return { record, result };
|
|
194
237
|
} catch (err) {
|
|
195
238
|
lastErr = err;
|
|
196
239
|
// Stop retrying on abort (Ctrl-C / timeout cascade) or a non-retryable error.
|
|
@@ -204,7 +247,107 @@ async function runStep(
|
|
|
204
247
|
break;
|
|
205
248
|
}
|
|
206
249
|
}
|
|
207
|
-
return { ...base, status: "fail", durationMs: Date.now() - start, error: errMessage(lastErr) };
|
|
250
|
+
return { record: { ...base, status: "fail", durationMs: Date.now() - start, error: errMessage(lastErr) } };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ── Effect steps (#1834) ──────────────────────────────────────────────────────
|
|
254
|
+
|
|
255
|
+
/** The step data the executor synthesizes to read a receipt through the store
|
|
256
|
+
* activities (`receiptRead`/`receiptWrite` — provided by the receipt row's
|
|
257
|
+
* lexicon, #1835, or a mock store in tests via `receiptActivities`). */
|
|
258
|
+
function receiptReadStep(step: EffectStep): ActivityStep {
|
|
259
|
+
return {
|
|
260
|
+
kind: "activity",
|
|
261
|
+
fn: "receiptRead",
|
|
262
|
+
args: {
|
|
263
|
+
receipt: step.receipt,
|
|
264
|
+
...(step.expectation !== undefined ? { expectation: step.expectation } : {}),
|
|
265
|
+
},
|
|
266
|
+
profile: "fastIdempotent",
|
|
267
|
+
outcomeAttribute: { name: "EffectApplied", from: "applied" },
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** A skipped-record for a step that will not run. */
|
|
272
|
+
function skippedRecord(phaseName: string, fn: string, args?: Record<string, unknown>): StepRecord {
|
|
273
|
+
return { phase: phaseName, fn, args: args ?? {}, status: "skipped", durationMs: 0 };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Run one effect step: read-compare-run-write. On a match the nested steps are
|
|
278
|
+
* recorded as skipped ("effect already applied") and nothing is written. On a
|
|
279
|
+
* mismatch the nested steps run in authored order; only when every one
|
|
280
|
+
* succeeds is the receipt written — last, once (the sole writer, #1703
|
|
281
|
+
* decision 3). Any failure leaves the receipt untouched (stale), so the next
|
|
282
|
+
* run re-proposes the effect.
|
|
283
|
+
*/
|
|
284
|
+
async function runEffectStep(
|
|
285
|
+
step: EffectStep,
|
|
286
|
+
phaseName: string,
|
|
287
|
+
activities: Map<string, ActivityFn>,
|
|
288
|
+
profiles: Record<string, ActivityProfile>,
|
|
289
|
+
resultsById: Map<string, unknown>,
|
|
290
|
+
signal?: AbortSignal,
|
|
291
|
+
): Promise<{ records: StepRecord[]; failed: boolean }> {
|
|
292
|
+
const records: StepRecord[] = [];
|
|
293
|
+
|
|
294
|
+
const read = await runStep(receiptReadStep(step), phaseName, activities, profiles, resultsById, signal);
|
|
295
|
+
records.push(read.record);
|
|
296
|
+
if (read.record.status === "fail") return { records, failed: true };
|
|
297
|
+
|
|
298
|
+
const result = read.result as Partial<ReceiptReadResult> | undefined;
|
|
299
|
+
if (typeof result?.expectation !== "string") {
|
|
300
|
+
records.push({
|
|
301
|
+
phase: phaseName,
|
|
302
|
+
fn: `effect:${step.receipt.name}`,
|
|
303
|
+
args: {},
|
|
304
|
+
status: "fail",
|
|
305
|
+
durationMs: 0,
|
|
306
|
+
error: "receiptRead returned no expectation — the receipt store activity must return { current, expectation }",
|
|
307
|
+
});
|
|
308
|
+
return { records, failed: true };
|
|
309
|
+
}
|
|
310
|
+
const expectation = result.expectation;
|
|
311
|
+
|
|
312
|
+
if (result.current === expectation) {
|
|
313
|
+
// Effect already applied — skip the nested steps, write nothing.
|
|
314
|
+
for (const nested of step.steps) {
|
|
315
|
+
if (nested.kind === "activity") records.push(skippedRecord(phaseName, nested.fn, nested.args));
|
|
316
|
+
}
|
|
317
|
+
return { records, failed: false };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Gates are pre-flighted by findGate; only activities remain here.
|
|
321
|
+
const nestedActivities = step.steps.filter(isActivity);
|
|
322
|
+
for (let i = 0; i < nestedActivities.length; i++) {
|
|
323
|
+
const ran = await runStep(nestedActivities[i], phaseName, activities, profiles, resultsById, signal);
|
|
324
|
+
records.push(ran.record);
|
|
325
|
+
if (ran.record.status === "fail") {
|
|
326
|
+
// Receipt left untouched (stale) — the next run re-proposes the effect.
|
|
327
|
+
for (const skipped of nestedActivities.slice(i + 1)) {
|
|
328
|
+
records.push(skippedRecord(phaseName, skipped.fn, skipped.args));
|
|
329
|
+
}
|
|
330
|
+
records.push(skippedRecord(phaseName, "receiptWrite"));
|
|
331
|
+
return { records, failed: true };
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Sole writer of the receipt: on success of every nested step, last.
|
|
336
|
+
const wrote = await runStep(
|
|
337
|
+
{
|
|
338
|
+
kind: "activity",
|
|
339
|
+
fn: "receiptWrite",
|
|
340
|
+
args: { receipt: step.receipt, expectation },
|
|
341
|
+
profile: "fastIdempotent",
|
|
342
|
+
},
|
|
343
|
+
phaseName,
|
|
344
|
+
activities,
|
|
345
|
+
profiles,
|
|
346
|
+
resultsById,
|
|
347
|
+
signal,
|
|
348
|
+
);
|
|
349
|
+
records.push(wrote.record);
|
|
350
|
+
return { records, failed: wrote.record.status === "fail" };
|
|
208
351
|
}
|
|
209
352
|
|
|
210
353
|
/** Run a phase. Throws PhaseFailure (with records so far) if any step fails. */
|
|
@@ -212,37 +355,60 @@ async function runPhase(
|
|
|
212
355
|
phase: PhaseDefinition,
|
|
213
356
|
activities: Map<string, ActivityFn>,
|
|
214
357
|
profiles: Record<string, ActivityProfile>,
|
|
358
|
+
resultsById: Map<string, unknown>,
|
|
215
359
|
signal?: AbortSignal,
|
|
216
360
|
): Promise<StepRecord[]> {
|
|
217
|
-
// Defensive: gates are pre-flighted, but never execute one if it slips
|
|
218
|
-
|
|
361
|
+
// Defensive: gates are pre-flighted, but never execute one if it slips
|
|
362
|
+
// through — including a gate nested inside an effect step.
|
|
363
|
+
const gate =
|
|
364
|
+
phase.steps.find(isGate) ??
|
|
365
|
+
phase.steps.filter(isEffect).flatMap((e) => e.steps).find(isGate);
|
|
219
366
|
if (gate) throw new LocalGateUnsupportedError(gate.signalName);
|
|
220
367
|
|
|
221
|
-
const steps = phase.steps.filter(isActivity);
|
|
222
|
-
|
|
223
368
|
if (phase.parallel) {
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
369
|
+
const eff = phase.steps.find(isEffect);
|
|
370
|
+
if (eff) {
|
|
371
|
+
throw new Error(
|
|
372
|
+
`effect step "${eff.receipt.name}" cannot run in a parallel phase — read-compare-run-write is ordered`,
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
const steps = phase.steps.filter(isActivity);
|
|
376
|
+
const records = (
|
|
377
|
+
await Promise.all(steps.map((s) => runStep(s, phase.name, activities, profiles, resultsById, signal)))
|
|
378
|
+
).map((r) => r.record);
|
|
227
379
|
if (records.some((r) => r.status === "fail")) throw new PhaseFailure(records);
|
|
228
380
|
return records;
|
|
229
381
|
}
|
|
230
382
|
|
|
383
|
+
const steps = phase.steps.filter((s): s is ActivityStep | EffectStep => !isGate(s));
|
|
231
384
|
const records: StepRecord[] = [];
|
|
385
|
+
|
|
386
|
+
const skipRemaining = (from: number) => {
|
|
387
|
+
for (const skipped of steps.slice(from)) {
|
|
388
|
+
if (isEffect(skipped)) {
|
|
389
|
+
records.push(skippedRecord(phase.name, `effect:${skipped.receipt.name}`));
|
|
390
|
+
} else {
|
|
391
|
+
records.push(skippedRecord(phase.name, skipped.fn, skipped.args));
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
|
|
232
396
|
for (let i = 0; i < steps.length; i++) {
|
|
233
|
-
const
|
|
397
|
+
const step = steps[i];
|
|
398
|
+
if (isEffect(step)) {
|
|
399
|
+
const { records: effRecords, failed } = await runEffectStep(step, phase.name, activities, profiles, resultsById, signal);
|
|
400
|
+
records.push(...effRecords);
|
|
401
|
+
if (failed) {
|
|
402
|
+
skipRemaining(i + 1);
|
|
403
|
+
throw new PhaseFailure(records);
|
|
404
|
+
}
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
const { record } = await runStep(step, phase.name, activities, profiles, resultsById, signal);
|
|
234
408
|
records.push(record);
|
|
235
409
|
if (record.status === "fail") {
|
|
236
410
|
// Mark the remaining steps in this phase as skipped, then abort.
|
|
237
|
-
|
|
238
|
-
records.push({
|
|
239
|
-
phase: phase.name,
|
|
240
|
-
fn: skipped.fn,
|
|
241
|
-
args: skipped.args ?? {},
|
|
242
|
-
status: "skipped",
|
|
243
|
-
durationMs: 0,
|
|
244
|
-
});
|
|
245
|
-
}
|
|
411
|
+
skipRemaining(i + 1);
|
|
246
412
|
throw new PhaseFailure(records);
|
|
247
413
|
}
|
|
248
414
|
}
|
|
@@ -266,13 +432,30 @@ export async function runOpLocally(
|
|
|
266
432
|
const gate = findGate(config);
|
|
267
433
|
if (gate) throw new LocalGateUnsupportedError(gate.signalName);
|
|
268
434
|
|
|
435
|
+
// Effect steps are ordered (read-compare-run-write): refuse them in a
|
|
436
|
+
// parallel phase up front, with the phase named, rather than mid-run.
|
|
437
|
+
for (const phase of [...config.phases, ...(config.onFailure ?? [])]) {
|
|
438
|
+
const eff = phase.parallel ? phase.steps.find(isEffect) : undefined;
|
|
439
|
+
if (eff) {
|
|
440
|
+
throw new Error(
|
|
441
|
+
`phase "${phase.name}": effect step "${eff.receipt.name}" cannot run in a ` +
|
|
442
|
+
`parallel phase — read-compare-run-write is ordered`,
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
269
447
|
const records: StepRecord[] = [];
|
|
270
448
|
const start = Date.now();
|
|
271
449
|
|
|
450
|
+
// Completed step id → its activity result (#1290). Populated as main-phase
|
|
451
|
+
// steps finish, so a later step's step-output references resolve to real
|
|
452
|
+
// values before reaching an activity function — see `runStep`.
|
|
453
|
+
const resultsById = new Map<string, unknown>();
|
|
454
|
+
|
|
272
455
|
try {
|
|
273
456
|
for (const phase of config.phases) {
|
|
274
457
|
if (signal?.aborted) throw new PhaseFailure([]);
|
|
275
|
-
records.push(...(await runPhase(phase, activities, profiles, signal)));
|
|
458
|
+
records.push(...(await runPhase(phase, activities, profiles, resultsById, signal)));
|
|
276
459
|
}
|
|
277
460
|
} catch (err) {
|
|
278
461
|
if (err instanceof PhaseFailure) records.push(...err.records);
|
|
@@ -282,7 +465,7 @@ export async function runOpLocally(
|
|
|
282
465
|
if (!signal?.aborted) {
|
|
283
466
|
for (const phase of [...(config.onFailure ?? [])].reverse()) {
|
|
284
467
|
try {
|
|
285
|
-
records.push(...(await runPhase(phase, activities, profiles, signal)));
|
|
468
|
+
records.push(...(await runPhase(phase, activities, profiles, resultsById, signal)));
|
|
286
469
|
} catch (compErr) {
|
|
287
470
|
if (compErr instanceof PhaseFailure) records.push(...compErr.records);
|
|
288
471
|
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { classifyOpVerbClass, isGated } from "./op-verb-class";
|
|
3
|
+
import type { OpConfig } from "./types";
|
|
4
|
+
|
|
5
|
+
function cfg(phases: OpConfig["phases"], onFailure?: OpConfig["onFailure"]): Pick<OpConfig, "phases" | "onFailure"> {
|
|
6
|
+
return { phases, ...(onFailure ? { onFailure } : {}) };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe("classifyOpVerbClass", () => {
|
|
10
|
+
test("read-only for a WatchOp-shaped composition (snapshot + diff)", () => {
|
|
11
|
+
const config = cfg([
|
|
12
|
+
{ name: "Snapshot", steps: [{ kind: "activity", fn: "lifecycleSnapshot", args: { env: "prod" } }] },
|
|
13
|
+
{ name: "Diff", steps: [{ kind: "activity", fn: "lifecycleDiff", args: { env: "prod" } }] },
|
|
14
|
+
]);
|
|
15
|
+
expect(classifyOpVerbClass(config)).toBe("read-only");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("read-only for an empty phase list", () => {
|
|
19
|
+
expect(classifyOpVerbClass(cfg([]))).toBe("read-only");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("mutating for nativeApply with deleteMode never", () => {
|
|
23
|
+
const config = cfg([
|
|
24
|
+
{ name: "Apply", steps: [{ kind: "activity", fn: "nativeApply", args: { target: "kubectl", env: "prod", output: "dist", deleteMode: "never" } }] },
|
|
25
|
+
]);
|
|
26
|
+
expect(classifyOpVerbClass(config)).toBe("mutating");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("mutating for nativeApply with no deleteMode at all", () => {
|
|
30
|
+
const config = cfg([
|
|
31
|
+
{ name: "Apply", steps: [{ kind: "activity", fn: "nativeApply", args: { target: "kubectl", env: "prod", output: "dist" } }] },
|
|
32
|
+
]);
|
|
33
|
+
expect(classifyOpVerbClass(config)).toBe("mutating");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("destructive for nativeApply with deleteMode owned-only", () => {
|
|
37
|
+
const config = cfg([
|
|
38
|
+
{ name: "Apply", steps: [{ kind: "activity", fn: "nativeApply", args: { target: "kubectl", env: "prod", output: "dist", deleteMode: "owned-only" } }] },
|
|
39
|
+
]);
|
|
40
|
+
expect(classifyOpVerbClass(config)).toBe("destructive");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("destructive for nativeApply with deleteMode gated", () => {
|
|
44
|
+
const config = cfg([
|
|
45
|
+
{ name: "Apply", steps: [{ kind: "activity", fn: "nativeApply", args: { target: "kubectl", env: "prod", output: "dist", deleteMode: "gated" } }] },
|
|
46
|
+
]);
|
|
47
|
+
expect(classifyOpVerbClass(config)).toBe("destructive");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("destructive for envTeardown regardless of args", () => {
|
|
51
|
+
const config = cfg([{ name: "Teardown", steps: [{ kind: "activity", fn: "envTeardown", args: { env: "staging" } }] }]);
|
|
52
|
+
expect(classifyOpVerbClass(config)).toBe("destructive");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("destructive for awsDelete/azDelete/gcpDelete/chantTeardown", () => {
|
|
56
|
+
for (const fn of ["awsDelete", "azDelete", "gcpDelete", "chantTeardown"]) {
|
|
57
|
+
const config = cfg([{ name: "P", steps: [{ kind: "activity", fn, args: {} }] }]);
|
|
58
|
+
expect(classifyOpVerbClass(config)).toBe("destructive");
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("fail-closed: an unrecognized activity fn is mutating, not read-only", () => {
|
|
63
|
+
const config = cfg([{ name: "P", steps: [{ kind: "activity", fn: "someBespokeCustomActivity", args: {} }] }]);
|
|
64
|
+
expect(classifyOpVerbClass(config)).toBe("mutating");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("a single destructive step outranks other read-only/mutating steps in the same Op", () => {
|
|
68
|
+
const config = cfg([
|
|
69
|
+
{ name: "Diff", steps: [{ kind: "activity", fn: "lifecycleDiff", args: { env: "prod" } }] },
|
|
70
|
+
{ name: "Teardown", steps: [{ kind: "activity", fn: "envTeardown", args: { env: "prod" } }] },
|
|
71
|
+
]);
|
|
72
|
+
expect(classifyOpVerbClass(config)).toBe("destructive");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("checks onFailure phases too", () => {
|
|
76
|
+
const config = cfg(
|
|
77
|
+
[{ name: "Apply", steps: [{ kind: "activity", fn: "lifecycleDiff", args: { env: "prod" } }] }],
|
|
78
|
+
[{ name: "Rollback", steps: [{ kind: "activity", fn: "envTeardown", args: { env: "prod" } }] }],
|
|
79
|
+
);
|
|
80
|
+
expect(classifyOpVerbClass(config)).toBe("destructive");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("looks inside an effect step's nested activity steps", () => {
|
|
84
|
+
const config = cfg([
|
|
85
|
+
{
|
|
86
|
+
name: "Seed",
|
|
87
|
+
steps: [
|
|
88
|
+
{
|
|
89
|
+
kind: "effect",
|
|
90
|
+
receipt: { name: "seeded", lexicon: "aws" } as never,
|
|
91
|
+
steps: [{ kind: "activity", fn: "nativeApply", args: { target: "kubectl", env: "prod", output: "dist", deleteMode: "gated" } }],
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
]);
|
|
96
|
+
expect(classifyOpVerbClass(config)).toBe("destructive");
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("isGated", () => {
|
|
101
|
+
test("false with no gate step", () => {
|
|
102
|
+
expect(isGated(cfg([{ name: "Apply", steps: [{ kind: "activity", fn: "nativeApply", args: {} }] }]))).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("true with a gate step in the main phases", () => {
|
|
106
|
+
expect(
|
|
107
|
+
isGated(
|
|
108
|
+
cfg([
|
|
109
|
+
{ name: "Approve", steps: [{ kind: "gate", signalName: "approve-x" }] },
|
|
110
|
+
{ name: "Apply", steps: [{ kind: "activity", fn: "nativeApply", args: {} }] },
|
|
111
|
+
]),
|
|
112
|
+
),
|
|
113
|
+
).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("true with a gate step in onFailure", () => {
|
|
117
|
+
expect(
|
|
118
|
+
isGated(
|
|
119
|
+
cfg(
|
|
120
|
+
[{ name: "Apply", steps: [{ kind: "activity", fn: "nativeApply", args: {} }] }],
|
|
121
|
+
[{ name: "Rollback", steps: [{ kind: "gate", signalName: "approve-rollback" }] }],
|
|
122
|
+
),
|
|
123
|
+
),
|
|
124
|
+
).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Op verb classification (#1484) — generalizes `delete: "never" | "owned-only"
|
|
3
|
+
* | "gated"` (`../lifecycle` teardown/apply's `DeleteMode`) up one level, to
|
|
4
|
+
* the whole Op: read-only, mutating, or destructive. `ConvergeOp`'s
|
|
5
|
+
* dial × verb-class matrix (the issue's "Autonomy" table) needs to answer
|
|
6
|
+
* "what kind of thing is this dispatched Op, from the outside" for an
|
|
7
|
+
* arbitrary declared Op — including one this project's authors wrote, not
|
|
8
|
+
* just the pre-built composites — so classification walks `OpConfig`'s
|
|
9
|
+
* steps rather than trusting a self-reported tag.
|
|
10
|
+
*
|
|
11
|
+
* There is no existing per-Op verb-class field (`searchAttributes` is a free
|
|
12
|
+
* `Record<string,string>` an author may or may not set — `ApplyOp` tags
|
|
13
|
+
* `{ Apply: "true" }` but nothing requires it, and it carries no verb-class
|
|
14
|
+
* semantics on its own) and no existing "which activity names are
|
|
15
|
+
* destructive" registry (`../lint/pipeline-change-gate.ts`'s classifier is
|
|
16
|
+
* shaped for a `Component` composition's capability-verb steps, not an
|
|
17
|
+
* `OpConfig`'s activity steps — a genuinely different graph). This module is
|
|
18
|
+
* that registry's Op-shaped sibling: a small, explicit set of known
|
|
19
|
+
* mutating/destructive activity function names, plus the one piece of
|
|
20
|
+
* structured signal that already exists (`nativeApply`'s `deleteMode` arg).
|
|
21
|
+
*
|
|
22
|
+
* Fail-closed by construction: an activity `fn` this module doesn't
|
|
23
|
+
* recognize is classified `mutating`, never `read-only` — an unknown step
|
|
24
|
+
* might do anything, and "unknown never remediates" (the issue's own honesty
|
|
25
|
+
* requirement for symptoms) applies here too: this module never silently
|
|
26
|
+
* clears an Op to free-run just because its steps aren't in the known-safe
|
|
27
|
+
* list.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { findGate } from "./local-executor";
|
|
31
|
+
import type { ActivityStep, EffectStep, OpConfig, StepDefinition } from "./types";
|
|
32
|
+
|
|
33
|
+
export type OpVerbClass = "read-only" | "mutating" | "destructive";
|
|
34
|
+
|
|
35
|
+
/** Activity function names known to be read-only — verification, observation, audit. Anything not in this set is treated as at least `mutating` (fail-closed). */
|
|
36
|
+
const READ_ONLY_ACTIVITY_FNS: ReadonlySet<string> = new Set([
|
|
37
|
+
"lifecycleSnapshot",
|
|
38
|
+
"lifecycleDiff",
|
|
39
|
+
"httpCheck",
|
|
40
|
+
"policyGate",
|
|
41
|
+
"workflowSupplyChainAudit",
|
|
42
|
+
"pipelineSupplyChainAudit",
|
|
43
|
+
"receiptStaleness",
|
|
44
|
+
"waitForStack",
|
|
45
|
+
"waitForReady",
|
|
46
|
+
"waitForArgoSync",
|
|
47
|
+
"spriteReadFile",
|
|
48
|
+
"spriteListDir",
|
|
49
|
+
"listCheckpoints",
|
|
50
|
+
"convergeTick",
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
/** Activity function names that always delete/destroy, regardless of args. */
|
|
54
|
+
const ALWAYS_DESTRUCTIVE_ACTIVITY_FNS: ReadonlySet<string> = new Set([
|
|
55
|
+
"envTeardown",
|
|
56
|
+
"chantTeardown",
|
|
57
|
+
"azDelete",
|
|
58
|
+
"awsDelete",
|
|
59
|
+
"gcpDelete",
|
|
60
|
+
"spriteDestroy",
|
|
61
|
+
"k3dDown",
|
|
62
|
+
"k3sUninstall",
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
function stepsOf(config: Pick<OpConfig, "phases" | "onFailure">): ActivityStep[] {
|
|
66
|
+
const all: StepDefinition[] = [...config.phases, ...(config.onFailure ?? [])].flatMap((p) => p.steps);
|
|
67
|
+
const flat: ActivityStep[] = [];
|
|
68
|
+
for (const step of all) {
|
|
69
|
+
if (step.kind === "activity") flat.push(step);
|
|
70
|
+
else if (step.kind === "effect") flat.push(...(step as EffectStep).steps.filter((s): s is ActivityStep => s.kind === "activity"));
|
|
71
|
+
}
|
|
72
|
+
return flat;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Classify an Op's own composition. Pure — reads only the passed config, no
|
|
77
|
+
* I/O. `nativeApply`'s `deleteMode` (`"never" | "owned-only" | "gated"`) is
|
|
78
|
+
* read straight off the step's `args` — `"owned-only"`/`"gated"` are
|
|
79
|
+
* destructive (both can delete a live resource), `"never"`/absent is merely
|
|
80
|
+
* mutating (creates/updates only). A step whose `fn` matches neither the
|
|
81
|
+
* read-only nor the destructive set is `mutating` by default (fail-closed).
|
|
82
|
+
*/
|
|
83
|
+
export function classifyOpVerbClass(config: Pick<OpConfig, "phases" | "onFailure">): OpVerbClass {
|
|
84
|
+
let sawMutating = false;
|
|
85
|
+
|
|
86
|
+
for (const step of stepsOf(config)) {
|
|
87
|
+
if (ALWAYS_DESTRUCTIVE_ACTIVITY_FNS.has(step.fn)) return "destructive";
|
|
88
|
+
|
|
89
|
+
if (step.fn === "nativeApply") {
|
|
90
|
+
const deleteMode = step.args?.deleteMode;
|
|
91
|
+
if (deleteMode === "owned-only" || deleteMode === "gated") return "destructive";
|
|
92
|
+
sawMutating = true;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (step.fn === "compensateApply") {
|
|
97
|
+
// Rollback compensation — mutating (it re-applies a prior state), not
|
|
98
|
+
// itself a fresh destructive act.
|
|
99
|
+
sawMutating = true;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (READ_ONLY_ACTIVITY_FNS.has(step.fn)) continue;
|
|
104
|
+
|
|
105
|
+
// Unknown or known-mutating fn: fail-closed to mutating.
|
|
106
|
+
sawMutating = true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return sawMutating ? "mutating" : "read-only";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Does this Op have its own approval gate anywhere in its phases (main or `onFailure`)? Thin re-export of `findGate` under the name this module's callers reach for. */
|
|
113
|
+
export function isGated(config: Pick<OpConfig, "phases" | "onFailure">): boolean {
|
|
114
|
+
return findGate(config as OpConfig) !== undefined;
|
|
115
|
+
}
|
package/src/op/op.test.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { describe, expect, it } from "vitest";
|
|
6
|
-
import { Op, phase, activity, gate, build, kubectlApply, helmInstall,
|
|
7
|
-
waitForStack, gitlabPipeline, lifecycleSnapshot, shell, teardown, policyGate } from "./builders";
|
|
6
|
+
import { Op, phase, activity, gate, build, kubectlApply, helmInstall, helmInstallPinned,
|
|
7
|
+
waitForStack, gitlabPipeline, lifecycleSnapshot, shell, ensureSecret, teardown, policyGate } from "./builders";
|
|
8
8
|
import { DECLARABLE_MARKER, type Declarable } from "../declarable";
|
|
9
9
|
|
|
10
10
|
// ── Op() ──────────────────────────────────────────────────────────────────────
|
|
@@ -163,6 +163,16 @@ describe("pre-built shortcuts", () => {
|
|
|
163
163
|
expect(a.profile).toBe("longInfra");
|
|
164
164
|
});
|
|
165
165
|
|
|
166
|
+
it("helmInstallPinned() produces a helmInstall activity keyed by contentDigest, no chart", () => {
|
|
167
|
+
const a = helmInstallPinned("my-release", "sha256:abc", { namespace: "web" });
|
|
168
|
+
expect(a.fn).toBe("helmInstall");
|
|
169
|
+
expect(a.args?.name).toBe("my-release");
|
|
170
|
+
expect(a.args?.contentDigest).toBe("sha256:abc");
|
|
171
|
+
expect(a.args?.namespace).toBe("web");
|
|
172
|
+
expect(a.args?.chart).toBeUndefined();
|
|
173
|
+
expect(a.profile).toBe("longInfra");
|
|
174
|
+
});
|
|
175
|
+
|
|
166
176
|
it("waitForStack() produces waitForStack activity with k8sWait profile", () => {
|
|
167
177
|
const a = waitForStack("my-stack");
|
|
168
178
|
expect(a.fn).toBe("waitForStack");
|
|
@@ -190,6 +200,19 @@ describe("pre-built shortcuts", () => {
|
|
|
190
200
|
expect(a.args?.cmd).toBe("echo hello");
|
|
191
201
|
});
|
|
192
202
|
|
|
203
|
+
it("ensureSecret() produces ensureSecret activity with name/keys args and no material-shaped field", () => {
|
|
204
|
+
const a = ensureSecret("master-key", ["MASTER_SECRETS_KEY"], {
|
|
205
|
+
metadata: { "chant.dev/provenance": "generated-once" },
|
|
206
|
+
});
|
|
207
|
+
expect(a.fn).toBe("ensureSecret");
|
|
208
|
+
expect(a.args?.name).toBe("master-key");
|
|
209
|
+
expect(a.args?.keys).toEqual(["MASTER_SECRETS_KEY"]);
|
|
210
|
+
expect(a.args?.metadata).toEqual({ "chant.dev/provenance": "generated-once" });
|
|
211
|
+
// The step is contract-only: nothing on it could carry secret material.
|
|
212
|
+
expect(a.args && "value" in a.args).toBe(false);
|
|
213
|
+
expect(a.args && "data" in a.args).toBe(false);
|
|
214
|
+
});
|
|
215
|
+
|
|
193
216
|
it("teardown() produces chantTeardown activity with longInfra profile", () => {
|
|
194
217
|
const a = teardown("./project");
|
|
195
218
|
expect(a.fn).toBe("chantTeardown");
|