@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,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pipeline-change gate class (#1569): changes to the pipeline itself
|
|
3
|
+
* route to a stricter gate.
|
|
4
|
+
*
|
|
5
|
+
* The honest concession in the PR-flow teardown (rubric-product-research,
|
|
6
|
+
* 06-objections §1): chant Ops have a `shell` step, so a governed plane is
|
|
7
|
+
* not automatically immune to "the change edits the machinery that applies
|
|
8
|
+
* changes." What chant *can* claim — and a plain PR flow structurally cannot
|
|
9
|
+
* — is that pipeline changes are distinguishable: a composition is data over
|
|
10
|
+
* a bounded verb registry, and the COMP rules already police the escape
|
|
11
|
+
* hatches (COMP006 — every `shell` step carries a declared reason;
|
|
12
|
+
* COMP005 — no noun-shaped escape hatches, `./rules/comp/`). What was missing
|
|
13
|
+
* was the routing: nothing classified a change as "touches the pipeline" and
|
|
14
|
+
* required a stricter gate for it, so a change to a composition rode through
|
|
15
|
+
* the same gate as a replica-count bump.
|
|
16
|
+
*
|
|
17
|
+
* This is the cheap first cut #1569 asks for: the registry-only predicate.
|
|
18
|
+
* A composition that contains only registry verbs is ordinary; a `shell`
|
|
19
|
+
* step (the deliberate escape hatch) or a change to the set of verbs a
|
|
20
|
+
* composition invokes trips the stricter class. comp005/comp006 already make
|
|
21
|
+
* both decidable from the text — this module reuses the same walk
|
|
22
|
+
* (`./rules/comp/support.ts`) rather than re-deriving it.
|
|
23
|
+
*
|
|
24
|
+
* Like #1568's `evaluateUnobservedGate`
|
|
25
|
+
* (../lifecycle/unobserved-gate.ts), this is a reusable predicate, not a
|
|
26
|
+
* runtime: classify a change and get back a verdict; the caller (a
|
|
27
|
+
* `lint.policies` check routed through `policyGate`, an Op step, #1487's
|
|
28
|
+
* gate-as-fact once it lands) decides what "stricter" means in its own
|
|
29
|
+
* context. The stricter class composes with #1487's principle that
|
|
30
|
+
* destructive verbs always wait for a person — pipeline changes are the
|
|
31
|
+
* second category that should never free-run, because they change what
|
|
32
|
+
* "gated" means for everything after them. That is why the default policy
|
|
33
|
+
* below is `"human-always"`, not `"stricter-gate"`.
|
|
34
|
+
*/
|
|
35
|
+
import type { Component } from "../components/component";
|
|
36
|
+
import { STARTER_VERB_FAMILIES } from "../components/starter-plugin";
|
|
37
|
+
import { walkComponent } from "./rules/comp/support";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Fallback registry when no project registry is supplied: core's own starter
|
|
41
|
+
* verbs, the same fallback COMP005 uses for a direct unit test
|
|
42
|
+
* (`./rules/comp/comp005-capability-kind-is-noun.ts`). A real `chant lint` /
|
|
43
|
+
* gate run passes the project's resolved `knownKinds` (core's starter set
|
|
44
|
+
* plus the active lexicons' leaves) so a real project's cloud verbs are never
|
|
45
|
+
* mistaken for an out-of-registry escape hatch.
|
|
46
|
+
*/
|
|
47
|
+
const CORE_STARTER_KINDS: ReadonlySet<string> = new Set(Object.values(STARTER_VERB_FAMILIES).flat());
|
|
48
|
+
|
|
49
|
+
/** Why a composition (or a change to one) tripped the pipeline-change class. */
|
|
50
|
+
export type PipelineChangeReason =
|
|
51
|
+
| { kind: "shell-step"; phaseName: string }
|
|
52
|
+
| { kind: "unregistered-verb"; stepKind: string; phaseName: string }
|
|
53
|
+
| { kind: "verb-set-changed"; added: string[]; removed: string[] };
|
|
54
|
+
|
|
55
|
+
export interface PipelineChangeClassification {
|
|
56
|
+
/** True when any reason below applies. */
|
|
57
|
+
pipelineChange: boolean;
|
|
58
|
+
reasons: PipelineChangeReason[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Every non-gate step kind a component's composition invokes, across `deploy` and `rollback` (nested fan-out phases included, via `walkComponent`). `gate` steps are excluded — they are not a capability verb. */
|
|
62
|
+
export function componentVerbSet(component: Component): Set<string> {
|
|
63
|
+
const { steps } = walkComponent(component);
|
|
64
|
+
return new Set(steps.map((s) => s.step.kind));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Classify a single component's composition (the "cheap first cut", no
|
|
69
|
+
* before/after needed): a `shell` step trips the class unconditionally — it
|
|
70
|
+
* is the escape hatch by construction (COMP006) — and any step whose `kind`
|
|
71
|
+
* is outside `knownKinds` trips it too, since an unregistered verb is,
|
|
72
|
+
* structurally, the same kind of hole a `shell` step is: something the
|
|
73
|
+
* bounded registry does not account for.
|
|
74
|
+
*/
|
|
75
|
+
export function classifyComponentPipelineChange(
|
|
76
|
+
component: Component,
|
|
77
|
+
opts?: { knownKinds?: ReadonlySet<string> },
|
|
78
|
+
): PipelineChangeClassification {
|
|
79
|
+
const knownKinds = opts?.knownKinds ?? CORE_STARTER_KINDS;
|
|
80
|
+
const { steps } = walkComponent(component);
|
|
81
|
+
|
|
82
|
+
const reasons: PipelineChangeReason[] = [];
|
|
83
|
+
for (const { step, phaseName } of steps) {
|
|
84
|
+
if (step.kind === "shell") {
|
|
85
|
+
reasons.push({ kind: "shell-step", phaseName });
|
|
86
|
+
} else if (!knownKinds.has(step.kind)) {
|
|
87
|
+
reasons.push({ kind: "unregistered-verb", stepKind: step.kind, phaseName });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { pipelineChange: reasons.length > 0, reasons };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Classify a *change* to a component's composition between two revisions
|
|
96
|
+
* (#1569's "add or modify a shell step ... or the composition graph
|
|
97
|
+
* itself"): on top of `classifyComponentPipelineChange`'s single-composition
|
|
98
|
+
* check on `after`, a change also trips the class when the verb set itself
|
|
99
|
+
* changed — a verb added or removed changes what the composition can invoke,
|
|
100
|
+
* which is the routing signal even when every individual verb, before and
|
|
101
|
+
* after, is registry-clean. `before: undefined` (a brand-new component) never
|
|
102
|
+
* reports a verb-set change — there is nothing to diff against, and the
|
|
103
|
+
* single-composition check already covers a new component's own shell/
|
|
104
|
+
* unregistered-verb findings.
|
|
105
|
+
*/
|
|
106
|
+
export function classifyPipelineChange(
|
|
107
|
+
before: Component | undefined,
|
|
108
|
+
after: Component,
|
|
109
|
+
opts?: { knownKinds?: ReadonlySet<string> },
|
|
110
|
+
): PipelineChangeClassification {
|
|
111
|
+
const base = classifyComponentPipelineChange(after, opts);
|
|
112
|
+
if (!before) return base;
|
|
113
|
+
|
|
114
|
+
const beforeVerbs = componentVerbSet(before);
|
|
115
|
+
const afterVerbs = componentVerbSet(after);
|
|
116
|
+
const added = [...afterVerbs].filter((v) => !beforeVerbs.has(v)).sort();
|
|
117
|
+
const removed = [...beforeVerbs].filter((v) => !afterVerbs.has(v)).sort();
|
|
118
|
+
|
|
119
|
+
if (added.length === 0 && removed.length === 0) return base;
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
pipelineChange: true,
|
|
123
|
+
reasons: [...base.reasons, { kind: "verb-set-changed", added, removed }],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* How the gate routes a classified pipeline change.
|
|
129
|
+
*
|
|
130
|
+
* - `"human-always"` (the recommended default — see this module's doc
|
|
131
|
+
* comment) — a pipeline change never free-runs, mirroring #1487's "a
|
|
132
|
+
* destructive verb always waits for a person."
|
|
133
|
+
* - `"stricter-gate"` — route to a named stricter gate class rather than
|
|
134
|
+
* requiring a person on every run; the caller supplies what "stricter"
|
|
135
|
+
* means (an additional approval, a narrower blast-radius budget, ...).
|
|
136
|
+
*/
|
|
137
|
+
export type PipelineChangeGatePolicy = "stricter-gate" | "human-always";
|
|
138
|
+
|
|
139
|
+
export interface PipelineChangeGateVerdict extends PipelineChangeClassification {
|
|
140
|
+
/** What the policy says to do about it. `"none"` when the change is not a pipeline change at all. */
|
|
141
|
+
route: PipelineChangeGatePolicy | "none";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Evaluate a component composition change against a routing policy. Pure — no I/O. */
|
|
145
|
+
export function evaluatePipelineChangeGate(
|
|
146
|
+
before: Component | undefined,
|
|
147
|
+
after: Component,
|
|
148
|
+
policy: PipelineChangeGatePolicy = "human-always",
|
|
149
|
+
opts?: { knownKinds?: ReadonlySet<string> },
|
|
150
|
+
): PipelineChangeGateVerdict {
|
|
151
|
+
const classification = classifyPipelineChange(before, after, opts);
|
|
152
|
+
return { ...classification, route: classification.pipelineChange ? policy : "none" };
|
|
153
|
+
}
|
|
@@ -145,3 +145,100 @@ describe("isPostSynthCheck", () => {
|
|
|
145
145
|
// chant #1138 — `applyConfiguredSeverity` (the `lint.rules` severity-override
|
|
146
146
|
// pass over `PostSynthDiagnostic`s) is tested in `./config.test.ts`, where the
|
|
147
147
|
// function itself now lives — see `./config.ts`'s doc comment for why.
|
|
148
|
+
|
|
149
|
+
// ── chant #975 — ctx.docs: lazy, memoized, shared across every check ───────
|
|
150
|
+
describe("ctx.docs (chant #975)", () => {
|
|
151
|
+
/** A Map subclass that counts how many times it was iterated — the only
|
|
152
|
+
* way `parseOutputDocs` reads its outputs — so the tests below can prove
|
|
153
|
+
* "parsed once" without reaching into module internals. */
|
|
154
|
+
class CountingOutputs extends Map<string, string> {
|
|
155
|
+
iterations = 0;
|
|
156
|
+
[Symbol.iterator](): IterableIterator<[string, string]> {
|
|
157
|
+
this.iterations++;
|
|
158
|
+
return super[Symbol.iterator]();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
test("is not computed until first accessed", () => {
|
|
163
|
+
const outputs = new CountingOutputs([["k8s", "kind: Namespace"]]);
|
|
164
|
+
const check: PostSynthCheck = {
|
|
165
|
+
id: "PS-DOCS-1",
|
|
166
|
+
description: "never touches ctx.docs",
|
|
167
|
+
check() {
|
|
168
|
+
return [];
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
runPostSynthChecks([check], createBuildResult({ outputs: outputs as never }));
|
|
172
|
+
expect(outputs.iterations).toBe(0);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("is parsed exactly once even when read by multiple checks", () => {
|
|
176
|
+
const outputs = new CountingOutputs([
|
|
177
|
+
["k8s", "apiVersion: v1\nkind: Namespace\nmetadata:\n name: ns-a"],
|
|
178
|
+
]);
|
|
179
|
+
let seenByFirst: unknown;
|
|
180
|
+
let seenBySecond: unknown;
|
|
181
|
+
const checks: PostSynthCheck[] = [
|
|
182
|
+
{
|
|
183
|
+
id: "PS-DOCS-2A",
|
|
184
|
+
description: "reads ctx.docs once",
|
|
185
|
+
check(ctx) {
|
|
186
|
+
seenByFirst = ctx.docs;
|
|
187
|
+
return [];
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
id: "PS-DOCS-2B",
|
|
192
|
+
description: "reads ctx.docs again, and a second time in the same check",
|
|
193
|
+
check(ctx) {
|
|
194
|
+
seenBySecond = ctx.docs;
|
|
195
|
+
void ctx.docs; // a second read within the same check — still no reparse
|
|
196
|
+
return [];
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
];
|
|
200
|
+
runPostSynthChecks(checks, createBuildResult({ outputs: outputs as never }));
|
|
201
|
+
expect(outputs.iterations).toBe(1);
|
|
202
|
+
// Every reader gets the exact same array instance, not an equal copy.
|
|
203
|
+
expect(seenByFirst).toBe(seenBySecond);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("parses ctx.outputs into the expected OutputDoc shape", () => {
|
|
207
|
+
const outputs = new Map<string, string>([
|
|
208
|
+
["k8s", "apiVersion: v1\nkind: Namespace\nmetadata:\n name: ns-a"],
|
|
209
|
+
]);
|
|
210
|
+
const check: PostSynthCheck = {
|
|
211
|
+
id: "PS-DOCS-3",
|
|
212
|
+
description: "reads a manifest field off ctx.docs",
|
|
213
|
+
check(ctx) {
|
|
214
|
+
const doc = (ctx.docs ?? [])[0];
|
|
215
|
+
return [
|
|
216
|
+
{
|
|
217
|
+
checkId: "PS-DOCS-3",
|
|
218
|
+
severity: "info",
|
|
219
|
+
message: `kind=${(doc.value as { kind?: string }).kind}`,
|
|
220
|
+
},
|
|
221
|
+
];
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
const diags = runPostSynthChecks([check], createBuildResult({ outputs: outputs as never }));
|
|
225
|
+
expect(diags[0].message).toBe("kind=Namespace");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("two independent runPostSynthChecks calls each get their own cache", () => {
|
|
229
|
+
const outputsA = new CountingOutputs([["k8s", "kind: Namespace"]]);
|
|
230
|
+
const outputsB = new CountingOutputs([["k8s", "kind: Deployment"]]);
|
|
231
|
+
const reader: PostSynthCheck = {
|
|
232
|
+
id: "PS-DOCS-4",
|
|
233
|
+
description: "reads ctx.docs",
|
|
234
|
+
check(ctx) {
|
|
235
|
+
void ctx.docs;
|
|
236
|
+
return [];
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
runPostSynthChecks([reader], createBuildResult({ outputs: outputsA as never }));
|
|
240
|
+
runPostSynthChecks([reader], createBuildResult({ outputs: outputsB as never }));
|
|
241
|
+
expect(outputsA.iterations).toBe(1);
|
|
242
|
+
expect(outputsB.iterations).toBe(1);
|
|
243
|
+
});
|
|
244
|
+
});
|
package/src/lint/post-synth.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { Declarable } from "../declarable";
|
|
2
2
|
import type { SerializerResult } from "../serializer";
|
|
3
3
|
import type { Severity } from "./rule";
|
|
4
|
+
import { parseOutputDocs, type OutputDoc } from "./output-docs";
|
|
5
|
+
|
|
6
|
+
export { parseOutputDocs, pick, get, type OutputDoc } from "./output-docs";
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
9
|
* Context provided to post-synthesis checks.
|
|
@@ -16,6 +19,25 @@ export interface PostSynthContext {
|
|
|
16
19
|
* e.g. "no public buckets in prod". Undefined when no environment is set.
|
|
17
20
|
*/
|
|
18
21
|
env?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Parsed output documents (chant #975) — `ctx.outputs` run through
|
|
24
|
+
* `parseOutputDocs` once and cached. A lazy `readonly` getter, not a plain
|
|
25
|
+
* field: computed on first access and shared across every check in the
|
|
26
|
+
* run, so a check that only reads `entities` pays nothing, and no two
|
|
27
|
+
* checks re-parse the same YAML/JSON. See `./output-docs.ts`.
|
|
28
|
+
*
|
|
29
|
+
* Optional at the type level — NOT because it can be absent from a real
|
|
30
|
+
* build. Every context chant itself constructs (`runPostSynthChecks`
|
|
31
|
+
* below, `@intentius/chant-test-utils`'s `createPostSynthContext` and
|
|
32
|
+
* `makePostSynthCtx*`) wires it up via `createDocsAccessor` and it is
|
|
33
|
+
* always present there. It is typed optional only so the many lexicon
|
|
34
|
+
* tests that build a `PostSynthContext` object literal by hand (predating
|
|
35
|
+
* this field) keep compiling unchanged, per this issue's own "existing
|
|
36
|
+
* checks compile unchanged" constraint — a new check that wants `ctx.docs`
|
|
37
|
+
* should still get a real array from every context chant builds; guard
|
|
38
|
+
* with `ctx.docs ?? []` only when a context's provenance is unknown.
|
|
39
|
+
*/
|
|
40
|
+
readonly docs?: OutputDoc[];
|
|
19
41
|
/** Raw build result object */
|
|
20
42
|
buildResult: {
|
|
21
43
|
outputs: Map<string, string | SerializerResult>;
|
|
@@ -26,6 +48,25 @@ export interface PostSynthContext {
|
|
|
26
48
|
};
|
|
27
49
|
}
|
|
28
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Build the lazy, memoized `docs` accessor shared by `runPostSynthChecks`
|
|
53
|
+
* (below) and `@intentius/chant-test-utils`'s `createPostSynthContext` — the
|
|
54
|
+
* two places a `PostSynthContext` gets constructed. Returns a zero-arg
|
|
55
|
+
* function suitable for a `get docs()` object-literal accessor; the first
|
|
56
|
+
* call parses, every later call returns the same cached array.
|
|
57
|
+
*/
|
|
58
|
+
export function createDocsAccessor(
|
|
59
|
+
outputs: Map<string, string | SerializerResult>,
|
|
60
|
+
): () => OutputDoc[] {
|
|
61
|
+
let cached: OutputDoc[] | undefined;
|
|
62
|
+
return () => {
|
|
63
|
+
if (cached === undefined) {
|
|
64
|
+
cached = parseOutputDocs(outputs);
|
|
65
|
+
}
|
|
66
|
+
return cached;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
29
70
|
/**
|
|
30
71
|
* Extract the primary content string from a serializer output.
|
|
31
72
|
*/
|
|
@@ -33,6 +74,21 @@ export function getPrimaryOutput(output: string | SerializerResult): string {
|
|
|
33
74
|
return typeof output === "string" ? output : output.primary;
|
|
34
75
|
}
|
|
35
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Extract the ADDITIONAL files from a serializer output — everything
|
|
79
|
+
* {@link getPrimaryOutput} discards.
|
|
80
|
+
*
|
|
81
|
+
* Every post-synth check shipped before this one reads the primary output
|
|
82
|
+
* only, which means a sidecar file (a nested stack template, committed SOPS
|
|
83
|
+
* ciphertext) is invisible to all of them. That is the right default — the
|
|
84
|
+
* primary output is what appliers read — but a rule ABOUT a sidecar has to
|
|
85
|
+
* be able to see it, and `PostSynthContext.outputs` has carried the data all
|
|
86
|
+
* along. WK8504 (k8s) is the first caller.
|
|
87
|
+
*/
|
|
88
|
+
export function getAdditionalFiles(output: string | SerializerResult): Record<string, string> {
|
|
89
|
+
return typeof output === "string" ? {} : (output.files ?? {});
|
|
90
|
+
}
|
|
91
|
+
|
|
36
92
|
/**
|
|
37
93
|
* A diagnostic from a post-synthesis check.
|
|
38
94
|
*
|
|
@@ -100,11 +156,15 @@ export function runPostSynthChecks(
|
|
|
100
156
|
buildResult: PostSynthContext["buildResult"],
|
|
101
157
|
env?: string,
|
|
102
158
|
): PostSynthDiagnostic[] {
|
|
159
|
+
const getDocs = createDocsAccessor(buildResult.outputs);
|
|
103
160
|
const ctx: PostSynthContext = {
|
|
104
161
|
outputs: buildResult.outputs,
|
|
105
162
|
entities: buildResult.entities,
|
|
106
163
|
env,
|
|
107
164
|
buildResult,
|
|
165
|
+
get docs(): OutputDoc[] {
|
|
166
|
+
return getDocs();
|
|
167
|
+
},
|
|
108
168
|
};
|
|
109
169
|
|
|
110
170
|
const diagnostics: PostSynthDiagnostic[] = [];
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { coreReceiptChecks, RECEIPT_PLAIN_STORE_CHECK_ID } from "./receipt-checks";
|
|
3
|
+
import { runPostSynthChecks } from "./post-synth";
|
|
4
|
+
import { EffectReceipt, EFFECT_RECEIPT_MARKER } from "../effect-receipt";
|
|
5
|
+
import { DECLARABLE_MARKER, type Declarable } from "../declarable";
|
|
6
|
+
|
|
7
|
+
/** A lexicon-materialized receipt row: its own entityType, the receipt marker. */
|
|
8
|
+
function materializedReceipt(opts: {
|
|
9
|
+
lexicon: string;
|
|
10
|
+
entityType: string;
|
|
11
|
+
props?: Record<string, unknown>;
|
|
12
|
+
}): Declarable {
|
|
13
|
+
return {
|
|
14
|
+
[DECLARABLE_MARKER]: true,
|
|
15
|
+
[EFFECT_RECEIPT_MARKER]: true,
|
|
16
|
+
lexicon: opts.lexicon,
|
|
17
|
+
entityType: opts.entityType,
|
|
18
|
+
...(opts.props ? { props: opts.props } : {}),
|
|
19
|
+
} as unknown as Declarable;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function run(entities: Map<string, Declarable>) {
|
|
23
|
+
return runPostSynthChecks(coreReceiptChecks(), {
|
|
24
|
+
outputs: new Map(),
|
|
25
|
+
entities,
|
|
26
|
+
warnings: [],
|
|
27
|
+
errors: [],
|
|
28
|
+
sourceFileCount: 1,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("COR023: receipt materializes into a plain store (#1833)", () => {
|
|
33
|
+
test("check id", () => {
|
|
34
|
+
expect(RECEIPT_PLAIN_STORE_CHECK_ID).toBe("COR023");
|
|
35
|
+
expect(coreReceiptChecks().map((c) => c.id)).toContain("COR023");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("fires when a receipt is materialized into a Secret kind", () => {
|
|
39
|
+
const entities = new Map<string, Declarable>([
|
|
40
|
+
["seededReceipt", materializedReceipt({ lexicon: "k8s", entityType: "K8s::Core::Secret" })],
|
|
41
|
+
]);
|
|
42
|
+
const diags = run(entities);
|
|
43
|
+
expect(diags).toHaveLength(1);
|
|
44
|
+
expect(diags[0].checkId).toBe("COR023");
|
|
45
|
+
expect(diags[0].severity).toBe("error");
|
|
46
|
+
expect(diags[0].entity).toBe("seededReceipt");
|
|
47
|
+
expect(diags[0].message).toContain("K8s::Core::Secret");
|
|
48
|
+
expect(diags[0].message).toContain("plain store");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("fires when a plain kind selects a secret-capable variant (SSM SecureString)", () => {
|
|
52
|
+
const entities = new Map<string, Declarable>([
|
|
53
|
+
[
|
|
54
|
+
"migratedReceipt",
|
|
55
|
+
materializedReceipt({
|
|
56
|
+
lexicon: "aws",
|
|
57
|
+
entityType: "AWS::SSM::Parameter",
|
|
58
|
+
props: { Name: "/receipts/migrated", Type: "SecureString" },
|
|
59
|
+
}),
|
|
60
|
+
],
|
|
61
|
+
]);
|
|
62
|
+
const diags = run(entities);
|
|
63
|
+
expect(diags).toHaveLength(1);
|
|
64
|
+
expect(diags[0].message).toContain('Type: "SecureString"');
|
|
65
|
+
expect(diags[0].entity).toBe("migratedReceipt");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("passes: a receipt materialized into a plain store", () => {
|
|
69
|
+
const entities = new Map<string, Declarable>([
|
|
70
|
+
[
|
|
71
|
+
"migratedReceipt",
|
|
72
|
+
materializedReceipt({
|
|
73
|
+
lexicon: "aws",
|
|
74
|
+
entityType: "AWS::SSM::Parameter",
|
|
75
|
+
props: { Name: "/receipts/migrated", Type: "String" },
|
|
76
|
+
}),
|
|
77
|
+
],
|
|
78
|
+
]);
|
|
79
|
+
expect(run(entities)).toHaveLength(0);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("passes: the core Chant::EffectReceipt declaration itself", () => {
|
|
83
|
+
const receipt = EffectReceipt("seeded", {
|
|
84
|
+
effect: "db-seed",
|
|
85
|
+
flavor: "hash",
|
|
86
|
+
inputs: { schema: "v3" },
|
|
87
|
+
});
|
|
88
|
+
const entities = new Map<string, Declarable>([["seeded", receipt]]);
|
|
89
|
+
expect(run(entities)).toHaveLength(0);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("passes: a Secret-kind entity that is NOT a receipt is not this check's concern", () => {
|
|
93
|
+
const secret: Declarable = {
|
|
94
|
+
[DECLARABLE_MARKER]: true,
|
|
95
|
+
lexicon: "k8s",
|
|
96
|
+
entityType: "K8s::Core::Secret",
|
|
97
|
+
} as unknown as Declarable;
|
|
98
|
+
const entities = new Map<string, Declarable>([["dbSecret", secret]]);
|
|
99
|
+
expect(run(entities)).toHaveLength(0);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { PostSynthCheck, PostSynthDiagnostic } from "./post-synth";
|
|
2
|
+
import { isEffectReceipt } from "../effect-receipt";
|
|
3
|
+
import { isSensitiveKey } from "../deep-observation";
|
|
4
|
+
import { isResourceDeclarable } from "../declarable";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* COR023: Receipt Materializes Into a Plain Store (#1833, epic #1703)
|
|
8
|
+
*
|
|
9
|
+
* A receipt's materialization target must be a plain store — never a
|
|
10
|
+
* SecureString, a Secret, or any secret-capable kind. A receipt value is a
|
|
11
|
+
* witness (an existence marker or a `sha256:` digest,
|
|
12
|
+
* ../effect-receipt.ts), not a secret: parking it in a secret-capable store
|
|
13
|
+
* invites masking, rotation, and access-control semantics that defeat the
|
|
14
|
+
* observe-and-compare loop the receipt exists for, and quietly teaches
|
|
15
|
+
* readers that the value is sensitive when it must never be.
|
|
16
|
+
*
|
|
17
|
+
* This is the honest core half of the guard. Core recognizes receipts
|
|
18
|
+
* lexicon-independently through the marker (#1831) but cannot know what any
|
|
19
|
+
* lexicon's kinds mean, so it checks the two signals it does have, both
|
|
20
|
+
* reusing the secret-kind knowledge core already carries
|
|
21
|
+
* (SENSITIVE_KEY_PATTERNS, ../deep-observation.ts):
|
|
22
|
+
*
|
|
23
|
+
* - the materialized `entityType` names a secret-capable kind
|
|
24
|
+
* (`K8s::Core::Secret`, `AWS::SecretsManager::Secret`, ...);
|
|
25
|
+
* - the declared props select a secret-capable variant of an otherwise
|
|
26
|
+
* plain kind (a `Type`/`Kind` prop whose value matches secret/secure —
|
|
27
|
+
* SSM's `Type: "SecureString"` is the canonical case).
|
|
28
|
+
*
|
|
29
|
+
* The concrete per-kind enforcement is each materialization row's job — the
|
|
30
|
+
* aws row (#1835) pins SSM to plain `String` at the source.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/** Prop names that select a store variant. */
|
|
34
|
+
const VARIANT_PROP = /^(type|kind)$/i;
|
|
35
|
+
/** Variant values that make the store secret-capable. */
|
|
36
|
+
const SECRET_VARIANT = /secret|secure/i;
|
|
37
|
+
|
|
38
|
+
export const RECEIPT_PLAIN_STORE_CHECK_ID = "COR023";
|
|
39
|
+
|
|
40
|
+
const receiptPlainStoreCheck: PostSynthCheck = {
|
|
41
|
+
id: RECEIPT_PLAIN_STORE_CHECK_ID,
|
|
42
|
+
description:
|
|
43
|
+
"An effect receipt's materialization target must be a plain store — never a secret-capable kind or variant",
|
|
44
|
+
check(ctx) {
|
|
45
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
46
|
+
for (const [name, entity] of ctx.entities) {
|
|
47
|
+
if (!isEffectReceipt(entity)) continue;
|
|
48
|
+
|
|
49
|
+
if (isSensitiveKey(entity.entityType)) {
|
|
50
|
+
diagnostics.push({
|
|
51
|
+
checkId: RECEIPT_PLAIN_STORE_CHECK_ID,
|
|
52
|
+
severity: "error",
|
|
53
|
+
entity: name,
|
|
54
|
+
lexicon: entity.lexicon,
|
|
55
|
+
message:
|
|
56
|
+
`Effect receipt "${name}" materializes into secret-capable kind "${entity.entityType}" — ` +
|
|
57
|
+
`a receipt's target must be a plain store. The receipt value is a witness ` +
|
|
58
|
+
`(an existence marker or a sha256 digest), not a secret; a secret-capable store adds ` +
|
|
59
|
+
`masking and rotation semantics that defeat the receipt's observe-and-compare loop.`,
|
|
60
|
+
});
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (isResourceDeclarable(entity) && typeof entity.props === "object" && entity.props !== null) {
|
|
65
|
+
for (const [key, value] of Object.entries(entity.props as Record<string, unknown>)) {
|
|
66
|
+
if (VARIANT_PROP.test(key) && typeof value === "string" && SECRET_VARIANT.test(value)) {
|
|
67
|
+
diagnostics.push({
|
|
68
|
+
checkId: RECEIPT_PLAIN_STORE_CHECK_ID,
|
|
69
|
+
severity: "error",
|
|
70
|
+
entity: name,
|
|
71
|
+
lexicon: entity.lexicon,
|
|
72
|
+
message:
|
|
73
|
+
`Effect receipt "${name}" (${entity.entityType}) declares ${key}: "${value}" — ` +
|
|
74
|
+
`a secret-capable store variant. A receipt's target must be a plain store; ` +
|
|
75
|
+
`use the plain variant (e.g. SSM Type: "String") so the witness value stays ` +
|
|
76
|
+
`readable to the observe-and-compare loop.`,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return diagnostics;
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Core's own post-synth checks over effect receipts, run by `chant build`
|
|
88
|
+
* over the full build result (plugin checks are lexicon-scoped; receipts are
|
|
89
|
+
* recognized by marker, so this set is deliberately not).
|
|
90
|
+
*/
|
|
91
|
+
export function coreReceiptChecks(): PostSynthCheck[] {
|
|
92
|
+
return [receiptPlainStoreCheck];
|
|
93
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Component } from "../../../../../../components/component";
|
|
2
|
+
import { phase } from "../../../../../../components/component";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* COMP003 pass case (#1944): a bare "run-agent" step with no "noRollback"
|
|
6
|
+
* opt-out and no component-level "rollback" — must pass COMP003 because
|
|
7
|
+
* "run-agent"'s rollbackPolicy is "native" (#1941), not "needs-opt-out". This
|
|
8
|
+
* is the regression test proving the registry's declared rollbackPolicy for
|
|
9
|
+
* "run-agent" is wired correctly into ctx.rollbackPolicies, the same seam
|
|
10
|
+
* COMP005 uses for ctx.knownKinds — see comp.test.ts's FIXTURE_ROLLBACK_POLICIES.
|
|
11
|
+
*/
|
|
12
|
+
export const agentTurn: Component = {
|
|
13
|
+
name: "agent-turn",
|
|
14
|
+
archetype: "infra",
|
|
15
|
+
dependsOn: [],
|
|
16
|
+
deploy: [
|
|
17
|
+
phase("Run", [
|
|
18
|
+
{
|
|
19
|
+
kind: "run-agent",
|
|
20
|
+
agent: "claude",
|
|
21
|
+
task: { prompt: "run the migration script and report the result" },
|
|
22
|
+
workspace: {},
|
|
23
|
+
},
|
|
24
|
+
]),
|
|
25
|
+
],
|
|
26
|
+
};
|
|
@@ -23,11 +23,14 @@ const checks = loadComponentChecks();
|
|
|
23
23
|
/**
|
|
24
24
|
* Rollback dispositions the COMP003 fixtures reference, standing in for what
|
|
25
25
|
* `chant lint` derives from the project's registry (`cfn-deploy` has a native
|
|
26
|
-
* `rollback`; `run-migration` declares `needs-opt-out` in the aws lexicon
|
|
26
|
+
* `rollback`; `run-migration` declares `needs-opt-out` in the aws lexicon;
|
|
27
|
+
* `run-agent` declares `native` from phase 1 (#1941) — see
|
|
28
|
+
* `../../../components/verbs/run-agent.ts`'s `rollbackPolicy: "native"`).
|
|
27
29
|
*/
|
|
28
30
|
const FIXTURE_ROLLBACK_POLICIES = new Map<string, RollbackPolicy>([
|
|
29
31
|
["cfn-deploy", "native"],
|
|
30
32
|
["run-migration", "needs-opt-out"],
|
|
33
|
+
["run-agent", "native"],
|
|
31
34
|
]);
|
|
32
35
|
|
|
33
36
|
async function lintFixture(
|
|
@@ -124,6 +127,16 @@ describe("COMP003: mutating-no-rollback", () => {
|
|
|
124
127
|
expect(diagnostics.filter((d) => d.checkId === "COMP003")).toHaveLength(0);
|
|
125
128
|
});
|
|
126
129
|
|
|
130
|
+
it("does not flag a bare run-agent step — its registry-declared rollbackPolicy is \"native\" (#1941), never needing a noRollback opt-out (#1944)", async () => {
|
|
131
|
+
// Regression test: proves the registry's rollbackPolicy for "run-agent"
|
|
132
|
+
// is wired correctly into ctx.rollbackPolicies (the same seam COMP005
|
|
133
|
+
// uses for ctx.knownKinds), not just that COMP003 happens to stay quiet.
|
|
134
|
+
// See ../../../components/verbs/run-agent.ts's "rollbackPolicy: 'native'".
|
|
135
|
+
const diagnostics = await lintFixture("comp003", "pass", { rollbackPolicies: FIXTURE_ROLLBACK_POLICIES });
|
|
136
|
+
const hits = diagnostics.filter((d) => d.checkId === "COMP003" && d.component === "agent-turn");
|
|
137
|
+
expect(hits).toHaveLength(0);
|
|
138
|
+
});
|
|
139
|
+
|
|
127
140
|
it("flags a needs-opt-out step nested inside a fan-out phase, not just top-level phases", () => {
|
|
128
141
|
// Regression test: the rule used to iterate component.deploy directly and
|
|
129
142
|
// never recursed into nested Phase entries (a fan-out unit, e.g. the
|
|
@@ -163,6 +176,41 @@ describe("COMP003: mutating-no-rollback", () => {
|
|
|
163
176
|
expect(diagnostics[0].message).toContain("run-migration");
|
|
164
177
|
});
|
|
165
178
|
|
|
179
|
+
it("DOES flag a \"run-agent\"-kind step when a synthetic registry declares it needs-opt-out — proves the rule is conditional on the registry, not a hard-coded allowance for this one verb name (#1944)", () => {
|
|
180
|
+
// Companion negative case to the "does not flag a bare run-agent step"
|
|
181
|
+
// test above: same step kind, same shape, but with a synthetic
|
|
182
|
+
// rollbackPolicies map that declares "run-agent" as "needs-opt-out"
|
|
183
|
+
// instead of the real registry's "native". If this rule silently
|
|
184
|
+
// special-cased "run-agent" by name, this would incorrectly pass; it must
|
|
185
|
+
// flag exactly like any other needs-opt-out verb.
|
|
186
|
+
const [comp003] = checks.filter((c) => c.id === "COMP003");
|
|
187
|
+
const ctx = {
|
|
188
|
+
rollbackPolicies: new Map<string, RollbackPolicy>([["run-agent", "needs-opt-out"]]),
|
|
189
|
+
components: new Map([
|
|
190
|
+
[
|
|
191
|
+
"agent-turn",
|
|
192
|
+
{
|
|
193
|
+
component: {
|
|
194
|
+
name: "agent-turn",
|
|
195
|
+
dependsOn: [],
|
|
196
|
+
deploy: [
|
|
197
|
+
{
|
|
198
|
+
phase: "Run",
|
|
199
|
+
steps: [{ kind: "run-agent", agent: "claude", task: { prompt: "..." }, workspace: {} }],
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
},
|
|
203
|
+
filePath: "agent-turn.component.ts",
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
]),
|
|
207
|
+
};
|
|
208
|
+
const diagnostics = comp003.check(ctx as never);
|
|
209
|
+
expect(diagnostics).toHaveLength(1);
|
|
210
|
+
expect(diagnostics[0].component).toBe("agent-turn");
|
|
211
|
+
expect(diagnostics[0].message).toContain("run-agent");
|
|
212
|
+
});
|
|
213
|
+
|
|
166
214
|
it("a compensation sibling only counts within the same nested phase, not a same-named sibling phase elsewhere in the component", () => {
|
|
167
215
|
// Two different "Node" phases both contain a run-migration step; only the
|
|
168
216
|
// second one has a rollback-previous sibling. The first must still be
|