@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,109 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { buildChangeSet } from "./change-set";
|
|
3
|
+
import { evaluateUnobservedGate } from "./unobserved-gate";
|
|
4
|
+
import type { ChangeSet } from "./change-set";
|
|
5
|
+
import type { UnobservedReason } from "../observation";
|
|
6
|
+
|
|
7
|
+
const cleanChangeSet = (env = "prod"): ChangeSet =>
|
|
8
|
+
buildChangeSet(env, {
|
|
9
|
+
declared: new Set(["bucket"]),
|
|
10
|
+
observedNow: { bucket: { type: "Fake::Bucket", status: "OK" } },
|
|
11
|
+
observedThen: undefined,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const unobservedChangeSet = (reason: UnobservedReason, detail?: string): ChangeSet =>
|
|
15
|
+
buildChangeSet("prod", {
|
|
16
|
+
declared: new Set(["queue"]),
|
|
17
|
+
observedNow: {},
|
|
18
|
+
observedThen: undefined,
|
|
19
|
+
unobserved: { queue: { reason, ...(detail ? { detail } : {}) } },
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("evaluateUnobservedGate (#1568)", () => {
|
|
23
|
+
test("a clean change set passes under every policy", () => {
|
|
24
|
+
const cs = cleanChangeSet();
|
|
25
|
+
expect(evaluateUnobservedGate(cs)).toEqual({ pass: true, escalate: false, findings: [] });
|
|
26
|
+
expect(evaluateUnobservedGate(cs, "escalate")).toEqual({ pass: true, escalate: false, findings: [] });
|
|
27
|
+
expect(evaluateUnobservedGate(cs, { allow: [] })).toEqual({ pass: true, escalate: false, findings: [] });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("default policy (no argument) refuses a plan with an unobserved entity", () => {
|
|
31
|
+
const cs = unobservedChangeSet("no-credentials", "aws sts get-caller-identity: access denied");
|
|
32
|
+
const verdict = evaluateUnobservedGate(cs);
|
|
33
|
+
expect(verdict.pass).toBe(false);
|
|
34
|
+
expect(verdict.escalate).toBe(false);
|
|
35
|
+
expect(verdict.findings).toEqual([
|
|
36
|
+
{ name: "queue", reason: "no-credentials", detail: "aws sts get-caller-identity: access denied" },
|
|
37
|
+
]);
|
|
38
|
+
expect(verdict.detail).toContain("queue");
|
|
39
|
+
expect(verdict.detail).toContain("no credentials");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('explicit "refuse" behaves the same as the default', () => {
|
|
43
|
+
const cs = unobservedChangeSet("read-failed");
|
|
44
|
+
expect(evaluateUnobservedGate(cs, "refuse").pass).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('"escalate" does not fail the gate but flags escalate with the findings', () => {
|
|
48
|
+
const cs = unobservedChangeSet("no-binding");
|
|
49
|
+
const verdict = evaluateUnobservedGate(cs, "escalate");
|
|
50
|
+
expect(verdict.pass).toBe(true);
|
|
51
|
+
expect(verdict.escalate).toBe(true);
|
|
52
|
+
expect(verdict.findings).toHaveLength(1);
|
|
53
|
+
expect(verdict.findings[0].reason).toBe("no-binding");
|
|
54
|
+
expect(verdict.detail).toBeDefined();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("an allowed reason passes cleanly and is not counted as a finding", () => {
|
|
58
|
+
const cs = unobservedChangeSet("filtered");
|
|
59
|
+
const verdict = evaluateUnobservedGate(cs, { allow: ["filtered"] });
|
|
60
|
+
expect(verdict).toEqual({ pass: true, escalate: false, findings: [] });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("a reason outside the allow list still refuses", () => {
|
|
64
|
+
const cs = unobservedChangeSet("no-credentials");
|
|
65
|
+
const verdict = evaluateUnobservedGate(cs, { allow: ["filtered"] });
|
|
66
|
+
expect(verdict.pass).toBe(false);
|
|
67
|
+
expect(verdict.findings[0].reason).toBe("no-credentials");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("a mixed change set with one allowed and one unallowed reason only reports the unallowed one", () => {
|
|
71
|
+
const cs = buildChangeSet("prod", {
|
|
72
|
+
declared: new Set(["a", "b"]),
|
|
73
|
+
observedNow: {},
|
|
74
|
+
observedThen: undefined,
|
|
75
|
+
unobserved: {
|
|
76
|
+
a: { reason: "filtered" },
|
|
77
|
+
b: { reason: "unsupported-kind" },
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
const verdict = evaluateUnobservedGate(cs, { allow: ["filtered"] });
|
|
81
|
+
expect(verdict.pass).toBe(false);
|
|
82
|
+
expect(verdict.findings).toEqual([{ name: "b", reason: "unsupported-kind" }]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("never escalates or refuses on create/update/delete/adopt/noop — only `unobserved` counts", () => {
|
|
86
|
+
const cs = buildChangeSet("prod", {
|
|
87
|
+
declared: new Set(["created", "updated"]),
|
|
88
|
+
observedNow: {
|
|
89
|
+
updated: { type: "Fake::Resource", status: "DRIFTED" },
|
|
90
|
+
orphan: { type: "Fake::Resource", status: "OK" },
|
|
91
|
+
},
|
|
92
|
+
observedThen: { updated: { type: "Fake::Resource", status: "OK" } },
|
|
93
|
+
});
|
|
94
|
+
expect(evaluateUnobservedGate(cs)).toEqual({ pass: true, escalate: false, findings: [] });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("carries type and detail through into the finding when present", () => {
|
|
98
|
+
const cs = buildChangeSet("prod", {
|
|
99
|
+
declared: new Set(["disk"]),
|
|
100
|
+
observedNow: {},
|
|
101
|
+
observedThen: undefined,
|
|
102
|
+
unobserved: { disk: { reason: "unsupported-kind", detail: "no describe support for this kind yet", type: "Fake::Disk" } },
|
|
103
|
+
});
|
|
104
|
+
const verdict = evaluateUnobservedGate(cs);
|
|
105
|
+
expect(verdict.findings).toEqual([
|
|
106
|
+
{ name: "disk", type: "Fake::Disk", reason: "unsupported-kind", detail: "no describe support for this kind yet" },
|
|
107
|
+
]);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The unobserved gate predicate (#1568): a plan containing unobserved
|
|
3
|
+
* entities must not pass as clean.
|
|
4
|
+
*
|
|
5
|
+
* `buildChangeSet` (./change-set.ts) already refuses to guess when a lexicon
|
|
6
|
+
* could not look (#1089) — entries land as `action: "unobserved"`, carrying a
|
|
7
|
+
* total `unobservedReason` (`read-failed`, `no-credentials`, `no-binding`,
|
|
8
|
+
* `unsupported-kind`, `filtered`; see ../observation.ts). But nothing
|
|
9
|
+
* downstream is *required* to look: a gate that checks lint-clean,
|
|
10
|
+
* owned-only, and a blast-radius budget can walk right past a change set
|
|
11
|
+
* whose live read partially failed and call it clean, because none of those
|
|
12
|
+
* checks reads `unobserved` at all. A gate over an incomplete read is a gate
|
|
13
|
+
* over a guess.
|
|
14
|
+
*
|
|
15
|
+
* This is a reusable predicate, not a runtime — evaluate a `ChangeSet`
|
|
16
|
+
* against a policy and get back a verdict; the caller (a `lint.policies`
|
|
17
|
+
* check routed through `policyGate`, an Op step, #1487's gate-as-fact once it
|
|
18
|
+
* lands) decides what to do with a refusal (throw, diagnostic, a durable
|
|
19
|
+
* gated-run fact). Same instinct #1484 already adopts for `unknown` in the
|
|
20
|
+
* operating loop ("`unknown` never remediates"), surfaced here as something
|
|
21
|
+
* any gate can declare rather than a property of one runtime.
|
|
22
|
+
*/
|
|
23
|
+
import type { ChangeSet } from "./change-set";
|
|
24
|
+
import { unobservedReasonText, type UnobservedReason } from "../observation";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* How the gate treats the change set's unobserved set.
|
|
28
|
+
*
|
|
29
|
+
* - `"refuse"` (the default) — any unobserved entry not covered by `allow`
|
|
30
|
+
* fails the gate outright. Fail-closed, the right default for a policy set
|
|
31
|
+
* aimed at agent-proposed change sets: a hole must never launder into a
|
|
32
|
+
* clean verdict just because nobody configured otherwise.
|
|
33
|
+
* - `"escalate"` — the gate does not fail here; instead the verdict reports
|
|
34
|
+
* `escalate: true` so a caller can route the plan to a stricter gate class
|
|
35
|
+
* rather than the ordinary path (the same shape of routing decision #1569
|
|
36
|
+
* makes for pipeline changes).
|
|
37
|
+
* - `{ allow: [...] }` — the listed reasons are tolerated (e.g. `"filtered"`,
|
|
38
|
+
* intentional in a scoped run) and never fail or escalate the gate; any
|
|
39
|
+
* reason NOT in the list still refuses. `no-credentials` — a hole that must
|
|
40
|
+
* never launder into a clean verdict — should never appear in an `allow`
|
|
41
|
+
* list for a policy aimed at agent-proposed change sets.
|
|
42
|
+
*/
|
|
43
|
+
export type UnobservedGatePolicy = "refuse" | "escalate" | { allow: UnobservedReason[] };
|
|
44
|
+
|
|
45
|
+
/** One unobserved hole the gate found, carried into the refusal. */
|
|
46
|
+
export interface UnobservedGateFinding {
|
|
47
|
+
name: string;
|
|
48
|
+
type?: string;
|
|
49
|
+
reason: UnobservedReason;
|
|
50
|
+
detail?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface UnobservedGateVerdict {
|
|
54
|
+
/** False when the policy is `"refuse"` (default) and an unallowed unobserved entry exists. */
|
|
55
|
+
pass: boolean;
|
|
56
|
+
/** True when the policy is `"escalate"` and an unallowed unobserved entry exists — route to a stricter gate instead of failing here. */
|
|
57
|
+
escalate: boolean;
|
|
58
|
+
/** Every unobserved entry the policy did not allow. Empty when `pass` is true and `escalate` is false. */
|
|
59
|
+
findings: UnobservedGateFinding[];
|
|
60
|
+
/** Human-readable summary carrying the reasons into a refusal — the message a caller can surface as-is. Undefined when `findings` is empty. */
|
|
61
|
+
detail?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Evaluate a `ChangeSet`'s unobserved set against `policy`. Pure — reads only
|
|
66
|
+
* `cs.entries`, no I/O. Defaults to `"refuse"`.
|
|
67
|
+
*/
|
|
68
|
+
export function evaluateUnobservedGate(
|
|
69
|
+
cs: ChangeSet,
|
|
70
|
+
policy: UnobservedGatePolicy = "refuse",
|
|
71
|
+
): UnobservedGateVerdict {
|
|
72
|
+
const allowed = typeof policy === "object" ? new Set(policy.allow) : undefined;
|
|
73
|
+
|
|
74
|
+
const findings: UnobservedGateFinding[] = [];
|
|
75
|
+
for (const e of cs.entries) {
|
|
76
|
+
if (e.action !== "unobserved" || e.unobservedReason === undefined) continue;
|
|
77
|
+
if (allowed?.has(e.unobservedReason)) continue;
|
|
78
|
+
findings.push({
|
|
79
|
+
name: e.name,
|
|
80
|
+
...(e.type ? { type: e.type } : {}),
|
|
81
|
+
reason: e.unobservedReason,
|
|
82
|
+
...(e.unobservedDetail ? { detail: e.unobservedDetail } : {}),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (findings.length === 0) {
|
|
87
|
+
return { pass: true, escalate: false, findings: [] };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const detail =
|
|
91
|
+
`plan contains ${findings.length} unobserved ${findings.length === 1 ? "entity" : "entities"} — ` +
|
|
92
|
+
findings
|
|
93
|
+
.map((f) => `${f.name}${f.type ? ` (${f.type})` : ""}: ${unobservedReasonText(f.reason)}${f.detail ? ` (${f.detail})` : ""}`)
|
|
94
|
+
.join(", ");
|
|
95
|
+
|
|
96
|
+
if (policy === "escalate") {
|
|
97
|
+
return { pass: true, escalate: true, findings, detail };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// "refuse" (default) and `{ allow }` both fail closed on anything not allowed.
|
|
101
|
+
return { pass: false, escalate: false, findings, detail };
|
|
102
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { coreKnowledgeChecks, STALE_KNOWLEDGE_BINDING_CHECK_ID } from "./knowledge-checks";
|
|
3
|
+
import { runPostSynthChecks } from "./post-synth";
|
|
4
|
+
import { DECLARABLE_MARKER, type Declarable } from "../declarable";
|
|
5
|
+
import type { OkfBundle, OkfConcept } from "../okf-read";
|
|
6
|
+
|
|
7
|
+
function entity(): Declarable {
|
|
8
|
+
return { [DECLARABLE_MARKER]: true, lexicon: "aws", entityType: "AWS::S3::Bucket" } as unknown as Declarable;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function concept(overrides: Partial<OkfConcept> = {}): OkfConcept {
|
|
12
|
+
return {
|
|
13
|
+
path: "decisions/example.md",
|
|
14
|
+
type: "decision",
|
|
15
|
+
binds: [],
|
|
16
|
+
frontmatter: {},
|
|
17
|
+
body: "",
|
|
18
|
+
...overrides,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function run(bundle: OkfBundle, entities: Map<string, Declarable>) {
|
|
23
|
+
return runPostSynthChecks(coreKnowledgeChecks(bundle), {
|
|
24
|
+
outputs: new Map(),
|
|
25
|
+
entities,
|
|
26
|
+
warnings: [],
|
|
27
|
+
errors: [],
|
|
28
|
+
sourceFileCount: 1,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("COR026: stale knowledge binding (#1865)", () => {
|
|
33
|
+
test("check id", () => {
|
|
34
|
+
expect(STALE_KNOWLEDGE_BINDING_CHECK_ID).toBe("COR026");
|
|
35
|
+
expect(coreKnowledgeChecks({ concepts: [] }).map((c) => c.id)).toContain("COR026");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("fires once per unresolved binding, naming the concept path and the unresolved name", () => {
|
|
39
|
+
const bundle: OkfBundle = {
|
|
40
|
+
concepts: [concept({ path: "decisions/ghost.md", title: "Stale", binds: ["ghostBucket"] })],
|
|
41
|
+
};
|
|
42
|
+
const diags = run(bundle, new Map());
|
|
43
|
+
expect(diags).toHaveLength(1);
|
|
44
|
+
expect(diags[0].checkId).toBe("COR026");
|
|
45
|
+
expect(diags[0].severity).toBe("warning");
|
|
46
|
+
expect(diags[0].message).toContain("decisions/ghost.md");
|
|
47
|
+
expect(diags[0].message).toContain("ghostBucket");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("fires one diagnostic per unresolved name when a concept binds several", () => {
|
|
51
|
+
const bundle: OkfBundle = {
|
|
52
|
+
concepts: [concept({ binds: ["ghostOne", "ghostTwo"] })],
|
|
53
|
+
};
|
|
54
|
+
const diags = run(bundle, new Map());
|
|
55
|
+
expect(diags).toHaveLength(2);
|
|
56
|
+
expect(diags.map((d) => d.message).join("\n")).toContain("ghostOne");
|
|
57
|
+
expect(diags.map((d) => d.message).join("\n")).toContain("ghostTwo");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("does not fire when the bound name resolves to a discovered entity", () => {
|
|
61
|
+
const bundle: OkfBundle = { concepts: [concept({ binds: ["realBucket"] })] };
|
|
62
|
+
const entities = new Map<string, Declarable>([["realBucket", entity()]]);
|
|
63
|
+
expect(run(bundle, entities)).toHaveLength(0);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("does not fire for a concept with no binds at all — orphaned knowledge is legitimate", () => {
|
|
67
|
+
const bundle: OkfBundle = { concepts: [concept({ binds: [] })] };
|
|
68
|
+
expect(run(bundle, new Map())).toHaveLength(0);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("does not fire for an empty bundle (no knowledge directory)", () => {
|
|
72
|
+
expect(run({ concepts: [] }, new Map())).toHaveLength(0);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("build and synthesis are unaffected — severity is always warning, never error", () => {
|
|
76
|
+
const bundle: OkfBundle = { concepts: [concept({ binds: ["ghost"] })] };
|
|
77
|
+
const diags = run(bundle, new Map());
|
|
78
|
+
expect(diags.every((d) => d.severity === "warning")).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { PostSynthCheck, PostSynthDiagnostic } from "./post-synth";
|
|
2
|
+
import type { OkfBundle } from "../okf-read";
|
|
3
|
+
import { bindConcepts } from "../okf-read";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* COR026: Stale Knowledge Binding (#1865, design #1059, epic #1057)
|
|
7
|
+
*
|
|
8
|
+
* A concept's `binds` frontmatter key names a discovered entity's logical
|
|
9
|
+
* name (#1864, `../okf-read.ts`'s `bindConcepts`). A name that resolves to
|
|
10
|
+
* nothing is a stale binding — the entity was renamed or removed, or the
|
|
11
|
+
* concept was authored against a name that never existed — and per #1059's
|
|
12
|
+
* stated failure posture ("the spec permits broken links, which argues for
|
|
13
|
+
* warn") that is a warning, never a build failure: knowledge is deliberately
|
|
14
|
+
* softer than the typed graph it describes, and OKF itself tolerates broken
|
|
15
|
+
* links (epic #1057).
|
|
16
|
+
*
|
|
17
|
+
* The converse is deliberately silent: a concept with no `binds` at all is
|
|
18
|
+
* orphaned knowledge, not an omission (a runbook, a decision about the
|
|
19
|
+
* project as a whole), and `bindConcepts` never reports it as unresolved in
|
|
20
|
+
* the first place — see its own doc comment. A project with no knowledge
|
|
21
|
+
* bundle loads an empty one (`loadOkfBundle`), so this check produces no
|
|
22
|
+
* diagnostics either.
|
|
23
|
+
*
|
|
24
|
+
* Wired into `chant build` (`../cli/commands/build.ts`) alongside
|
|
25
|
+
* `coreReceiptChecks` — same reasons COR023/COR024 are build-owned rather
|
|
26
|
+
* than a per-file `LintRule`: the check needs the full discovered entity map
|
|
27
|
+
* (`ctx.entities`), not one `ts.SourceFile`, and that map only exists after
|
|
28
|
+
* discovery has run. Unlike COR023/024 it never reads `ctx.outputs` — the
|
|
29
|
+
* bundle is loaded once, up front (async — `loadOkfBundle` reads the
|
|
30
|
+
* filesystem), and closed over here so the check itself stays the same sync
|
|
31
|
+
* shape every other `PostSynthCheck` has.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
export const STALE_KNOWLEDGE_BINDING_CHECK_ID = "COR026";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Build the COR026 check for one loaded bundle. A factory, not a bare
|
|
38
|
+
* constant, because — unlike `coreReceiptChecks`, which needs nothing but
|
|
39
|
+
* `ctx.entities` — this check needs the bundle loaded from disk, and
|
|
40
|
+
* `loadOkfBundle` is async while `PostSynthCheck#check` is not. The caller
|
|
41
|
+
* loads the bundle once (`resolveKnowledgeDir` + `loadOkfBundle`) and passes
|
|
42
|
+
* it in; see `../cli/commands/build.ts`.
|
|
43
|
+
*/
|
|
44
|
+
export function staleKnowledgeBindingCheck(bundle: OkfBundle): PostSynthCheck {
|
|
45
|
+
return {
|
|
46
|
+
id: STALE_KNOWLEDGE_BINDING_CHECK_ID,
|
|
47
|
+
description:
|
|
48
|
+
"A knowledge concept's `binds` entry must name a discovered entity — a name that resolves to nothing is a stale binding",
|
|
49
|
+
check(ctx) {
|
|
50
|
+
const { unresolved } = bindConcepts(bundle, ctx.entities);
|
|
51
|
+
return unresolved.map(
|
|
52
|
+
({ concept, name }): PostSynthDiagnostic => ({
|
|
53
|
+
checkId: STALE_KNOWLEDGE_BINDING_CHECK_ID,
|
|
54
|
+
severity: "warning",
|
|
55
|
+
message:
|
|
56
|
+
`Knowledge concept "${concept.path}" binds "${name}", but no discovered entity has that name — ` +
|
|
57
|
+
`a stale binding. The entity may have been renamed or removed, or the concept was authored ` +
|
|
58
|
+
`against a name that never existed. Update the concept's \`binds\` frontmatter, or remove the ` +
|
|
59
|
+
`entry if the concept no longer applies.`,
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Core's own post-synth check over knowledge bindings, run by `chant build`
|
|
68
|
+
* over the full build result — unscoped, like `coreReceiptChecks`, since a
|
|
69
|
+
* concept's `binds` can name any discovered entity regardless of which
|
|
70
|
+
* lexicon owns it.
|
|
71
|
+
*/
|
|
72
|
+
export function coreKnowledgeChecks(bundle: OkfBundle): PostSynthCheck[] {
|
|
73
|
+
return [staleKnowledgeBindingCheck(bundle)];
|
|
74
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { coreOutputChecks, STRINGIFIED_REFERENCE_CHECK_ID } from "./output-checks";
|
|
3
|
+
import { runPostSynthChecks } from "./post-synth";
|
|
4
|
+
import type { SerializerResult } from "../serializer";
|
|
5
|
+
import type { Declarable } from "../declarable";
|
|
6
|
+
|
|
7
|
+
function run(outputs: Map<string, string | SerializerResult>) {
|
|
8
|
+
return runPostSynthChecks(coreOutputChecks(), {
|
|
9
|
+
outputs,
|
|
10
|
+
entities: new Map<string, Declarable>(),
|
|
11
|
+
warnings: [],
|
|
12
|
+
errors: [],
|
|
13
|
+
sourceFileCount: 1,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("COR025: stringified reference in serialized output (#1526)", () => {
|
|
18
|
+
test("check id", () => {
|
|
19
|
+
expect(STRINGIFIED_REFERENCE_CHECK_ID).toBe("COR025");
|
|
20
|
+
expect(coreOutputChecks().map((c) => c.id)).toContain("COR025");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("fires when a string output contains the [object Object] marker", () => {
|
|
24
|
+
const outputs = new Map<string, string | SerializerResult>([
|
|
25
|
+
["aws", '{"Resource": "arn:aws:s3:::bucket-[object Object]/*"}'],
|
|
26
|
+
]);
|
|
27
|
+
const diags = run(outputs);
|
|
28
|
+
expect(diags).toHaveLength(1);
|
|
29
|
+
expect(diags[0].checkId).toBe("COR025");
|
|
30
|
+
expect(diags[0].severity).toBe("error");
|
|
31
|
+
expect(diags[0].lexicon).toBe("aws");
|
|
32
|
+
expect(diags[0].message).toContain("[object Object]");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("fires when a SerializerResult's primary content contains the marker", () => {
|
|
36
|
+
const outputs = new Map<string, string | SerializerResult>([
|
|
37
|
+
["aws", { primary: 'Resource: "[object Object]/*"' } satisfies SerializerResult],
|
|
38
|
+
]);
|
|
39
|
+
const diags = run(outputs);
|
|
40
|
+
expect(diags).toHaveLength(1);
|
|
41
|
+
expect(diags[0].message).toContain('Resource: "[object Object]/*"');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("fires when a SerializerResult's nested file contains the marker, and names the file", () => {
|
|
45
|
+
const outputs = new Map<string, string | SerializerResult>([
|
|
46
|
+
[
|
|
47
|
+
"aws",
|
|
48
|
+
{
|
|
49
|
+
primary: "clean template",
|
|
50
|
+
files: { "network.template.json": '{"Resource": "[object Object]"}' },
|
|
51
|
+
} satisfies SerializerResult,
|
|
52
|
+
],
|
|
53
|
+
]);
|
|
54
|
+
const diags = run(outputs);
|
|
55
|
+
expect(diags).toHaveLength(1);
|
|
56
|
+
expect(diags[0].message).toContain("network.template.json");
|
|
57
|
+
expect(diags[0].message).toContain("[object Object]");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("dedupes repeated occurrences of the same line within a file", () => {
|
|
61
|
+
const line = 'Resource: "[object Object]/*"';
|
|
62
|
+
const outputs = new Map<string, string | SerializerResult>([["aws", `${line}\n${line}\n${line}`]]);
|
|
63
|
+
const diags = run(outputs);
|
|
64
|
+
expect(diags).toHaveLength(1);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("reports one diagnostic per distinct offending line", () => {
|
|
68
|
+
const outputs = new Map<string, string | SerializerResult>([
|
|
69
|
+
["aws", 'Resource: "[object Object]/*"\nPolicy: "[object Object]"'],
|
|
70
|
+
]);
|
|
71
|
+
const diags = run(outputs);
|
|
72
|
+
expect(diags).toHaveLength(2);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("passes: output with no stringified reference", () => {
|
|
76
|
+
const outputs = new Map<string, string | SerializerResult>([
|
|
77
|
+
["aws", '{"Resource": "arn:aws:s3:::my-bucket/*"}'],
|
|
78
|
+
]);
|
|
79
|
+
expect(run(outputs)).toHaveLength(0);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("passes: empty outputs", () => {
|
|
83
|
+
expect(run(new Map())).toHaveLength(0);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { SerializerResult } from "../serializer";
|
|
2
|
+
import type { PostSynthCheck, PostSynthDiagnostic } from "./post-synth";
|
|
3
|
+
import { getPrimaryOutput } from "./post-synth";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* COR025: Stringified Reference in Serialized Output (#1526)
|
|
7
|
+
*
|
|
8
|
+
* kubemicrovm-ops composed a bucket policy's resource ARN with a template
|
|
9
|
+
* literal — `` `${bucket.Arn}/*` `` — over an `AttrRef`, not a string.
|
|
10
|
+
* JavaScript stringifies any non-primitive interpolated into a template
|
|
11
|
+
* literal via `Object.prototype.toString`, so the emitted policy carried the
|
|
12
|
+
* literal substring `"[object Object]/*"` in place of the resolved ARN. That
|
|
13
|
+
* built clean and linted clean — nothing upstream of serialization sees a
|
|
14
|
+
* type error, because `AttrRef` IS an object and a template literal accepts
|
|
15
|
+
* any coercible value — and it deployed clean on floci, which does no
|
|
16
|
+
* policy validation. It failed the first real deploy with S3 rejecting the
|
|
17
|
+
* policy's `Resource` and rolling back the stack.
|
|
18
|
+
*
|
|
19
|
+
* `[object Object]` (and its array-of-objects sibling `[object Object,object
|
|
20
|
+
* Object]`) never appears in a serialized template/manifest on purpose: no
|
|
21
|
+
* lexicon's serializer ever emits it, and no cloud provider's schema ever
|
|
22
|
+
* expects it as a literal value. Its presence is only ever a reference that
|
|
23
|
+
* got coerced to a string instead of resolved. That makes this check cheap,
|
|
24
|
+
* universal across lexicons (it scans the emitted text, not any one
|
|
25
|
+
* lexicon's schema), and free of false positives worth caring about — see
|
|
26
|
+
* `stringifiedReferenceCheck` below.
|
|
27
|
+
*
|
|
28
|
+
* Scans `ctx.outputs` (the SYNTHESIZED text) rather than `ctx.entities`
|
|
29
|
+
* because the coercion already happened in the user's own declaration code,
|
|
30
|
+
* upstream of anything chant's model retains structurally — by the time an
|
|
31
|
+
* entity reaches `ctx.entities` its prop is already a plain string containing
|
|
32
|
+
* the marker, indistinguishable from any other string prop. The only place
|
|
33
|
+
* left to catch it lexicon-independently is the text chant actually emits.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const MARKER = "[object Object]";
|
|
37
|
+
|
|
38
|
+
export const STRINGIFIED_REFERENCE_CHECK_ID = "COR025";
|
|
39
|
+
|
|
40
|
+
/** Trim a matched line down to a readable snippet without truncating mid-marker. */
|
|
41
|
+
const MAX_SNIPPET_LENGTH = 200;
|
|
42
|
+
|
|
43
|
+
function lineContaining(text: string, index: number): string {
|
|
44
|
+
const lineStart = text.lastIndexOf("\n", index) + 1;
|
|
45
|
+
const nextNewline = text.indexOf("\n", index);
|
|
46
|
+
const lineEnd = nextNewline === -1 ? text.length : nextNewline;
|
|
47
|
+
const line = text.slice(lineStart, lineEnd).trim();
|
|
48
|
+
return line.length > MAX_SNIPPET_LENGTH ? `${line.slice(0, MAX_SNIPPET_LENGTH)}…` : line;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Every serialized file for one lexicon's output: the primary plus any nested files. */
|
|
52
|
+
function serializedFiles(output: string | SerializerResult): Record<string, string> {
|
|
53
|
+
const files: Record<string, string> = { primary: getPrimaryOutput(output) };
|
|
54
|
+
if (typeof output !== "string" && output.files) {
|
|
55
|
+
Object.assign(files, output.files);
|
|
56
|
+
}
|
|
57
|
+
return files;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const stringifiedReferenceCheck: PostSynthCheck = {
|
|
61
|
+
id: STRINGIFIED_REFERENCE_CHECK_ID,
|
|
62
|
+
description:
|
|
63
|
+
'A serialized output contains the literal substring "[object Object]" — a reference that was ' +
|
|
64
|
+
"stringified (e.g. through a template literal or string concatenation) instead of resolved. " +
|
|
65
|
+
"Always an authoring error, never intentional output.",
|
|
66
|
+
check(ctx) {
|
|
67
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
68
|
+
for (const [lexicon, output] of ctx.outputs) {
|
|
69
|
+
for (const [fileLabel, content] of Object.entries(serializedFiles(output))) {
|
|
70
|
+
const seenLines = new Set<string>();
|
|
71
|
+
let index = content.indexOf(MARKER);
|
|
72
|
+
while (index !== -1) {
|
|
73
|
+
const line = lineContaining(content, index);
|
|
74
|
+
const dedupeKey = `${fileLabel}::${line}`;
|
|
75
|
+
if (!seenLines.has(dedupeKey)) {
|
|
76
|
+
seenLines.add(dedupeKey);
|
|
77
|
+
const location = fileLabel === "primary" ? "" : ` (${fileLabel})`;
|
|
78
|
+
diagnostics.push({
|
|
79
|
+
checkId: STRINGIFIED_REFERENCE_CHECK_ID,
|
|
80
|
+
severity: "error",
|
|
81
|
+
lexicon,
|
|
82
|
+
message:
|
|
83
|
+
`Serialized output${location} contains "${MARKER}" — a reference was stringified ` +
|
|
84
|
+
`instead of resolved (a template literal or string concatenation over an object, ` +
|
|
85
|
+
`not the raw value/ref). Emitted line: ${line}`,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
index = content.indexOf(MARKER, index + MARKER.length);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return diagnostics;
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/** Core-owned post-synth checks over serialized output text (#1526). */
|
|
97
|
+
export function coreOutputChecks(): PostSynthCheck[] {
|
|
98
|
+
return [stringifiedReferenceCheck];
|
|
99
|
+
}
|