@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
|
@@ -53,3 +53,47 @@ describe("lexiconCompletions resource ranking (#600)", () => {
|
|
|
53
53
|
expect(items.length).toBe(100);
|
|
54
54
|
});
|
|
55
55
|
});
|
|
56
|
+
|
|
57
|
+
describe("lexiconCompletions deprecation marking (#1701)", () => {
|
|
58
|
+
/** One resource with a declared deprecation and an inferred one. */
|
|
59
|
+
function bucketIndex(): LexiconIndex {
|
|
60
|
+
return new LexiconIndex({
|
|
61
|
+
Bucket: {
|
|
62
|
+
resourceType: "Test::S3::Bucket",
|
|
63
|
+
kind: "resource",
|
|
64
|
+
lexicon: "test",
|
|
65
|
+
createOnly: ["AccessControl", "Runtime", "BucketName"],
|
|
66
|
+
deprecatedProperties: ["AccessControl", "Runtime"],
|
|
67
|
+
inferredDeprecations: ["Runtime"],
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const propCtx = (): CompletionContext =>
|
|
73
|
+
({
|
|
74
|
+
uri: "file:///t.ts",
|
|
75
|
+
content: "const b = new Bucket({\n ",
|
|
76
|
+
linePrefix: " ",
|
|
77
|
+
wordAtCursor: "",
|
|
78
|
+
position: { line: 1, character: 2 },
|
|
79
|
+
}) as unknown as CompletionContext;
|
|
80
|
+
|
|
81
|
+
test("a declared deprecation is marked deprecated", () => {
|
|
82
|
+
const items = lexiconCompletions(propCtx(), bucketIndex(), "Test resource");
|
|
83
|
+
const access = items.find((i) => i.label === "AccessControl");
|
|
84
|
+
expect(access?.deprecated).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("an inferred deprecation is not marked deprecated", () => {
|
|
88
|
+
const items = lexiconCompletions(propCtx(), bucketIndex(), "Test resource");
|
|
89
|
+
const runtime = items.find((i) => i.label === "Runtime");
|
|
90
|
+
expect(runtime).toBeDefined();
|
|
91
|
+
expect(runtime?.deprecated).toBeUndefined();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("a property with no deprecation signal is not marked", () => {
|
|
95
|
+
const items = lexiconCompletions(propCtx(), bucketIndex(), "Test resource");
|
|
96
|
+
const name = items.find((i) => i.label === "BucketName");
|
|
97
|
+
expect(name?.deprecated).toBeUndefined();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -20,6 +20,13 @@ export interface LexiconEntry {
|
|
|
20
20
|
writeOnly?: string[];
|
|
21
21
|
primaryIdentifier?: string[];
|
|
22
22
|
deprecatedProperties?: string[];
|
|
23
|
+
/**
|
|
24
|
+
* Subset of `deprecatedProperties` that only prose supports — a lexicon
|
|
25
|
+
* generator matched the property description against a deprecation regex
|
|
26
|
+
* rather than reading an upstream declaration (#1701). Editors do not strike
|
|
27
|
+
* these through; the basis is too weak to mark a property dead.
|
|
28
|
+
*/
|
|
29
|
+
inferredDeprecations?: string[];
|
|
23
30
|
conditionalCreateOnly?: string[];
|
|
24
31
|
replacementStrategy?: "delete_then_create" | "create_then_delete";
|
|
25
32
|
tagging?: { taggable: boolean; tagOnCreate: boolean; tagUpdatable: boolean };
|
|
@@ -127,7 +134,10 @@ export function lexiconCompletions(
|
|
|
127
134
|
const entry = index.getEntry(className);
|
|
128
135
|
const props = index.getPropertyNames(className);
|
|
129
136
|
if (props.length > 0) {
|
|
130
|
-
const
|
|
137
|
+
const inferred = new Set(entry?.inferredDeprecations ?? []);
|
|
138
|
+
const deprecatedSet = new Set(
|
|
139
|
+
(entry?.deprecatedProperties ?? []).filter((p) => !inferred.has(p)),
|
|
140
|
+
);
|
|
131
141
|
const filtered = wordAtCursor
|
|
132
142
|
? props.filter((p) => p.toLowerCase().startsWith(wordAtCursor.toLowerCase()))
|
|
133
143
|
: props;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { loadOkfBundle, bindConcepts } from "./okf-read";
|
|
5
|
+
import { buildOkfBundle } from "./okf";
|
|
6
|
+
import { DECLARABLE_MARKER, type Declarable } from "./declarable";
|
|
7
|
+
|
|
8
|
+
function decl<T extends object>(base: T): Declarable & T {
|
|
9
|
+
return { [DECLARABLE_MARKER]: true, ...base } as Declarable & T;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const TEST_DIR = join(import.meta.dirname, "__test_okf_read__");
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
mkdirSync(TEST_DIR, { recursive: true });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function write(relPath: string, content: string): void {
|
|
23
|
+
const full = join(TEST_DIR, relPath);
|
|
24
|
+
mkdirSync(dirname(full), { recursive: true });
|
|
25
|
+
writeFileSync(full, content);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("loadOkfBundle", () => {
|
|
29
|
+
test("a missing directory yields an empty bundle, not an error", async () => {
|
|
30
|
+
const bundle = await loadOkfBundle(join(TEST_DIR, "does-not-exist"));
|
|
31
|
+
expect(bundle.concepts).toEqual([]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("skips index.md and log.md, at any depth", async () => {
|
|
35
|
+
write("index.md", "---\nokf_version: '0.2'\n---\n");
|
|
36
|
+
write("log.md", "---\ntype: log\n---\nsome log\n");
|
|
37
|
+
write("nested/log.md", "---\ntype: log\n---\nnested log\n");
|
|
38
|
+
write("decisions/keep.md", "---\ntype: decision\ntitle: Keep me\n---\nbody\n");
|
|
39
|
+
|
|
40
|
+
const bundle = await loadOkfBundle(TEST_DIR);
|
|
41
|
+
expect(bundle.concepts.map((c) => c.path)).toEqual(["decisions/keep.md"]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("passes through unknown type values and extra frontmatter keys", async () => {
|
|
45
|
+
write("weird.md", "---\ntype: some-unknown-type\ncustom_field: hello\n---\nbody text\n");
|
|
46
|
+
|
|
47
|
+
const bundle = await loadOkfBundle(TEST_DIR);
|
|
48
|
+
expect(bundle.concepts).toHaveLength(1);
|
|
49
|
+
const [concept] = bundle.concepts;
|
|
50
|
+
expect(concept.type).toBe("some-unknown-type");
|
|
51
|
+
expect(concept.frontmatter.custom_field).toBe("hello");
|
|
52
|
+
expect(concept.body).toContain("body text");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("skips a file with unparseable frontmatter, warns, and does not affect other files", async () => {
|
|
56
|
+
write("broken.md", "not frontmatter at all\n");
|
|
57
|
+
write("fine.md", "---\ntype: decision\ntitle: Fine\n---\nbody\n");
|
|
58
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
59
|
+
|
|
60
|
+
const bundle = await loadOkfBundle(TEST_DIR);
|
|
61
|
+
|
|
62
|
+
expect(bundle.concepts.map((c) => c.path)).toEqual(["fine.md"]);
|
|
63
|
+
expect(warn).toHaveBeenCalled();
|
|
64
|
+
warn.mockRestore();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("never rejects the bundle for a malformed file — an empty frontmatter block loads with an empty type", async () => {
|
|
68
|
+
write("empty-front.md", "---\n---\nbody\n");
|
|
69
|
+
const bundle = await loadOkfBundle(TEST_DIR);
|
|
70
|
+
expect(bundle.concepts).toHaveLength(1);
|
|
71
|
+
expect(bundle.concepts[0].type).toBe("");
|
|
72
|
+
expect(bundle.concepts[0].binds).toEqual([]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("binds accepts a single name", async () => {
|
|
76
|
+
write("single.md", "---\ntype: decision\nbinds: myBucket\n---\nbody\n");
|
|
77
|
+
const bundle = await loadOkfBundle(TEST_DIR);
|
|
78
|
+
expect(bundle.concepts[0].binds).toEqual(["myBucket"]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("binds accepts a list", async () => {
|
|
82
|
+
write("multi.md", "---\ntype: decision\nbinds:\n - myBucket\n - vpc\n---\nbody\n");
|
|
83
|
+
const bundle = await loadOkfBundle(TEST_DIR);
|
|
84
|
+
expect(bundle.concepts[0].binds).toEqual(["myBucket", "vpc"]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("a concept without binds loads as unbound and is legitimate", async () => {
|
|
88
|
+
write("orphan.md", "---\ntype: runbook\ntitle: Incident response\n---\nbody\n");
|
|
89
|
+
const bundle = await loadOkfBundle(TEST_DIR);
|
|
90
|
+
expect(bundle.concepts[0].binds).toEqual([]);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("bindConcepts", () => {
|
|
95
|
+
test("returns bound concepts per entity name plus unresolved bindings", async () => {
|
|
96
|
+
write("bound.md", "---\ntype: decision\ntitle: Bound\nbinds: myBucket\n---\nbody\n");
|
|
97
|
+
write("multi.md", "---\ntype: decision\ntitle: Spans two\nbinds:\n - myBucket\n - vpc\n---\nbody\n");
|
|
98
|
+
write("stale.md", "---\ntype: decision\ntitle: Stale\nbinds: ghost\n---\nbody\n");
|
|
99
|
+
write("orphan.md", "---\ntype: runbook\ntitle: Orphan\n---\nbody\n");
|
|
100
|
+
|
|
101
|
+
const bundle = await loadOkfBundle(TEST_DIR);
|
|
102
|
+
const entities = new Map<string, Declarable>([
|
|
103
|
+
["myBucket", decl({ lexicon: "aws", entityType: "AWS::S3::Bucket" })],
|
|
104
|
+
["vpc", decl({ lexicon: "gcp", entityType: "Vpc" })],
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
const { bound, unresolved } = bindConcepts(bundle, entities);
|
|
108
|
+
|
|
109
|
+
expect(bound.get("myBucket")?.map((c) => c.title)).toEqual(["Bound", "Spans two"]);
|
|
110
|
+
expect(bound.get("vpc")?.map((c) => c.title)).toEqual(["Spans two"]);
|
|
111
|
+
expect(bound.has("ghost")).toBe(false);
|
|
112
|
+
expect(unresolved).toHaveLength(1);
|
|
113
|
+
expect(unresolved[0].name).toBe("ghost");
|
|
114
|
+
expect(unresolved[0].concept.title).toBe("Stale");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("an entity with no bound concepts is absent from the map, never present with []", async () => {
|
|
118
|
+
const bundle = await loadOkfBundle(join(TEST_DIR, "empty"));
|
|
119
|
+
const entities = new Map<string, Declarable>([["vpc", decl({ lexicon: "gcp", entityType: "Vpc" })]]);
|
|
120
|
+
const { bound, unresolved } = bindConcepts(bundle, entities);
|
|
121
|
+
expect(bound.size).toBe(0);
|
|
122
|
+
expect(unresolved).toEqual([]);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("round trip with buildOkfBundle (#1058) — the emitter never writes binds", () => {
|
|
127
|
+
test("reading back an emitted bundle yields zero bindings", async () => {
|
|
128
|
+
const bucket = decl({ lexicon: "aws", entityType: "AWS::S3::Bucket" });
|
|
129
|
+
const vpc = decl({ lexicon: "gcp", entityType: "Vpc" });
|
|
130
|
+
const entities = new Map<string, Declarable>([
|
|
131
|
+
["myBucket", bucket],
|
|
132
|
+
["vpc", vpc],
|
|
133
|
+
]);
|
|
134
|
+
const dependencies = new Map([["myBucket", new Set(["vpc"])]]);
|
|
135
|
+
|
|
136
|
+
for (const file of buildOkfBundle({ entities, dependencies })) {
|
|
137
|
+
write(file.path, file.content);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const bundle = await loadOkfBundle(TEST_DIR);
|
|
141
|
+
// index.md is reserved and skipped; the two entity concepts load.
|
|
142
|
+
expect(bundle.concepts.map((c) => c.path).sort()).toEqual(["aws/myBucket.md", "gcp/vpc.md"]);
|
|
143
|
+
expect(bundle.concepts.every((c) => c.binds.length === 0)).toBe(true);
|
|
144
|
+
|
|
145
|
+
const { bound, unresolved } = bindConcepts(bundle, entities);
|
|
146
|
+
expect(bound.size).toBe(0);
|
|
147
|
+
expect(unresolved).toEqual([]);
|
|
148
|
+
});
|
|
149
|
+
});
|
package/src/okf-read.ts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join, relative, sep } from "node:path";
|
|
4
|
+
import type { Declarable } from "./declarable";
|
|
5
|
+
import { splitFrontmatter } from "./okf";
|
|
6
|
+
import { parseYAML } from "./yaml";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* OKF bundle reader (#1864, design #1059, epic #1057) — the input half of
|
|
10
|
+
* `./okf.ts`'s emitter: loads a project-authored knowledge bundle from disk
|
|
11
|
+
* and binds its concepts to discovered entities.
|
|
12
|
+
*
|
|
13
|
+
* Tolerant to the letter of the OKF v0.2 spec (§11), the same posture
|
|
14
|
+
* `okfConformanceProblems` enforces on the emit side: unknown `type` values
|
|
15
|
+
* and extra frontmatter keys pass through untouched, a file with unparseable
|
|
16
|
+
* or missing frontmatter is skipped with a warning, and nothing here ever
|
|
17
|
+
* rejects the bundle — a missing knowledge directory included.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const RESERVED_OKF_FILES = new Set(["index.md", "log.md"]);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* One authored concept document, parsed from a bundle. Deliberately thinner
|
|
24
|
+
* than the emit side's internal `Concept` (`./okf.ts`) — this is what a
|
|
25
|
+
* *reader* trusts, and per the #1059 design that is `type`/`title` plus the
|
|
26
|
+
* single frontmatter key chant interprets, `binds`. Everything else an
|
|
27
|
+
* author wrote rides along in {@link frontmatter} unexamined, so a consumer
|
|
28
|
+
* that needs a project-specific key (a future `category`, say) can still
|
|
29
|
+
* reach it without a reader change.
|
|
30
|
+
*/
|
|
31
|
+
export interface OkfConcept {
|
|
32
|
+
/** Bundle-relative path (forward-slash separated), e.g. "decisions/public-assets.md". */
|
|
33
|
+
path: string;
|
|
34
|
+
/** The frontmatter `type`. May be empty — the reader never rejects a concept for it, unlike the emitter's conformance check. */
|
|
35
|
+
type: string;
|
|
36
|
+
/** Frontmatter `title`, when the author set one. */
|
|
37
|
+
title?: string;
|
|
38
|
+
/**
|
|
39
|
+
* This concept's `binds` frontmatter key, normalized to a list — one name,
|
|
40
|
+
* several, or `[]` for "declared no binding, and that's legitimate."
|
|
41
|
+
* Structural-looking keys (`name`, `lexicon`, `kind`) are deliberately
|
|
42
|
+
* *not* surfaced here or anywhere else on this type: the reader interprets
|
|
43
|
+
* exactly `binds` (#1059's precedence-by-construction), so authored and
|
|
44
|
+
* inferred knowledge cannot conflict at the fact level.
|
|
45
|
+
*/
|
|
46
|
+
binds: string[];
|
|
47
|
+
/** Every frontmatter key this concept's document carries, `binds` included, verbatim. */
|
|
48
|
+
frontmatter: Record<string, unknown>;
|
|
49
|
+
/** Markdown body (everything after the closing `---`), unchanged. */
|
|
50
|
+
body: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A loaded OKF knowledge bundle: every authored concept found, reserved files already excluded. */
|
|
54
|
+
export interface OkfBundle {
|
|
55
|
+
concepts: OkfConcept[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** `binds` accepts one name or a list (#1059); anything else is treated as "no binding" rather than rejecting the concept. */
|
|
59
|
+
function normalizeBinds(value: unknown): string[] {
|
|
60
|
+
if (typeof value === "string") return value.length > 0 ? [value] : [];
|
|
61
|
+
if (Array.isArray(value)) return value.filter((v): v is string => typeof v === "string" && v.length > 0);
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Recursively collect every `.md` file under `dir`, as bundle-relative (forward-slash) paths paired with their absolute path. */
|
|
66
|
+
async function findMarkdownFiles(dir: string): Promise<Array<{ relPath: string; fullPath: string }>> {
|
|
67
|
+
const found: Array<{ relPath: string; fullPath: string }> = [];
|
|
68
|
+
|
|
69
|
+
async function scan(current: string): Promise<void> {
|
|
70
|
+
let entries;
|
|
71
|
+
try {
|
|
72
|
+
entries = await readdir(current, { withFileTypes: true });
|
|
73
|
+
} catch {
|
|
74
|
+
return; // unreadable directory: silently skip, same posture as findInfraFiles
|
|
75
|
+
}
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
const fullPath = join(current, entry.name);
|
|
78
|
+
if (entry.isDirectory()) {
|
|
79
|
+
await scan(fullPath);
|
|
80
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
81
|
+
const relPath = relative(dir, fullPath).split(sep).join("/");
|
|
82
|
+
found.push({ relPath, fullPath });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
await scan(dir);
|
|
88
|
+
return found;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Load every authored concept from an OKF knowledge bundle directory.
|
|
93
|
+
* `index.md`/`log.md` are skipped (reserved, by basename, at any depth —
|
|
94
|
+
* matching `okfConformanceProblems`'s check on the emit side). A directory
|
|
95
|
+
* that does not exist yields an empty bundle, not an error: the #1059 design
|
|
96
|
+
* treats a missing `knowledge/` as "this project has none yet," the same
|
|
97
|
+
* posture as a project with no lexicons configured.
|
|
98
|
+
*
|
|
99
|
+
* A file that fails to parse — no frontmatter block, unparseable YAML, or
|
|
100
|
+
* frontmatter that isn't a mapping — is skipped with a `console.warn` and
|
|
101
|
+
* does not affect any other file. Nothing here throws.
|
|
102
|
+
*/
|
|
103
|
+
export async function loadOkfBundle(dir: string): Promise<OkfBundle> {
|
|
104
|
+
if (!existsSync(dir)) return { concepts: [] };
|
|
105
|
+
|
|
106
|
+
const concepts: OkfConcept[] = [];
|
|
107
|
+
for (const { relPath, fullPath } of await findMarkdownFiles(dir)) {
|
|
108
|
+
if (RESERVED_OKF_FILES.has(relPath.split("/").pop()!)) continue;
|
|
109
|
+
|
|
110
|
+
let content: string;
|
|
111
|
+
try {
|
|
112
|
+
content = await readFile(fullPath, "utf8");
|
|
113
|
+
} catch {
|
|
114
|
+
console.warn(`[chant] warning: knowledge bundle concept "${relPath}" could not be read, skipping`);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const split = splitFrontmatter(content);
|
|
119
|
+
if (!split) {
|
|
120
|
+
console.warn(`[chant] warning: knowledge bundle concept "${relPath}" has no frontmatter block, skipping`);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let parsed: unknown;
|
|
125
|
+
try {
|
|
126
|
+
parsed = parseYAML(split.frontmatter);
|
|
127
|
+
} catch {
|
|
128
|
+
console.warn(`[chant] warning: knowledge bundle concept "${relPath}" has unparseable frontmatter, skipping`);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
132
|
+
console.warn(`[chant] warning: knowledge bundle concept "${relPath}" frontmatter is not a mapping, skipping`);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const frontmatter = parsed as Record<string, unknown>;
|
|
137
|
+
concepts.push({
|
|
138
|
+
path: relPath,
|
|
139
|
+
type: typeof frontmatter.type === "string" ? frontmatter.type : "",
|
|
140
|
+
title: typeof frontmatter.title === "string" ? frontmatter.title : undefined,
|
|
141
|
+
binds: normalizeBinds(frontmatter.binds),
|
|
142
|
+
frontmatter,
|
|
143
|
+
body: split.body,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
concepts.sort((a, b) => a.path.localeCompare(b.path));
|
|
148
|
+
return { concepts };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** {@link bindConcepts}'s result: bound concepts per entity, plus every binding that resolved nothing. */
|
|
152
|
+
export interface BindConceptsResult {
|
|
153
|
+
/**
|
|
154
|
+
* Bound concepts keyed by entity logical name — the same names that key
|
|
155
|
+
* `DiscoveryResult.entities` (`./discovery/index.ts`). An entity with no
|
|
156
|
+
* bound concepts is absent from the map, never present with `[]`.
|
|
157
|
+
*/
|
|
158
|
+
bound: Map<string, OkfConcept[]>;
|
|
159
|
+
/** Every `binds` entry that named no entity in `entities` — chant's own broken-link posture (#1059): a warning for the caller to surface (COR022, #1865), never a load failure. */
|
|
160
|
+
unresolved: UnresolvedBinding[];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** One `binds` entry that resolved to nothing. */
|
|
164
|
+
export interface UnresolvedBinding {
|
|
165
|
+
/** The concept whose `binds` named this entity. */
|
|
166
|
+
concept: OkfConcept;
|
|
167
|
+
/** The unresolved logical name. */
|
|
168
|
+
name: string;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Resolve every concept's `binds` against a project's discovered entities.
|
|
173
|
+
* `entities` is the same shape `./okf.ts`'s emitter takes, `DiscoveryResult`'s
|
|
174
|
+
* own `entities` map — a bare logical name is unambiguous within one
|
|
175
|
+
* discovery pass (#1059's "Rejected options" on binding ambiguity).
|
|
176
|
+
*
|
|
177
|
+
* A concept with no `binds` contributes to neither `bound` nor `unresolved`;
|
|
178
|
+
* an unbound concept is legitimate, not an omission (a runbook, a decision
|
|
179
|
+
* about the project as a whole).
|
|
180
|
+
*/
|
|
181
|
+
export function bindConcepts(bundle: OkfBundle, entities: Map<string, Declarable>): BindConceptsResult {
|
|
182
|
+
const bound = new Map<string, OkfConcept[]>();
|
|
183
|
+
const unresolved: UnresolvedBinding[] = [];
|
|
184
|
+
|
|
185
|
+
for (const concept of bundle.concepts) {
|
|
186
|
+
for (const name of concept.binds) {
|
|
187
|
+
if (entities.has(name)) {
|
|
188
|
+
if (!bound.has(name)) bound.set(name, []);
|
|
189
|
+
bound.get(name)!.push(concept);
|
|
190
|
+
} else {
|
|
191
|
+
unresolved.push({ concept, name });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return { bound, unresolved };
|
|
197
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activity contract tests (chant #1288 Stage 1).
|
|
3
|
+
*
|
|
4
|
+
* Exercises the four failure classes named in the issue, using a small
|
|
5
|
+
* lifecycleDiff-shaped contract so the test doesn't need a real lexicon.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, it } from "vitest";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { activity, phase, effect } from "./builders";
|
|
11
|
+
import type { OpConfig } from "./types";
|
|
12
|
+
import {
|
|
13
|
+
activityContract,
|
|
14
|
+
isActivityContract,
|
|
15
|
+
collectActivityContracts,
|
|
16
|
+
validateActivitySteps,
|
|
17
|
+
type ActivityContract,
|
|
18
|
+
} from "./activity-contract";
|
|
19
|
+
import { stepOutput } from "./step-output-ref";
|
|
20
|
+
import { EffectReceipt } from "../effect-receipt";
|
|
21
|
+
|
|
22
|
+
// ── activityContract() / isActivityContract() ──────────────────────────────
|
|
23
|
+
|
|
24
|
+
describe("activityContract()", () => {
|
|
25
|
+
it("brands its result so isActivityContract recognizes it", () => {
|
|
26
|
+
const contract = activityContract("noop", z.strictObject({}));
|
|
27
|
+
expect(isActivityContract(contract)).toBe(true);
|
|
28
|
+
expect(contract.name).toBe("noop");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("isActivityContract rejects a plain object with the same shape", () => {
|
|
32
|
+
expect(isActivityContract({ name: "noop", args: z.strictObject({}) })).toBe(false);
|
|
33
|
+
expect(isActivityContract(null)).toBe(false);
|
|
34
|
+
expect(isActivityContract(undefined)).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("collectActivityContracts()", () => {
|
|
39
|
+
it("collects every exported contract, keyed by its declared name — ignores non-contract exports", () => {
|
|
40
|
+
const mod = {
|
|
41
|
+
shellCmdContract: activityContract("shellCmd", z.strictObject({ cmd: z.string() })),
|
|
42
|
+
httpCheckContract: activityContract("httpCheck", z.strictObject({ url: z.string() })),
|
|
43
|
+
unrelatedExport: "not a contract",
|
|
44
|
+
SOME_CONSTANT: 42,
|
|
45
|
+
};
|
|
46
|
+
const into = new Map<string, ActivityContract>();
|
|
47
|
+
collectActivityContracts(mod, into);
|
|
48
|
+
expect([...into.keys()].sort()).toEqual(["httpCheck", "shellCmd"]);
|
|
49
|
+
expect(into.get("shellCmd")!.name).toBe("shellCmd");
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// ── validateActivitySteps() ─────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
const lifecycleDiffContract = activityContract(
|
|
56
|
+
"lifecycleDiff",
|
|
57
|
+
z.strictObject({ env: z.string(), live: z.boolean().optional() }),
|
|
58
|
+
z.object({ output: z.string(), exitCode: z.number(), drifted: z.boolean() }),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const helmInstallContract = activityContract(
|
|
62
|
+
"helmInstall",
|
|
63
|
+
z.strictObject({ name: z.string(), chart: z.string().optional(), namespace: z.string().optional() }),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
function opWith(config: Partial<OpConfig> & Pick<OpConfig, "phases">): Pick<OpConfig, "name" | "phases" | "onFailure"> {
|
|
67
|
+
return { name: "test-op", ...config };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
describe("validateActivitySteps() — passing Ops", () => {
|
|
71
|
+
it("returns no issues for a step matching its contract exactly", () => {
|
|
72
|
+
const config = opWith({ phases: [phase("Diff", [activity("lifecycleDiff", { env: "prod", live: true })])] });
|
|
73
|
+
const issues = validateActivitySteps(config, new Map([["lifecycleDiff", lifecycleDiffContract]]));
|
|
74
|
+
expect(issues).toEqual([]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("skips a step whose fn has no registered contract — non-breaking by design", () => {
|
|
78
|
+
const config = opWith({ phases: [phase("Deploy", [activity("somethingUnregistered", { anything: "goes" })])] });
|
|
79
|
+
const issues = validateActivitySteps(config, new Map([["lifecycleDiff", lifecycleDiffContract]]));
|
|
80
|
+
expect(issues).toEqual([]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("a valid outcomeAttribute.from path against the declared return schema passes", () => {
|
|
84
|
+
const step = activity("lifecycleDiff", { env: "prod" });
|
|
85
|
+
step.outcomeAttribute = { name: "Drift", from: "drifted" };
|
|
86
|
+
const config = opWith({ phases: [phase("Diff", [step])] });
|
|
87
|
+
const issues = validateActivitySteps(config, new Map([["lifecycleDiff", lifecycleDiffContract]]));
|
|
88
|
+
expect(issues).toEqual([]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("a step-output reference standing in for a typed arg does not trip a false-positive type mismatch (#1290)", () => {
|
|
92
|
+
// args.env is z.string() — a StepOutputRef placeholder there is not a
|
|
93
|
+
// string, but it's not a real error either; TMP013 validates it against
|
|
94
|
+
// the producer's return schema separately.
|
|
95
|
+
const step = activity("lifecycleDiff", { env: stepOutput("diff", "name") as unknown as string });
|
|
96
|
+
const config = opWith({ phases: [phase("Diff", [step])] });
|
|
97
|
+
const issues = validateActivitySteps(config, new Map([["lifecycleDiff", lifecycleDiffContract]]));
|
|
98
|
+
expect(issues).toEqual([]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("a step-output reference elsewhere in args doesn't mask a genuinely unrecognized key", () => {
|
|
102
|
+
const step = activity("lifecycleDiff", { env: stepOutput("diff", "name") as unknown as string, typo: "oops" });
|
|
103
|
+
const config = opWith({ phases: [phase("Diff", [step])] });
|
|
104
|
+
const issues = validateActivitySteps(config, new Map([["lifecycleDiff", lifecycleDiffContract]]));
|
|
105
|
+
expect(issues.some((i) => i.message.includes("Unrecognized key") && i.message.includes("typo"))).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe("validateActivitySteps() — the four failure classes from #1288", () => {
|
|
110
|
+
it("flags an unknown profile — e.g. kubectlApply(..., { profile: \"longInfa\" })", () => {
|
|
111
|
+
const step = activity("kubectlApply", { manifest: "dist/k8s.yaml" }, "longInfa" as never);
|
|
112
|
+
const config = opWith({ phases: [phase("Deploy", [step])] });
|
|
113
|
+
const issues = validateActivitySteps(config, new Map());
|
|
114
|
+
expect(issues).toHaveLength(1);
|
|
115
|
+
expect(issues[0].message).toContain('unknown profile "longInfa"');
|
|
116
|
+
expect(issues[0].message).toContain("fastIdempotent");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("flags an arg key the schema doesn't recognize — e.g. helmInstall(..., { nameSpace: \"prod\" })", () => {
|
|
120
|
+
const step = activity("helmInstall", { name: "api", chart: "./chart", nameSpace: "prod" });
|
|
121
|
+
const config = opWith({ phases: [phase("Deploy", [step])] });
|
|
122
|
+
const issues = validateActivitySteps(config, new Map([["helmInstall", helmInstallContract]]));
|
|
123
|
+
expect(issues.some((i) => i.message.includes("Unrecognized key") && i.message.includes("nameSpace"))).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("flags a wrong/missing arg key — e.g. activity(\"lifecycleDiff\", { environment: \"prod\" })", () => {
|
|
127
|
+
const step = activity("lifecycleDiff", { environment: "prod" });
|
|
128
|
+
const config = opWith({ phases: [phase("Diff", [step])] });
|
|
129
|
+
const issues = validateActivitySteps(config, new Map([["lifecycleDiff", lifecycleDiffContract]]));
|
|
130
|
+
// `env` (required) is missing, and `environment` is an unrecognized key.
|
|
131
|
+
expect(issues.some((i) => i.message.includes("args.env"))).toBe(true);
|
|
132
|
+
expect(issues.some((i) => i.message.includes("Unrecognized key") && i.message.includes("environment"))).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("flags an outcomeAttribute.from path that doesn't exist on the declared return type", () => {
|
|
136
|
+
const step = activity("lifecycleDiff", { env: "prod" });
|
|
137
|
+
step.outcomeAttribute = { name: "Drift", from: "drifed" }; // typo: drifted
|
|
138
|
+
const config = opWith({ phases: [phase("Diff", [step])] });
|
|
139
|
+
const issues = validateActivitySteps(config, new Map([["lifecycleDiff", lifecycleDiffContract]]));
|
|
140
|
+
expect(issues.some((i) => i.message.includes('outcomeAttribute.from "drifed"'))).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("flags a wrong-typed arg value even when the key is right", () => {
|
|
144
|
+
const step = activity("lifecycleDiff", { env: 42 as unknown as string });
|
|
145
|
+
const config = opWith({ phases: [phase("Diff", [step])] });
|
|
146
|
+
const issues = validateActivitySteps(config, new Map([["lifecycleDiff", lifecycleDiffContract]]));
|
|
147
|
+
expect(issues.some((i) => i.message.includes("args.env"))).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("validateActivitySteps() — structural coverage", () => {
|
|
152
|
+
it("walks onFailure phases too", () => {
|
|
153
|
+
const step = activity("lifecycleDiff", { environment: "prod" });
|
|
154
|
+
const config = opWith({ phases: [], onFailure: [phase("Rollback", [step])] });
|
|
155
|
+
const issues = validateActivitySteps(config, new Map([["lifecycleDiff", lifecycleDiffContract]]));
|
|
156
|
+
expect(issues.length).toBeGreaterThan(0);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("walks activity steps nested inside an effect step", () => {
|
|
160
|
+
const receipt = EffectReceipt("seeded", {
|
|
161
|
+
effect: "db-seed",
|
|
162
|
+
flavor: "hash",
|
|
163
|
+
inputs: { file: "seed.sql", version: 3 },
|
|
164
|
+
});
|
|
165
|
+
const badStep = activity("lifecycleDiff", { environment: "prod" });
|
|
166
|
+
const effectStep = effect(receipt, [badStep]);
|
|
167
|
+
const config = opWith({ phases: [phase("Seed", [effectStep])] });
|
|
168
|
+
const issues = validateActivitySteps(config, new Map([["lifecycleDiff", lifecycleDiffContract]]));
|
|
169
|
+
expect(issues.length).toBeGreaterThan(0);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("reports every issue across multiple steps, not just the first", () => {
|
|
173
|
+
const step1 = activity("lifecycleDiff", { environment: "prod" });
|
|
174
|
+
const step2 = activity("kubectlApply", {}, "longInfa" as never);
|
|
175
|
+
const config = opWith({ phases: [phase("P", [step1, step2])] });
|
|
176
|
+
const issues = validateActivitySteps(config, new Map([["lifecycleDiff", lifecycleDiffContract]]));
|
|
177
|
+
expect(issues.some((i) => i.fn === "lifecycleDiff")).toBe(true);
|
|
178
|
+
expect(issues.some((i) => i.fn === "kubectlApply")).toBe(true);
|
|
179
|
+
});
|
|
180
|
+
});
|