@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
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* effect() step + receipt store seam + local-executor semantics (#1834, epic
|
|
3
|
+
* #1703). The contract under test:
|
|
4
|
+
*
|
|
5
|
+
* - the builder takes the typed EffectReceipt declaration only (no string
|
|
6
|
+
* form — enforced at the type level and at runtime);
|
|
7
|
+
* - read-compare-run-write: a matching receipt skips the nested steps; a
|
|
8
|
+
* mismatch runs them and writes the receipt ONLY on their success, last;
|
|
9
|
+
* - a nested-step failure leaves the receipt untouched (stale), so a rerun
|
|
10
|
+
* re-proposes the effect;
|
|
11
|
+
* - `receiptStaleness` (WatchOp's phase) is read-only: it reports absent and
|
|
12
|
+
* differing receipts as findings and never writes.
|
|
13
|
+
*/
|
|
14
|
+
import { describe, test, expect } from "vitest";
|
|
15
|
+
import { EffectReceipt, receiptExpectation, EXISTENCE_EXPECTATION } from "../effect-receipt";
|
|
16
|
+
import { INTRINSIC_MARKER, type Intrinsic } from "../intrinsic";
|
|
17
|
+
import { effect, phase } from "./builders";
|
|
18
|
+
import {
|
|
19
|
+
receiptActivities,
|
|
20
|
+
receiptCheckInput,
|
|
21
|
+
type ReceiptStore,
|
|
22
|
+
type EffectReceiptRef,
|
|
23
|
+
} from "./receipt-store";
|
|
24
|
+
import type { ActivityFn, ActivityProfile } from "./activity-registry";
|
|
25
|
+
import type { OpConfig, ActivityStep } from "./types";
|
|
26
|
+
import { runOpLocally, findGate, LocalGateUnsupportedError, OpRunFailure } from "./local-executor";
|
|
27
|
+
|
|
28
|
+
function fakeIntrinsic(json: unknown): Intrinsic {
|
|
29
|
+
return {
|
|
30
|
+
[INTRINSIC_MARKER]: true,
|
|
31
|
+
toJSON: () => json,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const seeded = EffectReceipt("seeded", {
|
|
36
|
+
effect: "db-seed",
|
|
37
|
+
flavor: "hash",
|
|
38
|
+
inputs: { file: "seed.sql", version: 3 },
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const step = (fn: string, args?: Record<string, unknown>): ActivityStep => ({
|
|
42
|
+
kind: "activity",
|
|
43
|
+
fn,
|
|
44
|
+
...(args ? { args } : {}),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// ── Builder ───────────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
describe("effect() builder", () => {
|
|
50
|
+
test("wraps nested steps with the receipt's identity data and stamps a static expectation", () => {
|
|
51
|
+
const s = effect(seeded, [step("shellCmd", { cmd: "npm run db:seed" })]);
|
|
52
|
+
expect(s.kind).toBe("effect");
|
|
53
|
+
expect(s.receipt).toEqual({
|
|
54
|
+
name: "seeded",
|
|
55
|
+
effect: "db-seed",
|
|
56
|
+
flavor: "hash",
|
|
57
|
+
inputs: { file: "seed.sql", version: 3 },
|
|
58
|
+
});
|
|
59
|
+
expect(s.expectation).toBe(receiptExpectation(seeded));
|
|
60
|
+
expect(s.steps.map((n) => (n.kind === "activity" ? n.fn : n.signalName))).toEqual(["shellCmd"]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("existence receipts always get the constant expectation, reference inputs or not", () => {
|
|
64
|
+
const r = EffectReceipt("booted", {
|
|
65
|
+
effect: "bootstrap",
|
|
66
|
+
flavor: "existence",
|
|
67
|
+
inputs: { endpoint: fakeIntrinsic({ __attrRef: { entity: "Db", attribute: "endpoint" } }) },
|
|
68
|
+
});
|
|
69
|
+
expect(effect(r, []).expectation).toBe(EXISTENCE_EXPECTATION);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("a hash receipt with reference inputs carries no synthesis-time expectation (resolves at run, #1703 decision 5)", () => {
|
|
73
|
+
const r = EffectReceipt("migrated", {
|
|
74
|
+
effect: "db-migrate",
|
|
75
|
+
flavor: "hash",
|
|
76
|
+
inputs: { endpoint: fakeIntrinsic({ __attrRef: { entity: "Db", attribute: "endpoint" } }) },
|
|
77
|
+
});
|
|
78
|
+
const s = effect(r, []);
|
|
79
|
+
expect(s.expectation).toBeUndefined();
|
|
80
|
+
expect("expectation" in s).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("preserves authored order and nested gates", () => {
|
|
84
|
+
const s = effect(seeded, [
|
|
85
|
+
{ kind: "gate", signalName: "approve-seed" },
|
|
86
|
+
step("shellCmd", { cmd: "seed" }),
|
|
87
|
+
]);
|
|
88
|
+
expect(s.steps.map((n) => n.kind)).toEqual(["gate", "activity"]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("refuses anything that is not the typed receipt declaration", () => {
|
|
92
|
+
expect(() => effect({ name: "seeded" } as never, [])).toThrow(/no string form/);
|
|
93
|
+
// Type level: a string receipt name must not compile.
|
|
94
|
+
// @ts-expect-error — receipt is the EffectReceipt declaration, never a string
|
|
95
|
+
expect(() => effect("seeded", [])).toThrow(/no string form/);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("effect steps do not nest", () => {
|
|
99
|
+
const inner = effect(seeded, []);
|
|
100
|
+
expect(() => effect(seeded, [inner as never])).toThrow(/do not nest/);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// ── Receipt store seam ────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
function memStore(initial?: Record<string, string>): {
|
|
107
|
+
store: ReceiptStore;
|
|
108
|
+
values: Map<string, string>;
|
|
109
|
+
writes: Array<{ name: string; expectation: string }>;
|
|
110
|
+
} {
|
|
111
|
+
const values = new Map(Object.entries(initial ?? {}));
|
|
112
|
+
const writes: Array<{ name: string; expectation: string }> = [];
|
|
113
|
+
const store: ReceiptStore = {
|
|
114
|
+
async read(r: EffectReceiptRef) {
|
|
115
|
+
return values.get(r.name);
|
|
116
|
+
},
|
|
117
|
+
async write(r: EffectReceiptRef, expectation: string) {
|
|
118
|
+
writes.push({ name: r.name, expectation });
|
|
119
|
+
values.set(r.name, expectation);
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
return { store, values, writes };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
describe("receiptActivities (the injectable store seam)", () => {
|
|
126
|
+
const input = receiptCheckInput(seeded);
|
|
127
|
+
const expected = receiptExpectation(seeded);
|
|
128
|
+
|
|
129
|
+
test("receiptRead: absent receipt reads as current null, applied false", async () => {
|
|
130
|
+
const { store } = memStore();
|
|
131
|
+
const { receiptRead } = receiptActivities(store);
|
|
132
|
+
expect(await receiptRead(input)).toEqual({ current: null, expectation: expected, applied: false });
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("receiptRead: matching receipt reads as applied true", async () => {
|
|
136
|
+
const { store } = memStore({ seeded: expected });
|
|
137
|
+
const { receiptRead } = receiptActivities(store);
|
|
138
|
+
expect(await receiptRead(input)).toEqual({ current: expected, expectation: expected, applied: true });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("receiptRead: refuses to invent an expectation for a reference-carrying hash receipt without a resolver", async () => {
|
|
142
|
+
const r = EffectReceipt("migrated", {
|
|
143
|
+
effect: "db-migrate",
|
|
144
|
+
flavor: "hash",
|
|
145
|
+
inputs: { endpoint: fakeIntrinsic("placeholder") },
|
|
146
|
+
});
|
|
147
|
+
const { store } = memStore();
|
|
148
|
+
const { receiptRead } = receiptActivities(store);
|
|
149
|
+
await expect(receiptRead(receiptCheckInput(r))).rejects.toThrow(/resolveExpectation/);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("receiptWrite stores the expectation", async () => {
|
|
153
|
+
const { store, writes, values } = memStore();
|
|
154
|
+
const { receiptWrite } = receiptActivities(store);
|
|
155
|
+
await receiptWrite({ receipt: input.receipt, expectation: expected });
|
|
156
|
+
expect(writes).toEqual([{ name: "seeded", expectation: expected }]);
|
|
157
|
+
expect(values.get("seeded")).toBe(expected);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("receiptStaleness reports absent and differing receipts as findings, and never writes", async () => {
|
|
161
|
+
const fresh = EffectReceipt("fresh", { effect: "e1", flavor: "hash", inputs: { v: 1 } });
|
|
162
|
+
const differs = EffectReceipt("differs", { effect: "e2", flavor: "hash", inputs: { v: 2 } });
|
|
163
|
+
const absent = EffectReceipt("absent", { effect: "e3", flavor: "existence" });
|
|
164
|
+
const { store, writes } = memStore({
|
|
165
|
+
fresh: receiptExpectation(fresh),
|
|
166
|
+
differs: "sha256:stale",
|
|
167
|
+
});
|
|
168
|
+
const { receiptStaleness } = receiptActivities(store);
|
|
169
|
+
const result = await receiptStaleness({
|
|
170
|
+
receipts: [fresh, differs, absent].map(receiptCheckInput),
|
|
171
|
+
});
|
|
172
|
+
expect(result.stale).toBe(true);
|
|
173
|
+
expect(result.findings).toEqual([
|
|
174
|
+
{ receipt: "differs", effect: "e2", kind: "differs", expected: receiptExpectation(differs), current: "sha256:stale" },
|
|
175
|
+
{ receipt: "absent", effect: "e3", kind: "absent", expected: EXISTENCE_EXPECTATION },
|
|
176
|
+
]);
|
|
177
|
+
expect(writes).toEqual([]);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("receiptStaleness with everything fresh reports stale false", async () => {
|
|
181
|
+
const { store } = memStore({ seeded: expected });
|
|
182
|
+
const { receiptStaleness } = receiptActivities(store);
|
|
183
|
+
expect(await receiptStaleness({ receipts: [input] })).toEqual({ stale: false, findings: [] });
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// ── Local executor: read-compare-run-write ────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
const PROFILES: Record<string, ActivityProfile> = {
|
|
190
|
+
fastIdempotent: { startToCloseTimeout: "5m", retry: { maximumAttempts: 1 } },
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
function activityMap(
|
|
194
|
+
store: ReceiptStore,
|
|
195
|
+
extra: Record<string, ActivityFn>,
|
|
196
|
+
): Map<string, ActivityFn> {
|
|
197
|
+
const map = new Map<string, ActivityFn>();
|
|
198
|
+
for (const [name, fn] of Object.entries(receiptActivities(store))) {
|
|
199
|
+
map.set(name, fn as unknown as ActivityFn);
|
|
200
|
+
}
|
|
201
|
+
for (const [name, fn] of Object.entries(extra)) map.set(name, fn);
|
|
202
|
+
return map;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function seedOp(): OpConfig {
|
|
206
|
+
return {
|
|
207
|
+
name: "seed-op",
|
|
208
|
+
overview: "",
|
|
209
|
+
phases: [phase("Seed", [effect(seeded, [step("runSeed")])])],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
describe("runOpLocally — effect steps", () => {
|
|
214
|
+
test("matching receipt skips the nested steps and writes nothing", async () => {
|
|
215
|
+
const { store, writes } = memStore({ seeded: receiptExpectation(seeded) });
|
|
216
|
+
const ran: string[] = [];
|
|
217
|
+
const activities = activityMap(store, { runSeed: async () => void ran.push("runSeed") });
|
|
218
|
+
const result = await runOpLocally(seedOp(), activities, PROFILES);
|
|
219
|
+
expect(result.ok).toBe(true);
|
|
220
|
+
expect(ran).toEqual([]);
|
|
221
|
+
expect(writes).toEqual([]);
|
|
222
|
+
// The read records the effect as already applied; the nested step is skipped.
|
|
223
|
+
expect(result.records.map((r) => [r.fn, r.status])).toEqual([
|
|
224
|
+
["receiptRead", "ok"],
|
|
225
|
+
["runSeed", "skipped"],
|
|
226
|
+
]);
|
|
227
|
+
expect(result.records[0].outcome).toEqual({ name: "EffectApplied", value: true });
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("mismatch runs the nested steps, then writes the receipt once, last", async () => {
|
|
231
|
+
const { store, writes } = memStore();
|
|
232
|
+
const order: string[] = [];
|
|
233
|
+
const activities = activityMap(
|
|
234
|
+
{ read: store.read, write: async (r, e) => { order.push("write"); await store.write(r, e); } },
|
|
235
|
+
{ runSeed: async () => void order.push("runSeed") },
|
|
236
|
+
);
|
|
237
|
+
const result = await runOpLocally(seedOp(), activities, PROFILES);
|
|
238
|
+
expect(result.ok).toBe(true);
|
|
239
|
+
expect(order).toEqual(["runSeed", "write"]);
|
|
240
|
+
expect(writes).toEqual([{ name: "seeded", expectation: receiptExpectation(seeded) }]);
|
|
241
|
+
expect(result.records.map((r) => [r.fn, r.status])).toEqual([
|
|
242
|
+
["receiptRead", "ok"],
|
|
243
|
+
["runSeed", "ok"],
|
|
244
|
+
["receiptWrite", "ok"],
|
|
245
|
+
]);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("a nested failure leaves the receipt untouched, and a rerun re-proposes the effect", async () => {
|
|
249
|
+
const { store, writes, values } = memStore();
|
|
250
|
+
let healthy = false;
|
|
251
|
+
const activities = activityMap(store, {
|
|
252
|
+
runSeed: async () => {
|
|
253
|
+
if (!healthy) throw new Error("seed blew up");
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
await expect(runOpLocally(seedOp(), activities, PROFILES)).rejects.toThrow(OpRunFailure);
|
|
258
|
+
expect(writes).toEqual([]);
|
|
259
|
+
expect(values.has("seeded")).toBe(false);
|
|
260
|
+
|
|
261
|
+
// The receipt is stale, so the next run runs the effect again — and only
|
|
262
|
+
// then writes.
|
|
263
|
+
healthy = true;
|
|
264
|
+
const rerun = await runOpLocally(seedOp(), activities, PROFILES);
|
|
265
|
+
expect(rerun.ok).toBe(true);
|
|
266
|
+
expect(writes).toEqual([{ name: "seeded", expectation: receiptExpectation(seeded) }]);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("the failed run records the write as skipped, not run", async () => {
|
|
270
|
+
const { store } = memStore();
|
|
271
|
+
const activities = activityMap(store, {
|
|
272
|
+
runSeed: async () => {
|
|
273
|
+
throw new Error("boom");
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
const failure = await runOpLocally(seedOp(), activities, PROFILES).catch((e: OpRunFailure) => e);
|
|
277
|
+
expect(failure).toBeInstanceOf(OpRunFailure);
|
|
278
|
+
expect((failure as OpRunFailure).result.records.map((r) => [r.fn, r.status])).toEqual([
|
|
279
|
+
["receiptRead", "ok"],
|
|
280
|
+
["runSeed", "fail"],
|
|
281
|
+
["receiptWrite", "skipped"],
|
|
282
|
+
]);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("a gate nested in an effect step is rejected up front, like any other gate", () => {
|
|
286
|
+
const config: OpConfig = {
|
|
287
|
+
name: "gated",
|
|
288
|
+
overview: "",
|
|
289
|
+
phases: [
|
|
290
|
+
phase("Seed", [effect(seeded, [{ kind: "gate", signalName: "approve-seed" }, step("runSeed")])]),
|
|
291
|
+
],
|
|
292
|
+
};
|
|
293
|
+
expect(findGate(config)?.signalName).toBe("approve-seed");
|
|
294
|
+
const { store } = memStore();
|
|
295
|
+
return expect(
|
|
296
|
+
runOpLocally(config, activityMap(store, { runSeed: async () => {} }), PROFILES),
|
|
297
|
+
).rejects.toThrow(LocalGateUnsupportedError);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("an effect step in a parallel phase is refused — read-compare-run-write is ordered", async () => {
|
|
301
|
+
const config: OpConfig = {
|
|
302
|
+
name: "par",
|
|
303
|
+
overview: "",
|
|
304
|
+
phases: [phase("P", [effect(seeded, [step("runSeed")])], { parallel: true })],
|
|
305
|
+
};
|
|
306
|
+
const { store } = memStore();
|
|
307
|
+
await expect(
|
|
308
|
+
runOpLocally(config, activityMap(store, { runSeed: async () => {} }), PROFILES),
|
|
309
|
+
).rejects.toThrow(/parallel phase/);
|
|
310
|
+
});
|
|
311
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `generateOpsPipeline` (#927) — generate mode's Op counterpart to
|
|
3
|
+
* `../components/cli-support.ts`'s `generateComponentsPipeline`. Mirrors that
|
|
4
|
+
* module's own test style (`cli-support.test.ts`): a minimal mocked lexicon
|
|
5
|
+
* plugin satisfying `isLexiconPlugin`, real `discoverOps()` resolution
|
|
6
|
+
* against this repo's actual `*.op.ts` fixtures (`examples/alb-deploy.op.ts`
|
|
7
|
+
* — see `./discover.test.ts`) so Op-name validation exercises the real
|
|
8
|
+
* discovery path rather than a stub.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, test, expect, vi } from "vitest";
|
|
12
|
+
import { generateOpsPipeline } from "./generate-pipeline";
|
|
13
|
+
import type { ScheduledOpSpec, OpPipelineResult } from "../lexicon";
|
|
14
|
+
|
|
15
|
+
vi.mock("@intentius/chant-lexicon-gitlab", () => ({
|
|
16
|
+
gitlab: {
|
|
17
|
+
name: "gitlab",
|
|
18
|
+
serializer: { name: "gitlab", rulePrefix: "GL", serialize: () => "" },
|
|
19
|
+
generate: () => {},
|
|
20
|
+
validate: () => [],
|
|
21
|
+
coverage: () => ({ total: 0, covered: 0 }),
|
|
22
|
+
package: () => "gitlab",
|
|
23
|
+
generateOpPipeline: (ops: ScheduledOpSpec[]): OpPipelineResult => ({
|
|
24
|
+
files: ops.map((o) => ({ name: `${o.name}.yml`, yaml: `# ${o.name} @ ${o.schedule}` })),
|
|
25
|
+
jobs: ops.map((o) => ({ jobName: o.name, op: o.name, schedule: o.schedule, findingMode: o.findingMode ?? "report" })),
|
|
26
|
+
}),
|
|
27
|
+
},
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
describe("generateOpsPipeline", () => {
|
|
31
|
+
test("errors when the target lexicon has no generateOpPipeline", async () => {
|
|
32
|
+
const result = await generateOpsPipeline([{ name: "alb-deploy", schedule: "0 6 * * *" }], "aws");
|
|
33
|
+
expect(result.success).toBe(false);
|
|
34
|
+
expect(result.error).toMatch(/does not support Op generate mode/);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("errors on an Op name that isn't discovered", async () => {
|
|
38
|
+
const result = await generateOpsPipeline(
|
|
39
|
+
[{ name: "definitely-not-a-real-op", schedule: "0 6 * * *" }],
|
|
40
|
+
"gitlab",
|
|
41
|
+
);
|
|
42
|
+
expect(result.success).toBe(false);
|
|
43
|
+
expect(result.error).toMatch(/Unknown Op\(s\): definitely-not-a-real-op/);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("validates every named Op and delegates to the lexicon plugin", async () => {
|
|
47
|
+
const specs: ScheduledOpSpec[] = [{ name: "alb-deploy", schedule: "0 6 * * *", findingMode: "issue" }];
|
|
48
|
+
const result = await generateOpsPipeline(specs, "gitlab");
|
|
49
|
+
expect(result.success).toBe(true);
|
|
50
|
+
expect(result.files).toEqual([{ name: "alb-deploy.yml", yaml: "# alb-deploy @ 0 6 * * *" }]);
|
|
51
|
+
expect(result.jobs).toEqual([{ jobName: "alb-deploy", op: "alb-deploy", schedule: "0 6 * * *", findingMode: "issue" }]);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate mode's Op counterpart (#927): synthesize scheduled CI for
|
|
3
|
+
* stateless Ops — the cron-triggered sibling of `generateComponentsPipeline`
|
|
4
|
+
* (../components/cli-support.ts, #563/#688). A component pipeline triggers a
|
|
5
|
+
* deploy-time graph on `workflow_dispatch`; this triggers already-built Ops
|
|
6
|
+
* on a cron, each via `chant run <name>` on the local executor — the same
|
|
7
|
+
* one-shot invocation documented at docs/src/content/docs/guide/ops.mdx. Core
|
|
8
|
+
* owns only Op discovery, so a typo'd name fails fast before any CI is
|
|
9
|
+
* generated; the CI-specific synthesis is contributed by the target
|
|
10
|
+
* lexicon's `generateOpPipeline` (../lexicon.ts), exactly like
|
|
11
|
+
* `generateComponentPipeline`.
|
|
12
|
+
*
|
|
13
|
+
* There is no dependency graph between scheduled Ops the way there is
|
|
14
|
+
* between components — `Op.depends` orders phases within one run, not
|
|
15
|
+
* separately scheduled cron triggers — so this does not call
|
|
16
|
+
* `resolveComponentGraph`; each `ScheduledOpSpec` is generated independently.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { discoverOps } from "./discover";
|
|
20
|
+
import {
|
|
21
|
+
isLexiconPlugin,
|
|
22
|
+
type LexiconPlugin,
|
|
23
|
+
type ComponentPipelineOptions,
|
|
24
|
+
type ScheduledOpSpec,
|
|
25
|
+
type OpPipelineFile,
|
|
26
|
+
type OpPipelineJob,
|
|
27
|
+
} from "../lexicon";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Load a lexicon's plugin (`@intentius/chant-lexicon-<name>`) to reach its
|
|
31
|
+
* `generateOpPipeline`. Tolerant: returns null when the package can't be
|
|
32
|
+
* resolved. Kept local (rather than imported from
|
|
33
|
+
* `../components/cli-support.ts`) so this module carries no dependency on
|
|
34
|
+
* `components/`.
|
|
35
|
+
*/
|
|
36
|
+
async function loadLexiconPlugin(name: string): Promise<LexiconPlugin | null> {
|
|
37
|
+
let mod: Record<string, unknown>;
|
|
38
|
+
try {
|
|
39
|
+
mod = (await import(`@intentius/chant-lexicon-${name}`)) as Record<string, unknown>;
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
for (const value of Object.values(mod)) {
|
|
44
|
+
if (isLexiconPlugin(value)) return value;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Result of generating scheduled CI for a set of Ops (generate mode's Op counterpart, #927). */
|
|
50
|
+
export interface GenerateOpsPipelineResult {
|
|
51
|
+
success: boolean;
|
|
52
|
+
/** One generated CI file per scheduled Op, when `success` is true. */
|
|
53
|
+
files?: OpPipelineFile[];
|
|
54
|
+
/** Every generated job, for a machine-readable view. */
|
|
55
|
+
jobs?: OpPipelineJob[];
|
|
56
|
+
error?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Synthesize scheduled CI for `ops` targeting `lexicon`: validate every named
|
|
61
|
+
* Op exists (`discoverOps`, under the git root — mirrors `chant run <name>`'s
|
|
62
|
+
* own resolution), then dispatch to the lexicon's `generateOpPipeline`. No
|
|
63
|
+
* deploy/audit logic is inlined into the generated YAML — the finding-mode
|
|
64
|
+
* behavior lives in the Op's own activity args, set by the composite that
|
|
65
|
+
* built it (`WorkflowAuditOp`, `PipelineAuditOp`, `ReconcileOp`, …); this
|
|
66
|
+
* function only wires the cron trigger and the token/permission surface the
|
|
67
|
+
* declared `findingMode` needs.
|
|
68
|
+
*/
|
|
69
|
+
export async function generateOpsPipeline(
|
|
70
|
+
ops: ScheduledOpSpec[],
|
|
71
|
+
lexicon: string,
|
|
72
|
+
options?: ComponentPipelineOptions,
|
|
73
|
+
cwd?: string,
|
|
74
|
+
): Promise<GenerateOpsPipelineResult> {
|
|
75
|
+
const plugin = await loadLexiconPlugin(lexicon);
|
|
76
|
+
if (!plugin?.generateOpPipeline) {
|
|
77
|
+
return {
|
|
78
|
+
success: false,
|
|
79
|
+
error: `Lexicon "${lexicon}" does not support Op generate mode (no generateOpPipeline). GitLab, GitHub, and Forgejo are supported today.`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const discovered = await discoverOps({ cwd });
|
|
84
|
+
if (discovered.errors.length > 0) {
|
|
85
|
+
return { success: false, error: discovered.errors.join("\n") };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const unknown = ops.filter((spec) => !discovered.ops.has(spec.name));
|
|
89
|
+
if (unknown.length > 0) {
|
|
90
|
+
const known = [...discovered.ops.keys()].sort().join(", ");
|
|
91
|
+
return {
|
|
92
|
+
success: false,
|
|
93
|
+
error: `Unknown Op(s): ${unknown.map((s) => s.name).join(", ")}.${known ? ` Known Ops: ${known}` : " No Ops discovered."}`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const { files, jobs } = plugin.generateOpPipeline(ops, options);
|
|
98
|
+
return { success: true, files, jobs };
|
|
99
|
+
}
|
package/src/op/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
export { Op, phase, activity, gate, build, kubectlApply, helmInstall, waitForStack, waitForReady,
|
|
2
|
-
gitlabPipeline, lifecycleSnapshot, shell, teardown, envTeardown, k3dUp, k3dDown,
|
|
1
|
+
export { Op, phase, activity, gate, effect, build, kubectlApply, helmInstall, helmInstallPinned, waitForStack, waitForReady,
|
|
2
|
+
gitlabPipeline, lifecycleSnapshot, shell, ensureSecret, teardown, envTeardown, k3dUp, k3dDown,
|
|
3
|
+
k3sInstall, k3sUninstall, flociUp, flociDown,
|
|
3
4
|
flociAzUp, flociAzDown, flociGcpUp, flociGcpDown, httpCheck,
|
|
4
5
|
azGroupEnsure, azGroupDelete, azApply, azDelete, awsApply, awsDelete, gcpApply, gcpDelete, policyGate,
|
|
5
6
|
spriteCreate, spriteExec, spriteCheckpoint, spriteRestore, listCheckpoints, spriteDestroy,
|
|
@@ -13,11 +14,47 @@ export { emulatorLifecycle, emulatorsOf, endpointEnvVars } from "./emulator-life
|
|
|
13
14
|
export type { EmulatorSpec, EmulatorCapability, EmulatorDeclaration, EmulatorUpArgs, EmulatorLifecycle } from "./emulator-lifecycle";
|
|
14
15
|
export { checkFreshness, compare, formatResult, latestRelease, parseVersion, unpinned } from "./emulator-freshness";
|
|
15
16
|
export type { FreshnessResult } from "./emulator-freshness";
|
|
16
|
-
export type { OpConfig, PhaseDefinition, StepDefinition, ActivityStep, GateStep } from "./types";
|
|
17
|
+
export type { OpConfig, PhaseDefinition, StepDefinition, ActivityStep, GateStep, EffectStep } from "./types";
|
|
18
|
+
export { receiptActivities, receiptCheckInput } from "./receipt-store";
|
|
19
|
+
export type {
|
|
20
|
+
ReceiptStore, EffectReceiptRef, ReceiptCheckInput, ReceiptActivities, ReceiptActivityOptions,
|
|
21
|
+
ReceiptReadArgs, ReceiptReadResult, ReceiptWriteArgs,
|
|
22
|
+
ReceiptStalenessArgs, ReceiptStalenessResult, ReceiptStaleFinding,
|
|
23
|
+
} from "./receipt-store";
|
|
17
24
|
export { discoverOps } from "./discover";
|
|
18
25
|
export type { DiscoveredOp, OpDiscoveryResult } from "./discover";
|
|
26
|
+
export { generateOpsPipeline } from "./generate-pipeline";
|
|
27
|
+
export type { GenerateOpsPipelineResult } from "./generate-pipeline";
|
|
19
28
|
export { loadActivities, loadProfiles, resolveActivity } from "./activity-registry";
|
|
20
29
|
export type { ActivityFn, ActivityProfile } from "./activity-registry";
|
|
21
30
|
export { runOpLocally, parseDuration, findGate, LocalGateUnsupportedError, OpRunFailure } from "./local-executor";
|
|
22
31
|
export type { StepRecord, OpRunResult } from "./local-executor";
|
|
23
32
|
export { renderHuman, renderJson } from "./local-output";
|
|
33
|
+
export {
|
|
34
|
+
activityContract, isActivityContract, collectActivityContracts, validateActivitySteps, KNOWN_ACTIVITY_PROFILES,
|
|
35
|
+
pathExistsInSchema,
|
|
36
|
+
} from "./activity-contract";
|
|
37
|
+
export type { ActivityContract, ActivityContractIssue } from "./activity-contract";
|
|
38
|
+
export { stepOutput, isStepOutputRef, collectStepOutputRefs, validateStepOutputRefs, validateStepOutputRefScope, makeOutProxy } from "./step-output-ref";
|
|
39
|
+
export type { StepOutputRef, WithStepRefs } from "./step-output-ref";
|
|
40
|
+
export type { NamedActivityStep } from "./builders";
|
|
41
|
+
export {
|
|
42
|
+
eq, neq, gt, gte, lt, lte, truthy, falsy, allOf, anyOf,
|
|
43
|
+
evaluatePredicate, isWellFormedPredicate, predicateReferencesField,
|
|
44
|
+
run, report,
|
|
45
|
+
when, duplicateRuleIds,
|
|
46
|
+
DEFAULT_FLAP_THRESHOLD,
|
|
47
|
+
} from "./converge-rule";
|
|
48
|
+
export type {
|
|
49
|
+
FieldComparisonOp, FieldTruthinessOp,
|
|
50
|
+
FieldComparisonPredicate, FieldTruthinessPredicate, AllOfPredicate, AnyOfPredicate, SymptomPredicate,
|
|
51
|
+
RunAction, ReportAction, RuleAction, ConvergeRule,
|
|
52
|
+
} from "./converge-rule";
|
|
53
|
+
export {
|
|
54
|
+
discoverConvergeOps, runOperatorRound, runOperatorForever, formatRoundLine,
|
|
55
|
+
DEFAULT_OPERATOR_INTERVAL_MS,
|
|
56
|
+
} from "./operator";
|
|
57
|
+
export type { OperatorTickEvent, OperatorRoundOptions, OperatorLoopOptions } from "./operator";
|
|
58
|
+
export { classifyOpVerbClass, isGated } from "./op-verb-class";
|
|
59
|
+
export type { OpVerbClass } from "./op-verb-class";
|
|
60
|
+
export { takeProfileAndId } from "./builders";
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
LocalGateUnsupportedError,
|
|
9
9
|
OpRunFailure,
|
|
10
10
|
} from "./local-executor";
|
|
11
|
+
import { stepOutput } from "./step-output-ref";
|
|
11
12
|
|
|
12
13
|
// Fast profiles so retry/timeout tests run in milliseconds.
|
|
13
14
|
const PROFILES: Record<string, ActivityProfile> = {
|
|
@@ -261,3 +262,94 @@ describe("runOpLocally — gate rejection", () => {
|
|
|
261
262
|
expect(gated?.signalName).toBe("g");
|
|
262
263
|
});
|
|
263
264
|
});
|
|
265
|
+
|
|
266
|
+
describe("runOpLocally — step-output references (#1290)", () => {
|
|
267
|
+
test("a later step receives the producer's resolved value, not the placeholder", async () => {
|
|
268
|
+
const received: unknown[] = [];
|
|
269
|
+
const activities = new Map<string, ActivityFn>([
|
|
270
|
+
["produce", async () => ({ driftedStacks: ["a", "b"], count: 2 })],
|
|
271
|
+
["consume", async (args) => { received.push(args); }],
|
|
272
|
+
]);
|
|
273
|
+
const config = op({
|
|
274
|
+
phases: [{ name: "P", steps: [
|
|
275
|
+
{ kind: "activity", fn: "produce", id: "diff" },
|
|
276
|
+
{ kind: "activity", fn: "consume", args: { stacks: stepOutput("diff", "driftedStacks") } },
|
|
277
|
+
] }],
|
|
278
|
+
});
|
|
279
|
+
const result = await runOpLocally(config, activities, PROFILES);
|
|
280
|
+
expect(result.ok).toBe(true);
|
|
281
|
+
expect(received).toEqual([{ stacks: ["a", "b"] }]);
|
|
282
|
+
// The recorded step also shows the resolved value, not the raw ref object.
|
|
283
|
+
expect(result.records[1].args).toEqual({ stacks: ["a", "b"] });
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("a whole-value reference (no path) passes the entire producer result", async () => {
|
|
287
|
+
const received: unknown[] = [];
|
|
288
|
+
const activities = new Map<string, ActivityFn>([
|
|
289
|
+
["produce", async () => ({ ok: true, detail: "x" })],
|
|
290
|
+
["consume", async (args) => { received.push(args); }],
|
|
291
|
+
]);
|
|
292
|
+
const config = op({
|
|
293
|
+
phases: [{ name: "P", steps: [
|
|
294
|
+
{ kind: "activity", fn: "produce", id: "p" },
|
|
295
|
+
{ kind: "activity", fn: "consume", args: { result: stepOutput("p") } },
|
|
296
|
+
] }],
|
|
297
|
+
});
|
|
298
|
+
await runOpLocally(config, activities, PROFILES);
|
|
299
|
+
expect(received).toEqual([{ result: { ok: true, detail: "x" } }]);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("resolves a reference nested inside an object and an array", async () => {
|
|
303
|
+
const received: unknown[] = [];
|
|
304
|
+
const activities = new Map<string, ActivityFn>([
|
|
305
|
+
["produce", async () => ({ name: "prod" })],
|
|
306
|
+
["consume", async (args) => { received.push(args); }],
|
|
307
|
+
]);
|
|
308
|
+
const config = op({
|
|
309
|
+
phases: [{ name: "P", steps: [
|
|
310
|
+
{ kind: "activity", fn: "produce", id: "p" },
|
|
311
|
+
{
|
|
312
|
+
kind: "activity",
|
|
313
|
+
fn: "consume",
|
|
314
|
+
args: { config: { tags: ["static", stepOutput("p", "name")] } },
|
|
315
|
+
},
|
|
316
|
+
] }],
|
|
317
|
+
});
|
|
318
|
+
await runOpLocally(config, activities, PROFILES);
|
|
319
|
+
expect(received).toEqual([{ config: { tags: ["static", "prod"] } }]);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test("an unresolvable path resolves to undefined instead of throwing", async () => {
|
|
323
|
+
const received: unknown[] = [];
|
|
324
|
+
const activities = new Map<string, ActivityFn>([
|
|
325
|
+
["produce", async () => ({})],
|
|
326
|
+
["consume", async (args) => { received.push(args); }],
|
|
327
|
+
]);
|
|
328
|
+
const config = op({
|
|
329
|
+
phases: [{ name: "P", steps: [
|
|
330
|
+
{ kind: "activity", fn: "produce", id: "p" },
|
|
331
|
+
{ kind: "activity", fn: "consume", args: { v: stepOutput("p", "a.b") } },
|
|
332
|
+
] }],
|
|
333
|
+
});
|
|
334
|
+
const result = await runOpLocally(config, activities, PROFILES);
|
|
335
|
+
expect(result.ok).toBe(true);
|
|
336
|
+
expect(received).toEqual([{ v: undefined }]);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("e2e: chant run (non-temporal) end to end — producer object flows to consumer", async () => {
|
|
340
|
+
const activities = new Map<string, ActivityFn>([
|
|
341
|
+
["lifecycleDiff", async () => ({ driftedStacks: ["stack-a"], drifted: true })],
|
|
342
|
+
["applyStacks", async (args) => ({ applied: args.stacks })],
|
|
343
|
+
]);
|
|
344
|
+
const config = op({
|
|
345
|
+
phases: [
|
|
346
|
+
{ name: "Diff", steps: [{ kind: "activity", fn: "lifecycleDiff", id: "diff", args: { env: "prod" } }] },
|
|
347
|
+
{ name: "Apply", steps: [{ kind: "activity", fn: "applyStacks", args: { stacks: stepOutput("diff", "driftedStacks") } }] },
|
|
348
|
+
],
|
|
349
|
+
});
|
|
350
|
+
const result = await runOpLocally(config, activities, PROFILES);
|
|
351
|
+
expect(result.ok).toBe(true);
|
|
352
|
+
expect(result.records.every((r) => r.status === "ok")).toBe(true);
|
|
353
|
+
expect(result.records[1].args).toEqual({ stacks: ["stack-a"] });
|
|
354
|
+
});
|
|
355
|
+
});
|